# Copyright (C) 2006-2007 Red Hat, Inc. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA. ''' Icons are small pictures that are used to decorate components. In Sugar, icons are SVG files that are re-coloured with a fill and a stroke colour. Typically, icons representing the system use a greyscale color palette, whereas icons representing people take on their selected XoColors. Designing a Sugar Icon ====================== If you want to make an icon to use in Sugar, start by designing something in a vector program, like Inkscape. When you are designing the icon, use a canvas that is 55x55px. When designing your icon, use 2 colors. For example, use a black stroke and a white fill color. You need to keep this consistent and remember those colors. You should also keep the subcell grid in mind when designing the icon. A grid cell (which the size of an icon) is made up of 5 by 5 subcells. To use this in Inkscape, enable the page grid (View -> Page Grid), the go to grid properties (File -> Document Properties -> Grids). In grid properties, set the "Spacing X" option to 11, "Spacing Y" to 11 and "Major grid line every" to 1. Before your icon is ready to be used in Sugar, it needs to be Sugarized. This converts the colors to SVG entities, which allows Sugar to change the colors of the icon. To do that, just run the `sugar-iconify`__ script. Usually, it "just works" like:: python path/to/sugar-iconify.py -o icon.svg __ https://github.com/GhostAlgorithm/sugariconify/blob/master/sugariconify.py Code Example ============ Example of using icons with badges: .. literalinclude:: ../examples/iconbadges.py Badge Icons =========== Badge icons are small icons that are attached to the normal icon. For example, a WiFi network icon might have a star badge attached to the bottom left corner (the "attach point") that indicates that the network is connected to. Badge icons are displayed at _BADGE_SIZE percent (45% currently), of their main icon. Badge icons are specified by the icon name, in the same sense that normal icons have a :any:`Icon.set_icon_name` function. Attach Points ------------- Where the badge icon is placed is defined by the main icon. By default, it is centered on 0, 0. That means that the 3 quarters of the icon will be cut off! Therefore, it is helpful to define the attach points. When Sugar loads the main icon, it looks for a `.icon` file. For example, if the icon path is resolved to `/theme/computer-xo.svg`, the `/theme/computer-xo.icon` will be tried to find the attach points. The `.icon` files are to be formatted as follows:: [Icon Data] AttachPoints=970,850 In this example, the badge will be centered at 97.0% on the X axis, and 85.0% on the Y axis. ''' import six import re import math import logging import os from six.moves.configparser import ConfigParser import gi gi.require_version('Rsvg', '2.0') gi.require_version('Gtk', '3.0') from gi.repository import GLib from gi.repository import GObject from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GdkPixbuf from gi.repository import Rsvg import cairo from sugar3.graphics import style from sugar3.graphics.xocolor import XoColor from sugar3.util import LRU _BADGE_SIZE = 0.45 class _SVGLoader(object): def __init__(self): self._cache = LRU(100) def load(self, file_name, entities, cache): if file_name in self._cache: icon = self._cache[file_name] else: icon_file = open(file_name, 'r') icon = icon_file.read() icon_file.close() if cache: self._cache[file_name] = icon for entity, value in list(entities.items()): if isinstance(value, six.string_types): xml = '' % (entity, value) icon = re.sub('' % entity, xml, icon) else: logging.error( 'Icon %s, entity %s is invalid.', file_name, entity) return Rsvg.Handle.new_from_data(icon.encode('utf-8')) class _IconInfo(object): def __init__(self): self.file_name = None self.attach_x = 0 self.attach_y = 0 class _BadgeInfo(object): def __init__(self): self.attach_x = 0 self.attach_y = 0 self.size = 0 self.icon_padding = 0 class _IconBuffer(object): _surface_cache = LRU(100) _loader = _SVGLoader() def __init__(self): self.icon_name = None self.icon_size = None self.file_name = None self.fill_color = None self.background_color = None self.stroke_color = None self.badge_name = None self.width = None self.height = None self.cache = False self.scale = 1.0 self.pixbuf = None def _get_cache_key(self, sensitive): if self.background_color is None: color = None else: color = (self.background_color.red, self.background_color.green, self.background_color.blue) return (self.icon_name, self.file_name, self.pixbuf, self.fill_color, self.stroke_color, self.badge_name, self.width, self.height, color, sensitive) def _load_svg(self, file_name): entities = {} if self.fill_color: entities['fill_color'] = self.fill_color if self.stroke_color: entities['stroke_color'] = self.stroke_color return self._loader.load(file_name, entities, self.cache) def _get_attach_points(self, info, size_request): has_attach_points_, attach_points = info.get_attach_points() attach_x = attach_y = 0 if attach_points: # this works only for Gtk < 3.14 # https://developer.gnome.org/gtk3/stable/GtkIconTheme.html # #gtk-icon-info-get-attach-points attach_x = float(attach_points[0].x) / size_request attach_y = float(attach_points[0].y) / size_request elif info.get_filename(): # try read from the .icon file icon_filename = info.get_filename().replace('.svg', '.icon') if icon_filename != info.get_filename() and \ os.path.exists(icon_filename): try: with open(icon_filename) as config_file: cp = ConfigParser() cp.readfp(config_file) attach_points_str = cp.get('Icon Data', 'AttachPoints') attach_points = attach_points_str.split(',') attach_x = float(attach_points[0].strip()) / 1000 attach_y = float(attach_points[1].strip()) / 1000 except Exception as e: logging.exception('Exception reading icon info: %s', e) return attach_x, attach_y def _get_icon_info(self, file_name, icon_name): icon_info = _IconInfo() if file_name: icon_info.file_name = file_name elif icon_name: theme = Gtk.IconTheme.get_default() size = 50 if self.width is not None: size = self.width info = theme.lookup_icon(icon_name, int(size), 0) if info: attach_x, attach_y = self._get_attach_points(info, size) icon_info.file_name = info.get_filename() icon_info.attach_x = attach_x icon_info.attach_y = attach_y del info else: logging.warning('No icon with the name %s was found in the ' 'theme.', icon_name) return icon_info def _draw_badge(self, context, size, sensitive, widget): theme = Gtk.IconTheme.get_default() badge_info = theme.lookup_icon(self.badge_name, int(size), 0) if badge_info: badge_file_name = badge_info.get_filename() if badge_file_name.endswith('.svg'): handle = self._loader.load(badge_file_name, {}, self.cache) icon_width = handle.props.width icon_height = handle.props.height pixbuf = handle.get_pixbuf() else: pixbuf = GdkPixbuf.Pixbuf.new_from_file(badge_file_name) icon_width = pixbuf.get_width() icon_height = pixbuf.get_height() context.scale(float(size) / icon_width, float(size) / icon_height) if not sensitive: pixbuf = self._get_insensitive_pixbuf(pixbuf, widget) Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0) context.paint() def _get_size(self, icon_width, icon_height, padding): if self.width is not None and self.height is not None: width = self.width + padding height = self.height + padding else: width = icon_width + padding height = icon_height + padding return width, height def _get_badge_info(self, icon_info, icon_width, icon_height): info = _BadgeInfo() if self.badge_name is None: return info info.size = int(_BADGE_SIZE * icon_width) info.attach_x = int(icon_info.attach_x * icon_width - info.size / 2) info.attach_y = int(icon_info.attach_y * icon_height - info.size / 2) if info.attach_x < 0 or info.attach_y < 0: info.icon_padding = max(-info.attach_x, -info.attach_y) elif info.attach_x + info.size > icon_width or \ info.attach_y + info.size > icon_height: x_padding = info.attach_x + info.size - icon_width y_padding = info.attach_y + info.size - icon_height info.icon_padding = max(x_padding, y_padding) return info def _get_xo_color(self): if self.stroke_color and self.fill_color: return XoColor('%s,%s' % (self.stroke_color, self.fill_color)) else: return None def _set_xo_color(self, xo_color): if xo_color: self.stroke_color = xo_color.get_stroke_color() self.fill_color = xo_color.get_fill_color() else: self.stroke_color = None self.fill_color = None def _get_insensitive_pixbuf(self, pixbuf, widget): if not (widget and widget.get_style()): return pixbuf icon_source = Gtk.IconSource() # Special size meaning "don't touch" icon_source.set_size(-1) icon_source.set_pixbuf(pixbuf) icon_source.set_state(Gtk.StateType.INSENSITIVE) icon_source.set_direction_wildcarded(False) icon_source.set_size_wildcarded(False) widget_style = widget.get_style() pixbuf = widget_style.render_icon( icon_source, widget.get_direction(), Gtk.StateType.INSENSITIVE, -1, widget, 'sugar-icon') return pixbuf def get_surface(self, sensitive=True, widget=None): cache_key = self._get_cache_key(sensitive) if cache_key in self._surface_cache: return self._surface_cache[cache_key] if self.pixbuf: # We alredy have the pixbuf for this icon. pixbuf = self.pixbuf icon_width = pixbuf.get_width() icon_height = pixbuf.get_height() icon_info = self._get_icon_info(self.file_name, self.icon_name) is_svg = False else: # We run two attempts at finding the icon. First, we try the icon # requested by the user. If that fails, we fall back on # document-generic. If that doesn't work out, bail. icon_width = None for (file_name, icon_name) in ((self.file_name, self.icon_name), (None, 'document-generic')): icon_info = self._get_icon_info(file_name, icon_name) if icon_info.file_name is None: return None is_svg = icon_info.file_name.endswith('.svg') if is_svg: try: handle = self._load_svg(icon_info.file_name) icon_width = handle.props.width icon_height = handle.props.height break except IOError: pass else: try: path = icon_info.file_name pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) icon_width = pixbuf.get_width() icon_height = pixbuf.get_height() break except GLib.GError: pass if icon_width is None: # Neither attempt found an icon for us to use return None badge_info = self._get_badge_info(icon_info, icon_width, icon_height) padding = badge_info.icon_padding width, height = self._get_size(icon_width, icon_height, padding) if self.background_color is None: surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(width), int(height)) context = cairo.Context(surface) else: surface = cairo.ImageSurface(cairo.FORMAT_RGB24, int(width), int(height)) context = cairo.Context(surface) context.set_source_color(self.background_color) context.paint() context.scale(float(width) / (icon_width + padding * 2), float(height) / (icon_height + padding * 2)) context.save() context.translate(padding, padding) if is_svg: if sensitive: handle.render_cairo(context) else: pixbuf = handle.get_pixbuf() pixbuf = self._get_insensitive_pixbuf(pixbuf, widget) Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0) context.paint() else: if not sensitive: pixbuf = self._get_insensitive_pixbuf(pixbuf, widget) Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0) context.paint() if self.badge_name: context.restore() context.translate(badge_info.attach_x, badge_info.attach_y) self._draw_badge(context, badge_info.size, sensitive, widget) self._surface_cache[cache_key] = surface return surface xo_color = property(_get_xo_color, _set_xo_color) class Icon(Gtk.Image): ''' The most basic Sugar icon class. Displays the icon given. You must set either the `file_name`, `file` or `icon_name` properties, otherwise, no icon will be visible. You should set the `pixel_size`, using constants the `*_ICON_SIZE` constants from :any:`sugar3.graphics.style`. You should set the color (either via `xo_color` or `fill_color` and `stroke_color`), otherwise the default black and white fill and stroke will be used. Keyword Args: file_name (str): a path to the SVG icon file file (object): same behaviour as file_name, but for :class:`sugar3.util.TempFilePath` type objects icon_name (str): a name of an icon in the theme to display. The icons in the theme include those in the sugar-artwork project and icons in the activity's '/icons' directory pixel_size (int): size of the icon, in pixels. Best to use the constants from :class:`sugar3.graphics.style`, as those constants are scaled based on the user's preferences xo_color (sugar3.graphics.xocolor.XoColor): color to display icon, a shortcut that just sets the fill_color and stroke_color fill_color (str): a string, like '#FFFFFF', that will serve as the fill color for the icon stroke_color (str): a string, like '#282828', that will serve as the stroke color for the icon icon_size: deprecated since 0.102.0, use pixel_size instead badge_name (str): the icon_name for a badge icon, see :any:`set_badge_name` alpha (float): transparency of the icon, defaults to 1.0 ''' __gtype_name__ = 'SugarIcon' _MENU_SIZES = (Gtk.IconSize.MENU, Gtk.IconSize.DND, Gtk.IconSize.SMALL_TOOLBAR, Gtk.IconSize.BUTTON) def __init__(self, **kwargs): self._buffer = _IconBuffer() # HACK: need to keep a reference to the path so it doesn't get garbage # collected while it's still used if it's a sugar3.util.TempFilePath. # See #1175 self._file = None self._alpha = 1.0 self._scale = 1.0 if 'icon_size' in kwargs: logging.warning("icon_size is deprecated. Use pixel_size instead.") GObject.GObject.__init__(self, **kwargs) def get_file(self): return self._file def set_file(self, file_name): self._file = file_name self._buffer.file_name = file_name file = GObject.Property(type=object, setter=set_file, getter=get_file) def get_pixbuf(self): ''' Returns the :class:`GdkPixbuf.Pixbuf` for the icon, if one has been loaded yet. If the icon has been drawn (:any:`do_draw`), the icon will be loaded. The pixbuf only contains the SVG icon that has been loaded and recoloured. It does not contain the badge. ''' return self._buffer.pixbuf def set_pixbuf(self, pixbuf): ''' Set the pixbuf. This will force the icon to be rendered with the given pixbuf. The icon will still be centered, badge added, etc. Args: pixbuf (GdkPixbuf.Pixbuf): pixbuf to set ''' self._buffer.pixbuf = pixbuf pixbuf = GObject.Property(type=object, setter=set_pixbuf, getter=get_pixbuf) ''' icon.props.pixbuf -> see :any:`get_pixbuf` and :any:`set_pixbuf` ''' def _sync_image_properties(self): if self._buffer.icon_name != self.props.icon_name: self._buffer.icon_name = self.props.icon_name if self._buffer.file_name != self.props.file: self._buffer.file_name = self.props.file pixel_size = None if self.props.pixel_size == -1: if self.props.icon_size in self._MENU_SIZES: pixel_size = style.SMALL_ICON_SIZE else: pixel_size = style.STANDARD_ICON_SIZE else: pixel_size = self.props.pixel_size width = height = pixel_size if self._buffer.width != width or self._buffer.height != height: self._buffer.width = width self._buffer.height = height def _icon_size_changed_cb(self, image, pspec): self._buffer.icon_size = self.props.pixel_size def _icon_name_changed_cb(self, image, pspec): self._buffer.icon_name = self.props.icon_name def _file_changed_cb(self, image, pspec): self._buffer.file_name = self.props.file def do_get_preferred_height(self): '''Gtk widget implementation method''' self._sync_image_properties() surface = self._buffer.get_surface() if surface: height = surface.get_height() elif self._buffer.height: height = self._buffer.height else: height = 0 return (height, height) def do_get_preferred_width(self): '''Gtk widget implementation method''' self._sync_image_properties() surface = self._buffer.get_surface() if surface: width = surface.get_width() elif self._buffer.width: width = self._buffer.width else: width = 0 return (width, width) def do_draw(self, cr): '''Gtk widget implementation method''' self._sync_image_properties() sensitive = (self.is_sensitive()) surface = self._buffer.get_surface(sensitive, self) if surface is None: return xpad, ypad = self.get_padding() xalign, yalign = self.get_alignment() requisition = self.get_child_requisition() if self.get_direction() != Gtk.TextDirection.LTR: xalign = 1.0 - xalign allocation = self.get_allocation() x = math.floor(xpad + (allocation.width - requisition.width) * xalign) y = math.floor(ypad + (allocation.height - requisition.height) * yalign) if self._scale != 1.0: cr.scale(self._scale, self._scale) margin = self._buffer.width * (1 - self._scale) / 2 x, y = x + margin, y + margin x = x / self._scale y = y / self._scale cr.set_source_surface(surface, x, y) if self._alpha == 1.0: cr.paint() else: cr.paint_with_alpha(self._alpha) def set_xo_color(self, value): ''' Set the colors used to display the icon Args: value (sugar3.graphics.xocolor.XoColor): new XoColor to use ''' if self._buffer.xo_color != value: self._buffer.xo_color = value self.queue_draw() xo_color = GObject.Property( type=object, getter=None, setter=set_xo_color) ''' icon.props.xo_color -> see :any:`set_xo_color`, note there is no getter ''' def set_fill_color(self, value): ''' Set the color used to fill the icon Args: value (str): SVG color string, like '#FFFFFF' ''' if self._buffer.fill_color != value: self._buffer.fill_color = value self.queue_draw() def get_fill_color(self): ''' Get the color used to fill the icon Returns: str, SVG color string, like '#FFFFFF' ''' return self._buffer.fill_color fill_color = GObject.Property( type=object, getter=get_fill_color, setter=set_fill_color) ''' icon.props.fill_color -> see :any:`get_fill_color` and :any:`set_fill_color` ''' def set_stroke_color(self, value): ''' Set the color used to paint the icon stroke Args: value (str): SVG color string, like '#282828' ''' if self._buffer.stroke_color != value: self._buffer.stroke_color = value self.queue_draw() def get_stroke_color(self): ''' Get the color used to paint the icon stroke Returns: str, SVG color string, like '#282828' ''' return self._buffer.stroke_color stroke_color = GObject.Property( type=object, getter=get_stroke_color, setter=set_stroke_color) ''' icon.props.stroke_color -> see :any:`get_stroke_color` and :any:`set_stroke_color` ''' def set_badge_name(self, value): ''' See the Badge Icons section at the top of the file. Args: value (str): the icon name for the badge ''' if self._buffer.badge_name != value: self._buffer.badge_name = value self.queue_resize() def get_badge_name(self): ''' Get the badge name, as set by :any:`set_badge_name` Returns: str, badge icon name ''' return self._buffer.badge_name badge_name = GObject.Property( type=str, getter=get_badge_name, setter=set_badge_name) ''' icon.props.badge_name -> see :any:`get_badge_name` and :any:`set_badge_name` ''' def get_badge_size(self): ''' Returns: int, size of badge icon, in pixels ''' return int(_BADGE_SIZE * self.props.pixel_size) def set_alpha(self, value): ''' Set the transparency for the icon. Defaults to 1.0, which is fully visible icon. Args: value (float): alpha value from 0.0 to 1.0 ''' if self._alpha != value: self._alpha = value self.queue_draw() alpha = GObject.Property( type=float, setter=set_alpha) ''' icon.props.alpha -> see :any:`set_alpha`, note no getter ''' def set_scale(self, value): ''' Scales the icon, with the transformation origin at the top left corner. Note that this only scales the resulting drawing, so at large scales the icon will appear pixilated. Args: value (float): new scaling factor ''' if self._scale != value: self._scale = value self.queue_draw() scale = GObject.Property( type=float, setter=set_scale) ''' icon.props.scale -> see :any:`set_scale`, note no getter ''' class EventIcon(Gtk.EventBox): ''' An Icon class that provides access to mouse events and that can act as a cursor-positioned palette invoker. The palette invoker can be used in 3 ways: 1. Set the palette during your constructor, see :any:`set_palette` 2. Override the create_palette method, see :any:`create_palette` 3. Set the tooltip, see :any:`create_tooltip` Otherwise, the icon setup api is the same as the basic :class:`Icon`. This EventIcon class supports the icon_name, stroke_color, fill_color, file_name, xo_color, pixel_size, scale and alpha keyword arguments as the :class:`Icon`. The added arguments are as follows: Keyword Args: background_color (Gdk.Color): the color to draw the icon on top of. It defaults to None, which means no background is drawn (transparent). The alpha channel of the Gdk.Color is disregarded. cache (bool): if True, the icon file contents will be cached to reduce disk usage palette (sugar3.graphics.palette.Palette): a palette to connect ''' __gsignals__ = { 'activate': (GObject.SignalFlags.RUN_FIRST, None, []), } __gtype_name__ = 'SugarEventIcon' def __init__(self, **kwargs): self._buffer = _IconBuffer() self._alpha = 1.0 Gtk.EventBox.__init__(self) self.set_visible_window(False) self.set_above_child(True) self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.TOUCH_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK) # Connect after the default so that the palette can silence events # for example, after a touch palette invocation self.connect_after('button-release-event', self.__button_release_event_cb) for key, value in six.iteritems(kwargs): self.set_property(key, value) from sugar3.graphics.palette import CursorInvoker self._palette_invoker = CursorInvoker() self._palette_invoker.attach(self) self.connect('destroy', self.__destroy_cb) def do_draw(self, cr): '''Gtk widget implementation method''' surface = self._buffer.get_surface() if surface: allocation = self.get_allocation() x = (allocation.width - surface.get_width()) / 2 y = (allocation.height - surface.get_height()) / 2 cr.set_source_surface(surface, x, y) if self._alpha == 1.0: cr.paint() else: cr.paint_with_alpha(self._alpha) def do_get_preferred_height(self): '''Gtk widget implementation method''' surface = self._buffer.get_surface() if surface: height = surface.get_height() elif self._buffer.height: height = self._buffer.height else: height = 0 return (height, height) def do_get_preferred_width(self): '''Gtk widget implementation method''' surface = self._buffer.get_surface() if surface: width = surface.get_width() elif self._buffer.width: width = self._buffer.width else: width = 0 return (width, width) def __destroy_cb(self, icon): if self._palette_invoker is not None: self._palette_invoker.detach() def set_file_name(self, value): if self._buffer.file_name != value: self._buffer.file_name = value self.queue_draw() def get_file_name(self): return self._buffer.file_name file_name = GObject.Property( type=object, getter=get_file_name, setter=set_file_name) def set_icon_name(self, value): if self._buffer.icon_name != value: self._buffer.icon_name = value self.queue_draw() def get_icon_name(self): return self._buffer.icon_name icon_name = GObject.Property( type=object, getter=get_icon_name, setter=set_icon_name) def set_xo_color(self, value): if self._buffer.xo_color != value: self._buffer.xo_color = value self.queue_draw() xo_color = GObject.Property( type=object, getter=None, setter=set_xo_color) def set_fill_color(self, value): if self._buffer.fill_color != value: self._buffer.fill_color = value self.queue_draw() def get_fill_color(self): return self._buffer.fill_color fill_color = GObject.Property( type=object, getter=get_fill_color, setter=set_fill_color) def set_stroke_color(self, value): if self._buffer.stroke_color != value: self._buffer.stroke_color = value self.queue_draw() def get_stroke_color(self): return self._buffer.stroke_color stroke_color = GObject.Property( type=object, getter=get_stroke_color, setter=set_stroke_color) def set_background_color(self, value): ''' Args: value (Gdk.Color): color use as background (alpha is ignored), or None meaning no background is drawn (transparent) ''' if self._buffer.background_color != value: self._buffer.background_color = value self.queue_draw() def get_background_color(self): ''' Returns: Gdk.Color, current background color, may be None ''' return self._buffer.background_color background_color = GObject.Property( type=object, getter=get_background_color, setter=set_background_color) ''' event_icon.props.get_background_color -> see :any:`set_background_color` and :any:`get_background_color` ''' def set_size(self, value): if self._buffer.width != value: self._buffer.width = value self._buffer.height = value self.queue_resize() def get_size(self): return self._buffer.width pixel_size = GObject.Property( type=object, getter=get_size, setter=set_size) def set_scale(self, value): if self._buffer.scale != value: self._buffer.scale = value self.queue_resize() def get_scale(self): return self._buffer.scale scale = GObject.Property( type=float, getter=get_scale, setter=set_scale) def set_alpha(self, alpha): if self._alpha != alpha: self._alpha = alpha self.queue_draw() alpha = GObject.Property( type=float, setter=set_alpha) def set_cache(self, value): ''' Sugar caches icon file contents in a smart cache. Currently, we use a LRU (Least Recently Used) algorithm to manage the cache. Args: value (bool): if True, the icon file will be cached in the LRU ''' self._buffer.cache = value def get_cache(self): ''' Returns: bool, if the icon file will be saved in the LRU ''' return self._buffer.cache cache = GObject.Property( type=bool, default=False, getter=get_cache, setter=set_cache) ''' event_icon.props.cache -> see :any:`set_cache` and :any:`get_cache` ''' def set_badge_name(self, value): if self._buffer.badge_name != value: self._buffer.badge_name = value self.queue_draw() def get_badge_name(self): return self._buffer.badge_name badge_name = GObject.Property( type=object, getter=get_badge_name, setter=set_badge_name) def create_palette(self): ''' The create_palette function is called when the palette needs to be invoked. For example, when the user has right clicked the icon or the user has hovered over the icon for a long time. The create_palette will only be called once or zero times. The palette returned will be stored and re-used if the user invokes the palette multiple times. Your create_palette implementation does not need to :any:`Gtk.Widget.show` the palette, as this will be done by the invoker. However, you still need to show the menu items, etc that you place in the palette. Returns: sugar3.graphics.palette.Palette, or None to indicate that you do not want a palette shown The default implementation returns None, to indicate no palette should be shown. ''' return None def get_palette(self): ''' Gets the current palette, either set by :any:`set_palette` or cached after a call to :any:`create_palette` ''' return self._palette_invoker.palette def set_palette(self, palette): ''' Sets the palette to show. If the palette is not None, this will override the palette set by create_palette. Args: palette (sugar3.graphics.palette.Palette): palette or None ''' self._palette_invoker.palette = palette palette = GObject.Property( type=object, setter=set_palette, getter=get_palette) ''' event_icon.props.palette -> see :any:`get_palette` and :any:`set_palette` ''' def get_palette_invoker(self): return self._palette_invoker def set_palette_invoker(self, palette_invoker): self._palette_invoker.detach() self._palette_invoker = palette_invoker palette_invoker = GObject.Property( type=object, setter=set_palette_invoker, getter=get_palette_invoker) def set_tooltip(self, text): ''' Creates a palette with the tooltip text. This will override any current palette set through :any:`set_palette` or that will ever be returned by :any:`create_palette`. Args: text (str): tooltip text ''' from sugar3.graphics.palette import Palette self.set_palette(Palette(text)) def __button_release_event_cb(self, icon, event): if event.button == 1: alloc = self.get_allocation() if 0 < event.x < alloc.width and 0 < event.y < alloc.height: self.emit('activate') class CanvasIcon(EventIcon): ''' An EventIcon with active and prelight states, and a styleable background. If the icon pops up a palette, the prelight state is set until the palette pops down. This is used to render a light grey highlight, however can be configured by Gtk+ CSS with the `:prelight` selector. Care should to use :any:`connect_to_palette_pop_events` for all palettes created and shown around this icon. ''' __gtype_name__ = 'SugarCanvasIcon' def __init__(self, **kwargs): EventIcon.__init__(self, **kwargs) self._button_down = False self.connect('enter-notify-event', self.__enter_notify_event_cb) self.connect('leave-notify-event', self.__leave_notify_event_cb) self.connect('button-press-event', self.__button_press_event_cb) self.connect('button-release-event', self.__button_release_event_cb) def connect_to_palette_pop_events(self, palette): ''' Connect to the palette's popup and popdown events, so that the prelight state is set at the right times. You should run this call before you :any:`EventIcon.set_palette` or before you return from your :any:`EventIcon.create_palette` function, eg: def create_palette(self): palette = ... self.connect_to_palette_pop_events(palette) return palette Args: palette (sugar3.graphics.palette.Palette): palette to connect ''' palette.connect('popup', self.__palette_popup_cb) palette.connect('popdown', self.__palette_popdown_cb) def do_draw(self, cr): '''Gtk widget implementation method''' allocation = self.get_allocation() context = self.get_style_context() Gtk.render_background(context, cr, 0, 0, allocation.width, allocation.height) EventIcon.do_draw(self, cr) def __enter_notify_event_cb(self, icon, event): self.set_state_flags(self.get_state_flags() | Gtk.StateFlags.PRELIGHT, clear=True) if self._button_down: self.set_state_flags(Gtk.StateFlags.ACTIVE, clear=False) def __leave_notify_event_cb(self, icon, event): if self.palette and self.palette.is_up(): return self.unset_state_flags(Gtk.StateFlags.PRELIGHT | Gtk.StateFlags.ACTIVE) def __button_press_event_cb(self, icon, event): if self.palette and not self.palette.is_up(): self._button_down = True self.set_state_flags( self.get_state_flags() | Gtk.StateFlags.ACTIVE, clear=True) def __button_release_event_cb(self, icon, event): self.unset_state_flags(Gtk.StateFlags.ACTIVE) self._button_down = False def __palette_popup_cb(self, palette): self.set_state_flags(Gtk.StateFlags.PRELIGHT, clear=True) def __palette_popdown_cb(self, palette): self.unset_state_flags(Gtk.StateFlags.PRELIGHT) if hasattr(CanvasIcon, 'set_css_name'): CanvasIcon.set_css_name('canvasicon') class CellRendererIcon(Gtk.CellRenderer): __gtype_name__ = 'SugarCellRendererIcon' __gsignals__ = { 'clicked': (GObject.SignalFlags.RUN_FIRST, None, [object]), } def __init__(self, treeview=None): # treeview is not used anymore, is here just to not break the API if treeview is not None: logging.warning('CellRendererIcon: treeview parameter in ' 'constructor is deprecated') self._buffer = _IconBuffer() self._buffer.cache = True self._xo_color = None self._fill_color = None self._stroke_color = None self._prelit_fill_color = None self._prelit_stroke_color = None self._active_state = False self._cached_offsets = None Gtk.CellRenderer.__init__(self) self._is_scrolling = False def connect_to_scroller(self, scrolled): scrolled.connect('scroll-start', self._scroll_start_cb) scrolled.connect('scroll-end', self._scroll_end_cb) def _scroll_start_cb(self, event): self._is_scrolling = True def _scroll_end_cb(self, event): self._is_scrolling = False def is_scrolling(self): return self._is_scrolling def create_palette(self): return None def set_file_name(self, value): if self._buffer.file_name != value: self._buffer.file_name = value file_name = GObject.Property(type=str, setter=set_file_name) def set_icon_name(self, value): if self._buffer.icon_name != value: self._buffer.icon_name = value icon_name = GObject.Property(type=str, setter=set_icon_name) def get_xo_color(self): return self._xo_color def set_xo_color(self, value): self._xo_color = value xo_color = GObject.Property(type=object, getter=get_xo_color, setter=set_xo_color) def set_fill_color(self, value): if self._fill_color != value: self._fill_color = value fill_color = GObject.Property(type=object, setter=set_fill_color) def set_stroke_color(self, value): if self._stroke_color != value: self._stroke_color = value stroke_color = GObject.Property(type=object, setter=set_stroke_color) def set_prelit_fill_color(self, value): if self._prelit_fill_color != value: self._prelit_fill_color = value prelit_fill_color = GObject.Property(type=object, setter=set_prelit_fill_color) def set_prelit_stroke_color(self, value): if self._prelit_stroke_color != value: self._prelit_stroke_color = value prelit_stroke_color = GObject.Property(type=object, setter=set_prelit_stroke_color) def set_background_color(self, value): if self._buffer.background_color != value: self._buffer.background_color = value background_color = GObject.Property(type=object, setter=set_background_color) def set_size(self, value): if self._buffer.width != value: self._buffer.width = value self._buffer.height = value self._cached_offsets = None size = GObject.Property(type=object, setter=set_size) def do_get_size(self, widget, cell_area, x_offset=None, y_offset=None, width=None, height=None): width = self._buffer.width + self.props.xpad * 2 height = self._buffer.height + self.props.ypad * 2 xoffset = 0 yoffset = 0 if width > 0 and height > 0 and cell_area is not None: if widget.get_direction() == Gtk.TextDirection.RTL: xoffset = 1.0 - self.props.xalign else: xoffset = self.props.xalign xoffset = max(xoffset * (cell_area.width - width), 0) yoffset = max(self.props.yalign * (cell_area.height - height), 0) self._cached_offsets = xoffset, yoffset return xoffset, yoffset, width, height def _get_offsets(self, widget, cell_area): if self._cached_offsets is not None: return self._cached_offsets xoffset, yoffset, width_, height_ = self.do_get_size(widget, cell_area) return xoffset, yoffset def do_activate(self, event, widget, path, background_area, cell_area, flags): pass def do_start_editing(self, event, widget, path, background_area, cell_area, flags): pass def do_render(self, cr, widget, background_area, cell_area, flags): if not self._is_scrolling: context = widget.get_style_context() context.save() context.add_class("sugar-icon-cell") def is_pointer_inside(): # widget is the treeview x, y = widget.get_pointer() x, y = widget.convert_widget_to_bin_window_coords(x, y) return ((cell_area.x <= x <= cell_area.x + cell_area.width) and (cell_area.y <= y <= cell_area.y + cell_area.height)) pointer_inside = is_pointer_inside() # The context will have prelight state if the mouse pointer is # in the entire row, but we want that state if the pointer is # in this cell only: if flags & Gtk.CellRendererState.PRELIT: if pointer_inside: if self._active_state: context.set_state(Gtk.StateFlags.ACTIVE) else: context.set_state(Gtk.StateFlags.NORMAL) Gtk.render_background( context, cr, background_area.x, background_area.y, background_area.width, background_area.height) if self._xo_color is not None: stroke_color = self._xo_color.get_stroke_color() fill_color = self._xo_color.get_fill_color() prelit_fill_color = None prelit_stroke_color = None else: stroke_color = self._stroke_color fill_color = self._fill_color prelit_fill_color = self._prelit_fill_color prelit_stroke_color = self._prelit_stroke_color has_prelit_colors = None not in [prelit_fill_color, prelit_stroke_color] if flags & Gtk.CellRendererState.PRELIT and has_prelit_colors and \ pointer_inside: self._buffer.fill_color = prelit_fill_color self._buffer.stroke_color = prelit_stroke_color else: self._buffer.fill_color = fill_color self._buffer.stroke_color = stroke_color else: if self._xo_color is not None: self._buffer.fill_color = self._xo_color.get_fill_color() self._buffer.stroke_color = self._xo_color.get_stroke_color() else: self._buffer.fill_color = self._fill_color self._buffer.stroke_color = self._stroke_color surface = self._buffer.get_surface() if surface is None: return xoffset, yoffset = self._get_offsets(widget, cell_area) x = cell_area.x + xoffset y = cell_area.y + yoffset cr.set_source_surface(surface, math.floor(x), math.floor(y)) cr.rectangle(cell_area.x, cell_area.y, cell_area.width, cell_area.height) cr.clip() cr.paint() def get_icon_state(base_name, perc, step=5): ''' Get the closest icon name for a given state in percent. First, you need a set of icons. They must be prefixed with `base_name`, for example "network-wireless". They must be suffixed with 3 digit percentage numbers, for example "-000", "-200", etc. Eventually, you get a collection of icon names like: * network-wireless-000 * network-wireless-020 * network-wireless-040 * network-wireless-060 * network-wireless-080 * network-wireless-100 All of these icons must be placed in the icon theme, such that they are addressable by their `icon_name`. Args: base_name (str): base icon name, eg `network-wireless` perc (float): desired percentage between 0 and 100, eg. 67.8 Keyword Arguments: step (int): step to increment to find all possible icons From the example above, we could step 5, because 0, 5, 10, 15, etc, includes all number suffixes in our set of icons (0, 20, 40, etc). If we had the number suffixes 0, 33, 66, 99, we could not use 5, as none of the numbers are divisible by 5. Returns: str, icon name that represent given state, or None if not found ''' strength = round(perc / step) * step icon_theme = Gtk.IconTheme.get_default() while strength <= 100 and strength >= 0: icon_name = '%s-%03d' % (base_name, strength) if icon_theme.has_icon(icon_name): return icon_name strength = strength + step def get_icon_file_name(icon_name): ''' Resolves a given icon name into a file path. Looks for any icon in them theme, including those in sugar-artwork and those in the activities '/icons/' directory. Returns: str, path to icon, or None is the icon is not found in the theme ''' icon_theme = Gtk.IconTheme.get_default() info = icon_theme.lookup_icon(icon_name, Gtk.IconSize.LARGE_TOOLBAR, 0) if not info: return None filename = info.get_filename() del info return filename def get_surface(**kwargs): ''' Get cairo surface of the icon. Supports the same arguments as :any:`Icon`, in exactly the same way. Returns: cairo surface or None if image was not found ''' icon = _IconBuffer() for key, value in list(kwargs.items()): icon.__setattr__(key, value) return icon.get_surface()