# 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-iconfiy`__ 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 re
import math
import logging
import os
from ConfigParser import ConfigParser

import gi
gi.require_version('Rsvg', '2.0')
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(50)

    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 entities.items():
            if isinstance(value, basestring):
                xml = '<!ENTITY %s "%s">' % (entity, value)
                icon = re.sub('<!ENTITY %s .*>' % 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(50)
    _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 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 GObject.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'

    # FIXME: deprecate icon_size
    _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

        # FIXME: deprecate icon_size
        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

        # FIXME: deprecate icon_size
        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.icon_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 kwargs.iteritems():
            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 kwargs.items():
        icon.__setattr__(key, value)
    return icon.get_surface()
