diff --git a/examples/iconify.py b/examples/iconify.py new file mode 100644 index 00000000..254a0a22 --- /dev/null +++ b/examples/iconify.py @@ -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() diff --git a/pyproject.toml b/pyproject.toml index cf5fe742..fd8847e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", @@ -74,6 +74,7 @@ font-fa5 = ["fonticon-fontawesome5"] font-fa6 = ["fonticon-fontawesome6"] font-mi6 = ["fonticon-materialdesignicons6"] font-mi7 = ["fonticon-materialdesignicons7"] +iconify = ["pyconify >=0.1.4"] [project.urls] Documentation = "https://pyapp-kit.github.io/superqt/" diff --git a/src/superqt/__init__.py b/src/superqt/__init__.py index f03ea3ff..8080da12 100644 --- a/src/superqt/__init__.py +++ b/src/superqt/__init__.py @@ -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, @@ -35,6 +36,7 @@ "QElidingLineEdit", "QEnumComboBox", "QLabeledDoubleRangeSlider", + "QIconifyIcon", "QLabeledDoubleSlider", "QLabeledRangeSlider", "QLabeledSlider", diff --git a/src/superqt/fonticon/__init__.py b/src/superqt/fonticon/__init__.py index 70ef5acd..732b1b25 100644 --- a/src/superqt/fonticon/__init__.py +++ b/src/superqt/fonticon/__init__.py @@ -10,6 +10,7 @@ "IconFontMeta", "IconOpts", "pulse", + "QIconifyIcon", "setTextIcon", "spin", ] diff --git a/src/superqt/iconify/__init__.py b/src/superqt/iconify/__init__.py new file mode 100644 index 00000000..92fbb813 --- /dev/null +++ b/src/superqt/iconify/__init__.py @@ -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: # pragma: no cover + raise ImportError( + "pyconify is required to use QIconifyIcon. " + "Please install it with `pip install pyconify` or use the " + "`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)) diff --git a/tests/test_iconify.py b/tests/test_iconify.py new file mode 100644 index 00000000..991c5b8c --- /dev/null +++ b/tests/test_iconify.py @@ -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") + 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()