1# Copyright (C) 2008-2010 Adam Olsen
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2, or (at your option)
6# any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16#
17#
18# The developers of the Exaile media player hereby grant permission
19# for non-GPL compatible GStreamer and Exaile plugins to be used and
20# distributed together with GStreamer and Exaile. This permission is
21# above and beyond the permissions granted by the GPL license by which
22# Exaile is covered. If you modify this code, you may extend this
23# exception to your version of the code, but you are not obligated to
24# do so. If you do not wish to do so, delete this exception statement
25# from your version.
26
27
28import logging
29from typing import Dict, Union
30
31from gi.repository import Gtk
32from gi.repository import Gdk
33from gi.repository import GdkPixbuf
34from gi.repository import GObject
35
36from xl import common, event, radio, settings, trax
37from xl.nls import gettext as _
38from xl.playlist import Playlist, SmartPlaylist
39from xlgui import icons, panel
40from xlgui.panel import menus
41from xlgui.widgets import dialogs
42from xlgui.widgets.common import DragTreeView
43from xlgui.widgets.smart_playlist_editor import SmartPlaylistEditor
44
45logger = logging.getLogger(__name__)
46
47
48class TrackWrapper:
49    def __init__(self, track, playlist):
50        self.track = track
51        self.playlist = playlist
52
53    def __str__(self):
54        text = self.track.get_tag_raw('title')
55        if text is not None:
56            text = ' / '.join(text)
57
58        if text:
59            artists = self.track.get_tag_raw('artist')
60            if artists:
61                text += ' - ' + ' / '.join(artists)
62            return text
63        return self.track.get_loc_for_io()
64
65
66class BasePlaylistPanelMixin(GObject.GObject):
67    """
68    Base playlist tree object.
69
70    Used by the radio and playlists panels to display playlists
71    """
72
73    # HACK: Notice that this is not __gsignals__; descendants need to manually
74    # merge this in. This is because new PyGObject doesn't like __gsignals__
75    # coming from mixin. See:
76    # * https://bugs.launchpad.net/bugs/714484
77    # * http://www.daa.com.au/pipermail/pygtk/2011-February/019394.html
78    _gsignals_ = {
79        'playlist-selected': (GObject.SignalFlags.RUN_LAST, None, (object,)),
80        'tracks-selected': (GObject.SignalFlags.RUN_LAST, None, (object,)),
81        'append-items': (GObject.SignalFlags.RUN_LAST, None, (object, bool)),
82        'replace-items': (GObject.SignalFlags.RUN_LAST, None, (object,)),
83        'queue-items': (GObject.SignalFlags.RUN_LAST, None, (object,)),
84    }
85
86    # Cache for custom playlists
87    playlist_nodes: Dict[Playlist, Gtk.TreeIter]
88
89    # Mapping to keep track of open "are you sure you want to delete" dialogs
90    deletion_dialogs: Dict[Union[Playlist, SmartPlaylist], Gtk.Dialog]
91
92    def __init__(self):
93        """
94        Initializes the mixin
95        """
96        GObject.GObject.__init__(self)
97        self.playlist_nodes = {}
98        self.track_image = icons.MANAGER.pixbuf_from_icon_name(
99            'audio-x-generic', Gtk.IconSize.SMALL_TOOLBAR
100        )
101        self.deletion_dialogs = {}
102
103    def remove_playlist(self, ignored=None):
104        """
105        Removes the selected playlist from the UI
106        and from the underlying manager
107        """
108        selected_playlist = self.tree.get_selected_page(raw=True)
109        if selected_playlist is None:
110            return
111        dialog = self.deletion_dialogs.get(selected_playlist)
112        if dialog:
113            dialog.present()
114            return
115
116        def on_response(dialog, response):
117            if response == Gtk.ResponseType.YES:
118                if isinstance(selected_playlist, SmartPlaylist):
119                    self.smart_manager.remove_playlist(selected_playlist.name)
120                else:
121                    self.playlist_manager.remove_playlist(selected_playlist.name)
122                    # Remove from {playlist: iter} cache.
123                    del self.playlist_nodes[selected_playlist]
124                # Remove from UI.
125                selection = self.tree.get_selection()
126                (model, iter) = selection.get_selected()
127                self.model.remove(iter)
128            del self.deletion_dialogs[selected_playlist]
129            dialog.destroy()
130
131        dialog = Gtk.MessageDialog(
132            buttons=Gtk.ButtonsType.YES_NO,
133            message_type=Gtk.MessageType.QUESTION,
134            text=_('Delete the playlist "%s"?') % selected_playlist.name,
135            transient_for=self.parent,
136        )
137        dialog.connect('response', on_response)
138        self.deletion_dialogs[selected_playlist] = dialog
139        dialog.present()
140
141    def rename_playlist(self, playlist):
142        """
143        Renames the playlist
144        """
145
146        if playlist is None:
147            return
148
149        # Ask for new name
150        dialog = dialogs.TextEntryDialog(
151            _("Enter the new name you want for your playlist"),
152            _("Rename Playlist"),
153            playlist.name,
154            parent=self.parent,
155        )
156
157        result = dialog.run()
158        name = dialog.get_value()
159
160        dialog.destroy()
161
162        if result != Gtk.ResponseType.OK or name == '':
163            return
164
165        if name in self.playlist_manager.playlists:
166            # name is already in use
167            dialogs.error(
168                self.parent, _("The playlist name you entered is already in use.")
169            )
170            return
171
172        selection = self.tree.get_selection()
173        (model, iter) = selection.get_selected()
174        model.set_value(iter, 1, name)
175
176        # Update the manager aswell
177        self.playlist_manager.rename_playlist(playlist, name)
178
179    def open_selected_playlist(self):
180        selection = self.tree.get_selection()
181        (model, iter) = selection.get_selected()
182        self.open_item(self.tree, model.get_path(iter), None)
183
184    def on_rating_changed(self, widget, rating):
185        """
186        Updates the rating of the selected tracks
187        """
188        tracks = self.get_selected_tracks()
189
190        for track in tracks:
191            track.set_rating(rating)
192
193        maximum = settings.get_option('rating/maximum', 5)
194        event.log_event('rating_changed', self, 100 * rating / maximum)
195
196    def open_item(self, tree, path, col):
197        """
198        Called when the user double clicks on a playlist,
199        also called when the user double clicks on a track beneath
200        a playlist.  When they active a track it opens the playlist
201        and starts playing that track
202        """
203        iter = self.model.get_iter(path)
204        item = self.model.get_value(iter, 2)
205        if item is not None:
206            if isinstance(item, (Playlist, SmartPlaylist)):
207                # for smart playlists
208                if hasattr(item, 'get_playlist'):
209                    try:
210                        item = item.get_playlist(self.collection)
211                    except Exception as e:
212                        logger.exception("Error loading smart playlist")
213                        dialogs.error(
214                            self.parent, _("Error loading smart playlist: %s") % str(e)
215                        )
216                        return
217                else:
218                    # Get an up to date copy
219                    item = self.playlist_manager.get_playlist(item.name)
220                    # item.set_is_custom(True)
221
222                #                self.controller.main.add_playlist(item)
223                self.emit('playlist-selected', item)
224            else:
225                self.emit('append-items', [item.track], True)
226
227    def add_new_playlist(self, tracks=[], name=None):
228        """
229        Adds a new playlist to the list of playlists. If name is
230        None or the name conflicts with an existing playlist, the
231        user will be queried for a new name.
232
233        Returns the name of the new playlist, or None if it was
234        not added.
235        """
236        if name:
237            if name in self.playlist_manager.playlists:
238                name = dialogs.ask_for_playlist_name(
239                    self.get_panel().get_toplevel(), self.playlist_manager, name
240                )
241        else:
242            if tracks:
243                artists = []
244                composers = []
245                albums = []
246
247                for track in tracks:
248                    artist = track.get_tag_display('artist', artist_compilations=False)
249
250                    if artist is not None:
251                        artists += [artist]
252
253                    composer = track.get_tag_display(
254                        'composer', artist_compilations=False
255                    )
256
257                    if composer is not None:
258                        composers += composer
259
260                    album = track.get_tag_display('album')
261
262                    if album is not None:
263                        albums += album
264
265                artists = list(set(artists))[:3]
266                composers = list(set(composers))[:3]
267                albums = list(set(albums))[:3]
268
269                if len(artists) > 0:
270                    name = artists[0]
271
272                    if len(artists) > 2:
273                        # TRANSLATORS: Playlist title suggestion with more
274                        # than two values
275                        name = _('%(first)s, %(second)s and others') % {
276                            'first': artists[0],
277                            'second': artists[1],
278                        }
279                    elif len(artists) > 1:
280                        # TRANSLATORS: Playlist title suggestion with two values
281                        name = _('%(first)s and %(second)s') % {
282                            'first': artists[0],
283                            'second': artists[1],
284                        }
285                elif len(composers) > 0:
286                    name = composers[0]
287
288                    if len(composers) > 2:
289                        # TRANSLATORS: Playlist title suggestion with more
290                        # than two values
291                        name = _('%(first)s, %(second)s and others') % {
292                            'first': composers[0],
293                            'second': composers[1],
294                        }
295                    elif len(composers) > 1:
296                        # TRANSLATORS: Playlist title suggestion with two values
297                        name = _('%(first)s and %(second)s') % {
298                            'first': composers[0],
299                            'second': composers[1],
300                        }
301                elif len(albums) > 0:
302                    name = albums[0]
303
304                    if len(albums) > 2:
305                        # TRANSLATORS: Playlist title suggestion with more
306                        # than two values
307                        name = _('%(first)s, %(second)s and others') % {
308                            'first': albums[0],
309                            'second': albums[1],
310                        }
311                    elif len(albums) > 1:
312                        # TRANSLATORS: Playlist title suggestion with two values
313                        name = _('%(first)s and %(second)s') % {
314                            'first': albums[0],
315                            'second': albums[1],
316                        }
317                else:
318                    name = ''
319
320            name = dialogs.ask_for_playlist_name(
321                self.get_panel().get_toplevel(), self.playlist_manager, name
322            )
323
324        if name is not None:
325            # Create the playlist from all of the tracks
326            new_playlist = Playlist(name)
327            new_playlist.extend(tracks)
328            # We are adding a completely new playlist with tracks so we save it
329            self.playlist_manager.save_playlist(new_playlist)
330
331        return name
332
333    def _load_playlist_nodes(self, playlist):
334        """
335        Loads the playlist tracks into the node for the specified playlist
336        """
337        if playlist not in self.playlist_nodes:
338            return
339
340        expanded = self.tree.row_expanded(
341            self.model.get_path(self.playlist_nodes[playlist])
342        )
343
344        self._clear_node(self.playlist_nodes[playlist])
345        parent = self.playlist_nodes[playlist]
346        for track in playlist:
347            if not track:
348                continue
349            wrapper = TrackWrapper(track, playlist)
350            row = (self.track_image, str(wrapper), wrapper)
351            self.model.append(parent, row)
352
353        if expanded:
354            self.tree.expand_row(
355                self.model.get_path(self.playlist_nodes[playlist]), False
356            )
357
358    def remove_selected_track(self):
359        """
360        Removes the selected track from its playlist
361        and saves the playlist
362        """
363        selection = self.tree.get_selection()
364        (model, iter) = selection.get_selected()
365        track = model.get_value(iter, 2)
366        if isinstance(track, TrackWrapper):
367            del track.playlist[track.playlist.index(track.track)]
368            # Update the list
369            self.model.remove(iter)
370            # TODO do we save the playlist after this??
371            self.playlist_manager.save_playlist(track.playlist, overwrite=True)
372
373
374class PlaylistsPanel(panel.Panel, BasePlaylistPanelMixin):
375    """
376    The playlists panel
377    """
378
379    __gsignals__ = BasePlaylistPanelMixin._gsignals_
380
381    ui_info = ('playlists.ui', 'PlaylistsPanel')
382
383    def __init__(self, parent, playlist_manager, smart_manager, collection, name):
384        """
385        Intializes the playlists panel
386
387        @param playlist_manager:  The playlist manager
388        """
389        panel.Panel.__init__(self, parent, name, _('Playlists'))
390        BasePlaylistPanelMixin.__init__(self)
391        self.playlist_manager = playlist_manager
392        self.smart_manager = smart_manager
393        self.collection = collection
394        self.box = self.builder.get_object('PlaylistsPanel')
395
396        self.playlist_name_info = 500
397        self.track_target = Gtk.TargetEntry.new("text/uri-list", 0, 0)
398        self.playlist_target = Gtk.TargetEntry.new(
399            "playlist_name", Gtk.TargetFlags.SAME_WIDGET, self.playlist_name_info
400        )
401        self.deny_targets = [Gtk.TargetEntry.new('', 0, 0)]
402
403        self.tree = PlaylistDragTreeView(self)
404        self.tree.connect('row-activated', self.open_item)
405        self.tree.set_headers_visible(False)
406        self.tree.connect('drag-motion', self.drag_motion)
407        self.tree.drag_source_set(
408            Gdk.ModifierType.BUTTON1_MASK,
409            [self.track_target, self.playlist_target],
410            Gdk.DragAction.COPY | Gdk.DragAction.MOVE,
411        )
412
413        self.scroll = Gtk.ScrolledWindow()
414        self.scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
415        self.scroll.add(self.tree)
416        self.scroll.set_shadow_type(Gtk.ShadowType.IN)
417        self.box.pack_start(self.scroll, True, True, 0)
418        self.box.show_all()
419
420        pb = Gtk.CellRendererPixbuf()
421        cell = Gtk.CellRendererText()
422        if settings.get_option('gui/ellipsize_text_in_panels', False):
423            from gi.repository import Pango
424
425            cell.set_property('ellipsize-set', True)
426            cell.set_property('ellipsize', Pango.EllipsizeMode.END)
427        col = Gtk.TreeViewColumn('Text')
428        col.pack_start(pb, False)
429        col.pack_start(cell, True)
430        col.set_attributes(pb, pixbuf=0)
431        col.set_attributes(cell, text=1)
432        self.tree.append_column(col)
433        self.model = Gtk.TreeStore(GdkPixbuf.Pixbuf, str, object)
434        self.tree.set_model(self.model)
435
436        # icons
437        self.folder = icons.MANAGER.pixbuf_from_icon_name(
438            'folder', Gtk.IconSize.SMALL_TOOLBAR
439        )
440        self.playlist_image = icons.MANAGER.pixbuf_from_icon_name(
441            'music-library', Gtk.IconSize.SMALL_TOOLBAR
442        )
443
444        # menus
445        self.playlist_menu = menus.PlaylistsPanelPlaylistMenu(self)
446        self.smart_menu = menus.PlaylistsPanelPlaylistMenu(self)
447        self.default_menu = menus.PlaylistPanelMenu(self)
448
449        self.track_menu = menus.TrackPanelMenu(self)
450
451        self._connect_events()
452        self._load_playlists()
453
454    @property
455    def menu(self):
456        """
457        Gets a menu for the selected item
458        :return: xlgui.widgets.menu.Menu or None if do not have it
459        """
460        model, it = self.tree.get_selection().get_selected()
461        pl = model[it][2]
462        return (
463            self.playlist_menu
464            if isinstance(pl, Playlist)
465            else self.smart_menu
466            if isinstance(pl, SmartPlaylist)
467            else self.track_menu
468            if isinstance(pl, TrackWrapper)
469            else self.default_menu
470        )
471
472    def _connect_events(self):
473        event.add_ui_callback(self.refresh_playlists, 'track_tags_changed')
474        event.add_ui_callback(
475            self._on_playlist_added, 'playlist_added', self.playlist_manager
476        )
477
478        self.tree.connect('key-release-event', self.on_key_released)
479
480    def _playlist_properties(self):
481        pl = self.tree.get_selected_page(raw=True)
482        if isinstance(pl, SmartPlaylist):
483            self.edit_selected_smart_playlist()
484
485    def refresh_playlists(self, type, track, tags):
486        """
487        wrapper so that multiple events dont cause multiple
488        reloads in quick succession
489        """
490        if settings.get_option('gui/sync_on_tag_change', True) and tags & {
491            'title',
492            'artist',
493        }:
494            self._refresh_playlists()
495
496    @common.glib_wait(500)
497    def _refresh_playlists(self):
498        """
499        Callback for when tags have changed and the playlists
500        need refreshing.
501        """
502        if settings.get_option('gui/sync_on_tag_change', True):
503            for playlist in self.playlist_nodes:
504                self._load_playlist_nodes(playlist)
505
506    def _on_playlist_added(self, type, object, playlist_name):
507
508        new_playlist = self.playlist_manager.get_playlist(playlist_name)
509
510        for oldpl in self.playlist_nodes:
511            if oldpl.name == playlist_name:  # Name already exists
512                if oldpl is not new_playlist:
513                    node = self.playlist_nodes[oldpl]
514                    # Replace the playlist object in {playlist: iter} cache.
515                    del self.playlist_nodes[oldpl]
516                    self.playlist_nodes[new_playlist] = node
517                    # Replace the playlist object in tree model.
518                    self.model[node][2] = new_playlist
519                break
520        else:  # Name doesn't exist yet
521            self.playlist_nodes[new_playlist] = self.model.append(
522                self.custom, [self.playlist_image, playlist_name, new_playlist]
523            )
524            self.tree.expand_row(self.model.get_path(self.custom), False)
525
526        # Refresh the playlist subnodes.
527        self._load_playlist_nodes(new_playlist)
528
529    def _load_playlists(self):
530        """
531        Loads the currently saved playlists
532        """
533        self.smart = self.model.append(None, [self.folder, _("Smart Playlists"), None])
534
535        self.custom = self.model.append(
536            None, [self.folder, _("Custom Playlists"), None]
537        )
538
539        names = sorted(self.smart_manager.playlists)
540        for name in names:
541            self.model.append(
542                self.smart,
543                [self.playlist_image, name, self.smart_manager.get_playlist(name)],
544            )
545
546        names = sorted(self.playlist_manager.playlists)
547        for name in names:
548            playlist = self.playlist_manager.get_playlist(name)
549            self.playlist_nodes[playlist] = self.model.append(
550                self.custom, [self.playlist_image, name, playlist]
551            )
552            self._load_playlist_nodes(playlist)
553
554        self.tree.expand_row(self.model.get_path(self.smart), False)
555        self.tree.expand_row(self.model.get_path(self.custom), False)
556
557    def import_playlist(self):
558        """
559        Shows a dialog to ask the user to import a new playlist
560        """
561
562        def _on_playlists_selected(dialog, playlists):
563            for playlist in playlists:
564                self.add_new_playlist(playlist, playlist.name)
565
566        dialog = dialogs.PlaylistImportDialog(parent=self.parent)
567        dialog.connect('playlists-selected', _on_playlists_selected)
568        dialog.show()
569
570    def add_smart_playlist(self):
571        """
572        Shows a dialog for adding a new smart playlist
573        """
574        pl = SmartPlaylistEditor.create(
575            self.collection, self.smart_manager, self.parent
576        )
577        if pl:
578            self.model.append(self.smart, [self.playlist_image, pl.name, pl])
579
580    def edit_selected_smart_playlist(self):
581        """
582        Shows a dialog for editing the currently selected smart playlist
583        """
584        pl = self.tree.get_selected_page(raw=True)
585        self.edit_smart_playlist(pl)
586
587    def edit_smart_playlist(self, pl):
588        """
589        Shows a dialog for editing a smart playlist
590        """
591        pl = SmartPlaylistEditor.edit(
592            pl, self.collection, self.smart_manager, self.parent
593        )
594        if pl:
595            selection = self.tree.get_selection()
596            model, it = selection.get_selected()
597            model.set_value(it, 1, pl.name)
598            model.set_value(it, 2, pl)
599
600    def drag_data_received(self, tv, context, x, y, selection, info, etime):
601        """
602        Called when someone drags some thing onto the playlist panel
603        """
604        if info == self.playlist_name_info:
605            # We are being dragged a playlist so
606            # we have to reorder them
607            playlist_name = selection.get_text()
608            drag_source = self.tree.get_selected_page()
609            # verify names
610            if drag_source is not None:
611                if drag_source.name == playlist_name:
612                    drop_info = tv.get_dest_row_at_pos(x, y)
613                    drag_source_iter = self.playlist_nodes[drag_source]
614                    if drop_info:
615                        path, position = drop_info
616                        drop_target_iter = self.model.get_iter(path)
617                        drop_target = self.model.get_value(drop_target_iter, 2)
618                        if position == Gtk.TreeViewDropPosition.BEFORE:
619                            # Put the playlist before drop_target
620                            self.model.move_before(drag_source_iter, drop_target_iter)
621                            self.playlist_manager.move(
622                                playlist_name, drop_target.name, after=False
623                            )
624                        else:
625                            # put the playlist after drop_target
626                            self.model.move_after(drag_source_iter, drop_target_iter)
627                            self.playlist_manager.move(
628                                playlist_name, drop_target.name, after=True
629                            )
630            # Even though we are doing a move we still don't
631            # call the delete method because we take care
632            # of it above by moving instead of inserting/deleting
633            context.finish(True, False, etime)
634        else:
635            self._drag_data_received_uris(tv, context, x, y, selection, info, etime)
636
637    def _drag_data_received_uris(self, tv, context, x, y, selection, info, etime):
638        """
639        Called by drag_data_received when the user drags URIs onto us
640        """
641        locs = list(selection.get_uris())
642        drop_info = tv.get_dest_row_at_pos(x, y)
643        if drop_info:
644            path, position = drop_info
645            iter = self.model.get_iter(path)
646            drop_target = self.model.get_value(iter, 2)
647
648            # if the current item is a track, use the parent playlist
649            insert_index = None
650            if isinstance(drop_target, TrackWrapper):
651                current_playlist = drop_target.playlist
652                drop_target_index = current_playlist.index(drop_target.track)
653                # Adjust insert position based on drop position
654                if (
655                    position == Gtk.TreeViewDropPosition.BEFORE
656                    or position == Gtk.TreeViewDropPosition.INTO_OR_BEFORE
657                ):
658                    # By default adding tracks inserts it before so we do not
659                    # have to modify the insert index
660                    insert_index = drop_target_index
661                else:
662                    # If we want to go after we have to append 1
663                    insert_index = drop_target_index + 1
664            else:
665                current_playlist = drop_target
666
667            # Since the playlist do not have very good support for
668            # duplicate tracks we have to perform some trickery
669            # to make this work properly in all cases
670            try:
671                remove_track_index = current_playlist.index(
672                    self.tree.get_selected_track()
673                )
674            except ValueError:
675                remove_track_index = None
676            if insert_index is not None and remove_track_index is not None:
677                # Since remove_track_index will be removed before
678                # the new track is inserted we have to offset the
679                # insert index
680                if insert_index > remove_track_index:
681                    insert_index = insert_index - 1
682
683            # Delete the track before adding the other one
684            # so we do not get duplicates
685            # right now the playlist does not support
686            # duplicate tracks very well
687            if context.get_selected_action() == Gdk.DragAction.MOVE:
688                # On a move action the second True makes the
689                # drag_data_delete function called
690                context.finish(True, True, etime)
691            else:
692                context.finish(True, False, etime)
693
694            # Add the tracks we found to the internal playlist
695            # TODO: have it pass in existing tracks?
696            (tracks, playlists) = self.tree.get_drag_data(locs)
697
698            if insert_index is not None:
699                current_playlist[insert_index:insert_index] = tracks
700            else:
701                current_playlist.extend(tracks)
702
703            self._load_playlist_nodes(current_playlist)
704
705            # Do we save in the case when a user drags a file onto a playlist
706            # in the playlist panel? note that the playlist does not have to
707            # be open for this to happen
708            self.playlist_manager.save_playlist(current_playlist, overwrite=True)
709        else:
710            # If the user dragged files prompt for a new playlist name
711            # else if they dragged a playlist add the playlist
712
713            # We don't want the tracks in the playlists to be added to the
714            # master tracks list so we pass in False
715            (tracks, playlists) = self.tree.get_drag_data(locs, False)
716            # First see if they dragged any playlist files
717            for new_playlist in playlists:
718                # We are adding a completely new playlist with tracks so
719                # we save it. This will trigger playlist_added.
720                self.playlist_manager.save_playlist(new_playlist, overwrite=True)
721
722            # After processing playlist proceed to ask the user for the
723            # name of the new playlist to add and add the tracks to it
724            if len(tracks) > 0:
725                self.add_new_playlist(tracks)
726
727    def drag_data_delete(self, tv, context):
728        """
729        Called after a drag data operation is complete
730        and we want to delete the source data
731        """
732        if Gdk.drag_drop_succeeded(context):
733            self.remove_selected_track()
734
735    def drag_get_data(self, tv, context, selection_data, info, time):
736        """
737        Called when someone drags something from the playlist
738        """
739        # TODO based on info determine what we set in selection_data
740        if info == self.playlist_name_info:
741            pl = self.tree.get_selected_page()
742            if pl is not None:
743                selection_data.set_text(pl.name, len(pl.name))
744        else:
745            pl = self.tree.get_selected_page()
746            if pl is not None:
747                tracks = pl[:]
748            else:
749                tracks = self.tree.get_selected_tracks()
750
751            if not tracks:
752                return
753
754            for track in tracks:
755                DragTreeView.dragged_data[track.get_loc_for_io()] = track
756
757            uris = trax.util.get_uris_from_tracks(tracks)
758            selection_data.set_uris(uris)
759
760    def drag_motion(self, tv, context, x, y, time):
761        """
762        Sets the appropriate drag action based on what we are hovering over
763
764        hovering over playlists causes the copy action to occur
765        hovering over tracks within the same playlist causes the move
766            action to occur
767        hovering over tracks within different playlist causes the move
768            action to occur
769
770        Called on the destination widget
771        """
772        # Reset any target to be default to moving tracks
773        self.tree.enable_model_drag_dest([self.track_target], Gdk.DragAction.DEFAULT)
774        # Determine where the drag is coming from
775        dragging_playlist = False
776        if tv == self.tree:
777            selected_playlist = self.tree.get_selected_page()
778            if selected_playlist is not None:
779                dragging_playlist = True
780
781        # Find out where they are dropping onto
782        drop_info = tv.get_dest_row_at_pos(x, y)
783        if drop_info:
784            path, position = drop_info
785            iter = self.model.get_iter(path)
786            drop_target = self.model.get_value(iter, 2)
787
788            if isinstance(drop_target, Playlist):
789                if dragging_playlist:
790                    # If we drag onto  we copy, if we drag between we move
791                    if (
792                        position == Gtk.TreeViewDropPosition.INTO_OR_BEFORE
793                        or position == Gtk.TreeViewDropPosition.INTO_OR_AFTER
794                    ):
795                        Gdk.drag_status(context, Gdk.DragAction.COPY, time)
796                    else:
797                        Gdk.drag_status(context, Gdk.DragAction.MOVE, time)
798                        # Change target as well
799                        self.tree.enable_model_drag_dest(
800                            [self.playlist_target], Gdk.DragAction.DEFAULT
801                        )
802                else:
803                    Gdk.drag_status(context, Gdk.DragAction.COPY, time)
804            elif isinstance(drop_target, TrackWrapper):
805                # We are dragging onto another track
806                # make it a move operation if we are only dragging
807                # tracks within our widget
808                # We do a copy if we are draggin from another playlist
809                if Gtk.drag_get_source_widget(context) == tv and not dragging_playlist:
810                    Gdk.drag_status(context, Gdk.DragAction.MOVE, time)
811                else:
812                    Gdk.drag_status(context, Gdk.DragAction.COPY, time)
813            else:
814                # Prevent drop operation by changing the targets
815                self.tree.enable_model_drag_dest(
816                    self.deny_targets, Gdk.DragAction.DEFAULT
817                )
818                return False
819            return True
820        else:  # No drop info
821            if dragging_playlist:
822                Gdk.drag_status(context, Gdk.DragAction.MOVE, time)
823                # Change target as well
824                self.tree.enable_model_drag_dest(
825                    [self.playlist_target], Gdk.DragAction.DEFAULT
826                )
827                return True
828            return False
829
830    def on_key_released(self, widget, event):
831        """
832        Called when a key is released in the tree
833        """
834        if event.keyval == Gdk.KEY_Menu:
835            (mods, paths) = self.tree.get_selection().get_selected_rows()
836            if paths and paths[0]:
837                iter = self.model.get_iter(paths[0])
838                pl = self.model.get_value(iter, 2)
839                # Based on what is selected determines what
840                # menu we will show
841                if isinstance(pl, Playlist):
842                    Gtk.Menu.popup(
843                        self.playlist_menu, None, None, None, None, 0, event.time
844                    )
845                elif isinstance(pl, SmartPlaylist):
846                    Gtk.Menu.popup(
847                        self.smart_menu, None, None, None, None, 0, event.time
848                    )
849                elif isinstance(pl, TrackWrapper):
850                    Gtk.Menu.popup(
851                        self.track_menu, None, None, None, None, 0, event.time
852                    )
853                else:
854                    Gtk.Menu.popup(
855                        self.default_menu, None, None, None, None, 0, event.time
856                    )
857            return True
858
859        if event.keyval == Gdk.KEY_Left:
860            (mods, paths) = self.tree.get_selection().get_selected_rows()
861            if paths and paths[0]:
862                self.tree.collapse_row(paths[0])
863            return True
864
865        if event.keyval == Gdk.KEY_Right:
866            (mods, paths) = self.tree.get_selection().get_selected_rows()
867            if paths and paths[0]:
868                self.tree.expand_row(paths[0], False)
869            return True
870
871        if event.keyval == Gdk.KEY_Delete:
872            (mods, paths) = self.tree.get_selection().get_selected_rows()
873            if paths and paths[0]:
874                iter = self.model.get_iter(paths[0])
875                pl = self.model.get_value(iter, 2)
876                # Based on what is selected determines what
877                # menu we will show
878                if isinstance(pl, (Playlist, SmartPlaylist)):
879                    self.remove_playlist(pl)
880                elif isinstance(pl, TrackWrapper):
881                    self.remove_selected_track()
882            return True
883        return False
884
885    def _clear_node(self, node):
886        """
887        Clears a node of all children
888        """
889        iter = self.model.iter_children(node)
890        while True:
891            if not iter:
892                break
893            self.model.remove(iter)
894            iter = self.model.iter_children(node)
895
896
897class PlaylistDragTreeView(DragTreeView):
898    """
899    Custom DragTreeView to retrieve data from playlists
900    """
901
902    def __init__(self, container, receive=True, source=True):
903        DragTreeView.__init__(self, container, receive, source)
904        self.show_cover_drag_icon = False
905
906    def get_selection_empty(self):
907        '''Returns True if there are no selected items'''
908        return self.get_selection().count_selected_rows() == 0
909
910    def get_selection_is_computed(self):
911        """
912        Returns True if selection is a Smart Playlist
913        """
914        item = self.get_selected_item(raw=True)
915        return isinstance(item, SmartPlaylist)
916
917    def get_selected_tracks(self):
918        """
919        Used by the menu, just basically gets the selected
920        playlist and returns the tracks in it
921        """
922        playlist = self.get_selected_page()
923
924        if playlist is not None:
925            return [track for track in playlist]
926        else:
927            return [self.get_selected_track()]
928
929        return None
930
931    def get_selected_page(self, raw=False):
932        """
933        Retrieves the currently selected playlist in
934        the playlists panel.  If a non-playlist is
935        selected it returns None
936
937        @return: the playlist
938        """
939        item = self.get_selected_item(raw=raw)
940
941        if isinstance(item, (Playlist, SmartPlaylist)):
942            return item
943        else:
944            return None
945
946    def get_selected_track(self):
947        item = self.get_selected_item()
948
949        if not item:
950            return None
951
952        if isinstance(item, TrackWrapper):
953            return item.track
954        else:
955            return None
956
957    def get_selected_item(self, raw=False):
958        (model, iter) = self.get_selection().get_selected()
959
960        if not iter:
961            return None
962
963        item = model.get_value(iter, 2)
964
965        # for smart playlists
966        if isinstance(item, SmartPlaylist):
967            if raw:
968                return item
969            try:
970                return item.get_playlist(self.container.collection)
971            except Exception:
972                return None
973        if isinstance(item, radio.RadioItem):
974            if raw:
975                return item
976            return item.get_playlist()
977        elif isinstance(item, Playlist):
978            return item
979        elif isinstance(item, TrackWrapper):
980            return item
981        else:
982            return None
983