EventGhost/EventGhost

View on GitHub
eg/Icons.py

Summary

Maintainability
A
1 hr
Test Coverage
# -*- coding: utf-8 -*-
#
# This file is part of EventGhost.
# Copyright © 2005-2020 EventGhost Project <http://www.eventghost.net/>
#
# EventGhost 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 of the License, or (at your option)
# any later version.
#
# EventGhost 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 EventGhost. If not, see <http://www.gnu.org/licenses/>.

"""
:var gImageList: The global wx.ImageList of the module.

:undocumented: gIconCache, DISABLED_PIL, FOLDER_PIL, PLUGIN_PIL, ACTION_PIL
"""

import wx
from base64 import b64decode
from cStringIO import StringIO
from os.path import abspath, join
from PIL import Image

# Local imports
import eg

IMAGES_PATH = eg.imagesDir

gImageList = wx.ImageList(16, 16)
DISABLED_PIL = Image.open(join(IMAGES_PATH, "disabled.png"))
FOLDER_PIL = Image.open(join(IMAGES_PATH, "folder.png")).convert("RGBA")
PLUGIN_PIL = Image.open(join(IMAGES_PATH, "plugin.png"))
ACTION_PIL = Image.open(join(IMAGES_PATH, "action.png")).convert("RGBA")

class IconBase(object):
    """
    An object representing an icon with some memoization functionality.

    The icon is initialized by a file path (see PathIcon) or by a base64encoded
    string (see StringIcon). The object will not load/convert any data before
    an attribute is accessed. If for example the "pil" attribute is requested
    the first time, the object will load the underlying resource through the
    method self._pil() and store the result into self.pil for further requests.
    """
    cache = {}

    def __new__(cls, key):
        """
        If an instance of this data is already in the cache, returns the
        cached instance. Otherwise creates a new instance and adds it to the
        cache.
        """
        if key in cls.cache:
            return cls.cache[key]
        self = super(IconBase, cls).__new__(cls)
        cls.cache[key] = self
        self.key = key
        return self

    def __getattr__(self, name):
        """
        Implements the memoization magic for the icon.

        Only called if an attribute 'name' does not exist. The code will look
        if a corresponding method '_name' exists, calls this method and stores
        the result as 'self.name' (so __getattr__ gets not called again for
        this attribute) and returns the result.
        """
        funcName = "_Get" + name[0].upper() + name[1:]
        if not hasattr(self, funcName):
            raise AttributeError
        result = getattr(self, funcName)()
        setattr(self, name, result)
        return result

    def __getnewargs__(self):
        return (self.key,)

    def GetBitmap(self):
        """
        Return a wx.Bitmap of the icon.
        """
        return PilToBitmap(self.pil)

    def GetWxIcon(self):
        """
        Return a wx.Icon of the icon.
        """
        icon = wx.EmptyIcon()
        icon.CopyFromBitmap(PilToBitmap(self.pil))
        return icon

    def _GetDisabledIndex(self):
        """
        Creates a version of the icon with a "disabled" mark overlayed and
        returns its index inside the global wx.ImageList.
        """
        image = self.pil.copy()
        image.paste(DISABLED_PIL, None, DISABLED_PIL)
        return gImageList.Add(PilToBitmap(image))

    def _GetFolderIndex(self):
        """
        Creates a folder icon with a small version of the icon overlayed and
        returns its index inside the global wx.ImageList.
        """
        small = self.pil.resize((12, 12), Image.BICUBIC)
        image = FOLDER_PIL.copy()
        image.paste(small, (4, 4), small)
        return gImageList.Add(PilToBitmap(image))

    def _GetIndex(self):
        """
        Return the index of this icon inside the global wx.ImageList.
        """
        return gImageList.Add(PilToBitmap(self.pil))

    def _GetPil(self):
        """
        Return a PIL image of the icon.

        Abstract method that must be implemented in a subclass.
        """
        raise NotImplementedError


class ActionSubIcon(IconBase):
    def _GetPil(self):
        """
        Return a PIL image of the icon.
        """
        small = self.key.pil.resize((12, 12), Image.BICUBIC)
        image = ACTION_PIL.copy()
        image.paste(small, (4, 4), small)
        return image

