diff --git a/cloud_sync/icons.py b/cloud_sync/icons.py new file mode 100644 index 0000000..4771882 --- /dev/null +++ b/cloud_sync/icons.py @@ -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 = """ + +""" + +# Material Icons "storage" — three stacked server racks. Reads as +# "local storage" without the visual ambiguity of a floppy disk. +STORAGE_SVG = """ + +""" + + +# --------------------------------------------------------------------------- +# Header badges (32x32, full disc + glyph) +# --------------------------------------------------------------------------- + +# Light-gray disc with a dark exclamation. Used in the conflict dialog. +WARNING_BADGE_SVG = """ + + + +""" + +# Prism-green disc with a dark plus. Used in the login dialog. +PLUS_BADGE_SVG = """ + + + +""" + +# Prism-green disc with a circular arrow. Used in the progress window. +SYNC_BADGE_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 diff --git a/cloud_sync/ui_qt.py b/cloud_sync/ui_qt.py index d12d5c8..fa0cab8 100644 --- a/cloud_sync/ui_qt.py +++ b/cloud_sync/ui_qt.py @@ -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)