# Copyright (C) 2011 Canonical # # Authors: # Matthew McGowan # # 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 import cairo from gi.repository import Gtk, Gdk, Pango, GObject, GdkPixbuf, GLib from gettext import gettext as _ from softwarecenter.backend.installbackend import get_install_backend from softwarecenter.enums import Icons from softwarecenter.ui.gtk3.em import StockEms, em from softwarecenter.ui.gtk3.drawing import darken from softwarecenter.ui.gtk3.widgets.stars import Star, StarSize _HAND = Gdk.Cursor.new(Gdk.CursorType.HAND2) def _update_icon(image, icon, icon_size): if isinstance(icon, GdkPixbuf.Pixbuf): image = image.set_from_pixbuf(icon) elif isinstance(icon, Gtk.Image): image = image.set_from_pixbuf(icon.get_pixbuf()) elif isinstance(icon, str): image = image.set_from_icon_name(icon, icon_size) elif icon is None: image = Gtk.Image() else: msg = "Acceptable icon values: None, GdkPixbuf, GtkImage or str" raise TypeError(msg) return image class _Tile(object): MIN_WIDTH = em(7) def __init__(self): self.set_focus_on_click(False) self.set_relief(Gtk.ReliefStyle.NONE) self.box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) self.box.set_size_request(self.MIN_WIDTH, -1) self.add(self.box) def build_default(self, label, icon, icon_size): if icon is not None: if isinstance(icon, Gtk.Image): self.image = icon else: self.image = Gtk.Image() _update_icon(self.image, icon, icon_size) self.box.pack_start(self.image, True, True, 0) self.label = Gtk.Label.new(label) self.box.pack_start(self.label, True, True, 0) class TileButton(Gtk.Button, _Tile): def __init__(self): Gtk.Button.__init__(self) _Tile.__init__(self) class TileToggleButton(Gtk.RadioButton, _Tile): def __init__(self): Gtk.RadioButton.__init__(self) self.set_mode(False) _Tile.__init__(self) class LabelTile(TileButton): MIN_WIDTH = -1 def __init__(self, label, icon, icon_size=Gtk.IconSize.MENU): TileButton.__init__(self) self.build_default(label, icon, icon_size) self.label.set_line_wrap(True) context = self.label.get_style_context() context.add_class("label-tile") self.connect("enter-notify-event", self.on_enter) self.connect("leave-notify-event", self.on_leave) def do_draw(self, cr): cr.save() A = self.get_allocation() if self.has_focus(): Gtk.render_focus(self.get_style_context(), cr, 3, 3, A.width - 6, A.height - 6) cr.restore() for child in self: self.propagate_draw(child, cr) def on_enter(self, widget, event): window = self.get_window() window.set_cursor(_HAND) def on_leave(self, widget, event): window = self.get_window() window.set_cursor(None) class CategoryTile(TileButton): def __init__(self, label, icon, icon_size=Gtk.IconSize.DIALOG): TileButton.__init__(self) self.set_size_request(em(8), -1) self.build_default(label, icon, icon_size) self.label.set_justify(Gtk.Justification.CENTER) self.label.set_alignment(0.5, 0.0) self.label.set_line_wrap(True) self.box.set_border_width(StockEms.SMALL) context = self.label.get_style_context() context.add_class("category-tile") self.connect("enter-notify-event", self.on_enter) self.connect("leave-notify-event", self.on_leave) def do_draw(self, cr): cr.save() A = self.get_allocation() if self.has_focus(): Gtk.render_focus(self.get_style_context(), cr, 3, 3, A.width - 6, A.height - 6) cr.restore() for child in self: self.propagate_draw(child, cr) def on_enter(self, widget, event): window = self.get_window() window.set_cursor(_HAND) def on_leave(self, widget, event): window = self.get_window() window.set_cursor(None) _global_featured_tile_width = em(11) class FeaturedTile(TileButton): INSTALLED_OVERLAY_SIZE = 22 _MARKUP = '%s' def __init__(self, helper, doc, icon_size=48): TileButton.__init__(self) self._pressed = False label = helper.get_appname(doc) icon = helper.get_icon_at_size(doc, icon_size, icon_size) stats = helper.get_review_stats(doc) self.helper = helper helper.update_availability(doc) helper.connect("needs-refresh", self._on_needs_refresh, doc, icon_size) self.is_installed = helper.is_installed(doc) self._overlay = helper.icons.load_icon(Icons.INSTALLED_OVERLAY, self.INSTALLED_OVERLAY_SIZE, 0) # flags self.box.set_orientation(Gtk.Orientation.HORIZONTAL) self.box.set_spacing(StockEms.SMALL) self.content_left = Gtk.Box.new(Gtk.Orientation.VERTICAL, StockEms.MEDIUM) self.content_right = Gtk.Box.new(Gtk.Orientation.VERTICAL, 1) self.box.pack_start(self.content_left, False, False, 0) self.box.pack_start(self.content_right, False, False, 0) self.image = Gtk.Image() _update_icon(self.image, icon, icon_size) self.content_left.pack_start(self.image, False, False, 0) self.title = Gtk.Label.new(self._MARKUP % GLib.markup_escape_text(label)) self.title.set_alignment(0.0, 0.5) self.title.set_use_markup(True) self.title.set_tooltip_text(label) self.title.set_ellipsize(Pango.EllipsizeMode.END) self.content_right.pack_start(self.title, False, False, 0) categories = helper.get_categories(doc) if categories is not None: self.category = Gtk.Label.new('%s' % (em(0.6), GLib.markup_escape_text(categories))) self.category.set_use_markup(True) self.category.set_alignment(0.0, 0.5) self.category.set_ellipsize(Pango.EllipsizeMode.END) self.content_right.pack_start(self.category, False, False, 4) stats_a11y = None if stats is not None: self.stars = Star(size=StarSize.SMALL) self.stars.render_outline = True self.stars.set_rating(stats.ratings_average) self.rating_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, StockEms.SMALL) self.rating_box.pack_start(self.stars, False, False, 0) self.n_ratings = Gtk.Label.new( ' (%i)' % ( em(0.45), stats.ratings_total)) self.n_ratings.set_use_markup(True) self.n_ratings.set_name("subtle-label") self.n_ratings.set_alignment(0.0, 0.5) self.rating_box.pack_start(self.n_ratings, False, False, 0) self.content_right.pack_start(self.rating_box, False, False, 0) # TRANSLATORS: this is an accessibility description for eg orca and # is not visible in the ui stats_a11y = _('%(stars)d stars - %(reviews)d reviews') % { 'stars': stats.ratings_average, 'reviews': stats.ratings_total} # work out width tile needs to be to ensure ratings text is all # visible req_width = (self.stars.size_request().width + self.image.size_request().width + self.n_ratings.size_request().width + StockEms.MEDIUM * 3 ) global _global_featured_tile_width _global_featured_tile_width = max(_global_featured_tile_width, req_width) # TRANSLATORS: Free here means Gratis price = helper.get_display_price(doc) self.price = Gtk.Label.new( '%s' % (em(0.6), price)) self.price.set_use_markup(True) self.price.set_name("subtle-label") self.price.set_alignment(0.0, 0.5) self.content_right.pack_start(self.price, False, False, 0) self.set_name("featured-tile") a11y_name = '. '.join([t for t in [label, categories, stats_a11y, price] if t]) self.get_accessible().set_name(a11y_name) self.backend = get_install_backend() self.backend.connect("transaction-finished", self.on_transaction_finished, helper, doc) self.connect("enter-notify-event", self.on_enter) self.connect("leave-notify-event", self.on_leave) self.connect("button-press-event", self.on_press) self.connect("button-release-event", self.on_release) def destroy(self): # the disconnect the suff connected to "self" is taken care # of by this super() super(FeaturedTile, self).destroy() self.backend.disconnect_by_func(self.on_transaction_finished) self.helper.disconnect_by_func(self._on_needs_refresh) def _on_needs_refresh(self, helper, pkgname, doc, icon_size): icon = helper.get_icon_at_size(doc, icon_size, icon_size) _update_icon(self.image, icon, icon_size) def do_get_preferred_width(self): w = _global_featured_tile_width return w, w def do_draw(self, cr): # draw icons first for child in self: self.propagate_draw(child, cr) # then draw focus/installed overlay on top cr.save() A = self.get_allocation() if self._pressed: cr.translate(1, 1) if self.has_focus(): Gtk.render_focus(self.get_style_context(), cr, 3, 3, A.width - 6, A.height - 6) if self.is_installed: # paint installed tick overlay if self.get_direction() != Gtk.TextDirection.RTL: x = y = 36 else: x = A.width - 56 y = 36 Gdk.cairo_set_source_pixbuf(cr, self._overlay, x, y) cr.paint() cr.restore() def on_transaction_finished(self, backend, result, helper, doc): trans_pkgname = str(result.pkgname) pkgname = helper.get_pkgname(doc) if trans_pkgname != pkgname: return # update installed state helper.update_availability(doc) self.is_installed = helper.is_installed(doc) self.queue_draw() def on_enter(self, widget, event): window = self.get_window() window.set_cursor(_HAND) return True def on_leave(self, widget, event): window = self.get_window() window.set_cursor(None) self._pressed = False return True def on_press(self, widget, event): self._pressed = True def on_release(self, widget, event): if not self._pressed: return self.emit("clicked") self._pressed = False class ChannelSelector(Gtk.Button): PADDING = 0 def __init__(self, section_button): Gtk.Button.__init__(self) alignment = Gtk.Alignment.new(0.5, 0.5, 0.0, 1.0) alignment.set_padding(self.PADDING, self.PADDING, self.PADDING, self.PADDING) self.add(alignment) self.arrow = Gtk.Arrow.new(Gtk.ArrowType.DOWN, Gtk.ShadowType.IN) alignment.add(self.arrow) # vars self.parent_style_type = Gtk.Toolbar self.section_button = section_button self.popup = None self.connect("button-press-event", self.on_button_press) def do_draw(self, cr): cr.save() parent_style = self.get_ancestor(self.parent_style_type) context = parent_style.get_style_context() color = darken(context.get_border_color(Gtk.StateFlags.ACTIVE), 0.2) cr.set_line_width(1) a = self.get_allocation() lin = cairo.LinearGradient(0, 0, 0, a.height) lin.add_color_stop_rgba(0.1, color.red, color.green, color.blue, 0.0) # alpha lin.add_color_stop_rgba(0.5, color.red, color.green, color.blue, 1.0) # alpha lin.add_color_stop_rgba(1.0, color.red, color.green, color.blue, 0.1) # alpha cr.set_source(lin) cr.move_to(0.5, 0.5) cr.rel_line_to(0, a.height) cr.stroke() cr.move_to(a.width - 0.5, 0.5) cr.rel_line_to(0, a.height) cr.stroke() cr.restore() for child in self: self.propagate_draw(child, cr) def on_button_press(self, button, event): if self.popup is None: self.build_channel_selector() self.show_channel_sel_popup(self, event) #~ #~ def on_style_updated(self, widget): #~ context = widget.get_style_context() #~ context.save() #~ context.add_class("menu") #~ bgcolor = context.get_background_color(Gtk.StateFlags.NORMAL) #~ context.restore() #~ #~ self._dark_color = darken(bgcolor, 0.5) def show_channel_sel_popup(self, widget, event): def position_func(menu, (window, a)): if self.get_direction() != Gtk.TextDirection.RTL: tmpx = a.x else: tmpx = a.x + a.width - self.popup.get_allocation().width x, y = window.get_root_coords(tmpx, a.y + a.height) return (x, y, False) a = self.section_button.get_allocation() window = self.section_button.get_window() self.popup.popup(None, None, position_func, (window, a), event.button, event.time) def set_build_func(self, build_func): self.build_func = build_func def build_channel_selector(self): self.popup = Gtk.Menu() self.popup.set_name('toolbar-popup') # to set 'padding: 0;' self.popup.get_style_context().add_class('primary-toolbar') self.build_func(self.popup) class SectionSelector(TileToggleButton): MIN_WIDTH = em(5) _MARKUP = '%s' def __init__(self, label, icon, icon_size=Gtk.IconSize.DIALOG): TileToggleButton.__init__(self) markup = self._MARKUP % label self.build_default(markup, icon, icon_size) self.label.set_use_markup(True) self.label.set_justify(Gtk.Justification.CENTER) context = self.get_style_context() context.add_class("section-sel-bg") context = self.label.get_style_context() context.add_class("section-sel") self.draw_hint_has_channel_selector = False self._alloc = None self._bg_cache = {} self.connect('size-allocate', self.on_size_allocate) self.connect('style-updated', self.on_style_updated) # workaround broken engines (LP: #1021308) self.emit("style-updated") def on_size_allocate(self, *args): alloc = self.get_allocation() if (self._alloc is None or self._alloc.width != alloc.width or self._alloc.height != alloc.height): self._alloc = alloc # reset the bg cache self._bg_cache = {} def on_style_updated(self, *args): # also reset the bg cache self._bg_cache = {} def _cache_bg_for_state(self, state): a = self.get_allocation() # tmp surface on which we render the button bg as per the gtk # theme engine _surf = cairo.ImageSurface(cairo.FORMAT_ARGB32, a.width, a.height) cr = cairo.Context(_surf) context = self.get_style_context() context.save() context.set_state(state) Gtk.render_background(context, cr, -5, -5, a.width + 10, a.height + 10) Gtk.render_frame(context, cr, -5, -5, a.width + 10, a.height + 10) del cr context.restore() # new surface which will be cached which surf = cairo.ImageSurface(cairo.FORMAT_ARGB32, a.width, a.height) cr = cairo.Context(surf) # gradient for masking lin = cairo.LinearGradient(0, 0, 0, a.height) lin.add_color_stop_rgba(0.0, 1, 1, 1, 0.1) lin.add_color_stop_rgba(0.25, 1, 1, 1, 0.7) lin.add_color_stop_rgba(0.5, 1, 1, 1, 1.0) lin.add_color_stop_rgba(0.75, 1, 1, 1, 0.7) lin.add_color_stop_rgba(1.0, 1, 1, 1, 0.1) cr.set_source_surface(_surf, 0, 0) cr.mask(lin) del cr # cache the resulting surf... self._bg_cache[state] = surf def do_draw(self, cr): cr.save() state = self.get_state_flags() if self.get_active(): if state not in self._bg_cache: self._cache_bg_for_state(state) cr.set_source_surface(self._bg_cache[state], 0, 0) cr.paint() cr.restore() for child in self: self.propagate_draw(child, cr) class Link(Gtk.Label): __gsignals__ = { "clicked": (GObject.SignalFlags.RUN_LAST, None, (),) } def __init__(self, markup="", uri="none"): Gtk.Label.__init__(self) self._handler = 0 self.set_markup(markup, uri) def set_markup(self, markup="", uri="none"): markup = '%s' % (uri, markup) Gtk.Label.set_markup(self, markup) if self._handler == 0: self._handler = self.connect("activate-link", self.on_activate_link) # synonyms for set_markup def set_label(self, label): return self.set_markup(label) def set_text(self, text): return self.set_markup(text) def on_activate_link(self, uri, data): self.emit("clicked") def disable(self): self.set_sensitive(False) self.set_name("subtle-label") def enable(self): self.set_sensitive(True) self.set_name("label") class MoreLink(Gtk.Button): _MARKUP = '%s' _MORE = _("More") def __init__(self): Gtk.Button.__init__(self) self.label = Gtk.Label() self.label.set_padding(StockEms.SMALL, 0) self.label.set_markup(self._MARKUP % _(self._MORE)) self.add(self.label) self._init_event_handling() context = self.get_style_context() context.add_class("more-link") def _init_event_handling(self): self.connect("enter-notify-event", self.on_enter) self.connect("leave-notify-event", self.on_leave) def do_draw(self, cr): if self.has_focus(): layout = self.label.get_layout() a = self.get_allocation() e = layout.get_pixel_extents()[1] xo, yo = self.label.get_layout_offsets() Gtk.render_focus(self.get_style_context(), cr, xo - a.x - 3, yo - a.y - 1, e.width + 6, e.height + 2) for child in self: self.propagate_draw(child, cr) def on_enter(self, widget, event): window = self.get_window() window.set_cursor(_HAND) def on_leave(self, widget, event): window = self.get_window() window.set_cursor(None)