def __on_indicator_button_release_event(self, button, event, box, disc): """ Popup menu for track relative to button @param button as Gtk.Button @param event as Gdk.EventButton @param box as Gtk.Box @param disc as Disc """ def on_hide(widget): button.emit("clicked") from lollypop.widgets_context import ContextWidget context_widget = None for child in box.get_children(): if isinstance(child, ContextWidget): context_widget = child break image = button.get_image() if context_widget is None: image.set_from_icon_name("go-previous-symbolic", Gtk.IconSize.MENU) context_widget = ContextWidget(disc, button) context_widget.connect("hide", on_hide) context_widget.show() box.add(context_widget) else: image.set_from_icon_name("go-next-symbolic", Gtk.IconSize.MENU) context_widget.destroy() return True
class Row(Gtk.ListBoxRow): """ A row """ def __init__(self, rowid, num): """ Init row widgets @param rowid as int @param num as int @param show loved as bool """ # We do not use Gtk.Builder for speed reasons Gtk.ListBoxRow.__init__(self) self._artists_label = None self._track = Track(rowid) self.__number = num self.__preview_timeout_id = None self.__context_timeout_id = None self.__context = None self._indicator = IndicatorWidget(self._track.id) self.set_indicator(Lp().player.current_track.id == self._track.id, utils.is_loved(self._track.id)) self._row_widget = Gtk.EventBox() self._row_widget.connect("button-press-event", self.__on_button_press) self._row_widget.connect("enter-notify-event", self.__on_enter_notify) self._row_widget.connect("leave-notify-event", self.__on_leave_notify) self._grid = Gtk.Grid() self._grid.set_column_spacing(5) self._row_widget.add(self._grid) self._title_label = Gtk.Label.new(self._track.name) self._title_label.set_property("has-tooltip", True) self._title_label.connect("query-tooltip", self.__on_query_tooltip) self._title_label.set_property("hexpand", True) self._title_label.set_property("halign", Gtk.Align.START) self._title_label.set_ellipsize(Pango.EllipsizeMode.END) if self._track.non_album_artists: self._artists_label = Gtk.Label.new( GLib.markup_escape_text(", ".join( self._track.non_album_artists))) self._artists_label.set_use_markup(True) self._artists_label.set_property("has-tooltip", True) self._artists_label.connect("query-tooltip", self.__on_query_tooltip) self._artists_label.set_property("hexpand", True) self._artists_label.set_property("halign", Gtk.Align.END) self._artists_label.set_ellipsize(Pango.EllipsizeMode.END) self._artists_label.set_opacity(0.3) self._artists_label.set_margin_end(5) self._artists_label.show() self._duration_label = Gtk.Label.new( seconds_to_string(self._track.duration)) self._duration_label.get_style_context().add_class("dim-label") self._num_label = Gtk.Label() self._num_label.set_ellipsize(Pango.EllipsizeMode.END) self._num_label.set_property("valign", Gtk.Align.CENTER) self._num_label.set_width_chars(4) self._num_label.get_style_context().add_class("dim-label") self.update_num_label() self.__menu_button = Gtk.Button.new() # Here a hack to make old Gtk version support min-height css attribute # min-height = 24px, borders = 2px, we set directly on stack # min-width = 24px, borders = 2px, padding = 8px self.__menu_button.set_size_request(34, 26) self.__menu_button.set_relief(Gtk.ReliefStyle.NONE) self.__menu_button.get_style_context().add_class("menu-button") self.__menu_button.get_style_context().add_class("track-menu-button") self._grid.add(self._num_label) self._grid.add(self._title_label) if self._artists_label is not None: self._grid.add(self._artists_label) self._grid.add(self._duration_label) self._grid.add(self.__menu_button) self.add(self._row_widget) self.get_style_context().add_class("trackrow") def show_spinner(self): """ Show spinner """ self._indicator.show_spinner() def set_indicator(self, playing, loved): """ Show indicator @param widget name as str @param playing as bool @param loved as bool """ self._indicator.clear() if playing: self.get_style_context().remove_class("trackrow") self.get_style_context().add_class("trackrowplaying") if loved: self._indicator.play_loved() else: self._indicator.play() else: self.get_style_context().remove_class("trackrowplaying") self.get_style_context().add_class("trackrow") if loved and self.__context is None: self._indicator.loved() else: self._indicator.empty() def set_number(self, num): """ Set number @param number as int """ self.__number = num def update_duration(self): """ Update duration for row """ # Get a new track to get new duration (cache) track = Track(self._track.id) self._duration_label.set_text(seconds_to_string(track.duration)) def update_num_label(self): """ Update position label for row """ if Lp().player.track_in_queue(self._track): self._num_label.get_style_context().add_class("queued") pos = Lp().player.get_track_position(self._track.id) self._num_label.set_text(str(pos)) elif self.__number > 0: self._num_label.get_style_context().remove_class("queued") self._num_label.set_text(str(self.__number)) else: self._num_label.get_style_context().remove_class("queued") self._num_label.set_text("") @property def id(self): """ Get object id @return Current id as int """ return self._track.id ####################### # PRIVATE # ####################### def __play_preview(self): """ Play track @param widget as Gtk.Widget """ Lp().player.preview.set_property("uri", self._track.uri) Lp().player.preview.set_state(Gst.State.PLAYING) self.set_indicator(True, False) self.__preview_timeout_id = None def __on_map(self, widget): """ Fix for Gtk < 3.18, if we are in a popover, do not show menu button """ widget = self.get_parent() while widget is not None: if isinstance(widget, Gtk.Popover): break widget = widget.get_parent() if widget is None: self._grid.add(self.__menu_button) self.__menu_button.show() def __on_artist_button_press(self, eventbox, event): """ Go to artist page @param eventbox as Gtk.EventBox @param event as Gdk.EventButton """ Lp().window.show_artists_albums(self._album.artist_ids) return True def __on_enter_notify(self, widget, event): """ Set image on buttons now, speed reason @param widget as Gtk.Widget @param event as Gdk.Event """ if self.__context_timeout_id is not None: GLib.source_remove(self.__context_timeout_id) self.__context_timeout_id = None if Lp().settings.get_value("preview-output").get_string() != "": self.__preview_timeout_id = GLib.timeout_add( 500, self.__play_preview) if self.__menu_button.get_image() is None: image = Gtk.Image.new_from_icon_name("go-previous-symbolic", Gtk.IconSize.MENU) image.set_opacity(0.2) self.__menu_button.set_image(image) self.__menu_button.connect("clicked", self.__on_button_clicked) self._indicator.update_button() def __on_leave_notify(self, widget, event): """ Stop preview @param widget as Gtk.Widget @param event as Gdk.Event """ allocation = widget.get_allocation() if event.x <= 0 or\ event.x >= allocation.width or\ event.y <= 0 or\ event.y >= allocation.height: if self.__context is not None and\ self.__context_timeout_id is None: self.__context_timeout_id = GLib.timeout_add( 1000, self.__on_button_clicked, self.__menu_button) if Lp().settings.get_value("preview-output").get_string() != "": if self.__preview_timeout_id is not None: GLib.source_remove(self.__preview_timeout_id) self.__preview_timeout_id = None self.set_indicator( Lp().player.current_track.id == self._track.id, utils.is_loved(self._track.id)) Lp().player.preview.set_state(Gst.State.NULL) def __on_button_press(self, widget, event): """ Popup menu for track relative to track row @param widget as Gtk.Widget @param event as Gdk.Event """ if self.__context is not None: self.__on_button_clicked(self.__menu_button) if event.button == 3: if GLib.getenv("WAYLAND_DISPLAY") != "" and\ self.get_ancestor(Gtk.Popover) is not None: print("https://bugzilla.gnome.org/show_bug.cgi?id=774148") window = widget.get_window() if window == event.window: self.__popup_menu(widget, event.x, event.y) # Happens when pressing button over menu btn else: self.__on_button_clicked(self.__menu_button) return True elif event.button == 2: if self._track.id in Lp().player.queue: Lp().player.del_from_queue(self._track.id) else: Lp().player.append_to_queue(self._track.id) def __on_button_clicked(self, button): """ Popup menu for track relative to button @param button as Gtk.Button """ self.__context_timeout_id = None image = self.__menu_button.get_image() if self.__context is None: image.set_from_icon_name("go-next-symbolic", Gtk.IconSize.MENU) self.__context = ContextWidget(self._track, button) self.__context.set_property("halign", Gtk.Align.END) self.__context.show() self._duration_label.hide() self._grid.insert_next_to(button, Gtk.PositionType.LEFT) self._grid.attach_next_to(self.__context, button, Gtk.PositionType.LEFT, 1, 1) self.set_indicator(Lp().player.current_track.id == self._track.id, False) else: image.set_from_icon_name("go-previous-symbolic", Gtk.IconSize.MENU) self.__context.destroy() self._duration_label.show() self.__context = None self.set_indicator(Lp().player.current_track.id == self._track.id, utils.is_loved(self._track.id)) def __popup_menu(self, widget, xcoordinate=None, ycoordinate=None): """ Popup menu for track @param widget as Gtk.Button @param xcoordinate as int (or None) @param ycoordinate as int (or None) """ popover = TrackMenuPopover(self._track, TrackMenu(self._track)) if xcoordinate is not None and ycoordinate is not None: rect = widget.get_allocation() rect.x = xcoordinate rect.y = ycoordinate rect.width = rect.height = 1 popover.set_relative_to(widget) popover.set_pointing_to(rect) popover.connect("closed", self.__on_closed) self.get_style_context().add_class("track-menu-selected") popover.show() def __on_closed(self, widget): """ Remove selected style @param widget as Gtk.Popover """ self.get_style_context().remove_class("track-menu-selected") def __on_query_tooltip(self, widget, x, y, keyboard, tooltip): """ Show tooltip if needed @param widget as Gtk.Widget @param x as int @param y as int @param keyboard as bool @param tooltip as Gtk.Tooltip """ text = "" layout = widget.get_layout() label = widget.get_text() if layout.is_ellipsized(): text = "%s" % (GLib.markup_escape_text(label)) widget.set_tooltip_markup(text)
class AlbumDetailedWidget(Gtk.Bin, AlbumWidget): """ Widget with cover and tracks """ __gsignals__ = { "populated": (GObject.SignalFlags.RUN_FIRST, None, ()), "overlayed": (GObject.SignalFlags.RUN_FIRST, None, (bool,)) } def __init__(self, album_id, genre_ids, artist_ids, art_size): """ Init detailed album widget @param album id as int @param genre ids as [int] @param artist ids as [int] @param lazy as LazyLoadingView @param art size as ArtSize """ Gtk.Bin.__init__(self) AlbumWidget.__init__(self, album_id, genre_ids, artist_ids, art_size) self._rounded_class = "rounded-icon-small" self.__width = None self.__context = None # Cover + rating + spacing self.__height = ArtSize.BIG + 26 self.__orientation = None self.__child_height = TrackRow.get_best_height(self) # Header + separator + spacing + margin self.__requested_height = self.__child_height + 6 # Discs to load, will be emptied self.__discs = self._album.discs self.__locked_widget_right = True self.set_property("height-request", self.__height) self.connect("size-allocate", self.__on_size_allocate) builder = Gtk.Builder() builder.add_from_resource("/org/gnome/Lollypop/AlbumDetailedWidget.ui") builder.connect_signals(self) self._widget = builder.get_object("widget") album_info = builder.get_object("albuminfo") title_label = builder.get_object("title") title_label.set_property("has-tooltip", True) artist_label = builder.get_object("artist") artist_label.set_property("has-tooltip", True) year_label = builder.get_object("year") self.__header = builder.get_object("header") self.__overlay = builder.get_object("overlay") self.__duration_label = builder.get_object("duration") self.__context_button = builder.get_object("context") if art_size == ArtSize.NONE: self._cover = None rating = RatingWidget(self._album) rating.set_hexpand(True) rating.set_property("halign", Gtk.Align.END) rating.set_property("valign", Gtk.Align.CENTER) rating.show() self.__header.attach(rating, 4, 0, 1, 1) loved = LovedWidget(self._album) loved.set_property("halign", Gtk.Align.END) loved.set_property("valign", Gtk.Align.CENTER) loved.show() self.__header.attach(loved, 5, 0, 1, 1) artist_label.set_text(", ".join(self._album.artists)) artist_label.show() if self._album.year is not None: year_label.set_label(str(self._album.year)) year_label.show() else: self.__duration_label.set_hexpand(True) builder = Gtk.Builder() builder.add_from_resource("/org/gnome/Lollypop/CoverBox.ui") builder.connect_signals(self) self._play_button = builder.get_object("play-button") self._action_button = builder.get_object("action-button") self._action_event = builder.get_object("action-event") self._cover = builder.get_object("cover") self.__coverbox = builder.get_object("coverbox") # 6 for 2*3px (application.css) self.__coverbox.set_property("width-request", art_size + 6) if art_size == ArtSize.BIG: self._cover.get_style_context().add_class("cover-frame") self._artwork_button = builder.get_object("artwork-button") if self._album.year is not None: year_label.set_label(str(self._album.year)) year_label.show() grid = Gtk.Grid() grid.set_column_spacing(10) grid.set_property("halign", Gtk.Align.CENTER) grid.show() rating = RatingWidget(self._album) loved = LovedWidget(self._album) rating.show() loved.show() grid.add(rating) grid.add(loved) self.__coverbox.add(grid) self._widget.attach(self.__coverbox, 0, 0, 1, 1) if Lp().window.get_view_width() < WindowSize.MEDIUM: self.__coverbox.hide() if len(artist_ids) > 1: artist_label.set_text(", ".join(self._album.artists)) artist_label.show() elif art_size == ArtSize.HEADER: # Here we are working around default CoverBox ui # Do we really need to have another ui file? # So just hack values on the fly self._cover.get_style_context().add_class("small-cover-frame") overlay_grid = builder.get_object("overlay-grid") overlay_grid.set_margin_bottom(2) overlay_grid.set_margin_end(2) overlay_grid.set_column_spacing(0) play_event = builder.get_object("play-event") play_event.set_margin_start(2) play_event.set_margin_bottom(2) album_info.attach(self.__coverbox, 0, 0, 1, 1) artist_label.set_text(", ".join(self._album.artists)) artist_label.show() self.__set_duration() self.__box = Gtk.Grid() self.__box.set_column_homogeneous(True) self.__box.set_property("valign", Gtk.Align.START) self.__box.show() album_info.add(self.__box) self._tracks_left = {} self._tracks_right = {} self.set_cover() self.update_state() title_label.set_label(self._album.name) for disc in self.__discs: self.__add_disc_container(disc.number) self.__set_disc_height(disc) self.add(self._widget) # We start transparent, we switch opaque at size allocation # This prevent artifacts self.set_opacity(0) self._lock_overlay = False def update_playing_indicator(self): """ Update playing indicator """ for disc in self._album.discs: self._tracks_left[disc.number].update_playing( Lp().player.current_track.id) self._tracks_right[disc.number].update_playing( Lp().player.current_track.id) def update_duration(self, track_id): """ Update duration for current track @param track id as int """ for disc in self._album.discs: self._tracks_left[disc.number].update_duration(track_id) self._tracks_right[disc.number].update_duration(track_id) def populate(self): """ Populate tracks @thread safe """ if self.__discs: disc = self.__discs.pop(0) mid_tracks = int(0.5 + len(disc.tracks) / 2) self.populate_list_left(disc.tracks[:mid_tracks], disc, 1) self.populate_list_right(disc.tracks[mid_tracks:], disc, mid_tracks + 1) def is_populated(self): """ Return True if populated @return bool """ return len(self.__discs) == 0 def populate_list_left(self, tracks, disc, pos): """ Populate left list, thread safe @param tracks as [Track] @param disc as Disc @param pos as int """ GLib.idle_add(self.__add_tracks, tracks, self._tracks_left, disc.number, pos) def populate_list_right(self, tracks, disc, pos): """ Populate right list, thread safe @param tracks as [Track] @param disc as Disc @param pos as int """ # 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, disc, pos) else: GLib.idle_add(self.__add_tracks, tracks, self._tracks_right, disc.number, pos) def get_current_ordinate(self, parent): """ If current track in widget, return it ordinate, @param parent widget as Gtk.Widget @return y as int """ for dic in [self._tracks_left, self._tracks_right]: for widget in dic.values(): for child in widget.get_children(): if child.id == Lp().player.current_track.id: return child.translate_coordinates(parent, 0, 0)[1] return None def set_filter_func(self, func): """ Set filter function """ for widget in self._tracks_left.values(): widget.set_filter_func(func) for widget in self._tracks_right.values(): widget.set_filter_func(func) @property def boxes(self): """ @return [Gtk.ListBox] """ boxes = [] for widget in self._tracks_left.values(): boxes.append(widget) for widget in self._tracks_right.values(): boxes.append(widget) return boxes @property def requested_height(self): """ Requested height """ if self.__requested_height < self.__height: return self.__height else: return self.__requested_height ####################### # PROTECTED # ####################### def _on_query_tooltip(self, widget, x, y, keyboard, tooltip): """ Show tooltip if needed @param widget as Gtk.Widget @param x as int @param y as int @param keyboard as bool @param tooltip as Gtk.Tooltip """ layout = widget.get_layout() if layout.is_ellipsized(): tooltip.set_text(widget.get_label()) else: return False return True def _on_context_clicked(self, button): """ Show context widget @param button as Gtk.Button """ image = button.get_image() if self.__context is None: image.set_from_icon_name("go-previous-symbolic", Gtk.IconSize.MENU) self.__context = ContextWidget(self._album, button) self.__context.set_property("halign", Gtk.Align.START) self.__context.set_property("valign", Gtk.Align.CENTER) self.__context.show() self.__header.insert_next_to(button, Gtk.PositionType.RIGHT) self.__header.attach_next_to(self.__context, button, Gtk.PositionType.RIGHT, 1, 1) else: image.set_from_icon_name("go-next-symbolic", Gtk.IconSize.MENU) self.__context.destroy() self.__context = None def _on_album_updated(self, scanner, album_id, destroy): """ On album modified, disable it @param scanner as CollectionScanner @param album id as int @param destroy as bool """ if self._album.id != album_id: return removed = False for dic in [self._tracks_left, self._tracks_right]: for widget in dic.values(): for child in widget.get_children(): track = Track(child.id) if track.album.id == Type.NONE: removed = True if removed: for dic in [self._tracks_left, self._tracks_right]: for widget in dic.values(): for child in widget.get_children(): child.destroy() self.__discs = self._album.discs self.__set_duration() self.populate() AlbumWidget._on_album_updated(self, scanner, album_id, destroy) ####################### # PRIVATE # ####################### def __set_duration(self): """ Set album duration """ duration = Lp().albums.get_duration(self._album.id, self._album.genre_ids) hours = int(duration / 3600) mins = int(duration / 60) if hours > 0: mins -= hours * 60 if mins > 0: self.__duration_label.set_text(_("%s h %s m") % (hours, mins)) else: self.__duration_label.set_text(_("%s h") % hours) else: self.__duration_label.set_text(_("%s m") % mins) def __set_disc_height(self, disc): """ Set disc widget height @param disc as Disc """ count_tracks = len(disc.tracks) mid_tracks = int(0.5 + count_tracks / 2) left_height = self.__child_height * mid_tracks right_height = self.__child_height * (count_tracks - mid_tracks) if left_height > right_height: self.__requested_height += left_height else: self.__requested_height += right_height self._tracks_left[disc.number].set_property("height-request", left_height) self._tracks_right[disc.number].set_property("height-request", right_height) def __add_disc_container(self, disc_number): """ Add disc container to box @param disc_number as int """ self._tracks_left[disc_number] = TracksWidget() self._tracks_right[disc_number] = TracksWidget() self._tracks_left[disc_number].connect("activated", self.__on_activated) self._tracks_right[disc_number].connect("activated", self.__on_activated) self._tracks_left[disc_number].show() self._tracks_right[disc_number].show() def __pop_menu(self, widget): """ Popup menu for album @param widget as Gtk.Button @param album id as int """ ancestor = self.get_ancestor(Gtk.Popover) # Get album real genre ids (not contextual) popover = Gtk.Popover.new_from_model(widget, AlbumMenu(self._album, ancestor is not None)) if ancestor is not None: Lp().window.view.show_popover(popover) else: popover.connect("closed", self.__on_pop_menu_closed) self.get_style_context().add_class("album-menu-selected") popover.show() def __add_tracks(self, tracks, widget, disc_number, i): """ Add tracks for to tracks widget @param tracks as [int] @param widget as TracksWidget @param disc number as int @param i as int """ if self._loading == Loading.STOP: self._loading = Loading.NONE return if not tracks: if widget == self._tracks_right: self._loading |= Loading.RIGHT elif widget == self._tracks_left: self._loading |= Loading.LEFT if self._loading == Loading.ALL: self.emit("populated") self.__locked_widget_right = False return track = tracks.pop(0) if not Lp().settings.get_value("show-tag-tracknumber"): track_number = i else: track_number = track.number row = TrackRow(track.id, track_number, self._artist_ids) row.show() widget[disc_number].add(row) GLib.idle_add(self.__add_tracks, tracks, widget, disc_number, i + 1) 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 # We want vertical orientation # when not enought place for cover or tracks if allocation.width < WindowSize.MEDIUM or ( allocation.width < WindowSize.MONSTER and self._art_size == ArtSize.BIG): orientation = Gtk.Orientation.VERTICAL else: orientation = Gtk.Orientation.HORIZONTAL if orientation != self.__orientation: self.__orientation = orientation redraw = True if redraw: for child in self.__box.get_children(): self.__box.remove(child) # Grid index idx = 0 # Disc label width / right box position if orientation == Gtk.Orientation.VERTICAL: width = 1 pos = 0 else: width = 2 pos = 1 for disc in self._album.discs: show_label = len(self._album.discs) > 1 if show_label: label = Gtk.Label() disc_text = _("Disc %s") % disc.number disc_names = self._album.disc_names(disc.number) if disc_names: disc_text += ": " + ", ".join(disc_names) label.set_text(disc_text) label.set_property("halign", Gtk.Align.START) label.get_style_context().add_class("dim-label") label.show() eventbox = Gtk.EventBox() eventbox.add(label) eventbox.connect("realize", self.__on_disc_label_realize) eventbox.connect("button-press-event", self.__on_disc_press_event, disc.number) eventbox.show() self.__box.attach(eventbox, 0, idx, width, 1) idx += 1 GLib.idle_add(self.__box.attach, self._tracks_left[disc.number], 0, idx, 1, 1) if orientation == Gtk.Orientation.VERTICAL: idx += 1 GLib.idle_add(self.__box.attach, self._tracks_right[disc.number], pos, idx, 1, 1) idx += 1 GLib.idle_add(self.set_opacity, 1) if self._art_size == ArtSize.BIG: if allocation.width < WindowSize.MEDIUM: self.__coverbox.hide() else: self.__coverbox.show() def __on_disc_label_realize(self, eventbox): """ Set mouse cursor @param eventbox as Gtk.EventBox """ eventbox.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2)) def __on_disc_press_event(self, eventbox, event, idx): """ Add/Remove disc to/from queue @param eventbox as Gtk.EventBox @param event as Gdk.Event @param idx as int """ disc = None for d in self._album.discs: if d.number == idx: disc = d break if disc is None: return for track in disc.tracks: if Lp().player.track_in_queue(track): Lp().player.del_from_queue(track.id, False) else: Lp().player.append_to_queue(track.id, False) Lp().player.emit("queue-changed") def __on_pop_menu_closed(self, widget): """ Remove selected style @param widget as Gtk.Popover """ self.get_style_context().remove_class("album-menu-selected") def __on_activated(self, widget, track_id): """ On track activation, play track @param widget as TracksWidget @param track id as int """ # 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: # Do not update album list if not Lp().player.is_party and not\ Lp().settings.get_enum("playback") == NextContext.STOP: # If in artist view, reset album list if self._artist_ids: Lp().player.set_albums(track_id, self._artist_ids, self._album.genre_ids) # Else, add album if missing elif not Lp().player.has_album(self._album): Lp().player.add_album(self._album) # Clear albums if user clicked on a track from a new album elif Lp().settings.get_enum("playback") == NextContext.STOP: if not Lp().player.has_album(self._album): Lp().player.clear_albums() track = Track(track_id) Lp().player.load(track)
class Row(Gtk.ListBoxRow): """ A row """ def __init__(self, track, list_type): """ Init row widgets @param track as Track @param list_type as RowListType """ # We do not use Gtk.Builder for speed reasons Gtk.ListBoxRow.__init__(self) self._list_type = list_type self._artists_label = None self._track = track self.__preview_timeout_id = None self.__context_timeout_id = None self.__context = None self._indicator = IndicatorWidget(self, list_type) # We do not use set_indicator() here, we do not want widget to be # populated if App().player.current_track.id == self._track.id: self._indicator.play() elif self._track.loved: self._indicator.loved(self._track.loved) self._row_widget = Gtk.EventBox() self._row_widget.connect("destroy", self._on_destroy) self._row_widget.connect("button-release-event", self.__on_button_release_event) self._row_widget.connect("enter-notify-event", self.__on_enter_notify_event) self._row_widget.connect("leave-notify-event", self.__on_leave_notify_event) self._grid = Gtk.Grid() self._grid.set_column_spacing(5) self._row_widget.add(self._grid) self._title_label = Gtk.Label.new(self._track.name) self._title_label.set_property("has-tooltip", True) self._title_label.connect("query-tooltip", self.__on_query_tooltip) self._title_label.set_property("hexpand", True) self._title_label.set_property("halign", Gtk.Align.START) self._title_label.set_property("xalign", 0) self._title_label.set_ellipsize(Pango.EllipsizeMode.END) featuring_artist_ids = track.featuring_artist_ids if featuring_artist_ids: artists = [] for artist_id in featuring_artist_ids: artists.append(App().artists.get_name(artist_id)) self._artists_label = Gtk.Label.new(GLib.markup_escape_text( ", ".join(artists))) self._artists_label.set_use_markup(True) self._artists_label.set_property("has-tooltip", True) self._artists_label.connect("query-tooltip", self.__on_query_tooltip) self._artists_label.set_property("hexpand", True) self._artists_label.set_property("halign", Gtk.Align.END) self._artists_label.set_ellipsize(Pango.EllipsizeMode.END) self._artists_label.set_opacity(0.3) self._artists_label.set_margin_end(5) self._artists_label.show() self._duration_label = Gtk.Label.new( seconds_to_string(self._track.duration)) self._duration_label.get_style_context().add_class("dim-label") self._num_label = Gtk.Label() self._num_label.set_ellipsize(Pango.EllipsizeMode.END) self._num_label.set_property("valign", Gtk.Align.CENTER) self._num_label.set_width_chars(4) self._num_label.get_style_context().add_class("dim-label") self.update_number_label() self.__menu_button = Gtk.Button.new() self.__menu_button.set_relief(Gtk.ReliefStyle.NONE) self.__menu_button.get_style_context().add_class("menu-button") self.__menu_button.get_style_context().add_class("track-menu-button") if list_type & (RowListType.READ_ONLY | RowListType.POPOVER): self.__menu_button.set_opacity(0) self.__menu_button.set_sensitive(False) self._grid.add(self._num_label) self._grid.add(self._title_label) if self._artists_label is not None: self._grid.add(self._artists_label) self._grid.add(self._duration_label) self._grid.add(self.__menu_button) self.add(self._row_widget) self.get_style_context().add_class("trackrow") def set_indicator(self, playing, loved): """ Show indicator @param widget name as str @param playing as bool @param loved as bool """ self._indicator.clear() if playing: self.get_style_context().remove_class("trackrow") self.get_style_context().add_class("trackrowplaying") if loved == 1: self._indicator.play_loved() else: self._indicator.play() else: self.get_style_context().remove_class("trackrowplaying") self.get_style_context().add_class("trackrow") if loved != 0 and self.__context is None: self._indicator.loved(loved) else: self._indicator.button() def update_number_label(self): """ Update position label for row """ if App().player.track_in_queue(self._track): self._num_label.get_style_context().add_class("queued") pos = App().player.get_track_position(self._track.id) self._num_label.set_text(str(pos)) elif self._track.number > 0: self._num_label.get_style_context().remove_class("queued") self._num_label.set_text(str(self._track.number)) else: self._num_label.get_style_context().remove_class("queued") self._num_label.set_text("") @property def row_widget(self): """ Get row main widget @return Gtk.Widget """ return self._row_widget @property def track(self): """ Get row track @return Track """ return self._track ####################### # PROTECTED # ####################### def _get_menu(self): """ Return TrackMenu """ from lollypop.pop_menu import TrackMenu return TrackMenu(self._track) def _on_destroy(self, widget): """ We need to stop timeout idle to prevent __on_indicator_button_release_event() segfaulting """ if self.__context_timeout_id is not None: GLib.source_remove(self.__context_timeout_id) self.__context_timeout_id = None ####################### # PRIVATE # ####################### def __play_preview(self): """ Play track @param widget as Gtk.Widget """ App().player.preview.set_property("uri", self._track.uri) App().player.preview.set_state(Gst.State.PLAYING) self.set_indicator(True, False) self.__preview_timeout_id = None def __popup_menu(self, eventbox, xcoordinate=None, ycoordinate=None): """ Popup menu for track @param eventbox as Gtk.EventBox @param xcoordinate as int (or None) @param ycoordinate as int (or None) """ from lollypop.pop_menu import TrackMenuPopover, RemoveMenuPopover if self.get_state_flags() & Gtk.StateFlags.SELECTED: # Get all selected rows rows = [self] r = self.previous_row while r is not None: if r.get_state_flags() & Gtk.StateFlags.SELECTED: rows.append(r) r = r.previous_row r = self.next_row while r is not None: if r.get_state_flags() & Gtk.StateFlags.SELECTED: rows.append(r) r = r.next_row popover = RemoveMenuPopover(rows) else: popover = TrackMenuPopover(self._track, self._get_menu()) if xcoordinate is not None and ycoordinate is not None: rect = eventbox.get_allocation() rect.x = xcoordinate rect.y = ycoordinate rect.width = rect.height = 1 popover.set_relative_to(eventbox) popover.set_pointing_to(rect) popover.connect("closed", self.__on_closed) self.get_style_context().add_class("track-menu-selected") popover.popup() def __on_artist_button_press(self, eventbox, event): """ Go to artist page @param eventbox as Gtk.EventBox @param event as Gdk.EventButton """ App().window.container.show_artists_albums(self._album.artist_ids) return True def __on_enter_notify_event(self, widget, event): """ Set image on buttons now, speed reason @param widget as Gtk.Widget @param event as Gdk.Event """ if self.__context_timeout_id is not None: GLib.source_remove(self.__context_timeout_id) self.__context_timeout_id = None if App().settings.get_value("preview-output").get_string() != "": self.__preview_timeout_id = GLib.timeout_add(500, self.__play_preview) if self.__menu_button.get_image() is None: image = Gtk.Image.new_from_icon_name("go-previous-symbolic", Gtk.IconSize.MENU) self.__menu_button.set_image(image) self.__menu_button.connect( "button-release-event", self.__on_indicator_button_release_event) self._indicator.button() def __on_leave_notify_event(self, widget, event): """ Stop preview @param widget as Gtk.Widget @param event as Gdk.Event """ def close_indicator(): """ Simulate a release event """ self.__on_indicator_button_release_event(self.__menu_button, event) allocation = widget.get_allocation() if event.x <= 0 or\ event.x >= allocation.width or\ event.y <= 0 or\ event.y >= allocation.height: if self.__context is not None and\ self.__context_timeout_id is None: self.__context_timeout_id = GLib.timeout_add( 1000, close_indicator) if App().settings.get_value("preview-output").get_string() != "": if self.__preview_timeout_id is not None: GLib.source_remove(self.__preview_timeout_id) self.__preview_timeout_id = None App().player.preview.set_state(Gst.State.NULL) self.set_indicator(App().player.current_track.id == self._track.id, self._track.loved) def __on_button_release_event(self, widget, event): """ Handle button press event: |_ 1 => activate |_ 2 => queue |_ 3 => menu @param widget as Gtk.Widget @param event as Gdk.EventButton """ if event.state & Gdk.ModifierType.CONTROL_MASK and\ self._list_type & RowListType.DND: if self.get_state_flags() & Gtk.StateFlags.SELECTED: self.set_state_flags(Gtk.StateFlags.NORMAL, True) else: self.set_state_flags(Gtk.StateFlags.SELECTED, True) self.grab_focus() elif event.state & Gdk.ModifierType.SHIFT_MASK and\ self._list_type & RowListType.DND: self.emit("do-selection") elif event.button == 3: window = widget.get_window() if window == event.window: self.__popup_menu(widget, event.x, event.y) # Happens when pressing button over menu btn else: self.__on_indicator_button_release_event(self.__menu_button, event) elif event.button == 2: if self._track.id in App().player.queue: App().player.remove_from_queue(self._track.id) else: App().player.append_to_queue(self._track.id) elif event.state & Gdk.ModifierType.MOD1_MASK: App().player.clear_albums() App().player.reset_history() App().player.load(self._track) else: self.activate() return True def __on_indicator_button_release_event(self, button, event): """ Popup menu for track relative to button @param button as Gtk.Button @param event as Gdk.EventButton """ def on_hide(widget): self.__on_indicator_button_release_event(button, event) self.__context_timeout_id = None image = self.__menu_button.get_image() if self.__context is None: image.set_from_icon_name("go-next-symbolic", Gtk.IconSize.MENU) self.__context = ContextWidget(self._track, button) self.__context.connect("hide", on_hide) self.__context.set_property("halign", Gtk.Align.END) self.__context.show() self._duration_label.hide() self._grid.insert_next_to(button, Gtk.PositionType.LEFT) self._grid.attach_next_to(self.__context, button, Gtk.PositionType.LEFT, 1, 1) self.set_indicator(App().player.current_track.id == self._track.id, False) else: image.set_from_icon_name("go-previous-symbolic", Gtk.IconSize.MENU) self.__context.destroy() self._duration_label.show() self.__context = None self.set_indicator(App().player.current_track.id == self._track.id, self._track.loved) return True def __on_closed(self, widget): """ Remove selected style @param widget as Popover """ self.get_style_context().remove_class("track-menu-selected") def __on_query_tooltip(self, widget, x, y, keyboard, tooltip): """ Show tooltip if needed @param widget as Gtk.Widget @param x as int @param y as int @param keyboard as bool @param tooltip as Gtk.Tooltip """ text = "" layout = widget.get_layout() label = widget.get_text() if layout.is_ellipsized(): text = "%s" % (GLib.markup_escape_text(label)) widget.set_tooltip_markup(text)
class AlbumDetailedWidget(Gtk.Bin, AlbumWidget): """ Widget with cover and tracks """ __gsignals__ = { 'populated': (GObject.SignalFlags.RUN_FIRST, None, ()), 'overlayed': (GObject.SignalFlags.RUN_FIRST, None, (bool,)) } def __init__(self, album_id, genre_ids, artist_ids, art_size): """ Init detailed album widget @param album id as int @param genre ids as [int] @param artist ids as [int] @param lazy as LazyLoadingView @param art size as ArtSize """ Gtk.Bin.__init__(self) AlbumWidget.__init__(self, album_id, genre_ids, artist_ids, art_size) self._album.set_artists(artist_ids) self.__width = None self.__context = None # Cover + rating + spacing self.__height = ArtSize.BIG + 26 self.__orientation = None self.__child_height = TrackRow.get_best_height(self) # Header + separator + spacing + margin self.__requested_height = self.__child_height + 6 # Discs to load, will be emptied self.__discs = self._album.discs self.__locked_widget_right = True self.set_property('height-request', self.__height) self.connect('size-allocate', self.__on_size_allocate) builder = Gtk.Builder() builder.add_from_resource('/org/gnome/Lollypop/AlbumDetailedWidget.ui') builder.connect_signals(self) self._widget = builder.get_object('widget') album_info = builder.get_object('albuminfo') title_label = builder.get_object('title') title_label.set_property('has-tooltip', True) artist_label = builder.get_object('artist') artist_label.set_property('has-tooltip', True) year_label = builder.get_object('year') self.__header = builder.get_object('header') self.__overlay = builder.get_object('overlay') self.__duration_label = builder.get_object('duration') self.__context_button = builder.get_object('context') if art_size == ArtSize.NONE: self._cover = None rating = RatingWidget(self._album) rating.set_hexpand(True) rating.set_property('halign', Gtk.Align.END) rating.set_property('valign', Gtk.Align.CENTER) rating.show() self.__header.attach(rating, 4, 0, 1, 1) loved = LovedWidget(self._album) loved.set_property('halign', Gtk.Align.END) loved.set_property('valign', Gtk.Align.CENTER) loved.show() self.__header.attach(loved, 5, 0, 1, 1) artist_label.set_text(", ".join(self._album.artists)) artist_label.show() if self._album.year: year_label.set_label(self._album.year) year_label.show() else: self.__duration_label.set_hexpand(True) builder = Gtk.Builder() builder.add_from_resource('/org/gnome/Lollypop/CoverBox.ui') builder.connect_signals(self) self._play_button = builder.get_object('play-button') self._action_button = builder.get_object('action-button') self._action_event = builder.get_object('action-event') self._cover = builder.get_object('cover') self.__coverbox = builder.get_object('coverbox') # 6 for 2*3px (application.css) self.__coverbox.set_property('width-request', art_size + 6) if art_size == ArtSize.BIG: self._cover.get_style_context().add_class('cover-frame') self._artwork_button = builder.get_object('artwork-button') if self._album.year: year_label.set_label(self._album.year) year_label.show() grid = Gtk.Grid() grid.set_column_spacing(10) grid.set_property('halign', Gtk.Align.CENTER) grid.show() rating = RatingWidget(self._album) loved = LovedWidget(self._album) rating.show() loved.show() grid.add(rating) grid.add(loved) self.__coverbox.add(grid) self._widget.attach(self.__coverbox, 0, 0, 1, 1) if Lp().window.get_view_width() < WindowSize.MEDIUM: self.__coverbox.hide() if len(artist_ids) > 1: artist_label.set_text(", ".join(self._album.artists)) artist_label.show() elif art_size == ArtSize.HEADER: # Here we are working around default CoverBox ui # Do we really need to have another ui file? # So just hack values on the fly self._cover.set_halign(Gtk.Align.CENTER) self._cover.get_style_context().add_class('small-cover-frame') self.__coverbox.set_margin_bottom(5) # We want a smaller button, so reload image self._rounded_class = "rounded-icon-small" self._play_button.set_from_icon_name( "media-playback-start-symbolic", Gtk.IconSize.MENU) overlay_grid = builder.get_object('overlay-grid') overlay_grid.set_margin_bottom(2) overlay_grid.set_margin_end(2) overlay_grid.set_column_spacing(0) self._play_button.set_margin_start(2) self._play_button.set_margin_bottom(2) play_event = builder.get_object('play-event') play_event.set_property('halign', Gtk.Align.START) play_event.set_property('valign', Gtk.Align.END) album_info.attach(self.__coverbox, 0, 0, 1, 1) artist_label.set_text(", ".join(self._album.artists)) artist_label.show() self.__set_duration() self.__box = Gtk.Grid() self.__box.set_column_homogeneous(True) self.__box.set_property('valign', Gtk.Align.START) self.__box.show() album_info.add(self.__box) self._tracks_left = {} self._tracks_right = {} self.set_cover() self.update_state() title_label.set_label(self._album.name) for disc in self.__discs: self.__add_disc_container(disc.number) self.__set_disc_height(disc) self.add(self._widget) # We start transparent, we switch opaque at size allocation # This prevent artifacts self.set_opacity(0) if self._album.is_web and self._cover is not None: self._cover.get_style_context().add_class( 'cover-frame-web') def update_playing_indicator(self): """ Update playing indicator """ for disc in self._album.discs: self._tracks_left[disc.number].update_playing( Lp().player.current_track.id) self._tracks_right[disc.number].update_playing( Lp().player.current_track.id) def update_duration(self, track_id): """ Update duration for current track @param track id as int """ for disc in self._album.discs: self._tracks_left[disc.number].update_duration(track_id) self._tracks_right[disc.number].update_duration(track_id) def populate(self): """ Populate tracks @thread safe """ if self.__discs: disc = self.__discs.pop(0) mid_tracks = int(0.5 + len(disc.tracks) / 2) self.populate_list_left(disc.tracks[:mid_tracks], disc, 1) self.populate_list_right(disc.tracks[mid_tracks:], disc, mid_tracks + 1) def is_populated(self): """ Return True if populated @return bool """ return len(self.__discs) == 0 def populate_list_left(self, tracks, disc, pos): """ Populate left list, thread safe @param tracks as [Track] @param disc as Disc @param pos as int """ GLib.idle_add(self.__add_tracks, tracks, self._tracks_left, disc.number, pos) def populate_list_right(self, tracks, disc, pos): """ Populate right list, thread safe @param tracks as [Track] @param disc as Disc @param pos as int """ # 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, disc, pos) else: GLib.idle_add(self.__add_tracks, tracks, self._tracks_right, disc.number, pos) def get_current_ordinate(self, parent): """ If current track in widget, return it ordinate, @param parent widget as Gtk.Widget @return y as int """ for dic in [self._tracks_left, self._tracks_right]: for widget in dic.values(): for child in widget.get_children(): if child.id == Lp().player.current_track.id: return child.translate_coordinates(parent, 0, 0)[1] return None def set_filter_func(self, func): """ Set filter function """ for widget in self._tracks_left.values(): widget.set_filter_func(func) for widget in self._tracks_right.values(): widget.set_filter_func(func) @property def boxes(self): """ @return [Gtk.ListBox] """ boxes = [] for widget in self._tracks_left.values(): boxes.append(widget) for widget in self._tracks_right.values(): boxes.append(widget) return boxes @property def requested_height(self): """ Requested height """ if self.__requested_height < self.__height: return self.__height else: return self.__requested_height ####################### # PROTECTED # ####################### def _on_query_tooltip(self, widget, x, y, keyboard, tooltip): """ Show tooltip if needed @param widget as Gtk.Widget @param x as int @param y as int @param keyboard as bool @param tooltip as Gtk.Tooltip """ layout = widget.get_layout() if layout.is_ellipsized(): tooltip.set_text(widget.get_label()) else: return False return True def _on_context_clicked(self, button): """ Show context widget @param button as Gtk.Button """ image = button.get_image() if self.__context is None: image.set_from_icon_name('go-previous-symbolic', Gtk.IconSize.MENU) self.__context = ContextWidget(self._album, button) self.__context.set_property('halign', Gtk.Align.START) self.__context.set_property('valign', Gtk.Align.CENTER) self.__context.show() self.__header.insert_next_to(button, Gtk.PositionType.RIGHT) self.__header.attach_next_to(self.__context, button, Gtk.PositionType.RIGHT, 1, 1) else: image.set_from_icon_name('go-next-symbolic', Gtk.IconSize.MENU) self.__context.destroy() self.__context = None def _on_album_updated(self, scanner, album_id, destroy): """ On album modified, disable it @param scanner as CollectionScanner @param album id as int @param destroy as bool """ if self._album.id != album_id: return removed = False for dic in [self._tracks_left, self._tracks_right]: for widget in dic.values(): for child in widget.get_children(): track = Track(child.id) if track.album.id == Type.NONE: removed = True if removed: for dic in [self._tracks_left, self._tracks_right]: for widget in dic.values(): for child in widget.get_children(): child.destroy() self.__discs = self._album.discs self.__set_duration() self.populate() AlbumWidget._on_album_updated(self, scanner, album_id, destroy) ####################### # PRIVATE # ####################### def __set_duration(self): """ Set album duration """ duration = Lp().albums.get_duration(self._album.id, self._album.genre_ids) hours = int(duration / 3600) mins = int(duration / 60) if hours > 0: mins -= hours * 60 if mins > 0: self.__duration_label.set_text(_("%s h %s m") % (hours, mins)) else: self.__duration_label.set_text(_("%s h") % hours) else: self.__duration_label.set_text(_("%s m") % mins) def __set_disc_height(self, disc): """ Set disc widget height @param disc as Disc """ count_tracks = len(disc.tracks) mid_tracks = int(0.5 + count_tracks / 2) left_height = self.__child_height * mid_tracks right_height = self.__child_height * (count_tracks - mid_tracks) if left_height > right_height: self.__requested_height += left_height else: self.__requested_height += right_height self._tracks_left[disc.number].set_property('height-request', left_height) self._tracks_right[disc.number].set_property('height-request', right_height) def __add_disc_container(self, disc_number): """ Add disc container to box @param disc_number as int """ self._tracks_left[disc_number] = TracksWidget() self._tracks_right[disc_number] = TracksWidget() self._tracks_left[disc_number].connect('activated', self.__on_activated) self._tracks_right[disc_number].connect('activated', self.__on_activated) self._tracks_left[disc_number].show() self._tracks_right[disc_number].show() def __pop_menu(self, widget): """ Popup menu for album @param widget as Gtk.Button @param album id as int """ ancestor = self.get_ancestor(Gtk.Popover) # Get album real genre ids (not contextual) genre_ids = Lp().albums.get_genre_ids(self._album.id) if genre_ids and genre_ids[0] == Type.CHARTS: popover = AlbumMenuPopover(self._album, None) popover.set_relative_to(widget) popover.set_position(Gtk.PositionType.BOTTOM) elif self._album.is_web: popover = AlbumMenuPopover(self._album, AlbumMenu(self._album, ancestor is not None)) popover.set_relative_to(widget) else: popover = Gtk.Popover.new_from_model( widget, AlbumMenu(self._album, ancestor is not None)) if ancestor is not None: Lp().window.view.show_popover(popover) else: popover.connect('closed', self.__on_pop_menu_closed) self.get_style_context().add_class('album-menu-selected') popover.show() def __add_tracks(self, tracks, widget, disc_number, i): """ Add tracks for to tracks widget @param tracks as [int] @param widget as TracksWidget @param disc number as int @param i as int """ if self._loading == Loading.STOP: self._loading = Loading.NONE return if not tracks: if widget == self._tracks_right: self._loading |= Loading.RIGHT elif widget == self._tracks_left: self._loading |= Loading.LEFT if self._loading == Loading.ALL: self.emit('populated') self.__locked_widget_right = False return track = tracks.pop(0) if not Lp().settings.get_value('show-tag-tracknumber'): track_number = i else: track_number = track.number row = TrackRow(track.id, track_number) row.show() widget[disc_number].add(row) GLib.idle_add(self.__add_tracks, tracks, widget, disc_number, i + 1) 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 __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 # We want vertical orientation # when not enought place for cover or tracks if allocation.width < WindowSize.MEDIUM or ( allocation.width < WindowSize.MONSTER and self._art_size == ArtSize.BIG): orientation = Gtk.Orientation.VERTICAL else: orientation = Gtk.Orientation.HORIZONTAL if orientation != self.__orientation: self.__orientation = orientation redraw = True if redraw: for child in self.__box.get_children(): self.__box.remove(child) # Grid index idx = 0 # Disc label width / right box position if orientation == Gtk.Orientation.VERTICAL: width = 1 pos = 0 else: width = 2 pos = 1 for disc in self._album.discs: show_label = len(self._album.discs) > 1 if show_label: label = Gtk.Label() disc_text = _("Disc %s") % disc.number disc_names = self._album.disc_names(disc.number) if disc_names: disc_text += ": " + ", ".join(disc_names) label.set_text(disc_text) label.set_property('halign', Gtk.Align.START) label.get_style_context().add_class('dim-label') label.show() eventbox = Gtk.EventBox() eventbox.add(label) eventbox.connect('realize', self.__on_disc_label_realize) eventbox.connect('button-press-event', self.__on_disc_press_event, disc.number) eventbox.show() self.__box.attach(eventbox, 0, idx, width, 1) idx += 1 GLib.idle_add(self.__box.attach, self._tracks_left[disc.number], 0, idx, 1, 1) if orientation == Gtk.Orientation.VERTICAL: idx += 1 GLib.idle_add(self.__box.attach, self._tracks_right[disc.number], pos, idx, 1, 1) idx += 1 GLib.idle_add(self.set_opacity, 1) if self._art_size == ArtSize.BIG: if allocation.width < WindowSize.MEDIUM: self.__coverbox.hide() else: self.__coverbox.show() def __on_disc_label_realize(self, eventbox): """ Set mouse cursor @param eventbox as Gtk.EventBox """ eventbox.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2)) def __on_disc_press_event(self, eventbox, event, idx): """ Add/Remove disc to/from queue @param eventbox as Gtk.EventBox @param event as Gdk.Event @param idx as int """ disc = None for d in self._album.discs: if d.number == idx: disc = d break if disc is None: return for track in disc.tracks: if Lp().player.track_in_queue(track): Lp().player.del_from_queue(track.id, False) else: Lp().player.append_to_queue(track.id, False) Lp().player.emit('queue-changed') def __on_pop_menu_closed(self, widget): """ Remove selected style @param widget as Gtk.Popover """ self.get_style_context().remove_class('album-menu-selected') def __on_activated(self, widget, track_id): """ On track activation, play track @param widget as TracksWidget @param track id as int """ # Add to queue by default if Lp().player.locked: if track_id in Lp().player.get_queue(): Lp().player.del_from_queue(track_id) else: Lp().player.append_to_queue(track_id) else: # Do not update album list if not Lp().player.is_party and not\ Lp().settings.get_enum('playback') == NextContext.STOP: # If in artist view, reset album list if self._filter_ids and Type.CHARTS not in self._filter_ids: Lp().player.set_albums(track_id, self._filter_ids, self._album.genre_ids) # Else, add album if missing elif not Lp().player.has_album(self._album): Lp().player.add_album(self._album) # Clear albums if user clicked on a track from a new album elif Lp().settings.get_enum('playback') == NextContext.STOP: if not Lp().player.has_album(self._album): Lp().player.clear_albums() self.__show_spinner(widget, track_id) track = Track(track_id) Lp().player.load(track)
class AlbumDetailedWidget(Gtk.Bin, AlbumWidget, OverlayAlbumHelper, TracksView): """ Widget with cover and tracks """ __gsignals__ = { "populated": (GObject.SignalFlags.RUN_FIRST, None, ()), "left-loaded": (GObject.SignalFlags.RUN_FIRST, None, ()), "overlayed": (GObject.SignalFlags.RUN_FIRST, None, (bool,)) } def __init__(self, album, genre_ids, artist_ids, show_cover): """ Init detailed album widget @param album as Album @param label_height as int @param genre ids as [int] @param artist ids as [int] @param show_cover as bool """ Gtk.Bin.__init__(self) AlbumWidget.__init__(self, album, genre_ids, artist_ids) TracksView.__init__(self, RowListType.TWO_COLUMNS) self._widget = None self.__show_cover = show_cover self.__width_allocation = 0 self.connect("size-allocate", self.__on_size_allocate) def populate(self): """ Populate widget content """ if self._widget is None: OverlayAlbumHelper.__init__(self) self.__context = None grid = Gtk.Grid() grid.set_margin_start(5) grid.set_margin_end(5) grid.set_row_spacing(1) grid.set_vexpand(True) grid.show() self.__title_label = Gtk.Label() self.__title_label.set_margin_end(10) self.__title_label.set_ellipsize(Pango.EllipsizeMode.END) self.__title_label.get_style_context().add_class("dim-label") self.__title_label.set_property("has-tooltip", True) self.__title_label.connect("query-tooltip", self.__on_query_tooltip) self.__title_label.show() self.__artist_label = Gtk.Label() self.__artist_label.set_margin_end(10) self.__artist_label.set_ellipsize(Pango.EllipsizeMode.END) self.__artist_label.set_property("has-tooltip", True) self.__artist_label.connect("query-tooltip", self.__on_query_tooltip) self.__artist_label.show() self.__year_label = Gtk.Label() self.__year_label.set_margin_end(10) self.__year_label.get_style_context().add_class("dim-label") self.__year_label.show() self.__duration_label = Gtk.Label() self.__duration_label.get_style_context().add_class("dim-label") self.__duration_label.show() self.__context_button = Gtk.Button.new_from_icon_name( "go-next-symbolic", Gtk.IconSize.BUTTON) self.__context_button.set_relief(Gtk.ReliefStyle.NONE) self.__context_button.connect("clicked", self.__on_context_clicked) self.__context_button.get_style_context().add_class("menu-button") self.__context_button.get_style_context().add_class( "album-menu-button") self.__context_button.show() self._widget = Gtk.Grid() self._widget.set_orientation(Gtk.Orientation.VERTICAL) self._widget.set_row_spacing(2) self._widget.show() self.__header = Gtk.Grid() self.__header.add(self.__artist_label) self.__header.add(self.__title_label) self.__header.add(self.__year_label) self.__header.add(self.__context_button) self.__header.show() self._widget.add(self.__header) separator = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL) separator.show() self._widget.add(separator) loved = LovedWidget(self._album) loved.set_property("valign", Gtk.Align.CENTER) loved.set_margin_end(10) loved.show() rating = RatingWidget(self._album) rating.set_property("valign", Gtk.Align.CENTER) rating.set_property("halign", Gtk.Align.END) rating.set_margin_end(10) rating.show() if self.__show_cover: self.__header.add(self.__duration_label) self.__duration_label.set_hexpand(True) self.__duration_label.set_property("halign", Gtk.Align.END) eventbox = Gtk.EventBox() eventbox.connect("enter-notify-event", self._on_enter_notify) eventbox.connect("leave-notify-event", self._on_leave_notify) eventbox.connect("button-press-event", self._on_button_press) eventbox.show() self.set_property("valign", Gtk.Align.CENTER) self._artwork = App().art_helper.get_image(ArtSize.BIG, ArtSize.BIG, "cover-frame") self._artwork.show() eventbox.add(self._artwork) self.__duration_label.set_hexpand(True) self._overlay = Gtk.Overlay.new() self._overlay.add(eventbox) self._overlay.show() self.__coverbox = Gtk.Grid() self.__coverbox.set_row_spacing(2) self.__coverbox.set_margin_end(10) self.__coverbox.set_orientation(Gtk.Orientation.VERTICAL) self.__coverbox.show() self.__coverbox.attach(self._overlay, 0, 0, 2, 1) loved.set_property("halign", Gtk.Align.START) self.__coverbox.attach(rating, 0, 1, 1, 1) self.__coverbox.attach_next_to(loved, rating, Gtk.PositionType.RIGHT, 1, 1) if App().window.container.stack.get_allocation().width <\ Sizing.MEDIUM: self.__coverbox.hide() if len(self._artist_ids) > 1: self.__artist_label.set_text( ", ".join(self._album.artists)) self.__artist_label.show() else: self._artwork = None loved.set_property("halign", Gtk.Align.END) self.__header.add(rating) self.__header.add(loved) rating.set_hexpand(True) self.__header.add(self.__duration_label) self.__artist_label.set_text(", ".join(self._album.artists)) self.__artist_label.show() self.__set_duration() album_name = GLib.markup_escape_text(self._album.name) artist_name = GLib.markup_escape_text( ", ".join(self._album.artists)) self.__title_label.set_markup("<b>%s</b>" % album_name) self.__artist_label.set_markup("<b>%s</b>" % artist_name) if self._album.year is not None: self.__year_label.set_label(str(self._album.year)) self.__year_label.show() self.set_selection() if self._artwork is None: TracksView.populate(self) self._widget.add(self._responsive_widget) self._responsive_widget.show() else: grid.add(self.__coverbox) self.set_artwork() grid.add(self._widget) self.add(grid) else: TracksView.populate(self) def get_current_ordinate(self, parent): """ If current track in widget, return it ordinate, @param parent widget as Gtk.Widget @return y as int """ for dic in [self._tracks_widget_left, self._tracks_widget_right]: for widget in dic.values(): for child in widget.get_children(): if child.track.id == App().player.current_track.id: return child.translate_coordinates(parent, 0, 0)[1] return None def set_filter_func(self, func): """ Set filter function """ for widget in self._tracks_widget_left.values(): widget.set_filter_func(func) for widget in self._tracks_widget_right.values(): widget.set_filter_func(func) def set_playing_indicator(self): """ Update playing indicator """ TracksView.set_playing_indicator(self) @property def requested_height(self): """ Requested height: Internal tracks or at least cover @return (minimal: int, maximal: int) """ from lollypop.widgets_row_track import TrackRow track_height = TrackRow.get_best_height(self) minimal_height = maximal_height = track_height + 20 count = self._album.tracks_count mid_tracks = int(0.5 + count / 2) left_height = track_height * mid_tracks right_height = track_height * (count - mid_tracks) if left_height > right_height: minimal_height += left_height else: minimal_height += right_height maximal_height += left_height + right_height # Add height for disc label if len(self._album.discs) > 1: minimal_height += track_height maximal_height += track_height # 26 is for loved and rating cover_height = ArtSize.BIG + 26 if minimal_height < cover_height: return (cover_height, cover_height) else: return (minimal_height, maximal_height) ####################### # PROTECTED # ####################### def _on_tracks_populated(self, disc_number): """ Emit populated signal @param disc_number as int """ self.emit("populated") def _on_album_updated(self, scanner, album_id, destroy): """ On album modified, disable it @param scanner as CollectionScanner @param album id as int @param destroy as bool """ TracksView._on_album_updated(self, scanner, album_id, destroy) AlbumWidget._on_album_updated(self, scanner, album_id, destroy) def _on_eventbox_button_press_event(self, widget, event): """ Show overlay if not shown @param widget as Gtk.Widget @param event as Gdk.Event """ # Here some code for touch screens # If mouse pointer activate Gtk.FlowBoxChild, overlay is on, # as enter notify event enabled it # Else, we are in touch screen, first time show overlay, next time # show popover if not self.is_overlay: self.show_overlay(True) return def _on_album_artwork(self, surface): """ Set album artwork @param surface as str """ if surface is None: self._artwork.set_from_icon_name("folder-music-symbolic", Gtk.IconSize.DIALOG) else: self._artwork.set_from_surface(surface) if self._responsive_widget is None: self._artwork.show() TracksView.populate(self) self._widget.add(self._responsive_widget) self._responsive_widget.show() ####################### # PRIVATE # ####################### def __set_duration(self): """ Set album duration """ duration = App().albums.get_duration(self._album.id, self._album.genre_ids) hours = int(duration / 3600) mins = int(duration / 60) if hours > 0: mins -= hours * 60 if mins > 0: self.__duration_label.set_text(_("%s h %s m") % (hours, mins)) else: self.__duration_label.set_text(_("%s h") % hours) else: self.__duration_label.set_text(_("%s m") % mins) def __on_query_tooltip(self, widget, x, y, keyboard, tooltip): """ Show tooltip if needed @param widget as Gtk.Widget @param x as int @param y as int @param keyboard as bool @param tooltip as Gtk.Tooltip """ layout = widget.get_layout() if layout.is_ellipsized(): tooltip.set_text(widget.get_label()) else: return False return True def __on_context_clicked(self, button): """ Show context widget @param button as Gtk.Button """ def on_hide(widget): button.emit("clicked") image = button.get_image() if self.__context is None: image.set_from_icon_name("go-previous-symbolic", Gtk.IconSize.MENU) self.__context = ContextWidget(self._album, button) self.__context.connect("hide", on_hide) self.__context.set_property("halign", Gtk.Align.START) self.__context.set_property("valign", Gtk.Align.CENTER) self.__context.show() self.__header.insert_next_to(button, Gtk.PositionType.RIGHT) self.__header.attach_next_to(self.__context, button, Gtk.PositionType.RIGHT, 1, 1) else: image.set_from_icon_name("go-next-symbolic", Gtk.IconSize.MENU) self.__context.destroy() self.__context = None def __on_size_allocate(self, widget, allocation): """ Update internals @param widget as Gtk.Widget @param allocation as Gtk.Allocation """ if self.__width_allocation == allocation.width: return self.__width_allocation = allocation.width (min_height, max_height) = self.requested_height if allocation.width < Sizing.MONSTER: self.set_size_request(-1, max_height) else: self.set_size_request(-1, min_height) if self._artwork is not None: # Use mainloop to let GTK get the event if allocation.width < Sizing.MEDIUM: GLib.idle_add(self.__coverbox.hide) else: GLib.idle_add(self.__coverbox.show)
class Row(Gtk.ListBoxRow): """ A row """ def __init__(self, rowid, num): """ Init row widgets @param rowid as int @param num as int @param show loved as bool """ # We do not use Gtk.Builder for speed reasons Gtk.ListBoxRow.__init__(self) self._artists_label = None self._track = Track(rowid) self.__number = num self.__preview_timeout_id = None self.__context_timeout_id = None self.__context = None self._indicator = IndicatorWidget(self._track.id) self.set_indicator(Lp().player.current_track.id == self._track.id, utils.is_loved(self._track.id)) self._row_widget = Gtk.EventBox() self._row_widget.connect('button-press-event', self.__on_button_press) self._row_widget.connect('enter-notify-event', self.__on_enter_notify) self._row_widget.connect('leave-notify-event', self.__on_leave_notify) self._grid = Gtk.Grid() self._grid.set_column_spacing(5) self._row_widget.add(self._grid) self._title_label = Gtk.Label.new(self._track.name) self._title_label.set_property('has-tooltip', True) self._title_label.connect('query-tooltip', self.__on_query_tooltip) self._title_label.set_property('hexpand', True) self._title_label.set_property('halign', Gtk.Align.START) self._title_label.set_ellipsize(Pango.EllipsizeMode.END) if self._track.non_album_artists: self._artists_label = Gtk.Label.new(GLib.markup_escape_text( ", ".join(self._track.non_album_artists))) self._artists_label.set_use_markup(True) self._artists_label.set_property('has-tooltip', True) self._artists_label.connect('query-tooltip', self.__on_query_tooltip) self._artists_label.set_property('hexpand', True) self._artists_label.set_property('halign', Gtk.Align.END) self._artists_label.set_ellipsize(Pango.EllipsizeMode.END) self._artists_label.set_opacity(0.3) self._artists_label.set_margin_end(5) self._artists_label.show() self._duration_label = Gtk.Label.new( seconds_to_string(self._track.duration)) self._duration_label.get_style_context().add_class('dim-label') self._num_label = Gtk.Label() self._num_label.set_ellipsize(Pango.EllipsizeMode.END) self._num_label.set_property('valign', Gtk.Align.CENTER) self._num_label.set_width_chars(4) self._num_label.get_style_context().add_class('dim-label') self.update_num_label() self.__menu_button = Gtk.Button.new() # Here a hack to make old Gtk version support min-height css attribute # min-height = 24px, borders = 2px, we set directly on stack # min-width = 24px, borders = 2px, padding = 8px self.__menu_button.set_size_request(34, 26) self.__menu_button.set_relief(Gtk.ReliefStyle.NONE) self.__menu_button.get_style_context().add_class('menu-button') self.__menu_button.get_style_context().add_class('track-menu-button') self._grid.add(self._num_label) self._grid.add(self._title_label) if self._artists_label is not None: self._grid.add(self._artists_label) self._grid.add(self._duration_label) # TODO Remove this later if Gtk.get_minor_version() > 16: self._grid.add(self.__menu_button) else: self.connect('map', self.__on_map) self.add(self._row_widget) self.get_style_context().add_class('trackrow') def show_spinner(self): """ Show spinner """ self._indicator.show_spinner() def set_indicator(self, playing, loved): """ Show indicator @param widget name as str @param playing as bool @param loved as bool """ self._indicator.clear() if playing: self.get_style_context().remove_class('trackrow') self.get_style_context().add_class('trackrowplaying') if loved: self._indicator.play_loved() else: self._indicator.play() else: self.get_style_context().remove_class('trackrowplaying') self.get_style_context().add_class('trackrow') if loved and self.__context is None: self._indicator.loved() else: self._indicator.empty() def set_number(self, num): """ Set number @param number as int """ self.__number = num def update_duration(self): """ Update duration for row """ # Get a new track to get new duration (cache) track = Track(self._track.id) self._duration_label.set_text(seconds_to_string(track.duration)) def update_num_label(self): """ Update position label for row """ if Lp().player.track_in_queue(self._track): self._num_label.get_style_context().add_class('queued') pos = Lp().player.get_track_position(self._track.id) self._num_label.set_text(str(pos)) elif self.__number > 0: self._num_label.get_style_context().remove_class('queued') self._num_label.set_text(str(self.__number)) else: self._num_label.get_style_context().remove_class('queued') self._num_label.set_text('') @property def id(self): """ Get object id @return Current id as int """ return self._track.id ####################### # PRIVATE # ####################### def __play_preview(self): """ Play track @param widget as Gtk.Widget """ Lp().player.preview.set_property('uri', self._track.uri) Lp().player.preview.set_state(Gst.State.PLAYING) self.set_indicator(True, False) self.__preview_timeout_id = None def __on_map(self, widget): """ Fix for Gtk < 3.18, if we are in a popover, do not show menu button """ widget = self.get_parent() while widget is not None: if isinstance(widget, Gtk.Popover): break widget = widget.get_parent() if widget is None: self._grid.add(self.__menu_button) self.__menu_button.show() def __on_enter_notify(self, widget, event): """ Set image on buttons now, speed reason @param widget as Gtk.Widget @param event as Gdk.Event """ if self.__context_timeout_id is not None: GLib.source_remove(self.__context_timeout_id) self.__context_timeout_id = None if Lp().settings.get_value('preview-output').get_string() != '': self.__preview_timeout_id = GLib.timeout_add(500, self.__play_preview) if self.__menu_button.get_image() is None: image = Gtk.Image.new_from_icon_name('go-previous-symbolic', Gtk.IconSize.MENU) image.set_opacity(0.2) self.__menu_button.set_image(image) self.__menu_button.connect('clicked', self.__on_button_clicked) self._indicator.update_button() def __on_leave_notify(self, widget, event): """ Stop preview @param widget as Gtk.Widget @param event as Gdk.Event """ allocation = widget.get_allocation() if event.x <= 0 or\ event.x >= allocation.width or\ event.y <= 0 or\ event.y >= allocation.height: if self.__context is not None and\ self.__context_timeout_id is None: self.__context_timeout_id = GLib.timeout_add( 1000, self.__on_button_clicked, self.__menu_button) if Lp().settings.get_value('preview-output').get_string() != '': if self.__preview_timeout_id is not None: GLib.source_remove(self.__preview_timeout_id) self.__preview_timeout_id = None self.set_indicator( Lp().player.current_track.id == self._track.id, utils.is_loved(self._track.id)) Lp().player.preview.set_state(Gst.State.NULL) def __on_button_press(self, widget, event): """ Popup menu for track relative to track row @param widget as Gtk.Widget @param event as Gdk.Event """ if self.__context is not None: self.__on_button_clicked(self.__menu_button) if event.button == 3: if GLib.getenv("WAYLAND_DISPLAY") != "" and\ self.get_ancestor(Gtk.Popover) is not None: print("https://bugzilla.gnome.org/show_bug.cgi?id=774148") window = widget.get_window() if window == event.window: self.__popup_menu(widget, event.x, event.y) # Happens when pressing button over menu btn else: self.__on_button_clicked(self.__menu_button) return True elif event.button == 2: if self._track.id in Lp().player.get_queue(): Lp().player.del_from_queue(self._track.id) else: Lp().player.append_to_queue(self._track.id) def __on_button_clicked(self, button): """ Popup menu for track relative to button @param button as Gtk.Button """ self.__context_timeout_id = None image = self.__menu_button.get_image() if self.__context is None: image.set_from_icon_name('go-next-symbolic', Gtk.IconSize.MENU) self.__context = ContextWidget(self._track, button) self.__context.set_property('halign', Gtk.Align.END) self.__context.show() self._duration_label.hide() self._grid.insert_next_to(button, Gtk.PositionType.LEFT) self._grid.attach_next_to(self.__context, button, Gtk.PositionType.LEFT, 1, 1) self.set_indicator(Lp().player.current_track.id == self._track.id, False) else: image.set_from_icon_name('go-previous-symbolic', Gtk.IconSize.MENU) self.__context.destroy() self._duration_label.show() self.__context = None self.set_indicator(Lp().player.current_track.id == self._track.id, utils.is_loved(self._track.id)) def __popup_menu(self, widget, xcoordinate=None, ycoordinate=None): """ Popup menu for track @param widget as Gtk.Button @param xcoordinate as int (or None) @param ycoordinate as int (or None) """ popover = TrackMenuPopover(self._track, TrackMenu(self._track)) if xcoordinate is not None and ycoordinate is not None: rect = widget.get_allocation() rect.x = xcoordinate rect.y = ycoordinate rect.width = rect.height = 1 popover.set_relative_to(widget) popover.set_pointing_to(rect) popover.connect('closed', self.__on_closed) self.get_style_context().add_class('track-menu-selected') popover.show() def __on_closed(self, widget): """ Remove selected style @param widget as Gtk.Popover """ self.get_style_context().remove_class('track-menu-selected') def __on_query_tooltip(self, widget, x, y, keyboard, tooltip): """ Show tooltip if needed @param widget as Gtk.Widget @param x as int @param y as int @param keyboard as bool @param tooltip as Gtk.Tooltip """ text = '' layout = widget.get_layout() label = widget.get_text() if layout.is_ellipsized(): text = "%s" % (GLib.markup_escape_text(label)) widget.set_tooltip_markup(text)
class AlbumDetailedWidget(Gtk.Bin, AlbumWidget): """ Widget with cover and tracks """ __gsignals__ = { 'populated': (GObject.SignalFlags.RUN_FIRST, None, ()), 'overlayed': (GObject.SignalFlags.RUN_FIRST, None, (bool,)) } def __init__(self, album_id, genre_ids, artist_ids, show_cover): """ Init detailed album widget @param album id as int @param genre ids as [int] @param artist ids as [int] @param lazy as LazyLoadingView @param show cover as bool """ Gtk.Bin.__init__(self) AlbumWidget.__init__(self, album_id, genre_ids, artist_ids) self._album.set_artists(artist_ids) self.__width = None self.__context = None # Cover + rating + spacing self.__height = ArtSize.BIG + 26 self.__orientation = None self.__child_height = TrackRow.get_best_height(self) # Header + separator + spacing + margin self.__requested_height = self.__child_height + 6 # Discs to load, will be emptied self.__discs = self._album.discs self.__locked_widget_right = True self.set_property('height-request', self.__height) self.connect('size-allocate', self.__on_size_allocate) builder = Gtk.Builder() builder.add_from_resource('/org/gnome/Lollypop/AlbumDetailedWidget.ui') self._widget = builder.get_object('widget') self.__header = builder.get_object('header') self.__overlay = builder.get_object('overlay') self._play_button = builder.get_object('play-button') self._artwork_button = builder.get_object('artwork-button') self._action_button = builder.get_object('action-button') self._action_event = builder.get_object('action-event') builder.connect_signals(self) rating = RatingWidget(self._album) rating.show() artist_label = builder.get_object('artist') if show_cover: self._cover = builder.get_object('cover') builder.get_object('duration').set_hexpand(True) self._cover.get_style_context().add_class('cover-frame') self.__coverbox = builder.get_object('coverbox') self.__coverbox.show() # 6 for 2*3px (application.css) self.__coverbox.set_property('width-request', ArtSize.BIG + 6) self.__coverbox.add(rating) if Lp().window.get_view_width() < WindowSize.MEDIUM: self.__coverbox.hide() if len(artist_ids) > 1: artist_label.set_text(", ".join(self._album.artists)) artist_label.show() else: self.__header.attach(rating, 4, 0, 1, 1) rating.set_hexpand(True) rating.set_property('halign', Gtk.Align.END) rating.set_property('valign', Gtk.Align.CENTER) artist_label.set_text(", ".join(self._album.artists)) artist_label.show() self._cover = None self.__duration_label = builder.get_object('duration') self.__set_duration() self.__box = Gtk.Grid() self.__box.set_column_homogeneous(True) self.__box.set_property('valign', Gtk.Align.START) self.__box.show() builder.get_object('albuminfo').add(self.__box) self._tracks_left = {} self._tracks_right = {} self.set_cover() self.update_state() builder.get_object('title').set_label(self._album.name) if self._album.year: year = builder.get_object('year') year.set_label(self._album.year) year.show() for disc in self.__discs: self.__add_disc_container(disc.number) self.__set_disc_height(disc) self.add(self._widget) # We start transparent, we switch opaque at size allocation # This prevent artifacts self.set_opacity(0) self.__context_button = builder.get_object('context') if self._album.is_web and show_cover: self._cover.get_style_context().add_class( 'cover-frame-web') def update_playing_indicator(self): """ Update playing indicator """ for disc in self._album.discs: self._tracks_left[disc.number].update_playing( Lp().player.current_track.id) self._tracks_right[disc.number].update_playing( Lp().player.current_track.id) def update_duration(self, track_id): """ Update duration for current track @param track id as int """ for disc in self._album.discs: self._tracks_left[disc.number].update_duration(track_id) self._tracks_right[disc.number].update_duration(track_id) def populate(self): """ Populate tracks @thread safe """ if self.__discs: disc = self.__discs.pop(0) mid_tracks = int(0.5 + len(disc.tracks) / 2) self.populate_list_left(disc.tracks[:mid_tracks], disc, 1) self.populate_list_right(disc.tracks[mid_tracks:], disc, mid_tracks + 1) def is_populated(self): """ Return True if populated @return bool """ return len(self.__discs) == 0 def populate_list_left(self, tracks, disc, pos): """ Populate left list, thread safe @param tracks as [Track] @param disc as Disc @param pos as int """ GLib.idle_add(self.__add_tracks, tracks, self._tracks_left, disc.number, pos) def populate_list_right(self, tracks, disc, pos): """ Populate right list, thread safe @param tracks as [Track] @param disc as Disc @param pos as int """ # 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, disc, pos) else: GLib.idle_add(self.__add_tracks, tracks, self._tracks_right, disc.number, pos) def get_current_ordinate(self, parent): """ If current track in widget, return it ordinate, @param parent widget as Gtk.Widget @return y as int """ for dic in [self._tracks_left, self._tracks_right]: for widget in dic.values(): for child in widget.get_children(): if child.id == Lp().player.current_track.id: return child.translate_coordinates(parent, 0, 0)[1] return None def set_filter_func(self, func): """ Set filter function """ for widget in self._tracks_left.values(): widget.set_filter_func(func) for widget in self._tracks_right.values(): widget.set_filter_func(func) @property def boxes(self): """ @return [Gtk.ListBox] """ boxes = [] for widget in self._tracks_left.values(): boxes.append(widget) for widget in self._tracks_right.values(): boxes.append(widget) return boxes @property def requested_height(self): """ Requested height """ if self.__requested_height < self.__height: return self.__height else: return self.__requested_height ####################### # PROTECTED # ####################### def _on_context_clicked(self, button): """ Show context widget @param button as Gtk.Button """ image = button.get_image() if self.__context is None: image.set_from_icon_name('go-previous-symbolic', Gtk.IconSize.MENU) self.__context = ContextWidget(self._album, button) self.__context.set_property('halign', Gtk.Align.START) self.__context.set_property('valign', Gtk.Align.CENTER) self.__context.show() self.__header.insert_next_to(button, Gtk.PositionType.RIGHT) self.__header.attach_next_to(self.__context, button, Gtk.PositionType.RIGHT, 1, 1) else: image.set_from_icon_name('go-next-symbolic', Gtk.IconSize.MENU) self.__context.destroy() self.__context = None def _on_album_updated(self, scanner, album_id, destroy): """ On album modified, disable it @param scanner as CollectionScanner @param album id as int @param destroy as bool """ if self._album.id != album_id: return removed = False for dic in [self._tracks_left, self._tracks_right]: for widget in dic.values(): for child in widget.get_children(): track = Track(child.id) if track.album.id == Type.NONE: removed = True if removed: for dic in [self._tracks_left, self._tracks_right]: for widget in dic.values(): for child in widget.get_children(): child.destroy() self.__discs = self._album.discs self.__set_duration() self.populate() AlbumWidget._on_album_updated(self, scanner, album_id, destroy) ####################### # PRIVATE # ####################### def __set_duration(self): """ Set album duration """ duration = Lp().albums.get_duration(self._album.id, self._album.genre_ids) hours = int(duration / 3600) mins = int(duration / 60) if hours > 0: mins -= hours * 60 if mins > 0: self.__duration_label.set_text(_("%s h %s m") % (hours, mins)) else: self.__duration_label.set_text(_("%s h") % hours) else: self.__duration_label.set_text(_("%s m") % mins) def __set_disc_height(self, disc): """ Set disc widget height @param disc as Disc """ count_tracks = len(disc.tracks) mid_tracks = int(0.5 + count_tracks / 2) left_height = self.__child_height * mid_tracks right_height = self.__child_height * (count_tracks - mid_tracks) if left_height > right_height: self.__requested_height += left_height else: self.__requested_height += right_height self._tracks_left[disc.number].set_property('height-request', left_height) self._tracks_right[disc.number].set_property('height-request', right_height) def __add_disc_container(self, disc_number): """ Add disc container to box @param disc_number as int """ self._tracks_left[disc_number] = TracksWidget() self._tracks_right[disc_number] = TracksWidget() self._tracks_left[disc_number].connect('activated', self.__on_activated) self._tracks_right[disc_number].connect('activated', self.__on_activated) self._tracks_left[disc_number].show() self._tracks_right[disc_number].show() def __pop_menu(self, widget): """ Popup menu for album @param widget as Gtk.Button @param album id as int """ ancestor = self.get_ancestor(Gtk.Popover) # Get album real genre ids (not contextual) genre_ids = Lp().albums.get_genre_ids(self._album.id) if genre_ids and genre_ids[0] == Type.CHARTS: popover = AlbumMenuPopover(self._album, None) popover.set_relative_to(widget) popover.set_position(Gtk.PositionType.BOTTOM) elif self._album.is_web: popover = AlbumMenuPopover(self._album, AlbumMenu(self._album, ancestor is not None)) popover.set_relative_to(widget) else: popover = Gtk.Popover.new_from_model( widget, AlbumMenu(self._album, ancestor is not None)) if ancestor is not None: Lp().window.view.show_popover(popover) else: popover.connect('closed', self.__on_pop_menu_closed) self.get_style_context().add_class('album-menu-selected') popover.show() def __add_tracks(self, tracks, widget, disc_number, i): """ Add tracks for to tracks widget @param tracks as [int] @param widget as TracksWidget @param disc number as int @param i as int """ if self._loading == Loading.STOP: self._loading = Loading.NONE return if not tracks: if widget == self._tracks_right: self._loading |= Loading.RIGHT elif widget == self._tracks_left: self._loading |= Loading.LEFT if self._loading == Loading.ALL: self.emit('populated') self.__locked_widget_right = False return track = tracks.pop(0) if not Lp().settings.get_value('show-tag-tracknumber'): track_number = i else: track_number = track.number row = TrackRow(track.id, track_number) row.show() widget[disc_number].add(row) GLib.idle_add(self.__add_tracks, tracks, widget, disc_number, i + 1) 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 __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.MEDIUM or ( allocation.width < WindowSize.MONSTER and self._cover is not None): orientation = Gtk.Orientation.VERTICAL else: orientation = Gtk.Orientation.HORIZONTAL if orientation != self.__orientation: self.__orientation = orientation redraw = True if redraw: for child in self.__box.get_children(): self.__box.remove(child) # Grid index idx = 0 # Disc label width / right box position if orientation == Gtk.Orientation.VERTICAL: width = 1 pos = 0 else: width = 2 pos = 1 for disc in self._album.discs: show_label = len(self._album.discs) > 1 if show_label: label = Gtk.Label() disc_text = _("Disc %s") % disc.number disc_names = self._album.disc_names(disc.number) if disc_names: disc_text += ": " + ", ".join(disc_names) label.set_text(disc_text) label.set_property('halign', Gtk.Align.START) label.get_style_context().add_class('dim-label') label.show() self.__box.attach(label, 0, idx, width, 1) idx += 1 GLib.idle_add(self.__box.attach, self._tracks_left[disc.number], 0, idx, 1, 1) if orientation == Gtk.Orientation.VERTICAL: idx += 1 GLib.idle_add(self.__box.attach, self._tracks_right[disc.number], pos, idx, 1, 1) idx += 1 GLib.idle_add(self.set_opacity, 1) if self._cover is not None: if allocation.width < WindowSize.MEDIUM: self.__coverbox.hide() else: self.__coverbox.show() def __on_pop_menu_closed(self, widget): """ Remove selected style @param widget as Gtk.Popover """ self.get_style_context().remove_class('album-menu-selected') def __on_activated(self, widget, track_id): """ On track activation, play track @param widget as TracksWidget @param track id as int """ # Add to queue by default if Lp().player.locked: if track_id in Lp().player.get_queue(): Lp().player.del_from_queue(track_id) else: Lp().player.append_to_queue(track_id) else: # Do not update album list if not Lp().player.is_party and not\ Lp().settings.get_enum('playback') == NextContext.STOP: # If in artist view, reset album list if self._filter_ids: Lp().player.set_albums(track_id, self._filter_ids, self._album.genre_ids) # Else, add album if missing elif not Lp().player.has_album(self._album): Lp().player.add_album(self._album) # Clear albums if user clicked on a track from a new album elif Lp().settings.get_enum('playback') == NextContext.STOP: if not Lp().player.has_album(self._album): Lp().player.clear_albums() self.__show_spinner(widget, track_id) track = Track(track_id) Lp().player.load(track)