feat(ui): inline SVG icons across all three dialogs
New module cloud_sync/icons.py:
- CLOUD_SVG, STORAGE_SVG: card glyphs (Material Icons, Apache-2.0)
- WARNING_BADGE_SVG, PLUS_BADGE_SVG, SYNC_BADGE_SVG: header badges
(hand-rolled — circle + vector primitives, no glyph font dependency)
- svg_pixmap(svg, size): QSvgRenderer-backed rasteriser; dual-binding
helper falls back from PySide6 to PyQt6.QtSvg.
Wired into ui_qt.py:
- Conflict dialog: warning badge SVG (light disc + dark !), cloud
card SVG, storage card SVG. Drops the QSS circle hack — entire
badge is one pixmap now.
- Login dialog: plus badge SVG (Prism-green disc + dark +).
- Progress window: sync arrow badge SVG (Prism-green disc + circular
arrow + head).
QSS shed: no more bg/border-radius/font-size acrobatics for badges
since the SVG includes the disc. Card icons are crisp at any DPI.
52 tests still green; pyz grows ~5 KB for the SVG strings + the new
icons module.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""Inline SVG icons + Qt pixmap rasteriser.
|
||||
|
||||
Icons are stored as SVG strings so the pyz stays a single file (no
|
||||
external assets). Rendering uses ``QSvgRenderer`` from whichever Qt
|
||||
binding loaded — PySide6 or PyQt6.
|
||||
|
||||
Color baked into the SVG (Qt's QSvgRenderer ignores stylesheet
|
||||
``currentColor`` inheritance). To recolour, edit the SVG string here
|
||||
or call :func:`recolor` to substitute the placeholder ``#FFFFFF``.
|
||||
|
||||
Card glyphs from the Material Icons set (Apache-2.0). Badge glyphs
|
||||
hand-rolled from pure SVG primitives so we don't pull a glyph font.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Card glyphs (24x24, single-color, drawn over card bg)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Material Icons "cloud" path data — outline of a stylised cumulus.
|
||||
CLOUD_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FFFFFF">
|
||||
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z"/>
|
||||
</svg>"""
|
||||
|
||||
# Material Icons "storage" — three stacked server racks. Reads as
|
||||
# "local storage" without the visual ambiguity of a floppy disk.
|
||||
STORAGE_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FFFFFF">
|
||||
<path d="M2 20h20v-4H2v4zm2-3h2v2H4v-2zM2 4v4h20V4H2zm4 3H4V5h2v2zm-4 7h20v-4H2v4zm2-3h2v2H4v-2z"/>
|
||||
</svg>"""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Header badges (32x32, full disc + glyph)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Light-gray disc with a dark exclamation. Used in the conflict dialog.
|
||||
WARNING_BADGE_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="16" fill="#D6DDE6"/>
|
||||
<rect x="14.5" y="7" width="3" height="11" rx="1.5" fill="#313131"/>
|
||||
<circle cx="16" cy="22.5" r="1.8" fill="#313131"/>
|
||||
</svg>"""
|
||||
|
||||
# Prism-green disc with a dark plus. Used in the login dialog.
|
||||
PLUS_BADGE_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="16" fill="#96DB59"/>
|
||||
<rect x="14.5" y="8" width="3" height="16" rx="1.5" fill="#1A1A1A"/>
|
||||
<rect x="8" y="14.5" width="16" height="3" rx="1.5" fill="#1A1A1A"/>
|
||||
</svg>"""
|
||||
|
||||
# Prism-green disc with a circular arrow. Used in the progress window.
|
||||
SYNC_BADGE_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="16" fill="#96DB59"/>
|
||||
<path d="M 23 17.5 A 7.5 7.5 0 1 1 22 11.5"
|
||||
stroke="#1A1A1A" stroke-width="2.6" fill="none" stroke-linecap="round"/>
|
||||
<polygon points="23.5,7.5 23.5,14 17.5,11.2" fill="#1A1A1A"/>
|
||||
</svg>"""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rasterise
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def svg_pixmap(svg: str, size: int) -> Any:
|
||||
"""Render an SVG string to a ``QPixmap`` of ``size`` × ``size`` px.
|
||||
|
||||
Caller is responsible for keeping the returned pixmap alive (e.g.
|
||||
by attaching it to a QLabel). Raises ``ImportError`` if no Qt
|
||||
binding is available.
|
||||
"""
|
||||
QtCore, QtGui, QtSvg = _import_qt_for_svg()
|
||||
pm = QtGui.QPixmap(size, size)
|
||||
pm.fill(QtCore.Qt.GlobalColor.transparent)
|
||||
renderer = QtSvg.QSvgRenderer(QtCore.QByteArray(svg.encode("utf-8")))
|
||||
painter = QtGui.QPainter(pm)
|
||||
try:
|
||||
renderer.render(painter)
|
||||
finally:
|
||||
painter.end()
|
||||
return pm
|
||||
|
||||
|
||||
def _import_qt_for_svg() -> tuple[Any, Any, Any]:
|
||||
try:
|
||||
from PySide6 import QtCore, QtGui, QtSvg # type: ignore
|
||||
return QtCore, QtGui, QtSvg
|
||||
except ImportError:
|
||||
from PyQt6 import QtCore, QtGui, QtSvg # type: ignore
|
||||
return QtCore, QtGui, QtSvg
|
||||
+26
-53
@@ -93,18 +93,6 @@ QLabel#title {
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
QLabel#badge {
|
||||
color: #313131;
|
||||
background: #96db59;
|
||||
border-radius: 16px;
|
||||
font-size: 18pt;
|
||||
font-weight: bold;
|
||||
qproperty-alignment: AlignCenter;
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
}
|
||||
QLabel#status {
|
||||
color: #c8c8c8;
|
||||
font-size: 10pt;
|
||||
@@ -166,10 +154,12 @@ class QtProgressWindow:
|
||||
outer.setContentsMargins(28, 24, 28, 20)
|
||||
outer.setSpacing(14)
|
||||
|
||||
from . import icons
|
||||
header = QtWidgets.QHBoxLayout()
|
||||
header.setSpacing(12)
|
||||
badge = QtWidgets.QLabel("↻")
|
||||
badge.setObjectName("badge")
|
||||
badge = QtWidgets.QLabel()
|
||||
badge.setPixmap(icons.svg_pixmap(icons.SYNC_BADGE_SVG, 32))
|
||||
badge.setFixedSize(32, 32)
|
||||
header.addWidget(badge)
|
||||
self._title_label = QtWidgets.QLabel("CLOUD SYNC")
|
||||
self._title_label.setObjectName("title")
|
||||
@@ -259,18 +249,6 @@ QLabel#title {
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
QLabel#badge {
|
||||
color: #313131;
|
||||
background: #96db59;
|
||||
border-radius: 16px;
|
||||
font-size: 20pt;
|
||||
font-weight: bold;
|
||||
qproperty-alignment: AlignCenter;
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
}
|
||||
QLabel#body {
|
||||
color: #c8c8c8;
|
||||
font-size: 10pt;
|
||||
@@ -341,6 +319,7 @@ def prompt_login_qt() -> str | None:
|
||||
focus accent, primary "Save and continue" button in Prism green.
|
||||
"""
|
||||
QtWidgets, QtCore, _ = import_qt()
|
||||
from . import icons
|
||||
app_existed = QtWidgets.QApplication.instance() is not None
|
||||
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
|
||||
if not app_existed:
|
||||
@@ -357,8 +336,9 @@ def prompt_login_qt() -> str | None:
|
||||
|
||||
header = QtWidgets.QHBoxLayout()
|
||||
header.setSpacing(12)
|
||||
badge = QtWidgets.QLabel("+")
|
||||
badge.setObjectName("badge")
|
||||
badge = QtWidgets.QLabel()
|
||||
badge.setPixmap(icons.svg_pixmap(icons.PLUS_BADGE_SVG, 32))
|
||||
badge.setFixedSize(32, 32)
|
||||
header.addWidget(badge)
|
||||
title = QtWidgets.QLabel("CONNECT CLOUD SAVE")
|
||||
title.setObjectName("title")
|
||||
@@ -449,18 +429,6 @@ QLabel#title {
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
QLabel#warning {
|
||||
color: #313131;
|
||||
background: #d6dde6;
|
||||
border-radius: 16px;
|
||||
font-size: 18pt;
|
||||
font-weight: bold;
|
||||
qproperty-alignment: AlignCenter;
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
}
|
||||
QLabel#body {
|
||||
color: #c8c8c8;
|
||||
font-size: 10pt;
|
||||
@@ -489,11 +457,6 @@ QLabel#cardSubtitle {
|
||||
font-size: 9pt;
|
||||
background: transparent;
|
||||
}
|
||||
QLabel#cardIcon {
|
||||
color: white;
|
||||
font-size: 24pt;
|
||||
background: transparent;
|
||||
}
|
||||
QPushButton#cancel {
|
||||
background: #303030;
|
||||
color: white;
|
||||
@@ -525,6 +488,7 @@ def prompt_conflict_qt(
|
||||
Cancel bottom-right. Card click commits and dismisses.
|
||||
"""
|
||||
QtWidgets, QtCore, Signal = import_qt()
|
||||
from . import icons
|
||||
app_existed = QtWidgets.QApplication.instance() is not None
|
||||
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
|
||||
if not app_existed:
|
||||
@@ -533,7 +497,7 @@ def prompt_conflict_qt(
|
||||
class _Card(QtWidgets.QFrame):
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self, glyph: str, title: str, subtitle: str) -> None:
|
||||
def __init__(self, pixmap: Any, title: str, subtitle: str) -> None:
|
||||
super().__init__()
|
||||
self.setObjectName("card")
|
||||
self.setCursor(QtCore.Qt.CursorShape.PointingHandCursor)
|
||||
@@ -544,9 +508,9 @@ def prompt_conflict_qt(
|
||||
row = QtWidgets.QHBoxLayout(self)
|
||||
row.setContentsMargins(18, 14, 18, 14)
|
||||
row.setSpacing(16)
|
||||
icon = QtWidgets.QLabel(glyph)
|
||||
icon.setObjectName("cardIcon")
|
||||
icon.setFixedWidth(40)
|
||||
icon = QtWidgets.QLabel()
|
||||
icon.setPixmap(pixmap)
|
||||
icon.setFixedSize(40, 40)
|
||||
icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
row.addWidget(icon)
|
||||
text_col = QtWidgets.QVBoxLayout()
|
||||
@@ -573,8 +537,9 @@ def prompt_conflict_qt(
|
||||
|
||||
header = QtWidgets.QHBoxLayout()
|
||||
header.setSpacing(12)
|
||||
warning = QtWidgets.QLabel("!")
|
||||
warning.setObjectName("warning")
|
||||
warning = QtWidgets.QLabel()
|
||||
warning.setPixmap(icons.svg_pixmap(icons.WARNING_BADGE_SVG, 32))
|
||||
warning.setFixedSize(32, 32)
|
||||
header.addWidget(warning)
|
||||
title = QtWidgets.QLabel("CLOUD CONFLICT")
|
||||
title.setObjectName("title")
|
||||
@@ -593,8 +558,16 @@ def prompt_conflict_qt(
|
||||
|
||||
outer.addSpacing(6)
|
||||
|
||||
cloud_card = _Card("☁", "Cloud Save", f"Modified {remote_modified}")
|
||||
local_card = _Card("▢", "Local Save", f"Modified {local_modified}")
|
||||
cloud_card = _Card(
|
||||
icons.svg_pixmap(icons.CLOUD_SVG, 32),
|
||||
"Cloud Save",
|
||||
f"Modified {remote_modified}",
|
||||
)
|
||||
local_card = _Card(
|
||||
icons.svg_pixmap(icons.STORAGE_SVG, 32),
|
||||
"Local Save",
|
||||
f"Modified {local_modified}",
|
||||
)
|
||||
outer.addWidget(cloud_card)
|
||||
outer.addWidget(local_card)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user