class PlaylistsWidget(Gtk.Grid): """ Show playlist tracks/albums """ __gsignals__ = { 'populated': (GObject.SignalFlags.RUN_FIRST, None, ()) } def __init__(self, playlist_ids): """ Init playlist Widget @param playlist ids as [int] """ Gtk.Grid.__init__(self) self.set_row_spacing(5) self.set_orientation(Gtk.Orientation.VERTICAL) self._playlist_ids = playlist_ids self._tracks_left = [] self._tracks_right = [] self._width = None self._orientation = None self._loading = Loading.NONE # Used to block widget2 populate while showing one column self._locked_widget_right = True self._box = Gtk.Grid() self._box.set_vexpand(True) self._box.set_column_homogeneous(True) self._box.show() self.connect('size-allocate', self._on_size_allocate) self._tracks_widget_left = TracksWidget(True) self._tracks_widget_left.set_vexpand(True) self._tracks_widget_right = TracksWidget(True) self._tracks_widget_right.set_vexpand(True) self._tracks_widget_left.connect('activated', self._on_activated) self._tracks_widget_right.connect('activated', self._on_activated) self._tracks_widget_left.show() self._tracks_widget_right.show() self.drag_dest_set(Gtk.DestDefaults.DROP | Gtk.DestDefaults.MOTION, [], Gdk.DragAction.MOVE) self.drag_dest_add_text_targets() self.connect('drag-data-received', self._on_drag_data_received) self.add(self._box) def get_id(self): """ Return playlist widget id @return int """ return Type.PLAYLISTS def show_overlay(self, bool): """ No overlay here now """ pass def update_state(self): """ No state to update """ pass def update_cover(self): """ No update cover for now """ pass def get_current_ordinate(self): """ If current track in widget, return it ordinate, @return y as int """ ordinate = None for child in self._tracks_widget_left.get_children() + \ self._tracks_widget_right.get_children(): if child.get_id() == Lp().player.current_track.id: ordinate = child.translate_coordinates(self._box, 0, 0)[1] return ordinate def populate_list_left(self, tracks, pos): """ Populate left list @param track's ids as array of int (not null) @param track position as int @thread safe """ # We reset width here to allow size allocation code to run self._width = None self._tracks_left = list(tracks) GLib.idle_add(self._add_tracks, tracks, self._tracks_widget_left, pos) def populate_list_right(self, tracks, pos): """ Populate right list @param track's ids as array of int (not null) @param track position as int @thread safe """ self._tracks_right = list(tracks) # If we are showing only one column, wait for widget1 if self._orientation == Gtk.Orientation.VERTICAL and\ self._locked_widget_right: GLib.timeout_add(100, self.populate_list_right, tracks, pos) else: # We reset width here to allow size allocation code to run self._width = None GLib.idle_add(self._add_tracks, tracks, self._tracks_widget_right, pos) def update_playing_indicator(self): """ Update playing indicator """ self._tracks_widget_left.update_playing(Lp().player.current_track.id) self._tracks_widget_right.update_playing(Lp().player.current_track.id) def stop(self): """ Stop loading """ self._loading = Loading.STOP def append(self, track_id): """ Add track to widget @param track id as int """ self._add_tracks([track_id], self._tracks_widget_right, -1) self._update_tracks() self._update_position() self._update_headers() self._tracks_widget_left.update_indexes(1) self._tracks_widget_right.update_indexes(len(self._tracks_left) + 1) def remove(self, track_id): """ Del track from widget @param track id as int """ children = self._tracks_widget_left.get_children() + \ self._tracks_widget_right.get_children() # Clear the widget if track_id is None: for child in children: child.destroy() self._update_tracks() else: for child in children: if child.get_id() == track_id: child.destroy() break self._update_tracks() self._update_position() self._update_headers() self._tracks_widget_left.update_indexes(1) self._tracks_widget_right.update_indexes( len(self._tracks_left) + 1) ####################### # PRIVATE # ####################### def _add_tracks(self, tracks, widget, pos, previous_album_id=None): """ Add tracks to list @param tracks id as array of [int] @param widget TracksWidget @param track position as int @param pos as int @param previous album id as int """ if self._loading == Loading.STOP: self._loading = Loading.NONE return if not tracks: if widget == self._tracks_widget_right: self._loading |= Loading.RIGHT elif widget == self._tracks_widget_left: self._loading |= Loading.LEFT if self._loading == Loading.ALL: self.emit('populated') self._locked_widget_right = False return track = Track(tracks.pop(0)) row = PlaylistRow(track.id, pos, track.album.id != previous_album_id) row.connect('track-moved', self._on_track_moved) row.show() widget.insert(row, pos) GLib.idle_add(self._add_tracks, tracks, widget, pos + 1, track.album.id) def _update_tracks(self): """ Update tracks based on current widget """ # Recalculate tracks self._tracks_left = [] self._tracks_right = [] for child in self._tracks_widget_left.get_children(): self._tracks_left.append(child.get_id()) for child in self._tracks_widget_right.get_children(): self._tracks_right.append(child.get_id()) def _update_position(self): """ Update widget position """ len_tracks1 = len(self._tracks_left) len_tracks2 = len(self._tracks_right) # Take first track from tracks2 and put it at the end of tracks1 if len_tracks2 > len_tracks1: src = self._tracks_right[0] if self._tracks_left: dst = self._tracks_left[-1] else: dst = -1 self._move_track(dst, src, False) # Take last track of tracks1 and put it at the bottom of tracks2 elif len_tracks1 - 1 > len_tracks2: src = self._tracks_left[-1] if self._tracks_right: dst = self._tracks_right[0] else: dst = -1 self._move_track(dst, src, True) self._update_tracks() def _update_headers(self): """ Update headers for all tracks """ self._tracks_widget_left.update_headers() prev_album_id = None if self._orientation == Gtk.Orientation.VERTICAL: if self._tracks_left: prev_album_id = Track(self._tracks_left[-1]).album.id self._tracks_widget_right.update_headers(prev_album_id) def _move_track(self, dst, src, up): """ Move track from src to row @param dst as int @param src as int @param up as bool @return (dst_widget as TracksWidget, src index as int, dst index as int) """ tracks1_len = len(self._tracks_left) tracks2_len = len(self._tracks_right) if src in self._tracks_left: src_widget = self._tracks_widget_left src_index = self._tracks_left.index(src) - 1 else: src_widget = self._tracks_widget_right src_index = self._tracks_right.index(src) - 1 if tracks1_len == 0 or dst in self._tracks_left: dst_widget = self._tracks_widget_left dst_tracks = self._tracks_left elif tracks2_len == 0 or dst in self._tracks_right: dst_widget = self._tracks_widget_right dst_tracks = self._tracks_right else: return # Remove src from src_widget for child in src_widget.get_children(): if child.get_id() == src: child.destroy() break src_track = Track(src) prev_track = Track() name = escape(src_track.name) index = 0 # Get previous track if dst != -1: for child in dst_widget.get_children(): if child.get_id() == dst: break index += 1 if not up: index += 1 # Get previous track (in dst context) prev_index = dst_tracks.index(dst) if up: prev_index -= 1 prev_track = Track(dst_tracks[prev_index]) # If we are listening to a compilation, prepend artist name if (src_track.album.artist_id == Type.COMPILATIONS or len(src_track.artist_ids) > 1 or src_track.album.artist_id not in src_track.artist_ids): name = "<b>%s</b>\n%s" % (escape(", ".join(src_track.artists)), name) self._tracks_left.insert(index, src_track.id) row = PlaylistRow(src_track.id, index, index == 0 or src_track.album.id != prev_track.album.id) row.connect('track-moved', self._on_track_moved) row.show() dst_widget.insert(row, index) return (src_widget, dst_widget, src_index, index) def _on_drag_data_received(self, widget, context, x, y, data, info, time): """ ONLY HAPPEN IN VERTICAL ORIENTATION Horizontal orientation is handled by TracksWidget @param widget as Gtk.Widget @param context as Gdk.DragContext @param x as int @param y as int @param data as Gtk.SelectionData @param info as int @param time as int """ try: try: child = self._tracks_widget_right.get_children()[-1] except: child = self._tracks_widget_left.get_children()[-1] self._on_track_moved(widget, child.get_id(), int(data.get_text()), False) except: pass def _on_track_moved(self, widget, dst, src, up): """ Move track from src to row Recalculate track position @param widget as TracksWidget @param dst as int @param src as int @param up as bool """ def update_playlist(): # Save playlist in db only if one playlist visible if len(self._playlist_ids) == 1 and self._playlist_ids[0] >= 0: Lp().playlists.clear(self._playlist_ids[0], False) tracks = [] for track_id in self._tracks_left + self._tracks_right: tracks.append(Track(track_id)) Lp().playlists.add_tracks(self._playlist_ids[0], tracks, False) if not (set(self._playlist_ids) - set(Lp().player.get_user_playlist_ids())): Lp().player.update_user_playlist(self._tracks_left + self._tracks_right) (src_widget, dst_widget, src_index, dst_index) = \ self._move_track(dst, src, up) self._update_tracks() self._update_position() self._update_headers() self._tracks_widget_left.update_indexes(1) self._tracks_widget_right.update_indexes(len(self._tracks_left) + 1) t = Thread(target=update_playlist) t.daemon = True t.start() def _on_size_allocate(self, widget, allocation): """ Change box max/min children @param widget as Gtk.Widget @param allocation as Gtk.Allocation """ if self._width == allocation.width: return self._width = allocation.width redraw = False if allocation.width < WindowSize.MONSTER: self._box.set_property('valign', Gtk.Align.START) orientation = Gtk.Orientation.VERTICAL else: self._box.set_property('valign', Gtk.Align.FILL) orientation = Gtk.Orientation.HORIZONTAL if orientation != self._orientation: self._orientation = orientation redraw = True self._box.set_orientation(orientation) if redraw: for child in self._box.get_children(): self._box.remove(child) GLib.idle_add(self._box.add, self._tracks_widget_left) GLib.idle_add(self._box.add, self._tracks_widget_right) self._update_headers() def _on_activated(self, widget, track_id): """ On track activation, play track @param widget as TracksWidget @param track as Track """ # Add to queue by default if Lp().player.locked or Lp().player.queued: if track_id in Lp().player.get_queue(): Lp().player.del_from_queue(track_id) else: Lp().player.append_to_queue(track_id) else: Lp().player.load(Track(track_id)) if not Lp().player.is_party(): Lp().player.populate_user_playlist_by_tracks( self._tracks_left + self._tracks_right, self._playlist_ids)
class PlaylistsWidget(Gtk.Grid): """ Show playlist tracks/albums """ __gsignals__ = {"populated": (GObject.SignalFlags.RUN_FIRST, None, ())} def __init__(self, playlist_ids): """ Init playlist Widget @param playlist ids as [int] """ Gtk.Grid.__init__(self) self.set_row_spacing(5) self.set_orientation(Gtk.Orientation.VERTICAL) self.__playlist_ids = playlist_ids self.__tracks_left = [] self.__tracks_right = [] self.__width = None self.__orientation = None self.__loading = Loading.NONE # Used to block widget2 populate while showing one column self.__locked_widget_right = True self.__grid = Gtk.Grid() self.__grid.set_vexpand(True) self.__grid.set_column_homogeneous(True) self.__grid.show() self.connect("size-allocate", self.__on_size_allocate) self.__tracks_widget_left = TracksWidget(True) self.__tracks_widget_left.set_vexpand(True) self.__tracks_widget_right = TracksWidget(True) self.__tracks_widget_right.set_vexpand(True) self.__tracks_widget_left.connect("activated", self.__on_activated) self.__tracks_widget_right.connect("activated", self.__on_activated) self.__tracks_widget_left.show() self.__tracks_widget_right.show() self.drag_dest_set(Gtk.DestDefaults.DROP | Gtk.DestDefaults.MOTION, [], Gdk.DragAction.MOVE) self.drag_dest_add_text_targets() self.connect("drag-data-received", self.__on_drag_data_received) self.add(self.__grid) @property def id(self): """ Return playlist widget id @return int """ return Type.PLAYLISTS @property def boxes(self): """ @return [Gtk.ListBox] """ return [self.__tracks_widget_left, self.__tracks_widget_right] def set_filter_func(self, func): """ Set filter function """ self.__tracks_widget_left.set_filter_func(func) self.__tracks_widget_right.set_filter_func(func) def show_overlay(self, bool): """ No overlay here now """ pass def update_state(self): """ No state to update """ pass def update_cover(self): """ No update cover for now """ pass def update_allocation(self): """ Update widget allocation """ self.__width = 0 self.__on_size_allocate(self, self.get_allocation()) def get_current_ordinate(self): """ If current track in widget, return it ordinate, @return y as int """ ordinate = None for child in self.__tracks_widget_left.get_children() + \ self.__tracks_widget_right.get_children(): if child.id == Lp().player.current_track.id: ordinate = child.translate_coordinates(self.__grid, 0, 0)[1] return ordinate def populate_list_left(self, tracks, pos): """ Populate left list @param track"s ids as array of int (not null) @param track position as int @thread safe """ # We reset width here to allow size allocation code to run self.__width = None self.__tracks_left = list(tracks) GLib.idle_add(self.__add_tracks, tracks, self.__tracks_widget_left, pos) def populate_list_right(self, tracks, pos): """ Populate right list @param track"s ids as array of int (not null) @param track position as int @thread safe """ self.__tracks_right = list(tracks) # If we are showing only one column, wait for widget1 if self.__orientation == Gtk.Orientation.VERTICAL and\ self.__locked_widget_right: GLib.timeout_add(100, self.populate_list_right, tracks, pos) else: # We reset width here to allow size allocation code to run self.__width = None GLib.idle_add(self.__add_tracks, tracks, self.__tracks_widget_right, pos) def update_playing_indicator(self): """ Update playing indicator """ self.__tracks_widget_left.update_playing(Lp().player.current_track.id) self.__tracks_widget_right.update_playing(Lp().player.current_track.id) def update_duration(self, track_id): """ Update duration for current track @param track id as int """ self.__tracks_widget_left.update_duration(track_id) self.__tracks_widget_right.update_duration(track_id) def stop(self): """ Stop loading """ self.__loading = Loading.STOP def insert(self, track_id, pos=-1): """ Add track to widget @param track id as int @param pos as int """ children_len = len(self.__tracks_widget_left.get_children() + self.__tracks_widget_right.get_children()) if pos > children_len / 2: widget = self.__tracks_widget_right pos -= len(self.__tracks_widget_left.get_children()) else: widget = self.__tracks_widget_left self.__add_tracks([track_id], widget, pos) self.__update_tracks() self.__update_position() self.__update_headers() self.__tracks_widget_left.update_indexes(1) self.__tracks_widget_right.update_indexes(len(self.__tracks_left) + 1) def remove(self, track_id): """ Del track from widget @param track id as int """ children = self.__tracks_widget_left.get_children() + \ self.__tracks_widget_right.get_children() # Clear the widget if track_id is None: for child in children: child.destroy() self.__update_tracks() else: for child in children: if child.id == track_id: child.destroy() break self.__update_tracks() self.__update_position() self.__update_headers() self.__tracks_widget_left.update_indexes(1) self.__tracks_widget_right.update_indexes( len(self.__tracks_left) + 1) ####################### # PRIVATE # ####################### def __add_tracks(self, tracks, widget, pos, previous_album_id=None): """ Add tracks to list @param tracks id as array of [int] @param widget TracksWidget @param track position as int @param pos as int @param previous album id as int """ if self.__loading == Loading.STOP: self.__loading = Loading.NONE return if not tracks: if widget == self.__tracks_widget_right: self.__loading |= Loading.RIGHT elif widget == self.__tracks_widget_left: self.__loading |= Loading.LEFT if self.__loading == Loading.ALL: self.emit("populated") self.__locked_widget_right = False return track = Track(tracks.pop(0)) row = PlaylistRow(track.id, pos, track.album.id != previous_album_id) row.connect("track-moved", self.__on_track_moved) row.show() widget.insert(row, pos) GLib.idle_add(self.__add_tracks, tracks, widget, pos + 1, track.album.id) def __update_tracks(self): """ Update tracks based on current widget """ # Recalculate tracks self.__tracks_left = [] self.__tracks_right = [] for child in self.__tracks_widget_left.get_children(): self.__tracks_left.append(child.id) for child in self.__tracks_widget_right.get_children(): self.__tracks_right.append(child.id) def __update_position(self): """ Update widget position """ len_tracks1 = len(self.__tracks_left) len_tracks2 = len(self.__tracks_right) # Take first track from tracks2 and put it at the end of tracks1 if len_tracks2 > len_tracks1: src = self.__tracks_right[0] if self.__tracks_left: dst = self.__tracks_left[-1] else: dst = -1 self.__move_track(dst, src, False) # Take last track of tracks1 and put it at the bottom of tracks2 elif len_tracks1 - 1 > len_tracks2: src = self.__tracks_left[-1] if self.__tracks_right: dst = self.__tracks_right[0] else: dst = -1 self.__move_track(dst, src, True) self.__update_tracks() def __update_headers(self): """ Update headers for all tracks """ self.__tracks_widget_left.update_headers() prev_album_id = None if self.__orientation == Gtk.Orientation.VERTICAL: if self.__tracks_left: prev_album_id = Track(self.__tracks_left[-1]).album.id self.__tracks_widget_right.update_headers(prev_album_id) def __show_spinner(self, widget, track_id): """ Show spinner for widget @param widget as TracksWidget @param track id as int """ track = Track(track_id) if track.is_web: widget.show_spinner(track_id) def __move_track(self, dst, src, up): """ Move track from src to row @param dst as int @param src as int @param up as bool @return (dst_widget as TracksWidget, src index as int, dst index as int) """ tracks1_len = len(self.__tracks_left) tracks2_len = len(self.__tracks_right) if src in self.__tracks_left: src_widget = self.__tracks_widget_left src_index = self.__tracks_left.index(src) - 1 else: src_widget = self.__tracks_widget_right src_index = self.__tracks_right.index(src) - 1 if tracks1_len == 0 or dst in self.__tracks_left: dst_widget = self.__tracks_widget_left dst_tracks = self.__tracks_left elif tracks2_len == 0 or dst in self.__tracks_right: dst_widget = self.__tracks_widget_right dst_tracks = self.__tracks_right else: return # Remove src from src_widget for child in src_widget.get_children(): if child.id == src: child.destroy() break src_track = Track(src) prev_track = Track() name = GLib.markup_escape_text(src_track.name) index = 0 # Get previous track if dst != -1: for child in dst_widget.get_children(): if child.id == dst: break index += 1 if not up: index += 1 # Get previous track (in dst context) prev_index = dst_tracks.index(dst) if up: prev_index -= 1 prev_track = Track(dst_tracks[prev_index]) # If we are listening to a compilation, prepend artist name if (src_track.album.artist_id == Type.COMPILATIONS or len(src_track.artist_ids) > 1 or src_track.album.artist_id not in src_track.artist_ids): name = "<b>%s</b>\n%s" % (GLib.markup_escape_text(", ".join( src_track.artists)), name) self.__tracks_left.insert(index, src_track.id) row = PlaylistRow( src_track.id, index, index == 0 or src_track.album.id != prev_track.album.id) row.connect("track-moved", self.__on_track_moved) row.show() dst_widget.insert(row, index) return (src_widget, dst_widget, src_index, index) def __on_drag_data_received(self, widget, context, x, y, data, info, time): """ ONLY HAPPEN IN VERTICAL ORIENTATION Horizontal orientation is handled by TracksWidget @param widget as Gtk.Widget @param context as Gdk.DragContext @param x as int @param y as int @param data as Gtk.SelectionData @param info as int @param time as int """ try: value = int(data.get_text()) try: child = self.__tracks_widget_right.get_children()[-1] except: child = self.__tracks_widget_left.get_children()[-1] self.__on_track_moved(widget, child.id, value, False) except: if len(self.__playlist_ids) == 1: Lp().playlists.import_uri(self.__playlist_ids[0], data.get_text()) def __on_track_moved(self, widget, dst, src, up): """ Move track from src to row Recalculate track position @param widget as TracksWidget @param dst as int @param src as int @param up as bool """ def update_playlist(): # Save playlist in db only if one playlist visible if len(self.__playlist_ids) == 1 and self.__playlist_ids[0] >= 0: Lp().playlists.clear(self.__playlist_ids[0], False) tracks = [] for track_id in self.__tracks_left + self.__tracks_right: tracks.append(Track(track_id)) Lp().playlists.add_tracks(self.__playlist_ids[0], tracks, False) if not (set(self.__playlist_ids) - set(Lp().player.get_user_playlist_ids())): Lp().player.update_user_playlist(self.__tracks_left + self.__tracks_right) (src_widget, dst_widget, src_index, dst_index) = \ self.__move_track(dst, src, up) self.__update_tracks() self.__update_position() self.__update_headers() self.__tracks_widget_left.update_indexes(1) self.__tracks_widget_right.update_indexes(len(self.__tracks_left) + 1) t = Thread(target=update_playlist) t.daemon = True t.start() def __on_size_allocate(self, widget, allocation): """ Change box max/min children @param widget as Gtk.Widget @param allocation as Gtk.Allocation """ if self.__width == allocation.width: return self.__width = allocation.width redraw = False if allocation.width < WindowSize.MONSTER or\ not Lp().settings.get_value("split-view"): self.__grid.set_property("valign", Gtk.Align.START) orientation = Gtk.Orientation.VERTICAL else: self.__grid.set_property("valign", Gtk.Align.FILL) orientation = Gtk.Orientation.HORIZONTAL if orientation != self.__orientation: self.__orientation = orientation redraw = True self.__grid.set_orientation(orientation) if redraw: for child in self.__grid.get_children(): self.__grid.remove(child) GLib.idle_add(self.__grid.add, self.__tracks_widget_left) GLib.idle_add(self.__grid.add, self.__tracks_widget_right) self.__update_headers() def __on_activated(self, widget, track_id): """ On track activation, play track @param widget as TracksWidget @param track as Track """ # Add to queue by default if Lp().player.locked: if track_id in Lp().player.queue: Lp().player.del_from_queue(track_id) else: Lp().player.append_to_queue(track_id) else: self.__show_spinner(widget, track_id) Lp().player.load(Track(track_id)) if not Lp().player.is_party: Lp().player.populate_user_playlist_by_tracks( self.__tracks_left + self.__tracks_right, self.__playlist_ids)