ActionSubIcon.cache = {}


class PathIcon(IconBase):
    def __new__(cls, path):
        """
        If an instance of this path is already in the cache, returns the
        cached instance. Otherwise creates a new instance and adds it to the
        cache.
        """
        return super(PathIcon, cls).__new__(cls, abspath(path))

    def _GetPil(self):
        """
        Return a PIL image of the icon.
        """
        return Image.open(self.key).convert("RGBA")


class PilIcon(IconBase):
    def _GetPil(self):
        """
        Return a PIL image of the icon.
        """
        return self.key


class PluginSubIcon(IconBase):
    def _GetPil(self):
        """
        Return a PIL image of the icon.
        """
        small = self.key.pil.resize((12, 12), Image.BICUBIC)
        image = PLUGIN_PIL.copy()
        image.paste(small, (4, 4), small)
        return image

PluginSubIcon.cache = {}


class StringIcon(IconBase):
    def _GetPil(self):
        """
        Return a PIL image of the icon.
        """
        stream = StringIO(b64decode(self.key))
        pil = Image.open(stream).convert("RGBA")
        stream.close()
        return pil


def ClearImageList():
    """
    Clear the global wxImageList.
    """
    gImageList.RemoveAll()
    # clear out all instance variables for all icons, except the key variable
    for clsType in (IconBase, ActionSubIcon, PluginSubIcon):
        for icon in clsType.cache.itervalues():
            icon.__dict__ = {"key": icon.key}

def CreateBitmapOnTopOfIcon(foregroundIcon, backgroundIcon, size=(12, 12)):
    small = foregroundIcon.pil.resize(size, Image.BICUBIC)
    pil = backgroundIcon.pil.copy()
    pil.paste(small, (16 - size[0], 16 - size[1]), small)
    return wx.BitmapFromBufferRGBA(pil.size[0], pil.size[1], str(pil.tobytes()))

def GetBitmap(filePath):
    """
    Returns a wx.Bitmap loaded from 'filePath'.

    Uses PIL functions, because this way we have better alpha channel
    handling.
    """
    return PilToBitmap(Image.open(filePath).convert("RGBA"))

def GetInternalBitmap(name):
    """
    Same as GetBitmap() but looks for the file in the programs images
    folder. Also appends the .png extension to the name.
    """
    return GetBitmap(join(IMAGES_PATH, name + ".png"))

def GetInternalImage(name):
    return wx.Image(join(eg.imagesDir, name + ".png"), wx.BITMAP_TYPE_PNG)

def PilToBitmap(pil):
    """
    Convert a PIL image to a wx.Bitmap (with alpha channel support).
    """
    return wx.BitmapFromBufferRGBA(pil.size[0], pil.size[1], str(pil.tobytes()))

# setup some commonly used icons
INFO_ICON = PathIcon(join(IMAGES_PATH, "info.png"))
ERROR_ICON = PathIcon(join(IMAGES_PATH, "error.png"))
DEBUG_ICON = PathIcon(join(IMAGES_PATH, "debug.png"))
NOTICE_ICON = PathIcon(join(IMAGES_PATH, "notice.png"))
WARNING_ICON = PathIcon(join(IMAGES_PATH, "warning.png"))
FOLDER_ICON = PathIcon(join(IMAGES_PATH, "folder.png"))
DISABLED_ICON = PathIcon(join(IMAGES_PATH, "disabled.png"))
PLUGIN_ICON = PathIcon(join(IMAGES_PATH, "plugin.png"))
EVENT_ICON = PathIcon(join(IMAGES_PATH, "event.png"))
ACTION_ICON = PathIcon(join(IMAGES_PATH, "action.png"))
MACRO_ICON = PathIcon(join(IMAGES_PATH, "macro.png"))
ADD_ICON = PathIcon(join(IMAGES_PATH, 'add.png'))
ROOT_ICON = PathIcon(join(IMAGES_PATH, 'root.png'))
AUTOSTART_ICON = PathIcon(join(IMAGES_PATH, 'Execute.png'))