-
Notifications
You must be signed in to change notification settings - Fork 18
feat: support icon color - followup #277
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
Changes from 29 commits
a2cea89
2d1bd89
6161c17
0717900
bf80d58
4937063
eb7288e
0cc07a6
f64f908
f684a35
e358722
99e529d
d1cd7b6
6f9aece
f8620e6
17b44e0
1889dc1
ad09219
5c58673
c8a57d9
c0bfa48
4ea87b1
e559ef2
dd13cc5
39d2e1c
f7486dd
59f88f2
39b27d9
d925906
de2be06
1744f13
a6894b9
c3e7bce
e8fa0d9
3103863
6714a9e
0cd8d04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,8 +2,8 @@ | |
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from qtpy.QtCore import Qt | ||
| from qtpy.QtWidgets import QMainWindow, QWidget | ||
| from qtpy.QtCore import QEvent, Qt | ||
| from qtpy.QtWidgets import QApplication, QMainWindow, QWidget | ||
|
|
||
| from app_model import Application | ||
|
|
||
|
|
@@ -12,13 +12,17 @@ | |
| if TYPE_CHECKING: | ||
| from collections.abc import Collection, Mapping, Sequence | ||
|
|
||
| from qtpy.QtCore import QObject | ||
|
|
||
|
|
||
| class QModelMainWindow(QMainWindow): | ||
| """QMainWindow with app-model support.""" | ||
|
|
||
| def __init__(self, app: Application | str, parent: QWidget | None = None) -> None: | ||
| super().__init__(parent) | ||
| self._app = Application.get_or_create(app) if isinstance(app, str) else app | ||
| if qapp := QApplication.instance(): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually... are you guys even using |
||
| qapp.installEventFilter(self) | ||
|
|
||
| def setModelMenuBar( | ||
| self, menu_ids: Mapping[str, str] | Sequence[str | tuple[str, str]] | ||
|
|
@@ -50,3 +54,12 @@ def addModelToolBar( | |
| else: | ||
| self.addToolBar(toolbar) | ||
| return toolbar | ||
|
|
||
| def eventFilter(self, a0: QObject | None, a1: QEvent | None) -> bool: | ||
| if a1 is not None and a1.type() in ( | ||
| QEvent.Type.ApplicationPaletteChange, | ||
| QEvent.Type.PaletteChange, | ||
| QEvent.Type.StyleChange, | ||
| ): | ||
| self._app.theme_mode_changed(self._app.theme_mode) | ||
| return False | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,23 +3,67 @@ | |
| from typing import TYPE_CHECKING | ||
|
|
||
| from qtpy.QtCore import QUrl | ||
| from qtpy.QtGui import QIcon | ||
| from qtpy.QtGui import QIcon, QPalette | ||
| from qtpy.QtWidgets import QApplication | ||
|
|
||
| if TYPE_CHECKING: | ||
| from typing import Literal | ||
|
|
||
| from qtpy.QtCore import QObject | ||
|
|
||
| from app_model.types import Icon | ||
|
|
||
|
|
||
| def to_qicon(icon: Icon, theme: Literal["dark", "light"] = "dark") -> QIcon: | ||
| def luma(r: float, g: float, b: float) -> float: | ||
| """Calculate the relative luminance of a color.""" | ||
| r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4 | ||
| g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4 | ||
| b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4 | ||
| return 0.2126 * r + 0.7152 * g + 0.0722 * b | ||
|
|
||
|
|
||
| def background_luma(qobj: QObject | None = None) -> float: | ||
| """Return background luminance of the first top level widget or QApp.""" | ||
| # using hasattr here because it will only work with a QWidget, but some of the | ||
| # things calling this function could conceivably only be a QObject | ||
| if hasattr(qobj, "palette"): | ||
| palette: QPalette = qobj.palette() # type: ignore | ||
| elif wdgts := QApplication.topLevelWidgets(): | ||
| palette = wdgts[0].palette() | ||
| else: # pragma: no cover | ||
| palette = QApplication.palette() | ||
| window_bgrd = palette.color(QPalette.ColorRole.Window) | ||
| return luma(window_bgrd.redF(), window_bgrd.greenF(), window_bgrd.blueF()) | ||
|
|
||
|
|
||
| LIGHT_COLOR = "#BCB4B4" | ||
| DARK_COLOR = "#6B6565" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just pointing out (for myself). This is an observable change for all users, even if they aren't touching the theme stuff. The old default icon color was black, and will now be gray. Probably not a big deal, but could perhaps be mentioned in the main PR comment for visibility |
||
|
|
||
|
|
||
| def to_qicon( | ||
| icon: Icon, | ||
| theme: Literal["dark", "light", None] = None, | ||
| color: str | None = None, | ||
| parent: QObject | None = None, | ||
| ) -> QIcon: | ||
| """Create QIcon from Icon.""" | ||
| from superqt import QIconifyIcon, fonticon | ||
|
|
||
| if theme is None: | ||
| theme = "dark" if background_luma(parent) < 0.5 else "light" | ||
| if color is None: | ||
| # use DARK_COLOR icon for light themes and vice versa | ||
| color = ( | ||
| (icon.color_dark or LIGHT_COLOR) | ||
| if theme == "dark" | ||
| else (icon.color_light or DARK_COLOR) | ||
| ) | ||
|
|
||
| if icn := getattr(icon, theme, ""): | ||
| if icn.startswith("file://"): | ||
| return QIcon(QUrl(icn).toLocalFile()) | ||
| elif ":" in icn: | ||
| return QIconifyIcon(icn) | ||
| return QIconifyIcon(icn, color=color) | ||
| else: | ||
| return fonticon.icon(icn) | ||
| return fonticon.icon(icn, color=color) | ||
| return QIcon() # pragma: no cover | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,11 @@ class Icon(_BaseModel): | |
| "[superqt.fonticon](https://pyapp-kit.github.io/superqt/utilities/fonticon/)" | ||
| " keys, such as `fa6s.arrow_down`", | ||
| ) | ||
| color_dark: str | None = Field( | ||
| None, # use light icon for dark themes | ||
| description="(Light) icon color to use for themes with dark backgrounds. " | ||
| "If not provided, a default is used.", | ||
| ) | ||
| light: str | None = Field( | ||
| default=None, | ||
| description="Icon path when a light theme is used. These may be " | ||
|
|
@@ -28,6 +33,11 @@ class Icon(_BaseModel): | |
| "[superqt.fonticon](https://pyapp-kit.github.io/superqt/utilities/fonticon/)" | ||
| " keys, such as `fa6s.arrow_down`", | ||
| ) | ||
| color_light: str | None = Field( | ||
| None, # use dark icon for light themes | ||
| description="(Dark) icon color to use for themes with light backgrounds. " | ||
| "If not provided, a default is used", | ||
| ) | ||
|
|
||
| @classmethod | ||
| def _validate(cls, v: Any) -> "Icon": | ||
|
|
@@ -37,6 +47,11 @@ def _validate(cls, v: Any) -> "Icon": | |
| return v | ||
| if isinstance(v, str): | ||
| v = {"dark": v, "light": v} | ||
| if isinstance(v, dict): | ||
| if "dark" in v: | ||
| v.setdefault("light", v["dark"]) | ||
| elif "light" in v: | ||
| v.setdefault("dark", v["light"]) | ||
| return cls(**v) | ||
|
|
||
| # for v2 | ||
|
|
@@ -45,6 +60,11 @@ def _validate(cls, v: Any) -> "Icon": | |
| def _model_val(cls, v: dict) -> dict: | ||
| if isinstance(v, str): | ||
| v = {"dark": v, "light": v} | ||
| if isinstance(v, dict): | ||
| if "dark" in v: | ||
| v.setdefault("light", v["dark"]) | ||
| elif "light" in v: | ||
| v.setdefault("dark", v["light"]) | ||
| return v | ||
|
|
||
|
|
||
|
|
@@ -53,6 +73,8 @@ class IconDict(TypedDict): | |
|
|
||
| dark: str | None | ||
| light: str | None | ||
| color_dark: str | None | ||
| color_light: str | None | ||
|
Comment on lines
+76
to
+77
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these may have been the names I used when I took a first stab at implementing this... but I'm realizing now it's a footgun. Do you think that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't the same issue present with |
||
|
|
||
|
|
||
| IconOrDict: TypeAlias = Icon | IconDict | ||
Uh oh!
There was an error while loading. Please reload this page.