完成0.01版本

This commit is contained in:
扫地僧 2026-04-05 22:43:43 +08:00
parent 104dfe9693
commit 77f718cca6
10 changed files with 268 additions and 110 deletions

View File

@ -1,7 +1,7 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
datas = [('assets', 'assets'), ('logo.png', '.')]
datas = [('assets', 'assets'), ('logo.png', '.'), ('logo.ico', '.')]
binaries = []
hiddenimports = ['qtawesome']
tmp_ret = collect_all('qtawesome')

BIN
logo.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 146 KiB

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 150 KiB

29
main.py
View File

@ -1,4 +1,5 @@
import sys
import os
import time
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication
@ -8,6 +9,17 @@ from ui.ball import FloatBall, BALL_SIZE
import ui.theme as theme
from db import database
__VERSION__ = "0.0.1"
# ===================== 打包兼容核心函数 =====================
def get_resource_path(relative_path):
"""
打包 EXE 后获取资源路径开发环境也能用
"""
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.abspath("."), relative_path)
# ==========================================================
def _wake_existing_or_exit() -> bool:
"""
@ -22,7 +34,7 @@ def _wake_existing_or_exit() -> bool:
import ctypes.wintypes
mutex_name = r"Global\CleanDesktopOrganizerSingleton"
title = "牛马软件柜"
title = "牛马软件柜 v" + __VERSION__
kernel32 = ctypes.windll.kernel32
user32 = ctypes.windll.user32
@ -34,16 +46,14 @@ def _wake_existing_or_exit() -> bool:
last_err = kernel32.GetLastError()
if last_err == ERROR_ALREADY_EXISTS:
# 轮询一小段时间,避免首次窗口尚未创建
hwnd = None
for _ in range(8): # ~2.4s
for _ in range(8):
hwnd = user32.FindWindowW(None, title)
if hwnd:
break
time.sleep(0.3)
if hwnd:
# SW_SHOW = 5
user32.ShowWindow(hwnd, 5)
user32.SetForegroundWindow(hwnd)
return False
@ -52,42 +62,41 @@ def _wake_existing_or_exit() -> bool:
def main():
# 必须在创建 QApplication 之前:不按各显示器缩放,逻辑像素固定(跨分辨率/跨屏拖动宽高保持一致)
if not _wake_existing_or_exit():
return
QApplication.setAttribute(Qt.ApplicationAttribute.AA_Use96Dpi, True)
# 这个策略同样要求在创建 QApplication 前设置
try:
QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
)
except Exception:
pass
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
init_db()
theme.load() # 从数据库读取上次主题
theme.load()
_set_autostart(True)
panel = PanelWindow()
ball = FloatBall()
panel._ball_ref = ball
panel._apply_pin_window_layer()
ball.clicked.connect(lambda: panel.show_near(ball.pos(), BALL_SIZE))
ball.right_clicked.connect(lambda pos: panel.tray.contextMenu().exec(pos))
# 判断是否有保存的位置:有则直接在原位显示,没有则用悬浮球旁边
has_saved = bool(database.get_setting("panel_x", ""))
if has_saved:
# _restore_geometry 已在 PanelWindow.__init__ 里执行,直接 show
panel.setWindowOpacity(0)
panel.show()
panel.raise_()
if hasattr(panel, "_ball_ref"):
panel._ball_ref.hide()
# 淡入
from PyQt6.QtCore import QPropertyAnimation
anim = QPropertyAnimation(panel, b"windowOpacity")
anim.setDuration(180)

Binary file not shown.

Binary file not shown.

View File

@ -1,55 +1,154 @@
import ctypes
import os
from PyQt6.QtWidgets import QWidget, QApplication
from PyQt6.QtCore import Qt, QPoint, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve, QRectF
from PyQt6.QtGui import (QPainter, QPixmap, QBrush, QColor, QPen,
QPainterPath, QRadialGradient, QIcon)
import sys
from ctypes import wintypes
BALL_SIZE = 60
LOGO_SIZE = 34
LOGO_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png")
from PyQt6.QtWidgets import QWidget, QApplication
from PyQt6.QtCore import Qt, QPoint, pyqtSignal, QPropertyAnimation, QEasingCurve, QRectF
from PyQt6.QtGui import (QPainter, QPixmap, QBrush, QColor,
QPainterPath, QRadialGradient, QLinearGradient,
QIcon, QRegion)
# 球体内容直径;外侧留一圈给投影
BALL_DIAM = 96
SHADOW_PAD = 5
# 窗口边长 = 圆直径含投影环main 里 show_near 用此尺寸
BALL_SIZE = BALL_DIAM + 1 * SHADOW_PAD
DRAG_THRESHOLD = 6
def _resource_path(*parts: str) -> str:
"""开发与 PyInstaller 打包后均能定位项目根目录资源。"""
base = sys._MEIPASS if hasattr(sys, "_MEIPASS") else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base, *parts)
def _resolve_logo_path():
for name in ("logo.png", "logo.ico"):
p = _resource_path(name)
if os.path.isfile(p):
return p
return None
def _load_logo_pixmap(path):
"""ICO 多尺寸时用 QIcon 取较大图,避免位图过小、放大后发糊或带边线感。"""
if not path:
return QPixmap()
if os.path.splitext(path)[1].lower() == ".ico":
pm = QIcon(path).pixmap(512, 512)
if not pm.isNull():
return pm
return QPixmap(path)
def _win32_set_ellipse_window_rgn(hwnd, w, h):
"""用系统区域裁切 HWND。"""
if not hwnd or w < 2 or h < 2:
return
gdi32 = ctypes.windll.gdi32
user32 = ctypes.windll.user32
hrgn = gdi32.CreateEllipticRgn(0, 0, w, h)
if not hrgn:
return
user32.SetWindowRgn(hwnd, hrgn, True)
def _win32_disable_dwm_rounded_shell(hwnd):
"""减弱 Win11 给无边框窗套的圆角矩形描边/阴影(与自绘圆叠在一起会像方框)。"""
if not hwnd:
return
try:
dwm = ctypes.windll.dwmapi
DWMWA_WINDOW_CORNER_PREFERENCE = 33
DWMWCP_DONOTROUND = 1
pref = ctypes.c_uint(DWMWCP_DONOTROUND)
dwm.DwmSetWindowAttribute(
wintypes.HWND(hwnd),
wintypes.DWORD(DWMWA_WINDOW_CORNER_PREFERENCE),
ctypes.byref(pref),
ctypes.sizeof(pref),
)
except Exception:
pass
class FloatBall(QWidget):
clicked = pyqtSignal()
right_clicked = pyqtSignal(QPoint)
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowStaysOnTopHint |
Qt.WindowType.Tool
)
if os.path.exists(LOGO_PATH):
self.setWindowIcon(QIcon(LOGO_PATH))
self._stays_on_top = False
self._apply_window_flags()
self._logo_path = _resolve_logo_path()
if self._logo_path:
self.setWindowIcon(QIcon(self._logo_path))
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
# 关键:让整个窗口区域都透明,不显示系统边框
self.setFixedSize(BALL_SIZE + 20, BALL_SIZE + 20) # 留出光晕空间
self.setAutoFillBackground(False)
# 正方形窗口边长 = 圆直径,绘制也用满圆,避免出现「大圆环套小图」
self.setFixedSize(BALL_SIZE, BALL_SIZE)
self._apply_circular_window_shape()
self.setCursor(Qt.CursorShape.PointingHandCursor)
self._logo = QPixmap(LOGO_PATH) if os.path.exists(LOGO_PATH) else QPixmap()
self._logo = _load_logo_pixmap(self._logo_path) if self._logo_path else QPixmap()
self._drag_start = QPoint()
self._dragging = False
self._hovered = False
self._glow = 0.0 # 0.0~1.0 光晕强度
# 光晕动画
self._glow_anim = QPropertyAnimation(self, b"_glow_prop")
self._glow_anim.setDuration(300)
self._glow_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
# 闲置淡出
self._idle_timer = QTimer(self)
self._idle_timer.setSingleShot(True)
self._idle_timer.setInterval(3000)
self._idle_timer.timeout.connect(self._fade_out)
self._place_default()
self._idle_timer.start()
# Qt property 用于动画
def set_stays_on_top(self, on_top):
"""与主面板图钉联动:仅图钉开启时置顶,避免挡在其他程序前面。"""
if on_top == self._stays_on_top:
return
self._stays_on_top = bool(on_top)
was_visible = self.isVisible()
geo = self.geometry()
self._apply_window_flags()
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, True)
if self._logo_path:
self.setWindowIcon(QIcon(self._logo_path))
self.setCursor(Qt.CursorShape.PointingHandCursor)
self._apply_circular_window_shape()
if was_visible:
self.setGeometry(geo)
self.show()
def _apply_circular_window_shape(self):
w, h = self.width(), self.height()
self.setMask(QRegion(0, 0, w, h, QRegion.RegionType.Ellipse))
if sys.platform == "win32":
wid = int(self.winId())
if wid:
_win32_set_ellipse_window_rgn(wid, w, h)
_win32_disable_dwm_rounded_shell(wid)
def showEvent(self, event):
super().showEvent(event)
if sys.platform == "win32":
wid = int(self.winId())
if wid:
_win32_set_ellipse_window_rgn(wid, self.width(), self.height())
_win32_disable_dwm_rounded_shell(wid)
def _apply_window_flags(self):
flags = (
Qt.WindowType.FramelessWindowHint
| Qt.WindowType.Tool
| Qt.WindowType.NoDropShadowWindowHint
)
if self._stays_on_top:
flags |= Qt.WindowType.WindowStaysOnTopHint
self.setWindowFlags(flags)
def _get_glow(self): return self._glow
def _set_glow(self, v): self._glow = v; self.update()
from PyQt6.QtCore import pyqtProperty
@ -57,79 +156,118 @@ class FloatBall(QWidget):
def _place_default(self):
screen = QApplication.primaryScreen().availableGeometry()
pad = (self.width() - BALL_SIZE) // 2
self.move(screen.right() - self.width() - 20 + pad,
screen.top() + screen.height() // 2 - self.height() // 2)
self.move(
screen.right() - self.width() - 20,
screen.top() + screen.height() // 2 - self.height() // 2,
)
def _paint_radius_outer(self):
w, h = self.width(), self.height()
return min(w, h) * 0.5 - 0.5
def _paint_radius_ball(self):
return self._paint_radius_outer() - SHADOW_PAD
# ── 绘制 ────────────────────────────────────────────
def paintEvent(self, event):
p = QPainter(self)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
pad = (self.width() - BALL_SIZE) // 2 # 光晕留白
cx = self.width() / 2
cy = self.height() / 2
r = BALL_SIZE / 2 - 1
w, h = self.width(), self.height()
cx = w * 0.5
cy = h * 0.5
r_out = self._paint_radius_outer()
r = self._paint_radius_ball()
g = self._glow
glow_strength = 0.28 + 0.72 * g
# 1. 外部光晕hover 时)
if self._glow > 0:
glow_r = r + 10 * self._glow
grad = QRadialGradient(cx, cy, glow_r)
grad.setColorAt(0, QColor(255, 255, 255, int(80 * self._glow)))
grad.setColorAt(0.5, QColor(255, 255, 255, int(30 * self._glow)))
grad.setColorAt(1, QColor(255, 255, 255, 0))
p.setBrush(QBrush(grad))
disc_out = QRectF(cx - r_out, cy - r_out, r_out * 2, r_out * 2)
disc = QRectF(cx - r, cy - r, r * 2, r * 2)
# 0. 外环投影(内容圆与窗口边之间的环形:下重上轻,模拟落地阴影)
p.setPen(Qt.PenStyle.NoPen)
p.drawEllipse(QRectF(cx - glow_r, cy - glow_r, glow_r * 2, glow_r * 2))
outer_path = QPainterPath()
outer_path.addEllipse(disc_out)
inner_hole = QPainterPath()
inner_hole.addEllipse(disc)
ring = outer_path.subtracted(inner_hole)
sh = QLinearGradient(0, cy - r_out, 0, cy + r_out)
sh.setColorAt(0.0, QColor(0, 0, 0, 0))
sh.setColorAt(0.45, QColor(0, 0, 0, 0))
sh.setColorAt(0.62, QColor(0, 0, 0, int(18 + 22 * glow_strength)))
sh.setColorAt(0.82, QColor(0, 0, 0, int(38 + 35 * g)))
sh.setColorAt(1.0, QColor(0, 0, 0, int(52 + 40 * g)))
p.fillPath(ring, QBrush(sh))
# 2. 圆形裁剪
clip = QPainterPath()
clip.addEllipse(QRectF(cx - r, cy - r, r * 2, r * 2))
clip.addEllipse(disc)
p.setClipPath(clip)
# 3. 白色半透明毛玻璃底
p.setBrush(QBrush(QColor(255, 255, 255, 210)))
p.setPen(Qt.PenStyle.NoPen)
p.drawEllipse(QRectF(cx - r, cy - r, r * 2, r * 2))
# 1. 毛玻璃底:半透明冷白渐变(磨砂基底)
frost_bg = QLinearGradient(0, cy - r, 0, cy + r)
frost_bg.setColorAt(0.0, QColor(255, 255, 255, int(175 + 35 * glow_strength)))
frost_bg.setColorAt(0.42, QColor(244, 249, 255, int(130 + 40 * glow_strength)))
frost_bg.setColorAt(1.0, QColor(228, 236, 248, int(155 + 45 * glow_strength)))
p.setBrush(QBrush(frost_bg))
p.drawEllipse(disc)
# 4. 顶部高光
hi = QRadialGradient(cx, cy - r * 0.3, r * 0.9)
hi.setColorAt(0, QColor(255, 255, 255, 140))
hi.setColorAt(1, QColor(255, 255, 255, 0))
p.setBrush(QBrush(hi))
p.drawEllipse(QRectF(cx - r, cy - r, r * 2, r * 2))
# 2. 斜向磨砂高光(模拟玻璃反光)
sheen = QLinearGradient(cx - r, cy - r, cx + r * 0.55, cy + r * 0.85)
sheen.setColorAt(0.0, QColor(255, 255, 255, 0))
sheen.setColorAt(0.38, QColor(255, 255, 255, int(38 + 45 * glow_strength)))
sheen.setColorAt(0.52, QColor(255, 255, 255, int(14 + 18 * glow_strength)))
sheen.setColorAt(1.0, QColor(255, 255, 255, 0))
p.setCompositionMode(QPainter.CompositionMode.CompositionMode_SoftLight)
p.setBrush(QBrush(sheen))
p.drawEllipse(disc)
p.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)
# 5. logo 居中
# 3. Logocover 铺满圆)
if not self._logo.isNull():
p.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)
cover = max(1, int(2 * r))
scaled = self._logo.scaled(
LOGO_SIZE, LOGO_SIZE,
Qt.AspectRatioMode.KeepAspectRatio,
cover, cover,
Qt.AspectRatioMode.KeepAspectRatioByExpanding,
Qt.TransformationMode.SmoothTransformation
)
lx = int(cx - scaled.width() / 2)
ly = int(cy - scaled.height() / 2)
p.drawPixmap(lx, ly, scaled)
# 6. 边框(取消裁剪后画)
# 4. 覆在图上的雾面(毛玻璃「蒙在内容上」)
veil = QLinearGradient(cx, cy - r, cx, cy + r)
veil.setColorAt(0.0, QColor(255, 255, 255, int(52 + 38 * g)))
veil.setColorAt(0.35, QColor(255, 255, 255, int(28 + 22 * g)))
veil.setColorAt(0.75, QColor(248, 252, 255, int(18 + 15 * g)))
veil.setColorAt(1.0, QColor(240, 245, 252, int(22 + 18 * g)))
p.setBrush(QBrush(veil))
p.drawEllipse(disc)
# 5. 内缘光晕(边缘略亮,悬停时更柔)
rim = QRadialGradient(cx, cy, r)
rim.setColorAt(0.0, QColor(255, 255, 255, 0))
rim.setColorAt(0.62, QColor(255, 255, 255, 0))
rim.setColorAt(0.88, QColor(255, 255, 255, int(28 + 55 * glow_strength)))
rim.setColorAt(0.97, QColor(255, 255, 255, int(45 + 85 * glow_strength)))
rim.setColorAt(1.0, QColor(255, 255, 255, int(22 + 40 * glow_strength)))
p.setBrush(QBrush(rim))
p.drawEllipse(disc)
# 6. 顶区高光(玻璃上沿反光)
hi = QRadialGradient(cx, cy - r * 0.22, r * 1.02)
hi.setColorAt(0.0, QColor(255, 255, 255, int(42 + 55 * glow_strength)))
hi.setColorAt(0.45, QColor(255, 255, 255, int(12 + 18 * glow_strength)))
hi.setColorAt(1.0, QColor(255, 255, 255, 0))
p.setBrush(QBrush(hi))
p.drawEllipse(disc)
p.setClipping(False)
p.setBrush(Qt.BrushStyle.NoBrush)
p.setPen(QPen(QColor(200, 200, 200, 120), 1.5))
p.drawEllipse(QRectF(cx - r, cy - r, r * 2, r * 2))
# 7. hover 时内圈白色光晕边
if self._glow > 0:
p.setPen(QPen(QColor(255, 255, 255, int(160 * self._glow)), 2))
p.drawEllipse(QRectF(cx - r + 1, cy - r + 1, (r - 1) * 2, (r - 1) * 2))
p.end()
# ── 鼠标事件 ────────────────────────────────────────
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._drag_start = event.globalPosition().toPoint()
self._dragging = False
self.setWindowOpacity(1.0)
self._idle_timer.stop()
elif event.button() == Qt.MouseButton.RightButton:
self.right_clicked.emit(event.globalPosition().toPoint())
@ -153,12 +291,9 @@ class FloatBall(QWidget):
self.clicked.emit()
else:
self._snap_to_edge()
self._idle_timer.start()
def enterEvent(self, event):
self._hovered = True
self.setWindowOpacity(1.0)
self._idle_timer.stop()
self._glow_anim.stop()
self._glow_anim.setStartValue(self._glow)
self._glow_anim.setEndValue(1.0)
@ -170,9 +305,7 @@ class FloatBall(QWidget):
self._glow_anim.setStartValue(self._glow)
self._glow_anim.setEndValue(0.0)
self._glow_anim.start()
self._idle_timer.start()
# ── 吸附边缘 ─────────────────────────────────────────
def _snap_to_edge(self):
screen = QApplication.primaryScreen().availableGeometry()
cx = self.x() + self.width() // 2
@ -197,10 +330,3 @@ class FloatBall(QWidget):
anim.setEndValue(ends[side])
self._snap_anim = anim
anim.start()
def _fade_out(self):
self._anim_fade = QPropertyAnimation(self, b"windowOpacity")
self._anim_fade.setDuration(600)
self._anim_fade.setStartValue(1.0)
self._anim_fade.setEndValue(0.3)
self._anim_fade.start()

View File

@ -314,9 +314,8 @@ class PanelWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("牛马软件柜")
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint
)
# 默认不置顶;仅图钉开启时与悬浮球一并置顶(见 _apply_pin_window_layer
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
# 任务栏/开始菜单图标:使用项目 logo.png
logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png")
if os.path.exists(logo_path):
@ -522,7 +521,9 @@ class PanelWindow(QWidget):
self.pin_btn = QPushButton()
self.pin_btn.setFixedSize(26, 26)
self.pin_btn.setToolTip("固定:开启后最小化只收缩内容,不变悬浮球")
self.pin_btn.setToolTip(
"固定:面板与悬浮球置顶;开启后最小化只收缩内容,不变悬浮球"
)
self.pin_btn.setStyleSheet("border:none; background:transparent;")
self.pin_btn.clicked.connect(self._toggle_pin)
@ -995,18 +996,45 @@ class PanelWindow(QWidget):
f"已创建分组「{group_name}」,共 {count} 个文件(实时读取)。",
)
def _toggle_pin(self):
self._pinned = not self._pinned
def _sync_pin_dependent_ui(self):
"""图钉图标、最小化提示、置顶标志(面板 + 悬浮球)。"""
ic = "#cccccc" if theme.name() == "dark" else "#555555"
self.pin_btn.setIcon(
qta.icon("fa5s.thumbtack", color="#f90" if self._pinned else ic)
)
self.pin_btn.setIconSize(QSize(13, 13))
# 更新最小化按钮 tooltip
if self._pinned:
self._min_btn.setToolTip("收缩内容区(固定模式)")
else:
self._min_btn.setToolTip("最小化到悬浮球")
self._apply_pin_window_layer()
def _apply_pin_window_layer(self):
"""图钉开 → 面板与悬浮球 WindowStaysOnTop关 → 普通叠放,不挡其他程序。"""
on_top = self._pinned
if getattr(self, "_pin_top_applied", None) is not on_top:
self._pin_top_applied = on_top
was_visible = self.isVisible()
geo = self.geometry()
flags = Qt.WindowType.FramelessWindowHint
if on_top:
flags |= Qt.WindowType.WindowStaysOnTopHint
self.setWindowFlags(flags)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
self.setAcceptDrops(True)
logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logo.png")
if os.path.exists(logo_path):
self.setWindowIcon(QIcon(logo_path))
if was_visible:
self.setGeometry(geo)
self.show()
ball = getattr(self, "_ball_ref", None)
if ball is not None:
ball.set_stays_on_top(on_top)
def _toggle_pin(self):
self._pinned = not self._pinned
self._sync_pin_dependent_ui()
def _on_min_click(self):
"""图钉开启时收缩内容区,否则最小化到悬浮球"""
@ -1139,7 +1167,6 @@ class PanelWindow(QWidget):
if hasattr(self, "_ball_ref"):
self._ball_ref.show()
self._ball_ref.setWindowOpacity(1.0)
self._ball_ref._idle_timer.start()
def toggle_near(self, ball_pos: QPoint, ball_size: int):
if self.isVisible():

View File

@ -181,7 +181,7 @@ class SettingsWindow(QDialog):
layout.addWidget(self._section_title("窗口行为"))
self._pin_check = QCheckBox("固定面板(不自动隐藏)")
self._pin_check = QCheckBox("固定面板(置顶、不自动隐藏)")
self._pin_check.setChecked(self._panel._pinned)
self._pin_check.toggled.connect(self._on_pin)
layout.addWidget(self._pin_check)
@ -213,11 +213,7 @@ class SettingsWindow(QDialog):
def _on_pin(self, checked: bool):
self._panel._pinned = checked
ic = "#cccccc" if theme.name() == "dark" else "#555555"
import qtawesome as qta
self._panel.pin_btn.setIcon(
qta.icon("fa5s.thumbtack", color="#f90" if checked else ic)
)
self._panel._sync_pin_dependent_ui()
def _on_icon_size(self):
val = self._icon_size_spin.value()