Theming and Styling
[[[appendices.theming_and_styling]]]
Overview
[[[appendices.theming_and_styling#overview]]]
borco_pyside.theming ([[plugins#field-toolkit]]) holds the app's palette-aware icon rendering:
recolored_svg_icon/RecoloredSvgIconEngine(svg_recolor.py) — a monochrome SVG asset recolored to a givenQColorand rendered fresh at whatever exact size Qt requests, via a customQIconEnginerather than a fixed-resolution baked pixmap.glyph_icon/GlyphIconEngine(glyph_icon.py) — the same "render fresh, no fixed-resolution cache" approach for a single icon-font glyph (e.g. Phosphor) instead of an SVG; both share thepixmap()plumbing viatheming/utils.py'spainted_pixmap.ApplicationPaletteChangeNotifier— one shared, application-wide event filter (created once perQApplicationviafor_application) that re-exposesQEvent.Type.ApplicationPaletteChangeas a plainpalette_changedsignal. ReplacesQApplication.paletteChanged(deprecated since Qt 6.0). Qt fires that event several times for a single theme switch (four on Windows, all with the identical new palette), so the notifier coalesces byQPalette.cacheKeyand emits once per real change — matching the old signal's once-per-switch, since each emit rebuilds every themed icon. A single shared filter also lets any number of handlers listen without each installing its own (which would make every event in the appO(handlers)).ActionIconThemeHandler/GlyphActionIconThemeHandler— keep oneQAction's icon (SVG- or glyph-backed, respectively) rebuilt in the current palette's colors on everyApplicationPaletteChangeNotifier.palette_changed.ActionIconThemeHandler'sflatparameter skips the checked-state color variant for an action living in a menu row, which paints noHighlight-colored backdrop behind its icon the way a toolbar's checked button chrome does — the row's own native checkmark communicates checked-ness there instead.ThemeModel— the single source of truth for the app's theme mode (Qt.ColorScheme.Unknown/Light/Dark), deliberately kept distinct fromQApplication.styleHints().colorScheme()itself: that property reports the resolved appearance, and Qt resolvesUnknownto whatever the OS actually is the moment it's queried — it never echoesUnknownback. Reading it as "the current mode" cannot tell "explicitly Light" apart from "Unknown, currently resolving to Light because the OS is in light mode", which breaks any control cycling or checking against it (#57). Every mode-driven view reads/writesThemeModel.modeand listens tomode_changedinstead — neverQStyleHints.colorSchemeChangeddirectly.ThemeManager— cycles a toolbar action through a sharedThemeModel's follow-system/ light/dark mode on each click, reflecting the current mode on the action's icon.ThemeMenu— builds and owns three checkable actions (default_action/light_action/dark_action, e.g. for aViewmenu's theme entries — their text/meaning has no legitimate per-caller variation, so the class builds them itself rather than taking them as parameters, the same wayDockableDialogbuilds its owntoggle_action) and wires them to a sharedThemeModel, exclusive via aQActionGroupso exactly one is checked at a time, matching the model's mode exactly (including the follow-system entry) rather than a resolved scheme. Placing the three actions in an actual menu is the caller's job.ThemeManagerandThemeMenuare independent of one another — each usable on its own — but both read/write the sameThemeModel, so picking a mode in either one shows up in the other.LineEditClearActionFilter(borco_pyside.widgets) — an app-wide consumer: installs a themed, glyph-rendered clear action on everyQLineEdit, including ones this app never constructs directly ([[plugins#field-toolkit]]'s field toolkit line edits, and any.ui-file-generated one).
1. Phosphor's "Duotone" weight cannot be used as a font
[[[appendices.theming_and_styling#duotone-font-limitation]]]
Symptom: rendering any glyph through Phosphor-Duotone.ttf via glyph_icon produced a flat,
fully-opaque shape — no lighter secondary region, regardless of which color was passed. One
specific codepoint (the calendar glyph, \uE108, used correctly by every other Phosphor weight)
rendered as a bare, unrecognizable solid bar rather than a calendar shape at all.
Investigation:
- Confirmed
Phosphor-Duotone.ttfis not an OpenType color font: a raw byte search of the file found noCOLR/CPAL/SVG/CBDT/sbixtable tag, only the plain outlineglyftable every other Phosphor weight uses. There is no embedded per-region alpha/color data a renderer could pick up in the first place. - Rendered several codepoints through the Duotone family directly: the star glyph (
\uE46A) produced a complete, correctly-shaped star — solid, single-color, no partial transparency anywhere in it. The calendar glyph (\uE108, otherwise identical across weights) produced only a plain bar. The two results are inconsistent with each other, suggesting the bundled Duotone.ttf's per-glyph coverage is itself incomplete, not just monochrome.
Root cause (confirmed against Phosphor's own project docs, not just inferred): duotone rendering
is not implemented in the font files at all. Phosphor's own README states it plainly — "The
duotone weight is not yet available for font implementations, as fonts do not support baked-in
alpha/opacity. In future there are plans to move to an SVG-based approach with full support for all
icon weights." Their web package achieves the two-opacity look via a CSS :after pseudo-element
layer at a fixed low opacity; their Flutter package achieves it by stacking two widgets. Neither
mechanism has anything to do with the .ttf file itself — a single glyph painted with one solid pen
color (what any plain text-rendering call, including GlyphIconEngine.paint, does) fundamentally
cannot reproduce it, independent of which codepoint is chosen.
Resolution: Phosphor-Duotone.ttf was removed from design/fonts/ and from every consumer
(main.qrc's font resources, app.py's ICON_FONT_RESOURCES) — it cannot serve the purpose it was
added for. If a genuine duotone look is wanted for some icon later, it needs a real two-layer
source: Phosphor's SVG assets, recolored per layer and composited via the existing
recolored_svg_icon/RecoloredSvgIconEngine pipeline (already used for the app's own brand icon),
not the icon font.