# Copyright (C) 2011 Canonical # # Authors: # Matthew McGowan # Michael Vogt # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; version 3. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from gi.repository import Gtk, Gdk, GObject, GLib, Pango from softwarecenter.utils import utf8 from softwarecenter.ui.gtk3.em import EM from softwarecenter.ui.gtk3.models.appstore2 import CategoryRowReference from stars import StarRenderer, StarSize class CellButtonIDs: INFO = 0 ACTION = 1 # custom cell renderer to support dynamic grow class CellRendererAppView(Gtk.CellRendererText): # x, y offsets for the overlay icon OVERLAY_XO = OVERLAY_YO = 2 # size of the install overlay icon OVERLAY_SIZE = 16 # ratings MAX_STARS = 5 STAR_SIZE = EM # initialize declared properties (LP: #965937) application = GObject.Property( type=GObject.TYPE_PYOBJECT, nick='document', blurb='a xapian document containing pkg information', flags=(GObject.PARAM_READWRITE | GObject.PARAM_CONSTRUCT), default=None) isactive = GObject.Property( type=bool, nick='isactive', blurb='is cell active/selected', flags=(GObject.PARAM_READWRITE | GObject.PARAM_CONSTRUCT), default=False) def __init__(self, icons, layout, show_ratings, overlay_icon_name): Gtk.CellRendererText.__init__(self) # the icon pixbuf to be displayed in the row self.icon = None # geometry-state values self.pixbuf_width = 0 self.apptitle_width = 0 self.apptitle_height = 0 self.normal_height = 0 self.selected_height = 0 self.show_ratings = show_ratings self.icon_x_offset = 0 self.icon_y_offset = 0 # button packing self.button_spacing = 0 self._buttons = { Gtk.PackType.START: [], Gtk.PackType.END: [] } self._all_buttons = {} # cache a layout self._layout = layout # star painter, paints stars self._stars = StarRenderer() self._stars.size = StarSize.SMALL # icon/overlay jazz try: self._installed = icons.load_icon(overlay_icon_name, self.OVERLAY_SIZE, 0) except GObject.GError: # icon not present in theme, probably because running uninstalled self._installed = icons.load_icon('emblem-system', self.OVERLAY_SIZE, 0) def _layout_get_pixel_width(self, layout): return layout.get_size()[0] / Pango.SCALE def _layout_get_pixel_height(self, layout): return layout.get_size()[1] / Pango.SCALE def _render_category(self, context, cr, app, cell_area, layout, xpad, ypad, is_rtl): layout.set_markup('%s' % app.display_name, -1) # work out max allowable layout width lw = self._layout_get_pixel_width(layout) lh = self._layout_get_pixel_height(layout) if not is_rtl: x = cell_area.x else: x = cell_area.x + cell_area.width - lw y = cell_area.y + (cell_area.height - lh) / 2 Gtk.render_layout(context, cr, x, y, layout) def _render_price(self, context, cr, app, layout, cell_area, xpad, ypad, is_rtl): layout.set_markup("%s" % self.model.get_display_price(app)) if is_rtl: x = cell_area.x + xpad else: x = (cell_area.x + cell_area.width - xpad - self._layout_get_pixel_width(layout)) Gtk.render_layout(context, cr, x, ypad + cell_area.y, layout) def _render_icon(self, cr, app, cell_area, xpad, ypad, is_rtl): # calc offsets so icon is nicely centered self.icon = self.model.get_icon(app) self.icon_x_offset = xpad + cell_area.x self.icon_y_offset = ypad + cell_area.y xo = (self.pixbuf_width - self.icon.get_width()) / 2 if not is_rtl: x = cell_area.x + xo + xpad else: x = cell_area.x + cell_area.width + xo - self.pixbuf_width - xpad y = cell_area.y + ypad # draw appicon pixbuf Gdk.cairo_set_source_pixbuf(cr, self.icon, x, y) cr.paint() # draw overlay if application is installed if self.model.is_installed(app): if not is_rtl: x += (self.pixbuf_width - self.OVERLAY_SIZE + self.OVERLAY_XO) else: x -= self.OVERLAY_XO y += (self.pixbuf_width - self.OVERLAY_SIZE + self.OVERLAY_YO) Gdk.cairo_set_source_pixbuf(cr, self._installed, x, y) cr.paint() def _render_summary(self, context, cr, app, cell_area, layout, xpad, ypad, star_width, is_rtl): layout.set_markup(self.model.get_markup(app), -1) # work out max allowable layout width layout.set_width(-1) lw = self._layout_get_pixel_width(layout) max_layout_width = (cell_area.width - self.pixbuf_width - 3 * xpad - star_width) max_layout_width = cell_area.width - self.pixbuf_width - 3 * xpad stats = self.model.get_review_stats(app) if self.show_ratings and stats: max_layout_width -= star_width + 6 * xpad if (self.props.isactive and self.model.get_transaction_progress(app) > 0): action_btn = self.get_button_by_name(CellButtonIDs.ACTION) max_layout_width -= (xpad + action_btn.width) if lw >= max_layout_width: layout.set_width((max_layout_width) * Pango.SCALE) layout.set_ellipsize(Pango.EllipsizeMode.END) lw = max_layout_width apptitle_extents = layout.get_line_readonly(0).get_pixel_extents()[1] self.apptitle_width = apptitle_extents.width self.apptitle_height = apptitle_extents.height if not is_rtl: x = cell_area.x + 2 * xpad + self.pixbuf_width else: x = (cell_area.x + cell_area.width - lw - self.pixbuf_width - 2 * xpad) y = cell_area.y + ypad Gtk.render_layout(context, cr, x, y, layout) def _render_rating(self, context, cr, app, cell_area, layout, xpad, ypad, star_width, star_height, is_rtl): stats = self.model.get_review_stats(app) if not stats: return sr = self._stars if not is_rtl: x = (cell_area.x + 3 * xpad + self.pixbuf_width + self.apptitle_width) else: x = (cell_area.x + cell_area.width - 3 * xpad - self.pixbuf_width - self.apptitle_width - star_width) y = cell_area.y + ypad + (self.apptitle_height - self.STAR_SIZE) / 2 sr.rating = stats.ratings_average sr.render_star(context, cr, x, y) # and nr-reviews in parenthesis to the right of the title nreviews = stats.ratings_total s = "(%i)" % nreviews layout.set_markup("%s" % s, -1) if not is_rtl: x += xpad + star_width else: x -= xpad + self._layout_get_pixel_width(layout) context.save() context.add_class("cellrenderer-avgrating-label") Gtk.render_layout(context, cr, x, y, layout) context.restore() def _render_progress(self, context, cr, progress, cell_area, ypad, is_rtl): percent = progress * 0.01 # per the spec, the progressbar should be the width of the action # button action_btn = self.get_button_by_name(CellButtonIDs.ACTION) x, _, w, h = action_btn.allocation # shift the bar to the top edge y = cell_area.y + ypad context.save() context.add_class("trough") Gtk.render_background(context, cr, x, y, w, h) Gtk.render_frame(context, cr, x, y, w, h) context.restore() bar_size = w * percent context.save() context.add_class("progressbar") if (bar_size > 0): if is_rtl: x += (w - bar_size) Gtk.render_activity(context, cr, x, y, bar_size, h) context.restore() def _render_buttons(self, context, cr, cell_area, layout, xpad, ypad, is_rtl): # layout buttons and paint y = cell_area.y + cell_area.height - ypad spacing = self.button_spacing if not is_rtl: start = Gtk.PackType.START end = Gtk.PackType.END xs = cell_area.x + 2 * xpad + self.pixbuf_width xb = cell_area.x + cell_area.width - xpad else: start = Gtk.PackType.END end = Gtk.PackType.START xs = cell_area.x + xpad xb = cell_area.x + cell_area.width - 2 * xpad - self.pixbuf_width for btn in self._buttons[start]: btn.set_position(xs, y - btn.height) btn.render(context, cr, layout) xs += btn.width + spacing for btn in self._buttons[end]: xb -= btn.width btn.set_position(xb, y - btn.height) btn.render(context, cr, layout) xb -= spacing def set_pixbuf_width(self, w): self.pixbuf_width = w def set_button_spacing(self, spacing): self.button_spacing = spacing def get_button_by_name(self, name): if name in self._all_buttons: return self._all_buttons[name] def get_buttons(self): btns = () for k, v in self._buttons.items(): btns += tuple(v) return btns def button_pack(self, btn, pack_type=Gtk.PackType.START): self._buttons[pack_type].append(btn) self._all_buttons[btn.name] = btn def button_pack_start(self, btn): self.button_pack(btn, Gtk.PackType.START) def button_pack_end(self, btn): self.button_pack(btn, Gtk.PackType.END) def do_set_property(self, pspec, value): setattr(self, pspec.name, value) def do_get_property(self, pspec): return getattr(self, pspec.name) def do_get_preferred_height_for_width(self, treeview, width): if not self.get_properties("isactive")[0]: return self.normal_height, self.normal_height return self.selected_height, self.selected_height def do_render(self, cr, widget, bg_area, cell_area, flags): app = self.props.application if not app: return self.model = widget.appmodel context = widget.get_style_context() xpad = self.get_property('xpad') ypad = self.get_property('ypad') star_width, star_height = self._stars.get_visible_size(context) is_rtl = widget.get_direction() == Gtk.TextDirection.RTL layout = self._layout # important! ensures correct text rendering, esp. when using hicolor # theme #~ if (flags & Gtk.CellRendererState.SELECTED) != 0: #~ # this follows the behaviour that gtk+ uses for states in #~ # treeviews #~ if widget.has_focus(): #~ state = Gtk.StateFlags.SELECTED #~ else: #~ state = Gtk.StateFlags.ACTIVE #~ else: #~ state = Gtk.StateFlags.NORMAL context.save() #~ context.set_state(state) if isinstance(app, CategoryRowReference): self._render_category(context, cr, app, cell_area, layout, xpad, ypad, is_rtl) return self._render_icon(cr, app, cell_area, xpad, ypad, is_rtl) self._render_summary(context, cr, app, cell_area, layout, xpad, ypad, star_width, is_rtl) # only show ratings if we have one if self.show_ratings: self._render_rating(context, cr, app, cell_area, layout, xpad, ypad, star_width, star_height, is_rtl) progress = self.model.get_transaction_progress(app) if progress > 0: self._render_progress(context, cr, progress, cell_area, ypad, is_rtl) elif self.model.is_purchasable(app): self._render_price(context, cr, app, layout, cell_area, xpad, ypad, is_rtl) # below is the stuff that is only done for the active cell if not self.props.isactive: return self._render_buttons(context, cr, cell_area, layout, xpad, ypad, is_rtl) context.restore() class CellButtonRenderer(object): def __init__(self, widget, name, use_max_variant_width=True): # use_max_variant_width is currently ignored. assumed to be True self.name = name self.markup_variants = {} self.current_variant = None self.xpad = 12 self.ypad = 4 self.allocation = [0, 0, 1, 1] self.state = Gtk.StateFlags.NORMAL self.has_focus = False self.visible = True self.widget = widget def _layout_reset(self, layout): layout.set_width(-1) layout.set_ellipsize(Pango.EllipsizeMode.NONE) @property def x(self): return self.allocation[0] @property def y(self): return self.allocation[1] @property def width(self): return self.allocation[2] @property def height(self): return self.allocation[3] def configure_geometry(self, layout): self._layout_reset(layout) max_size = (0, 0) for k, variant in self.markup_variants.items(): safe_markup = GLib.markup_escape_text(utf8(variant)) layout.set_markup(safe_markup, -1) size = layout.get_size() max_size = max(max_size, size) w, h = max_size w /= Pango.SCALE h /= Pango.SCALE self.set_size(w + 2 * self.xpad, h + 2 * self.ypad) def point_in(self, px, py): x, y, w, h = self.allocation return (px >= x and px <= x + w and py >= y and py <= y + h) def get_size(self): return self.allocation[2:] def set_position(self, x, y): self.allocation[:2] = int(x), int(y) def set_size(self, w, h): self.allocation[2:] = int(w), int(h) def set_state(self, state): if not isinstance(state, Gtk.StateFlags): msg = ("state should be of type Gtk.StateFlags, got %s" % type(state)) raise TypeError(msg) elif state == self.state: return self.state = state self.widget.queue_draw_area(*self.allocation) def set_sensitive(self, is_sensitive): if is_sensitive: state = Gtk.StateFlags.PRELIGHT else: state = Gtk.StateFlags.INSENSITIVE self.set_state(state) def show(self): self.visible = True def hide(self): self.visible = False def set_markup(self, markup): self.markup_variant = (markup,) def set_markup_variants(self, markup_variants): if not isinstance(markup_variants, dict): msg = type(markup_variants) raise TypeError("Expects a dict object, got %s" % msg) elif not markup_variants: return self.markup_variants = markup_variants self.current_variant = markup_variants.keys()[0] def set_variant(self, current_var): self.current_variant = current_var def is_sensitive(self): return self.state is not Gtk.StateFlags.INSENSITIVE def render(self, context, cr, layout): if not self.visible: return x, y, width, height = self.allocation context.save() context.add_class("cellrenderer-button") if self.has_focus: context.set_state(self.state | Gtk.StateFlags.FOCUSED) else: context.set_state(self.state) # render background and focal frame if has-focus context.save() context.add_class(Gtk.STYLE_CLASS_BUTTON) Gtk.render_background(context, cr, x, y, width, height) context.restore() if self.has_focus: Gtk.render_focus(context, cr, x + 3, y + 3, width - 6, height - 6) # position and render layout markup context.save() context.add_class(Gtk.STYLE_CLASS_BUTTON) layout.set_markup(self.markup_variants[self.current_variant], -1) layout_width = layout.get_pixel_extents()[1].width x = x + (width - layout_width) / 2 y += self.ypad Gtk.render_layout(context, cr, x, y, layout) context.restore() context.restore()