Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add QIcon backed by iconify #209

Merged
merged 16 commits into from
Oct 9, 2023
14 changes: 14 additions & 0 deletions examples/iconify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from qtpy.QtCore import QSize
from qtpy.QtWidgets import QApplication, QPushButton

from superqt import QIconifyIcon

app = QApplication([])

btn = QPushButton()
# search https://icon-sets.iconify.design for available icon keys
btn.setIcon(QIconifyIcon("fluent-emoji-flat:alarm-clock"))
btn.setIconSize(QSize(60, 60))
btn.show()

app.exec()
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ dependencies = [
# extras
# https://peps.python.org/pep-0621/#dependencies-optional-dependencies
[project.optional-dependencies]
test = ["pint", "pytest", "pytest-cov", "pytest-qt", "numpy", "cmap"]
test = ["pint", "pytest", "pytest-cov", "pytest-qt", "numpy", "cmap", "pyconify"]
dev = [
"black",
"ipython",
Expand All @@ -74,6 +74,7 @@ font-fa5 = ["fonticon-fontawesome5"]
font-fa6 = ["fonticon-fontawesome6"]
font-mi6 = ["fonticon-materialdesignicons6"]
font-mi7 = ["fonticon-materialdesignicons7"]
iconify = ["pyconify >=0.1.3"]

[project.urls]
Documentation = "https://pyapp-kit.github.io/superqt/"
Expand Down
2 changes: 2 additions & 0 deletions src/superqt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .collapsible import QCollapsible
from .combobox import QColorComboBox, QEnumComboBox, QSearchableComboBox
from .elidable import QElidingLabel, QElidingLineEdit
from .iconify import QIconifyIcon
from .selection import QSearchableListWidget, QSearchableTreeWidget
from .sliders import (
QDoubleRangeSlider,
Expand All @@ -35,6 +36,7 @@
"QElidingLineEdit",
"QEnumComboBox",
"QLabeledDoubleRangeSlider",
"QIconifyIcon",
"QLabeledDoubleSlider",
"QLabeledRangeSlider",
"QLabeledSlider",
Expand Down
1 change: 1 addition & 0 deletions src/superqt/fonticon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"IconFontMeta",
"IconOpts",
"pulse",
"QIconifyIcon",
"setTextIcon",
"spin",
]
Expand Down
75 changes: 75 additions & 0 deletions src/superqt/iconify/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from qtpy.QtGui import QIcon

if TYPE_CHECKING:
from typing import Literal

Flip = Literal["horizontal", "vertical", "horizontal,vertical"]
Rotation = Literal["90", "180", "270", 90, 180, 270, "-90", 1, 2, 3]


class QIconifyIcon(QIcon):
"""QIcon backed by an iconify icon.

Iconify includes 150,000+ icons from most major icon sets including Bootstrap,
FontAwesome, Material Design, and many more.

Search availble icons at https://icon-sets.iconify.design
Once you find one you like, use the key in the format `"prefix:name"` to create an
icon: `QIconifyIcon("bi:bell")`.

This class is a thin wrapper around the
[pyconify](https://github.com/pyapp-kit/pyconify) `svg_path` function. It pulls SVGs
from iconify, creates a temporary SVG file and uses it as the source for a QIcon.
SVGs are cached to disk, and persist across sessions (until `pyconify.clear_cache()`
is called).

Parameters
----------
*key: str
Icon set prefix and name. May be passed as a single string in the format
`"prefix:name"` or as two separate strings: `'prefix', 'name'`.
color : str, optional
Icon color. If not provided, the icon will appear black (the icon fill color
will be set to the string "currentColor").
flip : str, optional
Flip icon. Must be one of "horizontal", "vertical", "horizontal,vertical"
rotate : str | int, optional
Rotate icon. Must be one of 0, 90, 180, 270,
or 0, 1, 2, 3 (equivalent to 0, 90, 180, 270, respectively)
dir : str, optional
If 'dir' is not None, the file will be created in that directory, otherwise a
default
[directory](https://docs.python.org/3/library/tempfile.html#tempfile.mkstemp) is
used.

Examples
--------
>>> from qtpy.QtWidgets import QPushButton
>>> from superqt import QIconifyIcon
>>> btn = QPushButton()
>>> icon = QIconifyIcon("bi:alarm-fill", color="red", rotate=90)
>>> btn.setIcon(icon)
"""

def __init__(
self,
*key: str,
color: str | None = None,
flip: Flip | None = None,
rotate: Rotation | None = None,
dir: str | None = None,
):
try:
from pyconify import svg_path
except ModuleNotFoundError as e:
raise ImportError(
"pyconify is required to use QIconifyIcon. "
"Please install it with `pip install iconify` or use the "
tlambert03 marked this conversation as resolved.
Show resolved Hide resolved
"`pip install superqt[iconify]` extra."
) from e
self.path = svg_path(*key, color=color, flip=flip, rotate=rotate, dir=dir)
super().__init__(str(self.path))
22 changes: 22 additions & 0 deletions tests/test_iconify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import TYPE_CHECKING

import pytest
from qtpy.QtWidgets import QPushButton

from superqt import QIconifyIcon

if TYPE_CHECKING:
from pytestqt.qtbot import QtBot


def test_qiconify(qtbot: "QtBot", monkeypatch: "pytest.MonkeyPatch") -> None:
monkeypatch.setenv("PYCONIFY_CACHE", "0")
tlambert03 marked this conversation as resolved.
Show resolved Hide resolved
pytest.importorskip("pyconify")

icon = QIconifyIcon("bi:alarm-fill", color="red", rotate=90)
assert icon.path.name.endswith(".svg")

btn = QPushButton()
qtbot.addWidget(btn)
btn.setIcon(icon)
btn.show()