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._track = Track(rowid) self._number = num 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._grid = Gtk.Grid() self._grid.set_column_spacing(5) self._row_widget.add(self._grid) self._title_label = Gtk.Label.new(self._track.formated_name()) self._title_label.set_use_markup(True) self._title_label.set_property('has-tooltip', True) self._title_label.connect('query-tooltip', self._on_title_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) 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) 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 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: self._indicator.loved() else: self._indicator.empty() def set_number(self, num): """ Set number @param number as int """ self._number = num def update_num_label(self): """ Update position label for row """ if Lp().player.is_in_queue(self._track.id): 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('') def get_id(self): """ Get object id @return Current id as int """ return self._track.id ####################### # PRIVATE # ####################### 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) 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._menu_button.get_image() is None: image = Gtk.Image.new_from_icon_name('open-menu-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_button_press(self, widget, event): """ Popup menu for track relative to track row @param widget as Gtk.Widget @param event as Gdk.Event """ if event.button == 3 and Gtk.get_minor_version() > 16: 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._popup_menu(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, widget): """ Popup menu for track relative to button @param widget as Gtk.Button """ self._popup_menu(widget) 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.id, TrackMenu(self._track.id)) popover.set_relative_to(widget) 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_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_title_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 = self._title_label.get_layout() if layout.is_ellipsized(): self._title_label.set_tooltip_markup(self._track.formated_name()) elif len(self._track.artists) > 1: self._title_label.set_tooltip_text( ", ".join(self._track.artists[1:])) else: self._title_label.set_tooltip_text('')
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 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 Row(Gtk.ListBoxRow): """ A row """ def __init__(self, show_loved): """ Init row widgets @param show loved as bool """ # We do not use Gtk.Builder for speed reasons Gtk.ListBoxRow.__init__(self) self._indicator = IndicatorWidget() self._show_loved = show_loved self._object_id = None self._number = 0 self._row_widget = Gtk.EventBox() self._grid = Gtk.Grid() self._grid.set_column_spacing(5) self._row_widget.add(self._grid) self._title_label = Gtk.Label() self._title_label.set_property('has-tooltip', True) self._title_label.set_property('hexpand', True) self._title_label.set_property('halign', Gtk.Align.START) self._title_label.set_ellipsize(Pango.EllipsizeMode.END) self._duration_label = Gtk.Label() 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_width_chars(4) self._num_label.get_style_context().add_class('dim-label') self._grid.add(self._num_label) self._grid.add(self._title_label) self._grid.add(self._duration_label) self.add(self._row_widget) self.get_style_context().add_class('trackrow') def show_cover(self, show): """ Show cover @param show as bool """ pass def show_menu(self, show): """ Show menu """ pass def show_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 self._show_loved and 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 self._show_loved and loved: self._indicator.loved() else: self._indicator.empty() def set_num_label(self, label): """ Set num label @param label as string """ self._num_label.set_markup(label) def set_title_label(self, label): """ Set title label @param label as string """ self._title_label.set_markup(label) def set_duration_label(self, label): """ Set duration label @param label as string """ self._duration_label.set_text(label) def set_object_id(self, object_id): """ Store current object id @param object id as int """ self._object_id = object_id def get_object_id(self): """ Get object id @return Current object id as int """ return self._object_id def set_number(self, num): """ Set track number @param num as int """ self._number = num def get_number(self): """ Get track number @return num as int """ return self._number def set_cover(self, pixbuf, tooltip): """ Set cover @param cover as Gdk.Pixbuf @param tooltip as str """ pass ####################### # PRIVATE # ####################### def _on_title_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 = self._title_label.get_layout() if layout.is_ellipsized(): label = self._title_label.get_label() self._title_label.set_tooltip_markup(label) else: self._title_label.set_tooltip_text('')
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.__timeout_id = 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._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: 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.is_in_queue(self._track.id): 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.__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 Lp().settings.get_value('preview-output').get_string() != '': widget.connect('leave-notify-event', self.__on_leave_notify) self.__timeout_id = GLib.timeout_add(500, self.__play_preview) if self.__menu_button.get_image() is None: image = Gtk.Image.new_from_icon_name('open-menu-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 """ if self.__timeout_id is not None: GLib.source_remove(self.__timeout_id) self.__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) widget.disconnect_by_func(self.__on_leave_notify) 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 event.button == 3 and Gtk.get_minor_version() > 16: 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.__popup_menu(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, widget): """ Popup menu for track relative to button @param widget as Gtk.Button """ self.__popup_menu(widget) 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)) popover.set_relative_to(widget) 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_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 Row(Gtk.ListBoxRow): """ A row """ def __init__(self, show_loved): """ Init row widgets @param show loved as bool """ # We do not use Gtk.Builder for speed reasons Gtk.ListBoxRow.__init__(self) self._indicator = IndicatorWidget() self._show_loved = show_loved self._id = None self._number = 0 self._row_widget = Gtk.EventBox() self._grid = Gtk.Grid() self._grid.set_column_spacing(5) self._row_widget.add(self._grid) self._title_label = Gtk.Label() self._title_label.set_property('has-tooltip', True) self._title_label.set_property('hexpand', True) self._title_label.set_property('halign', Gtk.Align.START) self._title_label.set_ellipsize(Pango.EllipsizeMode.END) self._duration_label = Gtk.Label() 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._menu_button = Gtk.Button.new_from_icon_name('open-menu-symbolic', Gtk.IconSize.MENU) 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._menu_button.get_image().set_opacity(0.2) self._menu_button.show() self._row_widget.connect('button-press-event', self._on_button_press) self._menu_button.connect('clicked', self._on_button_clicked) self._grid.add(self._num_label) self._grid.add(self._title_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_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 self._show_loved and 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 self._show_loved and loved: self._indicator.loved() else: self._indicator.empty() def set_num_label(self, label, queued=False): """ Set num label @param label as string """ if queued: self._num_label.get_style_context().add_class('queued') else: self._num_label.get_style_context().remove_class('queued') self._num_label.set_markup(label) def set_title_label(self, label): """ Set title label @param label as string """ self._title_label.set_markup(label) def set_duration_label(self, label): """ Set duration label @param label as string """ self._duration_label.set_text(label) def set_id(self, id): """ Store current object id @param id as int """ self._id = id self._indicator.set_id(id) def get_id(self): """ Get object id @return Current id as int """ return self._id def set_number(self, num): """ Set track number @param num as int """ self._number = num def get_number(self): """ Get track number @return num as int """ return self._number def set_cover(self, pixbuf, tooltip): """ Set cover @param cover as Gdk.Pixbuf @param tooltip as str """ pass ####################### # PRIVATE # ####################### 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) 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 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._popup_menu(self._menu_button) return True elif event.button == 2: if self._id in Lp().player.get_queue(): Lp().player.del_from_queue(self._id) else: Lp().player.append_to_queue(self._id) def _on_button_clicked(self, widget): """ Popup menu for track relative to button @param widget as Gtk.Button """ self._popup_menu(widget) 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._id, TrackMenu(self._id)) popover.set_relative_to(widget) 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_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_title_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 = self._title_label.get_layout() if layout.is_ellipsized(): label = self._title_label.get_label() self._title_label.set_tooltip_markup(label) else: self._title_label.set_tooltip_text('')
class TrackRow(Gtk.ListBoxRow): """ A track row """ __gsignals__ = { "removed": (GObject.SignalFlags.RUN_FIRST, None, ()), } def get_best_height(widget): """ Calculate widget height @param widget as Gtk.Widget """ ctx = widget.get_pango_context() layout = Pango.Layout.new(ctx) layout.set_text("a", 1) font_height = int(layout.get_pixel_size()[1]) # application.css min_height = 32 if font_height > min_height: height = font_height else: height = min_height return height def __init__(self, track, album_artist_ids, view_type): """ Init row widgets @param track as Track @param album_artist_ids as [int] @param view_type as ViewType """ # We do not use Gtk.Builder for speed reasons Gtk.ListBoxRow.__init__(self) self.__view_type = view_type self._track = track self._grid = Gtk.Grid() self._grid.set_property("valign", Gtk.Align.CENTER) self._grid.set_column_spacing(5) self._grid.show() self._indicator = IndicatorWidget(self, view_type) self._indicator.show() self._grid.add(self._indicator) self._num_label = Gtk.Label.new() self._num_label.set_ellipsize(Pango.EllipsizeMode.END) self._num_label.set_width_chars(4) self._num_label.get_style_context().add_class("dim-label") self.update_number_label() self._grid.add(self._num_label) self.__title_label = Gtk.Label.new( GLib.markup_escape_text(self._track.name)) self.__title_label.set_use_markup(True) self.__title_label.set_property("has-tooltip", True) self.__title_label.connect("query-tooltip", 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) self.__title_label.show() self._grid.add(self.__title_label) featuring_artist_ids = track.get_featuring_artist_ids(album_artist_ids) if featuring_artist_ids: artists = [] for artist_id in featuring_artist_ids: artists.append(App().artists.get_name(artist_id)) artists_label = Gtk.Label.new( GLib.markup_escape_text(", ".join(artists))) artists_label.set_use_markup(True) artists_label.set_property("has-tooltip", True) artists_label.connect("query-tooltip", on_query_tooltip) artists_label.set_property("hexpand", True) artists_label.set_property("halign", Gtk.Align.END) artists_label.set_ellipsize(Pango.EllipsizeMode.END) artists_label.set_opacity(0.3) artists_label.set_margin_end(5) artists_label.show() self._grid.add(artists_label) duration = ms_to_string(self._track.duration) self.__duration_label = Gtk.Label.new(duration) self.__duration_label.get_style_context().add_class("dim-label") self.__duration_label.show() self._grid.add(self.__duration_label) self.__action_button = None if self.__view_type & (ViewType.PLAYBACK | ViewType.PLAYLISTS): self.__action_button = Gtk.Button.new_from_icon_name( "list-remove-symbolic", Gtk.IconSize.MENU) self.__action_button.set_tooltip_text(_("Remove from playlist")) elif self.__view_type & (ViewType.ALBUM | ViewType.ARTIST): self.__action_button = Gtk.Button.new_from_icon_name( "view-more-symbolic", Gtk.IconSize.MENU) if self.__action_button is None: self.__duration_label.set_margin_end(MARGIN_SMALL) else: self.__action_button.show() self.__action_button.connect("clicked", self.__on_action_button_clicked) self.__action_button.set_margin_end(MARGIN_SMALL) self.__action_button.set_relief(Gtk.ReliefStyle.NONE) context = self.__action_button.get_style_context() context.add_class("menu-button") self._grid.add(self.__action_button) self.add(self._grid) self.set_indicator(self._get_indicator_type()) self.update_duration() self.get_style_context().add_class("trackrow") def update_duration(self): """ Update track duration """ self._track.reset("duration") duration = ms_to_string(self._track.duration) self.__duration_label.set_label(duration) def set_indicator(self, indicator_type=None): """ Show indicator @param indicator_type as IndicatorType """ if indicator_type is None: indicator_type = self._get_indicator_type() self._indicator.clear() if indicator_type & IndicatorType.LOADING: self._indicator.set_opacity(1) self._indicator.load() elif indicator_type & IndicatorType.PLAY: self._indicator.set_opacity(1) self.set_state_flags(Gtk.StateFlags.VISITED, True) if indicator_type & IndicatorType.LOVED: self._indicator.play_loved() else: self._indicator.play() else: self.unset_state_flags(Gtk.StateFlags.VISITED) if indicator_type & IndicatorType.LOVED: self._indicator.set_opacity(1) self._indicator.loved() elif indicator_type & IndicatorType.SKIP: self._indicator.set_opacity(1) self._indicator.skip() else: self._indicator.set_opacity(0) def update_number_label(self): """ Update position label for row """ if App().player.is_in_queue(self._track.id): 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)) else: if self.__view_type & (ViewType.PLAYBACK | ViewType.PLAYLISTS) and\ len(self._track.album.discs) > 1: discnumber = App().tracks.get_discnumber(self._track.id) label = "%s - %s" % (self._track.number, discnumber) else: label = str(self._track.number) self._num_label.set_markup(label) self._num_label.get_style_context().remove_class("queued") self._num_label.show() def popup_menu(self, parent, x=None, y=None): """ Popup menu for track @param parent as Gtk.Widget @param x as int @param y as int """ def on_hidden(widget, hide): self.set_indicator() from lollypop.menu_objects import TrackMenu, TrackMenuExt from lollypop.widgets_menu import MenuBuilder menu = TrackMenu(self._track, self.__view_type) menu_widget = MenuBuilder(menu) menu_widget.show() if not self._track.storage_type & StorageType.EPHEMERAL: menu_ext = TrackMenuExt(self._track) menu_ext.show() menu_widget.add_widget(menu_ext) popover = popup_widget(menu_widget, parent, x, y, self) if popover is None: menu_widget.connect("hidden", on_hidden) else: popover.connect("hidden", on_hidden) @property def name(self): """ Get row name @return str """ return self.__title_label.get_text() @property def track(self): """ Get row track @return Track """ return self._track ####################### # PROTECTED # ####################### def _get_indicator_type(self): """ Get indicator type for current row @return IndicatorType """ indicator_type = IndicatorType.NONE if App().player.current_track.id == self._track.id: indicator_type |= IndicatorType.PLAY if self._track.loved == 1: indicator_type |= IndicatorType.LOVED elif self._track.loved == -1: indicator_type |= IndicatorType.SKIP return indicator_type ####################### # PRIVATE # ####################### def __on_action_button_clicked(self, button): """ Show row menu @param button as Gtk.Button """ if self.__view_type & (ViewType.PLAYBACK | ViewType.PLAYLISTS): emit_signal(self, "removed") else: self.popup_menu(button)
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 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.set_sensitive(False) self._track = Track(rowid) self._number = num self._indicator = IndicatorWidget(self._track.id) self.set_indicator(Lp().player.current_track.id == self._track.id, utils.is_loved(self._track.id)) self.set_sensitive(True) 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._grid = Gtk.Grid() self._grid.set_column_spacing(5) self._row_widget.add(self._grid) self._title_label = Gtk.Label.new(self._track.formated_name()) self._title_label.set_use_markup(True) self._title_label.set_property('has-tooltip', True) self._title_label.connect('query-tooltip', self._on_title_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) 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_image = Gtk.Image.new() self._menu_image.set_opacity(0.2) 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') self._menu_button.set_image(self._menu_image) self._menu_button.connect('clicked', self._on_button_clicked) self._grid.add(self._num_label) self._grid.add(self._title_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 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: self._indicator.loved() else: self._indicator.empty() def set_number(self, num): """ Set number @param number as int """ self._number = num def update_num_label(self): """ Update position label for row """ if Lp().player.is_in_queue(self._track.id): 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('') def get_id(self): """ Get object id @return Current id as int """ return self._track.id ####################### # PRIVATE # ####################### 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) 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._menu_image.get_pixbuf() is None: self._menu_image.set_from_icon_name('open-menu-symbolic', Gtk.IconSize.MENU) self._indicator.update_button() 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 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._popup_menu(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, widget): """ Popup menu for track relative to button @param widget as Gtk.Button """ self._popup_menu(widget) 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.id, TrackMenu(self._track.id)) popover.set_relative_to(widget) 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_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_title_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 = self._title_label.get_layout() if layout.is_ellipsized(): label = self._title_label.get_label() self._title_label.set_tooltip_markup(label) else: self._title_label.set_tooltip_text('')
class Row(Gtk.ListBoxRow): """ A row """ def __init__(self, track, album_artist_ids, view_type): """ Init row widgets @param track as Track @param album_artist_ids as [int] @param view_type as ViewType """ # We do not use Gtk.Builder for speed reasons Gtk.ListBoxRow.__init__(self) self._view_type = view_type self._artists_label = None self._track = track self.__filtered = False self.__next_row = None self.__previous_row = None self._indicator = IndicatorWidget(self, view_type) self._row_widget = Gtk.EventBox() self._row_widget.connect("destroy", self._on_destroy) self.__gesture = Gtk.GestureLongPress.new(self._row_widget) self.__gesture.connect("pressed", self.__on_gesture_pressed) self.__gesture.connect("end", self.__on_gesture_end) # We want to get release event after gesture self.__gesture.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) self.__gesture.set_button(0) self._grid = Gtk.Grid() self._grid.set_property("valign", Gtk.Align.CENTER) self._grid.set_column_spacing(5) self._row_widget.add(self._grid) self._title_label = Gtk.Label.new( GLib.markup_escape_text(self._track.name)) self._title_label.set_use_markup(True) self._title_label.set_property("has-tooltip", True) self._title_label.connect("query-tooltip", 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.get_featuring_artist_ids(album_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", 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() duration = seconds_to_string(self._track.duration) self._duration_label = Gtk.Label.new(duration) self._duration_label.get_style_context().add_class("dim-label") self._num_label = Gtk.Label.new() self._num_label.set_ellipsize(Pango.EllipsizeMode.END) self._num_label.set_width_chars(4) self._num_label.get_style_context().add_class("dim-label") self.update_number_label() 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) if self._view_type & ViewType.DND and\ self._view_type & ViewType.POPOVER: self.__action_button = Gtk.Button.new_from_icon_name( "list-remove-symbolic", Gtk.IconSize.MENU) self.__action_button.set_tooltip_text(_("Remove from playback")) elif not self._view_type & (ViewType.POPOVER | ViewType.SEARCH): self.__action_button = Gtk.Button.new_from_icon_name( "view-more-symbolic", Gtk.IconSize.MENU) else: self.__action_button = None if self.__action_button is not None: self.__action_button.set_margin_end(MARGIN_SMALL) self.__action_button.connect("button-release-event", self.__on_action_button_release_event) self.__action_button.set_relief(Gtk.ReliefStyle.NONE) context = self.__action_button.get_style_context() context.add_class("menu-button") context.add_class("track-menu-button") self._grid.add(self.__action_button) else: self._duration_label.set_margin_end(MARGIN_SMALL) self.add(self._row_widget) self.set_indicator(self._get_indicator_type()) self.update_duration() def update_duration(self): """ Update track duration """ self._track.reset("duration") duration = seconds_to_string(self._track.duration) self._duration_label.set_label(duration) def set_indicator(self, indicator_type=None): """ Show indicator @param indicator_type as IndicatorType """ if indicator_type is None: indicator_type = self._get_indicator_type() self._indicator.clear() if indicator_type & IndicatorType.LOADING: self._indicator.set_opacity(1) self._indicator.load() elif indicator_type & IndicatorType.PLAY: self._indicator.set_opacity(1) self.get_style_context().remove_class("trackrow") self.get_style_context().add_class("trackrowplaying") if indicator_type & IndicatorType.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 indicator_type & IndicatorType.LOVED: self._indicator.set_opacity(1) self._indicator.loved() elif indicator_type & IndicatorType.SKIP: self._indicator.set_opacity(1) self._indicator.skip() else: self._indicator.set_opacity(0) 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("") def set_filtered(self, b): """ Set widget filtered @param b as bool @return bool (should be shown) """ self.__filtered = b if b: self.set_state_flags(Gtk.StateFlags.NORMAL, True) else: self.set_state_flags(Gtk.StateFlags.VISITED, True) return True def set_next_row(self, row): """ Set next row @param row as Row """ self.__next_row = row def set_previous_row(self, row): """ Set previous row @param row as Row """ self.__previous_row = row @property def next_row(self): """ Get next row @return row as Row """ return self.__next_row @property def previous_row(self): """ Get previous row @return row as Row """ return self.__previous_row @property def filtered(self): """ True if filtered by parent """ return self.__filtered @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_indicator_type(self): """ Get indicator type for current row @return IndicatorType """ indicator_type = IndicatorType.NONE if App().player.current_track.id == self._track.id: indicator_type |= IndicatorType.PLAY if self._track.loved == 1: indicator_type |= IndicatorType.LOVED elif self._track.loved == -1: indicator_type |= IndicatorType.SKIP return indicator_type def _get_menu(self): """ Return TrackMenu """ from lollypop.menu_objects import TrackMenu return TrackMenu(self._track) def _check_track(self): """ Check track always valid, destroy if not """ pass def _on_destroy(self, widget): pass ####################### # PRIVATE # ####################### def __popup_menu(self, widget, xcoordinate=None, ycoordinate=None): """ Popup menu for track @param widget as Gtk.Widget @param xcoordinate as int (or None) @param ycoordinate as int (or None) """ def on_closed(widget): self.get_style_context().remove_class("track-menu-selected") self.set_indicator() # Event happens before Gio.Menu activation GLib.idle_add(self._check_track) 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 = widget.get_allocation() rect.x = xcoordinate rect.y = ycoordinate rect.width = rect.height = 1 popover.set_pointing_to(rect) popover.set_relative_to(widget) popover.connect("closed", on_closed) self.get_style_context().add_class("track-menu-selected") popover.popup() def __on_button_release_event(self, widget, event): """ Handle button release event @param widget as Gtk.Widget @param event as Gdk.Event """ widget.disconnect_by_func(self.__on_button_release_event) if event.state & Gdk.ModifierType.CONTROL_MASK and\ self._view_type & ViewType.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._view_type & ViewType.DND: self.emit("do-selection") elif event.button == 3: self.__popup_menu(self, event.x, event.y) 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) elif event.button == 1: self.activate() if self._track.is_web: self.set_indicator(IndicatorType.LOADING) return True def __on_gesture_pressed(self, gesture, x, y): """ Show current track menu @param gesture as Gtk.GestureLongPress @param x as float @param y as float """ if self._view_type & ViewType.DND and\ self._view_type & ViewType.POPOVER: self._track.album.remove_track(self._track) self.destroy() else: self.__popup_menu(self, x, y) def __on_gesture_end(self, gesture, sequence): """ Connect button release event Here because we only want this if a gesture was recognized This allow touch scrolling """ self._row_widget.connect("button-release-event", self.__on_button_release_event) def __on_action_button_release_event(self, button, event): """ Show row menu @param button as Gtk.Button @param event as Gdk.EventButton """ if not self.get_state_flags() & Gtk.StateFlags.PRELIGHT: return if self._view_type & ViewType.DND and\ self._view_type & ViewType.POPOVER: self._track.album.remove_track(self._track) self.destroy() App().player.set_next() App().player.set_prev() else: self.__popup_menu(button) return True