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, 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, 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)