コード例 #1
0
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
コード例 #2
0
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)
コード例 #3
0
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
コード例 #4
0
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
コード例 #5
0
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")