Source code for xlgui.icons

# Copyright (C) 2008-2010 Adam Olsen
#
# 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; either version 2, or (at your option)
# any later version.
#
# 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.
#
#
# The developers of the Exaile media player hereby grant permission
# for non-GPL compatible GStreamer and Exaile plugins to be used and
# distributed together with GStreamer and Exaile. This permission is
# above and beyond the permissions granted by the GPL license by which
# Exaile is covered. If you modify this code, you may extend this
# exception to your version of the code, but you are not obligated to
# do so. If you do not wish to do so, delete this exception statement
# from your version.

"""
    Provides methods for convenient icons and image handling
"""

import glob
from gi.repository import GdkPixbuf, GLib, Gtk
from itertools import imap, ifilter
import logging
import os

from xl import common, covers, settings
from xlgui.guiutil import pixbuf_from_data

logger = logging.getLogger(__name__)


[docs]class ExtendedPixbuf(object): """ A :class:`GdkPixbuf.Pixbuf` wrapper class allowing for interaction using standard operators Thus you can do the following: * ``pixbuf1 + pixbuf2`` (horizontally appends ``pixbuf2`` to ``pixbuf1``) * ``pixbuf * 5`` (multiplies the content of ``pixbuf``) * ``pixbuf1 & pixbuf2`` (simple composition of ``pixbuf2`` on ``pixbuf1``, the desired alpha value has to be included in the pixbufs themselves) * ``pixbuf1 < pixbuf2``, ``pixbuf1 > pixbuf2`` (compares the pixbuf dimensions) * ``pixbuf1 == pixbuf2`` (compares the pixel data, use the *is* operator to check for identity) Even more is possible with the provided verbose methods """ def __init__(self, pixbuf): self.pixbuf = GdkPixbuf.Pixbuf.new( pixbuf.get_colorspace(), pixbuf.get_has_alpha(), pixbuf.get_bits_per_sample(), pixbuf.get_width(), pixbuf.get_height(), ) pixbuf.copy_area( src_x=0, src_y=0, width=self.pixbuf.get_width(), height=self.pixbuf.get_height(), dest_pixbuf=self.pixbuf, dest_x=0, dest_y=0, ) def get_width(self): return self.pixbuf.get_width() def get_height(self): return self.pixbuf.get_height() def __add__(self, other): """ Horizontally appends a pixbuf to the current :param other: the pixbuf to append :type other: :class:`GdkPixbuf.Pixbuf` :returns: a new pixbuf :rtype: :class:`ExtendedPixbuf` """ return self.add_horizontal(other) def __mul__(self, multiplier): """ Horizontally multiplies the current pixbuf content :param multiplier: How often the pixbuf shall be multiplied :type multiplier: int :returns: a new pixbuf :rtype: :class:`ExtendedPixbuf` """ return self.multiply_horizontal(multiplier) def __and__(self, other): """ Composites a pixbuf on the current pixbuf at the location (0, 0) :param other: the pixbuf to composite :type other: :class:`GdkPixbuf.Pixbuf` :returns: a new pixbuf :rtype: :class:`ExtendedPixbuf` """ return self.composite_simple(other) def __lt__(self, other): """ True if the size (width * height) of the current pixbuf is lower than the size of the other pixbuf """ if isinstance(other, ExtendedPixbuf): other = other.pixbuf self_size = self.pixbuf.get_width() * self.pixbuf.get_height() other_size = other.get_width() * other.get_height() return self_size < other_size def __le__(self, other): """ True if the size (width * height) of the current pixbuf is lower than or equal to the size of the other pixbuf """ if isinstance(other, ExtendedPixbuf): other = other.pixbuf self_size = self.pixbuf.get_width() * self.pixbuf.get_height() other_size = other.get_width() * other.get_height() return self_size <= other_size def __gt__(self, other): """ True if the size (width * height) of the current pixbuf is higher than the size of the other pixbuf """ if isinstance(other, ExtendedPixbuf): other = other.pixbuf self_size = self.pixbuf.get_width() * self.pixbuf.get_height() other_size = other.get_width() * other.get_height() return self_size > other_size def __ge__(self, other): """ True if the size (width * height) of the current pixbuf is higher than or equal to the size of the other pixbuf """ if isinstance(other, ExtendedPixbuf): other = other.pixbuf self_size = self.pixbuf.get_width() * self.pixbuf.get_height() other_size = other.get_width() * other.get_height() return self_size >= other_size def __eq__(self, other): """ True if the pixels of the current pixbuf are the same as the pixels from the other pixbuf """ if isinstance(other, ExtendedPixbuf): other = other.pixbuf return self.pixbuf.get_pixels() == other.get_pixels() def __ne__(self, other): """ True if the pixels of the current pixbuf are not the same as the pixels from the other pixbuf """ return not self.__eq__(other)
[docs] def add_horizontal(self, other, spacing=0): """ Horizontally appends a pixbuf to the current :param other: the pixbuf to append :type other: :class:`GdkPixbuf.Pixbuf` :param spacing: amount of pixels between the pixbufs :type spacing: int :returns: a new pixbuf :rtype: :class:`ExtendedPixbuf` """ if isinstance(other, ExtendedPixbuf): other = other.pixbuf height = max(self.pixbuf.get_height(), other.get_height()) spacing = max(0, spacing) new_pixbuf = GdkPixbuf.Pixbuf.new( self.pixbuf.get_colorspace(), self.pixbuf.get_has_alpha(), self.pixbuf.get_bits_per_sample(), self.pixbuf.get_width() + spacing + other.get_width(), height, ) new_pixbuf.fill(0xFFFFFF00) self.pixbuf.copy_area( src_x=0, src_y=0, width=self.pixbuf.get_width(), height=self.pixbuf.get_height(), dest_pixbuf=new_pixbuf, dest_x=0, dest_y=0, ) other.copy_area( src_x=0, src_y=0, width=other.get_width(), height=other.get_height(), dest_pixbuf=new_pixbuf, dest_x=self.pixbuf.get_width() + spacing, dest_y=0, ) return ExtendedPixbuf(new_pixbuf)
[docs] def add_vertical(self, other, spacing=0): """ Vertically appends a pixbuf to the current :param other: the pixbuf to append :type other: :class:`GdkPixbuf.Pixbuf` :param spacing: amount of pixels between the pixbufs :type spacing: int :returns: a new pixbuf :rtype: :class:`ExtendedPixbuf` """ if isinstance(other, ExtendedPixbuf): other = other.pixbuf width = max(self.pixbuf.get_width(), other.get_width()) spacing = max(0, spacing) new_pixbuf = GdkPixbuf.Pixbuf.new( self.pixbuf.get_colorspace(), self.pixbuf.get_has_alpha(), self.pixbuf.get_bits_per_sample(), width, self.pixbuf.get_height() + spacing + other.get_height(), ) new_pixbuf.fill(0xFFFFFF00) self.pixbuf.copy_area( src_x=0, src_y=0, width=self.pixbuf.get_width(), height=self.pixbuf.get_height(), dest_pixbuf=new_pixbuf, dest_x=0, dest_y=0, ) other.copy_area( src_x=0, src_y=0, width=other.get_width(), height=other.get_height(), dest_pixbuf=new_pixbuf, dest_x=0, dest_y=self.pixbuf.get_height() + spacing, ) return ExtendedPixbuf(new_pixbuf)
[docs] def multiply_horizontal(self, multiplier, spacing=0): """ Horizontally multiplies the current pixbuf content :param multiplier: How often the pixbuf shall be multiplied :type multiplier: int :param spacing: amount of pixels between the pixbufs :type spacing: int :returns: a new pixbuf :rtype: :class:`ExtendedPixbuf` """ spacing = max(0, spacing) new_pixbuf = GdkPixbuf.Pixbuf.new( self.pixbuf.get_colorspace(), self.pixbuf.get_has_alpha(), self.pixbuf.get_bits_per_sample(), self.pixbuf.get_width() * multiplier + spacing * multiplier, self.pixbuf.get_height(), ) new_pixbuf.fill(0xFFFFFF00) for n in xrange(0, multiplier): self.pixbuf.copy_area( src_x=0, src_y=0, width=self.pixbuf.get_width(), height=self.pixbuf.get_height(), dest_pixbuf=new_pixbuf, dest_x=n * self.pixbuf.get_width() + n * spacing, dest_y=0, ) return ExtendedPixbuf(new_pixbuf)
[docs] def multiply_vertical(self, multiplier, spacing=0): """ Vertically multiplies the current pixbuf content :param multiplier: How often the pixbuf shall be multiplied :type multiplier: int :param spacing: amount of pixels between the pixbufs :type spacing: int :returns: a new pixbuf :rtype: :class:`ExtendedPixbuf` """ spacing = max(0, spacing) new_pixbuf = GdkPixbuf.Pixbuf.new( self.pixbuf.get_colorspace(), self.pixbuf.get_has_alpha(), self.pixbuf.get_bits_per_sample(), self.pixbuf.get_width(), self.pixbuf.get_height() * multiplier + spacing * multiplier, ) new_pixbuf.fill(0xFFFFFF00) for n in xrange(0, multiplier): self.pixbuf.copy_area( src_x=0, src_y=0, width=self.pixbuf.get_width(), height=self.pixbuf.get_height(), dest_pixbuf=new_pixbuf, dest_x=0, dest_y=n * self.pixbuf.get_height() + n * spacing, ) return ExtendedPixbuf(new_pixbuf)
[docs] def composite_simple(self, other): """ Composites a pixbuf on the current pixbuf at the location (0, 0) :param other: the pixbuf to composite :type other: :class:`GdkPixbuf.Pixbuf` :returns: a new pixbuf :rtype: :class:`ExtendedPixbuf` """ if isinstance(other, ExtendedPixbuf): other = other.pixbuf width = max(self.pixbuf.get_width(), other.get_width()) height = max(self.pixbuf.get_height(), other.get_height()) new_pixbuf = GdkPixbuf.Pixbuf.new( self.pixbuf.get_colorspace(), self.pixbuf.get_has_alpha(), self.pixbuf.get_bits_per_sample(), width, height, ) new_pixbuf.fill(0xFFFFFF00) for pixbuf in (self.pixbuf, other): pixbuf.composite( dest=new_pixbuf, dest_x=0, dest_y=0, dest_width=pixbuf.get_width(), dest_height=pixbuf.get_height(), offset_x=0, offset_y=0, scale_x=1, scale_y=1, interp_type=GdkPixbuf.InterpType.BILINEAR, # Alpha needs to be embedded in the pixbufs overall_alpha=255, ) return ExtendedPixbuf(new_pixbuf)
[docs] def move(self, offset_x, offset_y, resize=False): """ Moves the content of the current pixbuf within its boundaries (clips overlapping data) and optionally resizes the pixbuf to contain the movement :param offset_x: the amount of pixels to move in horizontal direction :type offset_x: int :param offset_y: the amount of pixels to move in vertical direction :type offset_y: int :param resize: whether to resize the pixbuf on movement :type resize: bool :returns: a new pixbuf :rtype: :class:`ExtendedPixbuf` """ width = self.pixbuf.get_width() height = self.pixbuf.get_height() if resize: width += offset_x height += offset_y new_pixbuf = GdkPixbuf.Pixbuf.new( self.pixbuf.get_colorspace(), self.pixbuf.get_has_alpha(), self.pixbuf.get_bits_per_sample(), width, height, ) new_pixbuf.fill(0xFFFFFF00) self.pixbuf.copy_area( src_x=0, src_y=0, width=self.pixbuf.get_width(), height=self.pixbuf.get_height(), dest_pixbuf=new_pixbuf, dest_x=offset_x, dest_y=offset_y, ) return ExtendedPixbuf(new_pixbuf)
def copy(self): """ Override to return same type """ return ExtendedPixbuf(GdkPixbuf.Pixbuf.copy(self.pixbuf)) def add_alpha(self, substitute_color, r, g, b): """ Override to return same type """ return ExtendedPixbuf( GdkPixbuf.Pixbuf.add_alpha(self.pixbuf, substitute_color, r, g, b) ) def scale_simple(self, dest_width, dest_height, interp_type): """ Override to return same type """ return ExtendedPixbuf( GdkPixbuf.Pixbuf.scale_simple( self.pixbuf, dest_width, dest_height, interp_type ) ) def composite_color_simple( self, dest_width, dest_height, interp_type, overall_alpha, check_size, color1, color2, ): """ Override to return same type """ return ExtendedPixbuf( GdkPixbuf.Pixbuf.composite_color_simple( self.pixbuf, dest_width, dest_height, interp_type, overall_alpha, check_size, color1, color2, ) ) def new_subpixbuf(self, src_x, src_y, width, height): """ Override to return same type """ return ExtendedPixbuf( GdkPixbuf.Pixbuf.new_subpixbuf(self.pixbuf, src_x, src_y, width, height) ) def rotate_simple(self, angle): """ Override to return same type """ return ExtendedPixbuf(GdkPixbuf.Pixbuf.rotate_simple(self.pixbuf, angle)) def flip(self, horizontal): """ Override to return sampe type """ return ExtendedPixbuf(GdkPixbuf.Pixbuf.flip(self.pixbuf, horizontal))
[docs]def extended_pixbuf_new_from_file(filename): """ Returns a new :class:`ExtendedPixbuf` containing an image loaded from the specified file :param filename: the name of the file containing the image to load :type filename: string :returns: a new pixbuf :rtype: :class:`ExtendedPixbuf` """ return ExtendedPixbuf(GdkPixbuf.Pixbuf.new_from_file(filename))
[docs]class IconManager(object): """ Provides convenience functions for managing icons and images in general """ def __init__(self): self.icon_theme = Gtk.IconTheme.get_default() # TODO: Make svg actually recognized self._sizes = [16, 22, 24, 32, 48, 128, 'scalable'] self._cache = {} self.rating_active_pixbuf = ExtendedPixbuf( self.pixbuf_from_icon_name('starred', size=Gtk.IconSize.MENU) ) self.rating_inactive_pixbuf = ExtendedPixbuf( self.pixbuf_from_icon_name('non-starred', size=Gtk.IconSize.MENU) ) # nobody actually sets the rating option, so don't handle it for now # event.add_ui_callback(self.on_option_set, 'rating_option_set')
[docs] def add_icon_name_from_directory(self, icon_name, directory): """ Registers an icon name from files found in a directory :param icon_name: the name for the icon :type icon_name: string :param directory: the location to search for icons :type directory: string :return: filesystem location of the highest-quality icon of this name, or None if not found :rtype: Optional[str] """ path = None for size in self._sizes: if isinstance(size, int): # WxH/icon_name.png and scalable/icon_name.svg sizedir = '%dx%d' % (size, size) else: sizedir = size filepath = os.path.join(directory, sizedir, icon_name) files = glob.glob('%s.*' % filepath) if files: path = files[0] icon_size = size if size != 'scalable' else -1 self.add_icon_name_from_file(icon_name, path, icon_size) else: # icon_nameW.png and icon_name.svg if isinstance(size, int): filename = '%s%d' % (icon_name, size) else: filename = icon_name filepath = os.path.join(directory, filename) files = glob.glob('%s.*' % filepath) if files: path = files[0] icon_size = size if size != 'scalable' else -1 self.add_icon_name_from_file(icon_name, path, icon_size) else: # Give up pass return path
[docs] def add_icon_name_from_file(self, icon_name, filename, size=None): """ Registers an icon name from a filename :param icon_name: the name for the icon :type icon_name: string :param filename: the filename of an image :type filename: string :param size: the size the icon shall be registered for :type size: int """ try: pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) self.add_icon_name_from_pixbuf(icon_name, pixbuf, size) except Exception as e: # Happens if, e.g., librsvg is not installed. logger.warning( 'Failed to add icon name "{icon_name}" ' 'from file "{filename}": {error}'.format( icon_name=icon_name, filename=filename, error=e.message ) ) pass
[docs] def add_icon_name_from_pixbuf(self, icon_name, pixbuf, size=None): """ Registers an icon name from a pixbuf :param icon_name: the name for the icon :type icon_name: string :param pixbuf: the pixbuf of an image :type pixbuf: :class:`GdkPixbuf.Pixbuf` :param size: the size the icon shall be registered for :type size: int """ if size is None: size = pixbuf.get_width() Gtk.IconTheme.add_builtin_icon(icon_name, size, pixbuf)
[docs] def pixbuf_from_icon_name(self, icon_name, size=Gtk.IconSize.BUTTON): """ Generates a pixbuf from an icon name :param icon_name: an icon name :type icon_name: string :param size: the size of the icon, will be tried to converted to a GTK icon size :type size: int or GtkIconSize :returns: the generated pixbuf :rtype: :class:`GdkPixbuf.Pixbuf` or None """ if isinstance(size, Gtk.IconSize): icon_size = Gtk.icon_size_lookup(size) size = icon_size[1] try: pixbuf = self.icon_theme.load_icon( icon_name, size, Gtk.IconLookupFlags.NO_SVG | Gtk.IconLookupFlags.FORCE_SIZE, ) except GLib.GError as e: logger.warning( 'Failed to get pixbuf from "{icon_name}": {error}'.format( icon_name=icon_name, error=e.message ) ) pixbuf = None # TODO: Check if fallbacks are necessary return pixbuf
[docs] @common.cached(limit=settings.get_option('rating/maximum', 5) * 3) def pixbuf_from_rating(self, rating, size_ratio=1): """ Returns a pixbuf representing a rating :param rating: the rating :type rating: int :returns: the rating pixbuf :rtype: :class:`GdkPixbuf.Pixbuf` """ maximum = settings.get_option('rating/maximum', 5) width = self.rating_active_pixbuf.get_width() height = self.rating_active_pixbuf.get_height() active_pixbuf = self.rating_active_pixbuf.scale_simple( int(width * size_ratio), int(height * size_ratio), GdkPixbuf.InterpType.BILINEAR, ) inactive_pixbuf = self.rating_inactive_pixbuf.scale_simple( int(width * size_ratio), int(height * size_ratio), GdkPixbuf.InterpType.BILINEAR, ) rating = max(0, rating) rating = min(rating, maximum) if rating == 0: return inactive_pixbuf * maximum elif rating == maximum: return active_pixbuf * maximum active_pixbufs = active_pixbuf * rating inactive_pixbufs = inactive_pixbuf * (maximum - rating) return active_pixbufs + inactive_pixbufs
def __create_drag_cover_icon(self, pixbuf_list, cover_width): """ Creates a stacked covers effect Put a transparent frame on it (look for `border`) :param pixbuf_list: list of GdkPixbuf.Pixbuf (covers) :param cover_width: int :return: GdkPixbuf.Pixbuf or None (if len(:param pixbuf_list:) == 0) """ if len(pixbuf_list) == 0: return None # Sizes cover_side = cover_width cover_portion = int(cover_side * 0.08) # 8% of sides covers_square_side = cover_side + cover_portion * (len(pixbuf_list) - 1) border = 4 total_width = total_height = covers_square_side + (2 * border) # The result pixbuf result_pixbuf = GdkPixbuf.Pixbuf.new( GdkPixbuf.Colorspace.RGB, True, 8, total_width, total_height ) result_pixbuf.fill(0x00000000) # Fill with transparent background covers_area = result_pixbuf.new_subpixbuf( src_x=border, src_y=border, width=covers_square_side, height=covers_square_side, ) # Write covers for i, pixbuf in enumerate(pixbuf_list): dest = cover_portion * i # Put in a diagonal pixbuf.copy_area( 0, 0, pixbuf.get_width(), pixbuf.get_height(), covers_area, dest, dest ) return result_pixbuf def get_drag_cover_icon(self, tracks): """ Get drag cover icon (stacked covers pixbuf) Asynchronous load covers for tracks taking at most 0.333 seconds :param tracks: iterable for tracks (xl.trax.Track) :return: GdkPixbuf.Pixbuf or None if none found """ cover_width = settings.get_option('gui/cover_width', 100) as_pixbuf = lambda data: pixbuf_from_data( data=data, size=(cover_width, cover_width) ) get_cover_for_tracks = covers.MANAGER.get_cover_for_tracks db_string_list = [] cover_for_tracks = lambda tracks: get_cover_for_tracks(tracks, db_string_list) filtered_covers = ifilter( None, imap(cover_for_tracks, tracks) ) # Remove None cover tracks async_loader = common.AsyncLoader(imap(as_pixbuf, filtered_covers)) async_loader.end(0.333) return self.__create_drag_cover_icon(async_loader.result, cover_width)
if 'EXAILE_BUILDING_DOCS' not in os.environ: MANAGER = IconManager() # vim: et sts=4 sw=4