class IconicFont:
"""Main class for managing iconic fonts."""
def __init__(self, *args):
super().__init__()
self.icon_cache = {}
self.fonts = {font.prefix: font for font in args}
def paint(
self,
painter: gui.Painter,
rect: QtCore.QRect,
mode: gui.QIcon.Mode,
state: gui.QIcon.State,
options: Iterable[dict[str, Any]],
):
color_str, char = COLOR_OPTIONS[state][mode]
for opt in options:
painter.save()
color = gui.Color(opt[color_str])
painter.setPen(color)
# A 16 pixel-high icon yields a font size of 14, which is pixel perfect
# for font-awesome. 16 * 0.875 = 14
# The reason why the glyph size is smaller than the icon size is to
# account for font bearing.
draw_size = round(0.875 * rect.height() * opt["scale_factor"])
# Animation setup hook
if (animation := opt.get("animation")) is not None:
animation.setup(painter, rect)
font = self.fonts[opt["prefix"]].get_font(draw_size)
painter.setFont(font)
if "offset" in opt:
rect.translate(
round(opt["offset"][0] * rect.width()),
round(opt["offset"][1] * rect.height()),
)
x_center = rect.width() * 0.5
y_center = rect.height() * 0.5
painter.translate(x_center, y_center)
transform = gui.Transform()
if opt.get("vflip") is True:
transform.scale(1, -1)
if opt.get("hflip") is True:
transform.scale(-1, 1)
if "rotated" in opt:
transform.rotate(opt["rotated"])
painter.setTransform(transform, True)
painter.translate(-x_center, -y_center)
if (opacity := opt.get("opacity")) is not None:
painter.setOpacity(opacity)
painter.drawText(rect, int(constants.ALIGN_CENTER), opt[char]) # type: ignore
painter.restore()
def has_valid_font_ids(self) -> bool:
"""Validates instance's font ids are loaded to QFontDatabase.
It is possible that QFontDatabase was reset or QApplication was recreated
in both cases it is possible that font is not available.
"""
# Check stored font ids are still available
return all(font.is_valid() for font in self.fonts.values())
def icon(self, *names, **kwargs) -> gui.QIcon:
"""Returns a gui.QIcon object corresponding to the provided icon name."""
cache_key = f"{names}{kwargs}"
if cache_key in self.icon_cache:
return self.icon_cache[cache_key]
opts = kwargs.pop("options", [{}] * len(names))
if len(opts) != len(names):
raise TypeError(f'"options" must be a list of size {len(names)}')
parsed_options = [self._parse_options(o, kwargs, n) for o, n in zip(opts, names)]
engine = chariconengine.CharIconEngine(self, parsed_options)
icon = gui.QIcon(engine)
self.icon_cache[cache_key] = icon
return icon
def _parse_options(
self, specific_options: dict, general_options: dict, name: str
) -> dict[str, Any]:
options = dict(_default_options, **general_options) | specific_options
# Handle icons for modes (Active, Disabled, Selected, Normal)
# and states (On, Off)
char = options.get("char", name)
on = options.get("on", char)
off = options.get("off", char)
active = options.get("active", on)
selected = options.get("selected", active)
disabled = options.get("disabled", char)
on_active = options.get("on_active", active)
on_selected = options.get("on_selected", selected)
on_disabled = options.get("on_disabled", disabled)
off_active = options.get("off_active", active)
off_selected = options.get("off_selected", selected)
off_disabled = options.get("off_disabled", disabled)
icon_dict = {
"char": char,
"on": on,
"off": off,
"active": active,
"selected": selected,
"disabled": disabled,
"on_active": on_active,
"on_selected": on_selected,
"on_disabled": on_disabled,
"off_active": off_active,
"off_selected": off_selected,
"off_disabled": off_disabled,
}
names = [icon_dict.get(kw, name) for kw in ICON_KW]
chars = []
for name in names:
if "." not in name:
raise Exception("Invalid icon name")
prefix, n = name.split(".")
if prefix not in self.fonts:
raise Exception(f"Invalid font prefix {prefix!r}")
if n not in self.fonts[prefix].charmap:
raise Exception(f"Invalid icon name {n!r} in font {prefix!r}")
chars.append(self.fonts[prefix].charmap[n])
options |= dict(zip(*(ICON_KW, chars)))
options["prefix"] = prefix
# Handle colors for modes (Active, Disabled, Selected, Normal)
# and states (On, Off)
color = options.get("color")
options.setdefault("color_on", color)
options.setdefault("color_active", options["color_on"])
options.setdefault("color_selected", options["color_active"])
options.setdefault("color_on_active", options["color_active"])
options.setdefault("color_on_selected", options["color_selected"])
options.setdefault("color_on_disabled", options["color_disabled"])
options.setdefault("color_off", color)
options.setdefault("color_off_active", options["color_active"])
options.setdefault("color_off_selected", options["color_selected"])
options.setdefault("color_off_disabled", options["color_disabled"])
return options