# 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)