class SelectionList(Gtk.ScrolledWindow): """ A list for artists/genres """ __gsignals__ = { 'item-selected': (GObject.SignalFlags.RUN_FIRST, None, ()), 'populated': (GObject.SignalFlags.RUN_FIRST, None, ()), } def __init__(self, mode): """ Init Selection list ui @param mode as SelectionMode """ Gtk.ScrolledWindow.__init__(self) self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) self._mode = mode self._last_motion_event = None self._previous_motion_y = 0.0 self._timeout = None self._to_select_ids = [] self._modifier = False self._updating = False # Sort disabled if False self._is_artists = False self._popover = SelectionPopover() 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') 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() 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.add(self._view) self.connect('motion_notify_event', self._on_motion_notify) self.get_vadjustment().connect('value_changed', self._on_scroll) self.connect('enter-notify-event', self._on_enter_notify) self.connect('leave-notify-event', self._on_leave_notify) Lp().art.connect('artist-artwork-changed', self._on_artist_artwork_changed) 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)], will be deleted @thread safe """ if len(self._model) > 0: self._updating = True self._add_values(values) self.emit('populated') self._updating = False def remove(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) """ 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)] @thread safe """ self._updating = True # 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) 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: 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() def get_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() self._updating = False ####################### # PRIVATE # ####################### def _add_value(self, value): """ Add value to the model @param value as [int, str] @thread safe """ if value[1] == "": string = _("Unknown") else: string = value[1] i = self._model.append([value[0], string, self._get_icon_name(value[0])]) 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) 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' 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.get_selected_ids() if not ids or self._mode == SelectionMode.NORMAL: 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 _hide_popover(self): """ Hide popover """ self._popover.hide() self._timeout = None 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_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') def _on_enter_notify(self, widget, event): """ Disable shortcuts @param widget as Gtk.widget @param event as Gdk.Event """ # FIXME Not needed with GTK >= 3.18 Lp().window.enable_global_shorcuts(False) def _on_leave_notify(self, widget, event): """ Hide popover @param widget as Gtk.widget @param event as GdK.Event """ # FIXME Not needed with GTK >= 3.18 Lp().window.enable_global_shorcuts(True) self._hide_popover() self._last_motion_event = None def _on_motion_notify(self, widget, event): """ Set motion event @param widget as Gtk.widget @param event as Gdk.Event """ if self._timeout is None: self._timeout = GLib.timeout_add(500, self._hide_popover) if event.x < 0.0 or event.y < 0.0: self._last_motion_event = None return if self._last_motion_event is None: self._last_motion_event = MotionEvent() self._last_motion_event.x = event.x self._last_motion_event.y = event.y def _on_scroll(self, adj): """ Show a popover with current letter @param adj as Gtk.Adjustement """ # Only show if scrolled window is huge if adj.get_upper() < adj.get_page_size() * 3: return if self._last_motion_event is None: return if self._timeout is not None: GLib.source_remove(self._timeout) self._timeout = None dest_row = self._view.get_dest_row_at_pos(self._last_motion_event.x, self._last_motion_event.y) if dest_row is None: return row = dest_row[0] if row is None: return row_iter = self._model.get_iter(row) if row_iter is None or self._model.get_value(row_iter, 0) < 0: return # We need to get artist sortname if self._is_artists: rowid = self._model.get_value(row_iter, 0) text = Lp().artists.get_sortname(rowid) else: text = self._model.get_value(row_iter, 1) if text: self._popover.set_text(" %s " % text[0].upper()) self._popover.set_relative_to(self) r = Gdk.Rectangle() r.x = self.get_allocated_width() r.y = self._last_motion_event.y r.width = 1 r.height = 1 self._popover.set_pointing_to(r) self._popover.set_position(Gtk.PositionType.RIGHT) self._popover.show() 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 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(escape(text)) return True return False
class SelectionList(Gtk.Bin): """ A list for artists/genres """ __gsignals__ = { 'item-selected': (GObject.SignalFlags.RUN_FIRST, None, ()), 'populated': (GObject.SignalFlags.RUN_FIRST, None, ()), } def __init__(self, fast_scroll): """ Init Selection list ui @param fast scroll as bool """ Gtk.Bin.__init__(self) self.__last_motion_event = None self.__previous_motion_y = 0.0 self.__timeout = None self.__to_select_ids = [] self.__modifier = False self.__populating = False self.__updating = False # Sort disabled if False self.__is_artists = False self.__fast_scroll = fast_scroll # Show button to scroll to top self.__popover = SelectionPopover() 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') 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() if self.__fast_scroll: self.__scrolled.set_vexpand(True) self.__scrolled.set_hexpand(True) grid = Gtk.Grid() grid.set_orientation(Gtk.Orientation.VERTICAL) grid.show() self.__button = Gtk.Button.new_from_icon_name( 'go-up-symbolic', Gtk.IconSize.MENU) self.__button.get_style_context().add_class('fast-scroll-button') self.__button.connect('clicked', self.__on_button_clicked) grid.add(self.__scrolled) grid.add(self.__button) self.add(grid) else: self.add(self.__scrolled) self.connect('motion_notify_event', self.__on_motion_notify) self.__scrolled.get_vadjustment().connect('value_changed', self.__on_scroll) 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 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)], 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) """ # 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)] @thread safe """ self.__updating = True # 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) 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() self.__updating = False def get_headers(self): """ Return list one headers @return items as [(int, str)] """ items = [] items.append((Type.POPULARS, _("Popular 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 ####################### # 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] @thread safe """ if value[1] == "": string = _("Unknown") else: string = value[1] i = self.__model.append( [value[0], string, self.__get_icon_name(value[0])]) 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) 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' 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 __hide_popover(self): """ Hide popover """ self.__popover.hide() self.__timeout = None def __on_enter_notify(self, widget, event): """ Disable shortcuts @param widget as Gtk.widget @param event as Gdk.Event """ # 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 """ # FIXME Not needed with GTK >= 3.18 Lp().window.enable_global_shortcuts(True) self.__hide_popover() self.__last_motion_event = None def __on_motion_notify(self, widget, event): """ Set motion event @param widget as Gtk.widget @param event as Gdk.Event """ if self.__timeout is None: self.__timeout = GLib.timeout_add(500, self.__hide_popover) if event.x < 0.0 or event.y < 0.0: self.__last_motion_event = None return if self.__last_motion_event is None: self.__last_motion_event = MotionEvent() self.__last_motion_event.x = event.x self.__last_motion_event.y = event.y def __on_scroll(self, adj): """ Show a popover with current letter @param adj as Gtk.Adjustement """ if self.__fast_scroll: top_path = self.__view.get_dest_row_at_pos(0, 0) if top_path is not None: row = top_path[0] if row is not None: row_iter = self.__model.get_iter(row) rowid = self.__model.get_value(row_iter, 0) if rowid < 0: self.__button.hide() else: self.__button.show() # Only show if self.__scrolled window is huge if adj.get_upper() < adj.get_page_size() * 3: return if self.__last_motion_event is None: return if self.__timeout is not None: GLib.source_remove(self.__timeout) self.__timeout = None path = self.__view.get_dest_row_at_pos(self.__last_motion_event.x, self.__last_motion_event.y) if path is None: return row = path[0] if row is None: return row_iter = self.__model.get_iter(row) if row_iter is None or self.__model.get_value(row_iter, 0) < 0: return # We need to get artist sortname if self.__is_artists: rowid = self.__model.get_value(row_iter, 0) text = Lp().artists.get_sortname(rowid) else: text = self.__model.get_value(row_iter, 1) if text: self.__popover.set_text(" %s " % text[0].upper()) self.__popover.set_relative_to(self) r = Gdk.Rectangle() r.x = self.get_allocated_width() r.y = self.__last_motion_event.y r.width = 1 r.height = 1 self.__popover.set_pointing_to(r) self.__popover.set_position(Gtk.PositionType.RIGHT) self.__popover.show() def __on_button_clicked(self, button): """ Move scrollbar at top """ self.__scrolled.get_vadjustment().set_value(0) 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(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(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(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")