class SelectionList(Gtk.Overlay): """ A list for artists/genres """ __gsignals__ = { "item-selected": (GObject.SignalFlags.RUN_FIRST, None, ()), "populated": (GObject.SignalFlags.RUN_FIRST, None, ()), "pass-focus": (GObject.SignalFlags.RUN_FIRST, None, ()) } def __init__(self, sidebar=True): """ Init Selection list ui @param sidebar as bool """ Gtk.Overlay.__init__(self) self.__was_visible = False self.__timeout = None self.__to_select_ids = [] self.__modifier = False self.__populating = False self.__updating = False # Sort disabled if False self.__is_artists = False builder = Gtk.Builder() builder.add_from_resource("/org/gnome/Lollypop/SelectionList.ui") builder.connect_signals(self) self.__selection = builder.get_object("selection") self.__selection.set_select_function(self.__selection_validation) self.__model = builder.get_object("model") self.__model.set_sort_column_id(0, Gtk.SortType.ASCENDING) self.__model.set_sort_func(0, self.__sort_items) self.__view = builder.get_object("view") if sidebar: self.__view.get_style_context().add_class("sidebar") self.__view.set_row_separator_func(self.__row_separator_func) self.__renderer0 = CellRendererArtist() self.__renderer0.set_property("ellipsize-set", True) self.__renderer0.set_property("ellipsize", Pango.EllipsizeMode.END) self.__renderer1 = Gtk.CellRendererPixbuf() # 16px for Gtk.IconSize.MENU self.__renderer1.set_fixed_size(16, -1) column = Gtk.TreeViewColumn("") column.set_expand(True) column.pack_start(self.__renderer0, True) column.add_attribute(self.__renderer0, "text", 1) column.add_attribute(self.__renderer0, "artist", 1) column.add_attribute(self.__renderer0, "rowid", 0) column.pack_start(self.__renderer1, False) column.add_attribute(self.__renderer1, "icon-name", 2) self.__view.append_column(column) self.__view.set_property("has_tooltip", True) self.__scrolled = Gtk.ScrolledWindow() self.__scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) self.__scrolled.add(self.__view) self.__scrolled.show() self.add(self.__scrolled) if Gtk.get_minor_version() > 14: self.__fast_scroll = FastScroll(self.__view, self.__model, self.__scrolled) self.add_overlay(self.__fast_scroll) else: self.__fast_scroll = None self.__scrolled.connect("enter-notify-event", self.__on_enter_notify) self.__scrolled.connect("leave-notify-event", self.__on_leave_notify) Lp().art.connect("artist-artwork-changed", self.__on_artist_artwork_changed) def hide(self): """ Hide widget, remember state """ self.__was_visible = self.is_visible() Gtk.Bin.hide(self) @property def was_visible(self): """ True if widget was visible on previous hide """ return self.__was_visible def mark_as_artists(self, is_artists): """ Mark list as artists list @param is_artists as bool """ self.__is_artists = is_artists self.__renderer0.set_is_artists(is_artists) def is_marked_as_artists(self): """ Return True if list is marked as artists """ return self.__is_artists def populate(self, values): """ Populate view with values @param [(int, str, optional str)], will be deleted @thread safe """ if self.__populating: return self.__populating = True if len(self.__model) > 0: self.__updating = True self.__add_values(values) self.emit("populated") self.__updating = False self.__populating = False def remove_value(self, object_id): """ Remove row from model @param object id as int """ for item in self.__model: if item[0] == object_id: self.__model.remove(item.iter) break def add_value(self, value): """ Add item to list @param value as (int, str, optional str) """ # Do not add value if already exists for item in self.__model: if item[0] == value[0]: return self.__updating = True self.__add_value(value) self.__updating = False def update_value(self, object_id, name): """ Update object with new name @param object id as int @param name as str """ self.__updating = True found = False for item in self.__model: if item[0] == object_id: item[1] = name found = True break if not found: self.__add_value((object_id, name)) self.__updating = False def update_values(self, values): """ Update view with values @param [(int, str, optional str)] @thread safe """ self.__updating = True if self.__is_artists and self.__fast_scroll is not None: self.__fast_scroll.clear() # Remove not found items but not devices value_ids = set([v[0] for v in values]) for item in self.__model: if item[0] > Type.DEVICES and not item[0] in value_ids: self.__model.remove(item.iter) # Add items which are not already in the list item_ids = set([i[0] for i in self.__model]) for value in values: if not value[0] in item_ids: self.__add_value(value) if self.__is_artists and self.__fast_scroll is not None: self.__fast_scroll.populate() self.__updating = False def get_value(self, object_id): """ Return value for id @param id as int @return value as string """ for item in self.__model: if item[0] == object_id: return item[1] return "" def will_be_selected(self): """ Return True if list will select items on populate @return selected as bool """ return self.__to_select_ids def select_ids(self, ids): """ Make treeview select first default item @param object id as int """ self.__to_select_ids = [] if ids: try: # Check if items are available for selection items = [] for i in list(ids): for item in self.__model: if item[0] == i: items.append(item) ids.remove(i) # Select later if ids: self.__to_select_ids = ids else: for item in items: self.__selection.select_iter(item.iter) # Scroll to first item if items: self.__view.scroll_to_cell(items[0].path, None, True, 0, 0) except: self.__last_motion_event = None self.__to_select_ids = ids else: self.__selection.unselect_all() def grab_focus(self): """ Grab focus on treeview """ self.__view.grab_focus() @property def selected_ids(self): """ Get selected ids @return array of ids as [int] """ selected_ids = [] (model, items) = self.__selection.get_selected_rows() if model is not None: for item in items: selected_ids.append(model[item][0]) return selected_ids def clear(self): """ Clear treeview """ self.__updating = True self.__model.clear() if self.__is_artists and self.__fast_scroll is not None: self.__fast_scroll.clear() self.__fast_scroll.clear_chars() self.__fast_scroll.hide() self.__updating = False def get_headers(self): """ Return headers @return items as [(int, str)] """ items = [] items.append((Type.POPULARS, _("Popular albums"))) if Lp().albums.has_loves(): items.append((Type.LOVED, _("Loved albums"))) items.append((Type.RECENTS, _("Recently added albums"))) items.append((Type.RANDOMS, _("Random albums"))) items.append((Type.PLAYLISTS, _("Playlists"))) items.append((Type.RADIOS, _("Radios"))) if Lp().settings.get_value("show-charts") and\ Lp().settings.get_value("network-access"): items.append((Type.CHARTS, _("The charts"))) if self.__is_artists: items.append((Type.ALL, _("All albums"))) else: items.append((Type.ALL, _("All artists"))) return items def get_pl_headers(self): """ Return playlist headers @return items as [(int, str)] """ items = [] items.append((Type.POPULARS, _("Popular tracks"))) items.append((Type.LOVED, Lp().playlists.LOVED)) items.append((Type.RECENTS, _("Recently played"))) items.append((Type.NEVER, _("Never played"))) items.append((Type.RANDOMS, _("Random tracks"))) items.append((Type.NOPARTY, _("Not in party"))) items.append((Type.SEPARATOR, "")) return items ####################### # PROTECTED # ####################### def _on_key_press_event(self, entry, event): """ Forward to popover history listbox if needed @param entry as Gtk.Entry @param event as Gdk.Event """ if event.keyval in [Gdk.KEY_Left, Gdk.KEY_Right]: self.emit("pass-focus") def _on_button_press_event(self, view, event): """ Handle modifier @param view as Gtk.TreeView @param event as Gdk.Event """ view.grab_focus() state = event.get_state() if state & Gdk.ModifierType.CONTROL_MASK or\ state & Gdk.ModifierType.SHIFT_MASK: self.__modifier = True def _on_button_release_event(self, view, event): """ Handle modifier @param view as Gtk.TreeView @param event as Gdk.Event """ self.__modifier = False 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 """ if keyboard: return True (exists, tx, ty, model, path, i) = self.__view.get_tooltip_context(x, y, False) if exists: ctx = self.__view.get_pango_context() layout = Pango.Layout.new(ctx) iterator = self.__model.get_iter(path) if iterator is not None: text = self.__model.get_value(iterator, 1) column = self.__view.get_column(0) (position, width) = column.cell_get_position(self.__renderer0) if Lp().settings.get_value("artist-artwork") and\ self.__is_artists: width -= ArtSize.ARTIST_SMALL +\ CellRendererArtist.xshift * 2 layout.set_ellipsize(Pango.EllipsizeMode.END) if self.__model.get_value(iterator, 0) < 0: width -= 8 layout.set_width(Pango.units_from_double(width)) layout.set_text(text, -1) if layout.is_ellipsized(): tooltip.set_markup(GLib.markup_escape_text(text)) return True return False def _on_selection_changed(self, selection): """ Forward as "item-selected" @param view as Gtk.TreeSelection """ if not self.__updating and not self.__to_select_ids: self.emit("item-selected") ####################### # PRIVATE # ####################### def __add_value(self, value): """ Add value to the model @param value as [int, str, optional str] @thread safe """ if value[1] == "": string = _("Unknown") sort = string else: string = value[1] if len(value) == 3: sort = value[2] else: sort = value[1] if value[0] > 0 and sort and self.__is_artists and\ self.__fast_scroll is not None: self.__fast_scroll.add_char(sort[0]) i = self.__model.append( [value[0], string, self.__get_icon_name(value[0]), sort]) if value[0] in self.__to_select_ids: self.__to_select_ids.remove(value[0]) self.__selection.select_iter(i) def __add_values(self, values): """ Add values to the list @param items as [(int,str)] @thread safe """ for value in values: self.__add_value(value) if self.__is_artists and self.__fast_scroll is not None: self.__fast_scroll.populate() self.__to_select_ids = [] def __get_icon_name(self, object_id): """ Return pixbuf for id @param ojbect_id as id """ icon = "" if object_id == Type.POPULARS: icon = "starred-symbolic" elif object_id == Type.PLAYLISTS: icon = "emblem-documents-symbolic" elif object_id == Type.ALL: if self.__is_artists: icon = "media-optical-cd-audio-symbolic" else: icon = "avatar-default-symbolic" elif object_id == Type.COMPILATIONS: icon = "system-users-symbolic" elif object_id == Type.RECENTS: icon = "document-open-recent-symbolic" elif object_id == Type.RADIOS: icon = "audio-input-microphone-symbolic" elif object_id < Type.DEVICES: icon = "multimedia-player-symbolic" elif object_id == Type.RANDOMS: icon = "media-playlist-shuffle-symbolic" elif object_id == Type.LOVED: icon = "emblem-favorite-symbolic" elif object_id == Type.NEVER: icon = "document-new-symbolic" elif object_id == Type.CHARTS: icon = "application-rss+xml-symbolic" elif object_id == Type.SPOTIFY: icon = "lollypop-spotify-symbolic" elif object_id == Type.ITUNES: icon = "lollypop-itunes-symbolic" elif object_id == Type.LASTFM: icon = "lollypop-lastfm-symbolic" elif object_id == Type.NOPARTY: icon = "emblem-music-symbolic" return icon def __sort_items(self, model, itera, iterb, data): """ Sort model """ if not self.__updating: return False a_index = model.get_value(itera, 0) b_index = model.get_value(iterb, 0) # Static vs static if a_index < 0 and b_index < 0: return a_index < b_index # Static entries always on top elif b_index < 0: return True # Static entries always on top if a_index < 0: return False # String comparaison for non static else: if self.__is_artists: a = Lp().artists.get_sortname(a_index) b = Lp().artists.get_sortname(b_index) else: a = model.get_value(itera, 1) b = model.get_value(iterb, 1) return strcoll(a, b) def __row_separator_func(self, model, iterator): """ Draw a separator if needed @param model as Gtk.TreeModel @param iterator as Gtk.TreeIter """ return model.get_value(iterator, 0) == Type.SEPARATOR def __selection_validation(self, selection, model, path, current): """ Check if selection is valid @param selection as Gtk.TreeSelection @param model as Gtk.TreeModel @param path as Gtk.TreePath @param current as bool @return bool """ ids = self.selected_ids if not ids: return True elif self.__modifier: iterator = self.__model.get_iter(path) value = self.__model.get_value(iterator, 0) if value < 0 and len(ids) > 1: return False else: static = False for i in ids: if i < 0: static = True if static: return False elif value > 0: return True else: return False else: return True def __on_enter_notify(self, widget, event): """ Disable shortcuts @param widget as Gtk.widget @param event as Gdk.Event """ if widget.get_vadjustment().get_upper() >\ widget.get_allocated_height() and self.__is_artists and\ self.__fast_scroll is not None: self.__fast_scroll.show() # FIXME Not needed with GTK >= 3.18 Lp().window.enable_global_shortcuts(False) def __on_leave_notify(self, widget, event): """ Hide popover @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.__is_artists and self.__fast_scroll is not None: self.__fast_scroll.hide() # FIXME Not needed with GTK >= 3.18 Lp().window.enable_global_shortcuts(True) def __on_artist_artwork_changed(self, art, artist): """ Update row """ if self.__is_artists: self.__renderer0.on_artist_artwork_changed(artist) for item in self.__model: if item[1] == artist: item[1] = artist break
class SelectionList(LazyLoadingView, GesturesHelper): """ A list for artists/genres """ __gsignals__ = { "expanded": (GObject.SignalFlags.RUN_FIRST, None, (bool, )) } def __init__(self, base_mask): """ Init Selection list ui @param base_mask as SelectionListMask """ LazyLoadingView.__init__(self, StorageType.ALL, ViewType.DEFAULT) self.__selection_pending_ids = [] self.__base_mask = base_mask self.__mask = SelectionListMask.NONE self.__animation_timeout_id = None self.__height = SelectionListRow.get_best_height(self) self._box = Gtk.ListBox() self._box.set_selection_mode(Gtk.SelectionMode.MULTIPLE) self._box.show() GesturesHelper.__init__(self, self._box) self.__scrolled = Gtk.ScrolledWindow() self.__scrolled.show() self.__scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) self.__scrolled.set_property("expand", True) self.__viewport = Gtk.Viewport() self.__scrolled.add(self.__viewport) self.__viewport.show() self.__viewport.add(self._box) self.connect("initialized", self.__on_initialized) self.get_style_context().add_class("sidebar") if self.__base_mask & SelectionListMask.FASTSCROLL: self.__overlay = Gtk.Overlay.new() self.__overlay.show() self.__overlay.add(self.__scrolled) self.__fastscroll = FastScroll(self._box, self.__scrolled) self.__overlay.add_overlay(self.__fastscroll) self.add(self.__overlay) self.__base_mask |= SelectionListMask.LABEL App().settings.connect("changed::artist-artwork", self.__on_artist_artwork_changed) App().art.connect("artist-artwork-changed", self.__on_artist_artwork_changed) else: self.__overlay = None App().settings.connect("changed::show-sidebar-labels", self.__on_show_sidebar_labels_changed) self.add(self.__scrolled) self.__menu_button = Gtk.Button.new_from_icon_name( "view-more-horizontal-symbolic", Gtk.IconSize.BUTTON) self.__menu_button.set_property("halign", Gtk.Align.CENTER) self.__menu_button.get_style_context().add_class("no-border") self.__menu_button.connect("clicked", lambda x: self.__popup_menu(None, x)) self.__menu_button.show() self.add(self.__menu_button) App().window.container.widget.connect("notify::folded", self.__on_container_folded) self.__on_container_folded(None, App().window.folded) def set_mask(self, mask): """ Mark list as artists list @param mask as SelectionListMask """ self.__mask = mask def add_mask(self, mask): """ Mark list as artists list @param mask as SelectionListMask """ self.__mask |= mask def populate(self, values): """ Populate view with values @param [(int, str, optional str)], will be deleted """ self._box.set_sort_func(None) self.__scrolled.get_vadjustment().set_value(0) self.clear() LazyLoadingView.populate(self, values) def remove_value(self, object_id): """ Remove id from list @param object_id as int """ for child in self._box.get_children(): if child.id == object_id: child.destroy() break def add_value(self, value): """ Add item to list @param value as (int, str, optional str) """ self._box.set_sort_func(self.__sort_func) child = self._get_child(value) child.populate() if self.mask & SelectionListMask.ARTISTS: self.__fastscroll.clear() self.__fastscroll.populate() def update_value(self, object_id, name): """ Update object with new name @param object_id as int @param name as str """ found = False for child in self._box.get_children(): if child.id == object_id: child.set_label(name) found = True break if not found: if self.__base_mask & SelectionListMask.FASTSCROLL: self.__fastscroll.clear() self.add_value((object_id, name, name)) def update_values(self, values): """ Update view with values @param [(int, str, optional str)] """ if self.mask & SelectionListMask.FASTSCROLL: self.__fastscroll.clear() # Remove not found items value_ids = set([v[0] for v in values]) for child in self._box.get_children(): if child.id not in value_ids: self.remove_value(child.id) # Add items which are not already in the list item_ids = set([child.id for child in self._box.get_children()]) for value in values: if not value[0] in item_ids: row = self._get_child(value) row.populate() if self.mask & SelectionListMask.ARTISTS: self.__fastscroll.populate() def select_ids(self, ids=[], activate=True): """ Select listbox items @param ids as [int] @param activate as bool """ if ids: rows = [] for row in self._box.get_children(): if row.id in ids: rows.append(row) if rows: self._box.unselect_all() for row in rows: self._box.select_row(row) if activate: rows[0].activate() else: self._box.unselect_all() def clear(self): """ Clear treeview """ self.stop() for child in self._box.get_children(): child.destroy() if self.__base_mask & SelectionListMask.FASTSCROLL: self.__fastscroll.clear() self.__fastscroll.clear_chars() def select_first(self): """ Select first available item """ try: self._box.unselect_all() row = self._box.get_children()[0] self._box.select_row(row) row.activate() except Exception as e: Logger.warning("SelectionList::select_first(): %s", e) def set_selection_pending_ids(self, pending_ids): """ Set selection pending ids @param pending_ids """ self.__selection_pending_ids = pending_ids def select_pending_ids(self): """ Select pending ids """ self.select_ids(self.__selection_pending_ids) self.__selection_pending_ids = [] def activate_child(self): """ Activated typeahead row """ self._box.unselect_all() for row in self._box.get_children(): style_context = row.get_style_context() if style_context.has_class("typeahead"): row.activate() style_context.remove_class("typeahead") @property def filtered(self): """ Get filtered children @return [Gtk.Widget] """ filtered = [] for child in self._box.get_children(): if isinstance(child, SelectionListRow): filtered.append(child) return filtered @property def overlay(self): """ Get list overlay @return overlay as Gtk.Overlay """ return self.__overlay @property def listbox(self): """ Get listbox @return Gtk.ListBox """ return self._box @property def mask(self): """ Get selection list type @return bit mask """ return self.__mask | self.__base_mask @property def args(self): return None @property def count(self): """ Get items count in list @return int """ return len(self._box.get_children()) @property def selected_ids(self): """ Get selected ids @return [int] """ return [row.id for row in self._box.get_selected_rows()] @property def scrolled(self): """ Get scrolled window @return Gtk.ScrolledWindow """ return self.__scrolled @property def selected_id(self): """ Get selected id @return int """ selected_row = self._box.get_selected_row() return None if selected_row is None else selected_row.id ####################### # PROTECTED # ####################### def _get_child(self, value): """ Get a child for view @param value as [(int, str, optional str)] @return row as SelectionListRow """ (rowid, name, sortname) = value if rowid > 0 and self.mask & SelectionListMask.ARTISTS: used = sortname if sortname else name self.__fastscroll.add_char(used[0]) row = SelectionListRow(rowid, name, sortname, self.mask, self.__height) row.show() self._box.add(row) return row def _scroll_to_child(self, row): """ Scroll to row @param row as SelectionListRow """ coordinates = row.translate_coordinates(self._box, 0, 0) if coordinates: self.__scrolled.get_vadjustment().set_value(coordinates[1]) def _on_primary_long_press_gesture(self, x, y): """ Show row menu @param x as int @param y as int """ self.__popup_menu(y) def _on_primary_press_gesture(self, x, y, event): """ Activate current row @param x as int @param y as int @param event as Gdk.Event """ row = self._box.get_row_at_y(y) if row is not None: (exists, state) = event.get_state() if state & Gdk.ModifierType.CONTROL_MASK or\ state & Gdk.ModifierType.SHIFT_MASK: pass else: self._box.unselect_all() def _on_secondary_press_gesture(self, x, y, event): """ Show row menu @param x as int @param y as int @param event as Gdk.Event """ self.__popup_menu(y) ####################### # PRIVATE # ####################### def __set_rows_mask(self, mask): """ Show labels on child @param status as bool """ for row in self._box.get_children(): row.set_mask(mask) if mask & SelectionListMask.ELLIPSIZE: self.__scrolled.set_hexpand(True) else: self.__scrolled.set_hexpand(False) def __sort_func(self, row_a, row_b): """ Sort rows @param row_a as SelectionListRow @param row_b as SelectionListRow """ a_index = row_a.id b_index = row_b.id # Static vs static if a_index < 0 and b_index < 0: return a_index < b_index # Static entries always on top elif b_index < 0: return True # Static entries always on top if a_index < 0: return False # String comparaison for non static else: if self.mask & SelectionListMask.ARTISTS: a = row_a.sortname b = row_b.sortname else: a = row_a.name b = row_b.name return strcoll(a, b) def __popup_menu(self, y=None, relative=None): """ Show menu at y or row @param y as int @param relative as Gtk.Widget """ if self.__base_mask & SelectionListMask.SIDEBAR: menu = None row_id = None if relative is None: relative = self._box.get_row_at_y(y) if relative is not None: row_id = relative.id if row_id is None: from lollypop.menu_selectionlist import SelectionListMenu menu = SelectionListMenu(self, self.mask, App().window.folded) elif not App().settings.get_value("save-state"): from lollypop.menu_selectionlist import SelectionListRowMenu menu = SelectionListRowMenu(row_id, App().window.folded) if menu is not None: from lollypop.widgets_menu import MenuBuilder menu_widget = MenuBuilder(menu) menu_widget.show() popup_widget(menu_widget, relative, None, None, None) def __on_artist_artwork_changed(self, object, value): """ Update row artwork @param object as GObject.Object @param value as str """ artist = value if object == App().art else None if self.mask & SelectionListMask.ARTISTS: for row in self._box.get_children(): if artist is None: row.set_style(self.__height) row.set_artwork() elif row.name == artist: row.set_artwork() break def __on_show_sidebar_labels_changed(self, settings, value): """ Show/hide labels @param settings as Gio.Settings @param value as str """ self.__on_container_folded(None, App().window.folded) def __on_initialized(self, selectionlist): """ Update fastscroll @param selectionlist as SelectionList """ if self.mask & SelectionListMask.ARTISTS: self.__fastscroll.populate() # Scroll to first selected item for row in self._box.get_selected_rows(): GLib.idle_add(self._scroll_to_child, row) break def __on_container_folded(self, leaflet, folded): """ Update internals @param leaflet as Handy.Leaflet @param folded as Gparam """ self.__base_mask &= ~(SelectionListMask.LABEL | SelectionListMask.ELLIPSIZE) self.__set_rows_mask(self.__base_mask) if self.__overlay is not None: self.__overlay.set_hexpand(folded) if App().window.folded or\ self.__base_mask & SelectionListMask.FASTSCROLL: self.__base_mask |= (SelectionListMask.LABEL | SelectionListMask.ELLIPSIZE) elif App().settings.get_value("show-sidebar-labels"): self.__base_mask |= SelectionListMask.LABEL GLib.timeout_add(200, self.__set_rows_mask, self.__base_mask | self.__mask)
class SelectionList(LazyLoadingView): """ A list for artists/genres """ __gsignals__ = { "populated": (GObject.SignalFlags.RUN_FIRST, None, ()), "pass-focus": (GObject.SignalFlags.RUN_FIRST, None, ()) } def __init__(self, base_type): """ Init Selection list ui @param base_type as SelectionListMask """ LazyLoadingView.__init__(self, ViewType.NOT_ADAPTIVE) self.__base_type = base_type self.__sort = False self.__mask = 0 self.__height = SelectionListRow.get_best_height(self) self._listbox = Gtk.ListBox() self._listbox.connect("button-release-event", self.__on_button_release_event) self._listbox.connect("key-press-event", self.__on_key_press_event) self._listbox.set_sort_func(self.__sort_func) self._listbox.set_selection_mode(Gtk.SelectionMode.MULTIPLE) self._listbox.set_filter_func(self._filter_func) self._listbox.show() self._viewport.add(self._listbox) overlay = Gtk.Overlay.new() overlay.set_hexpand(True) overlay.set_vexpand(True) overlay.show() overlay.add(self._scrolled) self.__fastscroll = FastScroll(self._listbox, self._scrolled) overlay.add_overlay(self.__fastscroll) self.add(overlay) self.get_style_context().add_class("sidebar") App().art.connect("artist-artwork-changed", self.__on_artist_artwork_changed) self.__type_ahead_popover = TypeAheadPopover() self.__type_ahead_popover.set_relative_to(self._scrolled) self.__type_ahead_popover.entry.connect("activate", self.__on_type_ahead_activate) self.__type_ahead_popover.entry.connect("changed", self.__on_type_ahead_changed) def mark_as(self, type): """ Mark list as artists list @param type as SelectionListMask """ self.__mask = self.__base_type | type def populate(self, values): """ Populate view with values @param [(int, str, optional str)], will be deleted """ self.__sort = False self._scrolled.get_vadjustment().set_value(0) self.clear() self.__add_values(values) def remove_value(self, object_id): """ Remove id from list @param object_id as int """ for child in self._listbox.get_children(): if child.id == object_id: child.destroy() break def add_value(self, value): """ Add item to list @param value as (int, str, optional str) """ self.__sort = True # Do not add value if already exists for child in self._listbox.get_children(): if child.id == value[0]: return row = self.__add_value(value[0], value[1], value[2]) row.populate() def update_value(self, object_id, name): """ Update object with new name @param object_id as int @param name as str """ found = False for child in self._listbox.get_children(): if child.id == object_id: child.set_label(name) found = True break if not found: self.__fastscroll.clear() row = self.__add_value(object_id, name, name) row.populate() if self.__mask & SelectionListMask.ARTISTS: self.__fastscroll.populate() def update_values(self, values): """ Update view with values @param [(int, str, optional str)] """ if self.__mask & SelectionListMask.ARTISTS: self.__fastscroll.clear() # Remove not found items value_ids = set([v[0] for v in values]) for child in self._listbox.get_children(): if child.id not in value_ids: self.remove_value(child.id) # Add items which are not already in the list item_ids = set([child.id for child in self._listbox.get_children()]) for value in values: if not value[0] in item_ids: row = self.__add_value(value[0], value[1], value[2]) row.populate() if self.__mask & SelectionListMask.ARTISTS: self.__fastscroll.populate() def select_ids(self, ids=[]): """ Select listbox items @param ids as [int] """ self._listbox.unselect_all() for row in self._listbox.get_children(): if row.id in ids: self._listbox.select_row(row) for row in self._listbox.get_selected_rows(): row.activate() break def grab_focus(self): """ Grab focus on treeview """ self._listbox.grab_focus() def clear(self): """ Clear treeview """ for child in self._listbox.get_children(): child.destroy() self.__fastscroll.clear() self.__fastscroll.clear_chars() def get_headers(self, mask): """ Return headers @param mask as SelectionListMask @return items as [(int, str)] """ lists = ShownLists.get(mask) if mask & SelectionListMask.LIST_ONE and App().window.is_adaptive: lists += [(Type.SEARCH, _("Search"), _("Search"))] lists += [ (Type.CURRENT, _("Current playlist"), _("Current playlist"))] if lists and\ App().settings.get_enum("sidebar-content") !=\ SidebarContent.DEFAULT: lists.append((Type.SEPARATOR, "", "")) return lists def get_playlist_headers(self): """ Return playlist headers @return items as [(int, str)] """ lists = ShownPlaylists.get() if lists and\ App().settings.get_enum("sidebar-content") !=\ SidebarContent.DEFAULT: lists.append((Type.SEPARATOR, "", "")) return lists def select_first(self): """ Select first available item """ try: self._listbox.unselect_all() row = self._listbox.get_children()[0] row.activate() except Exception as e: Logger.warning("SelectionList::select_first(): %s", e) def redraw(self): """ Redraw list """ for row in self._listbox.get_children(): row.set_artwork() @property def type_ahead_popover(self): """ Type ahead popover @return TypeAheadPopover """ return self.__type_ahead_popover @property def listbox(self): """ Get listbox @return Gtk.ListBox """ return self._listbox @property def should_destroy(self): """ True if view should be destroyed @return bool """ return False @property def mask(self): """ Get selection list type @return bit mask """ return self.__mask @property def count(self): """ Get items count in list @return int """ return len(self._listbox.get_children()) @property def selected_ids(self): """ Get selected ids @return array of ids as [int] """ return [row.id for row in self._listbox.get_selected_rows()] ####################### # PRIVATE # ####################### def __scroll_to_row(self, row): """ Scroll to row @param row as SelectionListRow """ coordinates = row.translate_coordinates(self._listbox, 0, 0) if coordinates: self._scrolled.get_vadjustment().set_value(coordinates[1]) def __add_values(self, values): """ Add values to the list @param items as [(int, str, str)] """ if values: (rowid, name, sortname) = values.pop(0) row = self.__add_value(rowid, name, sortname) self._lazy_queue.append(row) GLib.idle_add(self.__add_values, values) else: if self.__mask & SelectionListMask.ARTISTS: self.__fastscroll.populate() self.__sort = True self.emit("populated") self.lazy_loading() # Scroll to first selected item for row in self._listbox.get_selected_rows(): GLib.idle_add(self.__scroll_to_row, row) break def __add_value(self, rowid, name, sortname): """ Add value to list @param rowid as int @param name as str @param sortname as str @return row as SelectionListRow """ if rowid > 0 and sortname and name and\ self.__mask & SelectionListMask.ARTISTS: self.__fastscroll.add_char(sortname[0]) row = SelectionListRow(rowid, name, sortname, self.__mask, self.__height) row.show() self._listbox.add(row) return row def __sort_func(self, row_a, row_b): """ Sort rows @param row_a as SelectionListRow @param row_b as SelectionListRow """ if not self.__sort: return False a_index = row_a.id b_index = row_b.id # Static vs static if a_index < 0 and b_index < 0: return a_index < b_index # Static entries always on top elif b_index < 0: return True # Static entries always on top if a_index < 0: return False # String comparaison for non static else: if self.__mask & SelectionListMask.ARTISTS: a = row_a.sortname b = row_b.sortname else: a = row_a.name b = row_b.name return strcoll(a, b) def __on_key_press_event(self, listbox, event): """ Pass focus as signal @param listbox as Gtk.ListBox @param event as Gdk.Event """ if event.keyval in [Gdk.KEY_Left, Gdk.KEY_Right]: self.emit("pass-focus") def __on_button_release_event(self, listbox, event): """ Handle modifier @param listbox as Gtk.ListBox @param event as Gdk.Event """ if event.button != 1 and\ self.__base_type in [SelectionListMask.LIST_ONE, SelectionListMask.LIST_TWO]: from lollypop.menu_selectionlist import SelectionListMenu from lollypop.widgets_utils import Popover row = listbox.get_row_at_y(event.y) if row is not None: menu = SelectionListMenu(self, row.id, self.mask) popover = Popover() popover.bind_model(menu, None) popover.set_relative_to(listbox) rect = Gdk.Rectangle() rect.x = event.x rect.y = event.y rect.width = rect.height = 1 popover.set_pointing_to(rect) popover.popup() return True elif event.button == 1: state = event.get_state() static_selected = self.selected_ids and self.selected_ids[0] < 0 if (not state & Gdk.ModifierType.CONTROL_MASK and not state & Gdk.ModifierType.SHIFT_MASK) or\ static_selected: listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) row = listbox.get_row_at_y(event.y) if row is not None and not (row.id < 0 and self.selected_ids): # User clicked on random, clear cached one if row.id == Type.RANDOMS: App().albums.clear_cached_randoms() App().tracks.clear_cached_randoms() listbox.set_selection_mode(Gtk.SelectionMode.MULTIPLE) def __on_artist_artwork_changed(self, art, artist): """ Update row @param art as Art @param artist as str """ if self.__mask & SelectionListMask.ARTISTS: for row in self._listbox.get_children(): if row.id >= 0 and row.name == artist: row.set_artwork() break def __on_type_ahead_activate(self, entry): """ Close popover and activate row @param entry as Gtk.Entry """ self._listbox.unselect_all() self.__type_ahead_popover.popdown() for row in self._listbox.get_children(): style_context = row.get_style_context() if style_context.has_class("typeahead"): row.activate() style_context.remove_class("typeahead") def __on_type_ahead_changed(self, entry): """ Search row and scroll down @param entry as Gtk.Entry """ search = entry.get_text().lower() for row in self._listbox.get_children(): style_context = row.get_style_context() style_context.remove_class("typeahead") if not search: return for row in self._listbox.get_children(): if row.name.lower().find(search) != -1: style_context = row.get_style_context() style_context.add_class("typeahead") GLib.idle_add(self.__scroll_to_row, row) break
class SelectionList(Gtk.Overlay): """ A list for artists/genres """ __gsignals__ = { 'item-selected': (GObject.SignalFlags.RUN_FIRST, None, ()), 'populated': (GObject.SignalFlags.RUN_FIRST, None, ()), } def __init__(self, sidebar=True): """ Init Selection list ui @param sidebar as bool """ Gtk.Overlay.__init__(self) self.__was_visible = False self.__timeout = None self.__to_select_ids = [] self.__modifier = False self.__populating = False self.__updating = False # Sort disabled if False self.__is_artists = False builder = Gtk.Builder() builder.add_from_resource('/org/gnome/Lollypop/SelectionList.ui') builder.connect_signals(self) self.__selection = builder.get_object('selection') self.__selection.set_select_function(self.__selection_validation) self.__model = builder.get_object('model') self.__model.set_sort_column_id(0, Gtk.SortType.ASCENDING) self.__model.set_sort_func(0, self.__sort_items) self.__view = builder.get_object('view') if sidebar: self.__view.get_style_context().add_class('sidebar') self.__view.set_row_separator_func(self.__row_separator_func) self.__renderer0 = CellRendererArtist() self.__renderer0.set_property('ellipsize-set', True) self.__renderer0.set_property('ellipsize', Pango.EllipsizeMode.END) self.__renderer1 = Gtk.CellRendererPixbuf() # 16px for Gtk.IconSize.MENU self.__renderer1.set_fixed_size(16, -1) column = Gtk.TreeViewColumn('') column.set_expand(True) column.pack_start(self.__renderer0, True) column.add_attribute(self.__renderer0, 'text', 1) column.add_attribute(self.__renderer0, 'artist', 1) column.add_attribute(self.__renderer0, 'rowid', 0) column.pack_start(self.__renderer1, False) column.add_attribute(self.__renderer1, 'icon-name', 2) self.__view.append_column(column) self.__view.set_property('has_tooltip', True) self.__scrolled = Gtk.ScrolledWindow() self.__scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) self.__scrolled.add(self.__view) self.__scrolled.show() self.add(self.__scrolled) if Gtk.get_minor_version() > 14: self.__fast_scroll = FastScroll(self.__view, self.__model, self.__scrolled) self.add_overlay(self.__fast_scroll) else: self.__fast_scroll = None self.__scrolled.connect('enter-notify-event', self.__on_enter_notify) self.__scrolled.connect('leave-notify-event', self.__on_leave_notify) Lp().art.connect('artist-artwork-changed', self.__on_artist_artwork_changed) def hide(self): """ Hide widget, remember state """ self.__was_visible = self.is_visible() Gtk.Bin.hide(self) @property def was_visible(self): """ True if widget was visible on previous hide """ return self.__was_visible def mark_as_artists(self, is_artists): """ Mark list as artists list @param is_artists as bool """ self.__is_artists = is_artists self.__renderer0.set_is_artists(is_artists) def is_marked_as_artists(self): """ Return True if list is marked as artists """ return self.__is_artists def populate(self, values): """ Populate view with values @param [(int, str, optional str)], will be deleted @thread safe """ if self.__populating: return self.__populating = True if len(self.__model) > 0: self.__updating = True self.__add_values(values) self.emit('populated') self.__updating = False self.__populating = False def remove_value(self, object_id): """ Remove row from model @param object id as int """ for item in self.__model: if item[0] == object_id: self.__model.remove(item.iter) break def add_value(self, value): """ Add item to list @param value as (int, str, optional str) """ # Do not add value if already exists for item in self.__model: if item[0] == value[0]: return self.__updating = True self.__add_value(value) self.__updating = False def update_value(self, object_id, name): """ Update object with new name @param object id as int @param name as str """ self.__updating = True found = False for item in self.__model: if item[0] == object_id: item[1] = name found = True break if not found: self.__add_value((object_id, name)) self.__updating = False def update_values(self, values): """ Update view with values @param [(int, str, optional str)] @thread safe """ self.__updating = True if self.__is_artists and self.__fast_scroll is not None: self.__fast_scroll.clear() # Remove not found items but not devices value_ids = set([v[0] for v in values]) for item in self.__model: if item[0] > Type.DEVICES and not item[0] in value_ids: self.__model.remove(item.iter) # Add items which are not already in the list item_ids = set([i[0] for i in self.__model]) for value in values: if not value[0] in item_ids: self.__add_value(value) if self.__is_artists and self.__fast_scroll is not None: self.__fast_scroll.populate() self.__updating = False def get_value(self, object_id): """ Return value for id @param id as int @return value as string """ for item in self.__model: if item[0] == object_id: return item[1] return '' def will_be_selected(self): """ Return True if list will select items on populate @return selected as bool """ return self.__to_select_ids def select_ids(self, ids): """ Make treeview select first default item @param object id as int """ self.__to_select_ids = [] if ids: try: # Check if items are available for selection iters = [] for i in list(ids): for item in self.__model: if item[0] == i: iters.append(item.iter) ids.remove(i) # Select later if ids: self.__to_select_ids = ids else: for i in iters: self.__selection.select_iter(i) except: self.__last_motion_event = None self.__to_select_ids = ids else: self.__selection.unselect_all() @property def selected_ids(self): """ Get selected ids @return array of ids as [int] """ selected_ids = [] (model, items) = self.__selection.get_selected_rows() if model is not None: for item in items: selected_ids.append(model[item][0]) return selected_ids def clear(self): """ Clear treeview """ self.__updating = True self.__model.clear() if self.__is_artists and self.__fast_scroll is not None: self.__fast_scroll.clear() self.__fast_scroll.clear_chars() self.__fast_scroll.hide() self.__updating = False def get_headers(self): """ Return headers @return items as [(int, str)] """ items = [] items.append((Type.POPULARS, _("Popular albums"))) items.append((Type.LOVED, _("Loved albums"))) items.append((Type.RECENTS, _("Recently added albums"))) items.append((Type.RANDOMS, _("Random albums"))) items.append((Type.PLAYLISTS, _("Playlists"))) items.append((Type.RADIOS, _("Radios"))) if Lp().settings.get_value('show-charts') and\ Lp().settings.get_value('network-access'): items.append((Type.CHARTS, _("The charts"))) if self.__is_artists: items.append((Type.ALL, _("All albums"))) else: items.append((Type.ALL, _("All artists"))) return items def get_pl_headers(self): """ Return playlist headers @return items as [(int, str)] """ items = [] items.append((Type.POPULARS, _("Popular tracks"))) items.append((Type.LOVED, Lp().playlists.LOVED)) items.append((Type.RECENTS, _("Recently played"))) items.append((Type.NEVER, _("Never played"))) items.append((Type.RANDOMS, _("Random tracks"))) items.append((Type.SEPARATOR, '')) return items ####################### # PROTECTED # ####################### def _on_button_press_event(self, view, event): view.grab_focus() state = event.get_state() if state & Gdk.ModifierType.CONTROL_MASK or\ state & Gdk.ModifierType.SHIFT_MASK: self.__modifier = True def _on_button_release_event(self, view, event): self.__modifier = False 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 """ if keyboard: return True (exists, tx, ty, model, path, i) = self.__view.get_tooltip_context( x, y, False) if exists: ctx = self.__view.get_pango_context() layout = Pango.Layout.new(ctx) iterator = self.__model.get_iter(path) if iterator is not None: text = self.__model.get_value(iterator, 1) column = self.__view.get_column(0) (position, width) = column.cell_get_position(self.__renderer0) if Lp().settings.get_value('artist-artwork') and\ self.__is_artists: width -= ArtSize.ARTIST_SMALL +\ CellRendererArtist.xshift * 2 layout.set_ellipsize(Pango.EllipsizeMode.END) if self.__model.get_value(iterator, 0) < 0: width -= 8 layout.set_width(Pango.units_from_double(width)) layout.set_text(text, -1) if layout.is_ellipsized(): tooltip.set_markup(GLib.markup_escape_text(text)) return True return False def _on_selection_changed(self, selection): """ Forward as "item-selected" @param view as Gtk.TreeSelection """ if not self.__updating and not self.__to_select_ids: self.emit('item-selected') ####################### # PRIVATE # ####################### def __add_value(self, value): """ Add value to the model @param value as [int, str, optional str] @thread safe """ if value[1] == "": string = _("Unknown") sort = string else: string = value[1] if len(value) == 3: sort = value[2] else: sort = value[1] if value[0] > 0 and sort and self.__is_artists and\ self.__fast_scroll is not None: self.__fast_scroll.add_char(sort[0]) i = self.__model.append([value[0], string, self.__get_icon_name(value[0]), sort]) if value[0] in self.__to_select_ids: self.__to_select_ids.remove(value[0]) self.__selection.select_iter(i) def __add_values(self, values): """ Add values to the list @param items as [(int,str)] @thread safe """ for value in values: self.__add_value(value) if self.__is_artists and self.__fast_scroll is not None: self.__fast_scroll.populate() self.__to_select_ids = [] def __get_icon_name(self, object_id): """ Return pixbuf for id @param ojbect_id as id """ icon = '' if object_id == Type.POPULARS: icon = 'starred-symbolic' elif object_id == Type.PLAYLISTS: icon = 'emblem-documents-symbolic' elif object_id == Type.ALL: if self.__is_artists: icon = 'media-optical-cd-audio-symbolic' else: icon = 'avatar-default-symbolic' elif object_id == Type.COMPILATIONS: icon = 'system-users-symbolic' elif object_id == Type.RECENTS: icon = 'document-open-recent-symbolic' elif object_id == Type.RADIOS: icon = 'audio-input-microphone-symbolic' elif object_id < Type.DEVICES: icon = 'multimedia-player-symbolic' elif object_id == Type.RANDOMS: icon = 'media-playlist-shuffle-symbolic' elif object_id == Type.LOVED: icon = 'emblem-favorite-symbolic' elif object_id == Type.NEVER: icon = 'document-new-symbolic' elif object_id == Type.CHARTS: icon = 'application-rss+xml-symbolic' elif object_id == Type.SPOTIFY: icon = 'lollypop-spotify-symbolic' elif object_id == Type.ITUNES: icon = 'lollypop-itunes-symbolic' elif object_id == Type.LASTFM: icon = 'lollypop-lastfm-symbolic' return icon def __sort_items(self, model, itera, iterb, data): """ Sort model """ if not self.__updating: return False a_index = model.get_value(itera, 0) b_index = model.get_value(iterb, 0) # Static vs static if a_index < 0 and b_index < 0: return a_index < b_index # Static entries always on top elif b_index < 0: return True # Static entries always on top if a_index < 0: return False # String comparaison for non static else: if self.__is_artists: a = Lp().artists.get_sortname(a_index) b = Lp().artists.get_sortname(b_index) else: a = model.get_value(itera, 1) b = model.get_value(iterb, 1) return strcoll(a, b) def __row_separator_func(self, model, iterator): """ Draw a separator if needed @param model as Gtk.TreeModel @param iterator as Gtk.TreeIter """ return model.get_value(iterator, 0) == Type.SEPARATOR def __selection_validation(self, selection, model, path, current): """ Check if selection is valid @param selection as Gtk.TreeSelection @param model as Gtk.TreeModel @param path as Gtk.TreePath @param current as bool @return bool """ ids = self.selected_ids if not ids: return True elif self.__modifier: iterator = self.__model.get_iter(path) value = self.__model.get_value(iterator, 0) if value < 0 and len(ids) > 1: return False else: static = False for i in ids: if i < 0: static = True if static: return False elif value > 0: return True else: return False else: return True def __on_enter_notify(self, widget, event): """ Disable shortcuts @param widget as Gtk.widget @param event as Gdk.Event """ if widget.get_vadjustment().get_upper() >\ widget.get_allocated_height() and self.__is_artists and\ self.__fast_scroll is not None: self.__fast_scroll.show() # FIXME Not needed with GTK >= 3.18 Lp().window.enable_global_shortcuts(False) def __on_leave_notify(self, widget, event): """ Hide popover @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.__is_artists and self.__fast_scroll is not None: self.__fast_scroll.hide() # FIXME Not needed with GTK >= 3.18 Lp().window.enable_global_shortcuts(True) def __on_artist_artwork_changed(self, art, artist): """ Update row """ if self.__is_artists: self.__renderer0.on_artist_artwork_changed(artist) for item in self.__model: if item[1] == artist: item[1] = artist break
class SelectionList(BaseView, Gtk.Overlay): """ A list for artists/genres """ __gsignals__ = { "item-selected": (GObject.SignalFlags.RUN_FIRST, None, ()), "populated": (GObject.SignalFlags.RUN_FIRST, None, ()), "pass-focus": (GObject.SignalFlags.RUN_FIRST, None, ()) } def __init__(self, base_type): """ Init Selection list ui @param base_type as SelectionListMask """ Gtk.Overlay.__init__(self) BaseView.__init__(self) self.__base_type = base_type self.__timeout = None self.__modifier = False self.__populating = False self.__mask = 0 builder = Gtk.Builder() builder.add_from_resource("/org/gnome/Lollypop/SelectionList.ui") builder.connect_signals(self) self.__selection = builder.get_object("selection") self.__selection.set_select_function(self.__selection_validation) self.__selection.connect("changed", self.__on_selection_changed) self.__model = Gtk.ListStore(int, str, str, str) self.__model.set_sort_column_id(0, Gtk.SortType.ASCENDING) self.__model.set_sort_func(0, self.__sort_items) self.__view = builder.get_object("view") self.__view.set_model(self.__model) if base_type in [ SelectionListMask.LIST_ONE, SelectionListMask.LIST_TWO ]: self.__view.get_style_context().add_class("sidebar") self.__view.set_row_separator_func(self.__row_separator_func) self.__renderer0 = CellRendererArtist() self.__renderer0.set_property("ellipsize-set", True) self.__renderer0.set_property("ellipsize", Pango.EllipsizeMode.END) self.__renderer1 = Gtk.CellRendererPixbuf() # 16px for Gtk.IconSize.MENU self.__renderer1.set_fixed_size(16, -1) column = Gtk.TreeViewColumn("") column.set_expand(True) column.pack_start(self.__renderer0, True) column.add_attribute(self.__renderer0, "text", 1) column.add_attribute(self.__renderer0, "artist", 1) column.add_attribute(self.__renderer0, "rowid", 0) column.pack_start(self.__renderer1, False) column.add_attribute(self.__renderer1, "icon-name", 2) self.__view.append_column(column) self.__view.set_property("has_tooltip", True) self.__scrolled = Gtk.ScrolledWindow() self.__scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) self.__scrolled.add(self.__view) self.__scrolled.show() self.add(self.__scrolled) self.__fast_scroll = FastScroll(self.__view, self.__model, self.__scrolled) self.add_overlay(self.__fast_scroll) self.__scrolled.connect("enter-notify-event", self.__on_enter_notify) self.__scrolled.connect("leave-notify-event", self.__on_leave_notify) App().art.connect("artist-artwork-changed", self.__on_artist_artwork_changed) def mark_as(self, type): """ Mark list as artists list @param type as SelectionListMask """ self.__mask = self.__base_type | type self.__renderer0.set_is_artists(type & SelectionListMask.ARTISTS) def populate(self, values): """ Populate view with values @param [(int, str, optional str)], will be deleted """ if self.__populating: return self.__populating = True self.__selection.disconnect_by_func(self.__on_selection_changed) self.clear() self.__add_values(values) self.__selection.connect("changed", self.__on_selection_changed) self.__populating = False self.emit("populated") def remove_value(self, object_id): """ Remove row from model @param object id as int """ for item in self.__model: if item[0] == object_id: self.__model.remove(item.iter) break def add_value(self, value): """ Add item to list @param value as (int, str, optional str) """ # Do not add value if already exists for item in self.__model: if item[0] == value[0]: return self.__add_value(value) def update_value(self, object_id, name): """ Update object with new name @param object id as int @param name as str """ found = False for item in self.__model: if item[0] == object_id: item[1] = name found = True break if not found: self.__add_value((object_id, name, name)) def update_values(self, values): """ Update view with values @param [(int, str, optional str)] """ update_fast_scroll = self.__mask & SelectionListMask.ARTISTS and\ self.__fast_scroll is not None if update_fast_scroll: self.__fast_scroll.clear() # Remove not found items but not devices value_ids = set([v[0] for v in values]) for item in self.__model: if item[0] > Type.DEVICES and not item[0] in value_ids: self.__model.remove(item.iter) # Add items which are not already in the list item_ids = set([i[0] for i in self.__model]) for value in values: if not value[0] in item_ids: self.__add_value(value) if update_fast_scroll: self.__fast_scroll.populate() def get_value(self, object_id): """ Return value for id @param id as int @return value as string """ for item in self.__model: if item[0] == object_id: return item[1] return "" def select_ids(self, ids=[]): """ Make treeview select first default item @param object id as int """ if ids: try: # Check if items are available for selection items = [] for i in list(ids): for item in self.__model: if item[0] == i: items.append(item) self.__selection.disconnect_by_func( self.__on_selection_changed) for item in items: self.__selection.select_iter(item.iter) self.__selection.connect("changed", self.__on_selection_changed) self.emit("item-selected") # Scroll to first item if items: self.__view.scroll_to_cell(items[0].path, None, True, 0, 0) except: self.__last_motion_event = None else: self.__selection.unselect_all() def grab_focus(self): """ Grab focus on treeview """ self.__view.grab_focus() def clear(self): """ Clear treeview """ self.__model.clear() if self.__fast_scroll is not None: self.__fast_scroll.clear() self.__fast_scroll.clear_chars() self.__fast_scroll.hide() def get_headers(self, mask): """ Return headers @param mask as SelectionListMask @return items as [(int, str)] """ lists = ShownLists.get(mask) if mask & SelectionListMask.LIST_ONE and App().window.is_adaptive: lists += [(Type.SEARCH, _("Search"), _("Search"))] lists += [(Type.CURRENT, _("Current playlist"), _("Current playlist"))] if lists: lists.append((Type.SEPARATOR, "", "")) return lists def get_playlist_headers(self): """ Return playlist headers @return items as [(int, str)] """ lists = ShownPlaylists.get() if lists: lists.append((Type.SEPARATOR, "", "")) return lists def select_first(self): """ Select first available item """ self.__selection.select_iter(self.__model[0].iter) def redraw(self): """ Redraw list """ self.__view.set_model(None) self.__view.set_model(self.__model) @property def should_destroy(self): """ True if view should be destroyed @return bool """ return False @property def mask(self): """ Get selection list type @return bit mask """ return self.__mask @property def count(self): """ Get items count in list @return int """ return len(self.__model) @property def selected_ids(self): """ Get selected ids @return array of ids as [int] """ selected_ids = [] (model, items) = self.__selection.get_selected_rows() if model is not None: for item in items: selected_ids.append(model[item][0]) return selected_ids ####################### # PROTECTED # ####################### def _on_key_press_event(self, entry, event): """ Forward to popover history listbox if needed @param entry as Gtk.Entry @param event as Gdk.Event """ if event.keyval in [Gdk.KEY_Left, Gdk.KEY_Right]: self.emit("pass-focus") def _on_button_press_event(self, view, event): """ Handle modifier @param view as Gtk.TreeView @param event as Gdk.Event """ if event.button == 1: view.grab_focus() state = event.get_state() if state & Gdk.ModifierType.CONTROL_MASK or\ state & Gdk.ModifierType.SHIFT_MASK: self.__modifier = True elif self.__base_type in [ SelectionListMask.LIST_ONE, SelectionListMask.LIST_TWO ]: info = view.get_dest_row_at_pos(event.x, event.y) if info is not None: from lollypop.pop_menu_views import ViewsMenuPopover (path, position) = info iterator = self.__model.get_iter(path) rowid = self.__model.get_value(iterator, 0) popover = ViewsMenuPopover(self, rowid, self.mask) popover.set_relative_to(view) rect = Gdk.Rectangle() rect.x = event.x rect.y = event.y rect.width = rect.height = 1 popover.set_pointing_to(rect) popover.popup() return True def _on_button_release_event(self, view, event): """ Handle modifier @param view as Gtk.TreeView @param event as Gdk.Event """ self.__modifier = False 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 """ def shown_sidebar_tooltip(): App().shown_sidebar_tooltip = True if keyboard: return True (exists, tx, ty, model, path, i) = self.__view.get_tooltip_context(x, y, False) if exists: ctx = self.__view.get_pango_context() layout = Pango.Layout.new(ctx) iterator = self.__model.get_iter(path) if iterator is not None: text = self.__model.get_value(iterator, 1) column = self.__view.get_column(0) (position, width) = column.cell_get_position(self.__renderer0) if App().settings.get_value("artist-artwork") and\ self.__mask & SelectionListMask.ARTISTS: width -= ArtSize.ARTIST_SMALL +\ CellRendererArtist.xshift * 2 layout.set_ellipsize(Pango.EllipsizeMode.END) if self.__model.get_value(iterator, 0) < 0: width -= 8 layout.set_width(Pango.units_from_double(width)) layout.set_text(text, -1) if layout.is_ellipsized(): tooltip.set_markup(GLib.markup_escape_text(text)) return True elif not App().shown_sidebar_tooltip: GLib.timeout_add(1000, shown_sidebar_tooltip) tooltip.set_markup(_("Right click to configure")) return True return False ####################### # PRIVATE # ####################### def __add_value(self, value): """ Add value to the model @param value as [int, str, optional str] """ item_id = value[0] name = value[1] sort = value[2] if name == "": name = _("Unknown") icon_name = "dialog-warning-symbolic" icon_name = get_icon_name(item_id, self.__mask) if item_id > 0 and sort and\ self.__mask & SelectionListMask.ARTISTS and\ self.__fast_scroll is not None: self.__fast_scroll.add_char(sort[0]) self.__model.append([item_id, name, icon_name, sort]) def __add_values(self, values): """ Add values to the list @param items as [(int,str)] """ for value in values: self.__add_value(value) if self.__mask & SelectionListMask.ARTISTS and\ self.__fast_scroll is not None: self.__fast_scroll.populate() def __sort_items(self, model, itera, iterb, data): """ Sort model """ if self.__populating: return False a_index = model.get_value(itera, 0) b_index = model.get_value(iterb, 0) # Static vs static if a_index < 0 and b_index < 0: return a_index < b_index # Static entries always on top elif b_index < 0: return True # Static entries always on top if a_index < 0: return False # String comparaison for non static else: if self.__mask & SelectionListMask.ARTISTS: a = App().artists.get_sortname(a_index) b = App().artists.get_sortname(b_index) else: a = model.get_value(itera, 1) b = model.get_value(iterb, 1) return strcoll(a, b) def __row_separator_func(self, model, iterator): """ Draw a separator if needed @param model as Gtk.TreeModel @param iterator as Gtk.TreeIter """ return model.get_value(iterator, 0) == Type.SEPARATOR def __selection_validation(self, selection, model, path, current): """ Check if selection is valid @param selection as Gtk.TreeSelection @param model as Gtk.TreeModel @param path as Gtk.TreePath @param current as bool @return bool """ ids = self.selected_ids if not ids: return True elif self.__modifier: iterator = self.__model.get_iter(path) value = self.__model.get_value(iterator, 0) if value < 0 and len(ids) > 1: return False else: static = False for i in ids: if i < 0: static = True if static: return False elif value > 0: return True else: return False else: return True def __on_enter_notify(self, widget, event): """ Disable shortcuts @param widget as Gtk.widget @param event as Gdk.Event """ if widget.get_vadjustment().get_upper() >\ widget.get_allocated_height() and\ self.__mask & SelectionListMask.ARTISTS and\ self.__fast_scroll is not None: self.__fast_scroll.show() def __on_leave_notify(self, widget, event): """ Hide popover @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.__mask & SelectionListMask.ARTISTS\ and self.__fast_scroll is not None: self.__fast_scroll.hide() def __on_artist_artwork_changed(self, art, artist): """ Update row """ if self.__mask & SelectionListMask.ARTISTS: self.__renderer0.on_artist_artwork_changed(artist) for item in self.__model: if item[1] == artist: item[1] = artist break def __on_selection_changed(self, selection): """ Forward as "item-selected" @param view as Gtk.TreeSelection """ self.emit("item-selected")