blib/blib/icon_theme.py

243 lines
5 KiB
Python

from abc import ABC, abstractmethod
from typing import Any
from .errors import FileError
from .path import File
class Icon(dict[int, Any]):
"Represents an icon"
__slots__ = ("name",)
def __init__(self, name: str, icons: dict[int, Any] | None = None) -> None:
"""
Create a new ``Icon`` object
:param name: Name of the icon
:param icons: Icon data and their sizes
"""
dict.__init__(self, icons or {})
self.name: str = name
def __repr__(self) -> str:
sizes = ",".join(repr(size) for size in self.sizes)
return f"Icon({repr(self.name)}, sizes={sizes})"
@property
def sizes(self) -> tuple[int, ...]:
"Return a tuple of the available icon sizes"
return tuple(sorted(self.keys()))
@property
def small(self) -> Any:
"Return a 16x16 icon if available"
return self[16]
@property
def normal(self) -> Any:
"Return a 32x32 icon if available"
return self[32]
@property
def large(self) -> Any:
"Return a 64x64 icon if available"
return self[64]
@property
def extra_large(self) -> Any:
"Return a 128x128 icon if available"
return self[128]
@property
def scalable(self) -> Any:
"Return a scalable (usually svg) icon if available"
return self[0]
class IconTheme(ABC):
"Base class for icon themes"
name: str
"Name of the icon theme"
@abstractmethod
def get_icon(self, name: str) -> Icon: ...
class IconThemes(dict[str, IconTheme]):
"Represets a collection of icon themes"
def __init__(self, base_path: str = "/usr/share/icons", default: str = "hicolor") -> None:
"""
Create a new ``IconThemes`` object
:param base_path: Filesystem path to search for icon themes
:param default: Fallback theme to use when searching for icons
:raises ValueError: When the default theme cannot be found
"""
self.base_path: File = File(base_path).resolve()
"Filesystem path to search for icon themes"
self.themes: dict[str, IconTheme] = {}
"Loaded icon themes"
self.default: IconTheme = MemoryIconTheme("default")
"Fallback icon theme"
self.load(default)
def get_icon(self, theme: str, name: str) -> Icon:
"""
Get an icon. If the icon cannot be found in the specified theme, the default theme
will get searched.
:param theme: Icon theme to search
:param name: Name of the icon (case-insensitive)
:raises KeyError: If the icon cannot be found
"""
try:
self.themes[theme].get_icon(name)
except KeyError:
pass
return self.default.get_icon(name)
def load(self, default: str = "hicolor") -> None:
"""
Load themes from the base path
:params defualt: Fallback theme to use when searching for icons
"""
themes: dict[str, IconTheme]
fallback: IconTheme | None = None
for path in self.base_path.glob():
if path.isdir:
themes[path.name] = FileIconTheme(path)
if path.name.lower() == default.lower():
fallback = themes[path.name]
if fallback is None:
raise ValueError(f"Cannot find default theme: {default}")
self.default = fallback
self.themes = themes
class FileIconTheme(IconTheme):
"Represents an icon theme in a directory"
__slots__ = ("path", "name", "default", "_cache")
def __init__(self, base_path: str, name: str | None = None) -> None:
"""
Create a new ``FileIconTheme`` object
:param base_path: Path to the icon theme
:param name: Name of the icon theme
:raises blib.FileError: If the path is not a directory or does not exist
"""
self.path: File = File(base_path).resolve()
self.name: str = name or self.path.name
self._cache: dict[str, Icon | None] = {}
if not self.path.isdir:
raise FileError.IsFile(self.path)
if not self.path.exists:
raise FileError.NotFound(self.path)
def clear_cache(self) -> None:
"Clear the cached icon results"
self._cache.clear()
def get_icon(self, name: str) -> Icon:
if name in self._cache:
if (cached := self._cache[name]) is None:
raise KeyError(name)
return cached
icon = Icon(name)
for path in self.path.glob(recursive = True, ext = ["png", "svg", "svgz"]):
if name.lower() == path.stem.lower():
size_str = path.split(self.name)[1].split("/")[1]
if size_str != "scalable":
icon[int(size_str.split("x")[0])] = path
else:
icon[0] = path
if len(icon) == 0:
self._cache[name] = None
raise KeyError(name)
self._cache[name] = icon
return icon
class MemoryIconTheme(IconTheme):
__slots__ = ("name", "icons",)
def __init__(self, name: str, *icons: Icon) -> None:
"""
Create a new ``MemoryIconTheme`` object
:param name: Name of the icon theme
:param icons: List of icons to be included in the theme
"""
self.name: str = name
self.icons: dict[str, Icon] = {icon.name: icon for icon in icons}
def get_icon(self, name: str) -> Icon:
return self.icons[name]
def set_icon(self, name: str, size: int, data: bytes) -> None:
"""
Add an icon to the theme
:param name: Name of the icon
:param size: Size of the icon. Use ``0`` for scalable.
:param data: Raw data of the icon
"""
if name not in self.icons:
self.icons[name] = Icon(name)
self.icons[name][size] = data