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