This is the canonical reference for all windowed UI in the UEFN Toolbelt. Every tool, plugin, or feature that opens a PySide6 window — whether built by Ocean, a third-party plugin author, or an AI agent — must follow this guide exactly. Consistency is non-negotiable. Users should never be able to tell which window came from which tool.
core/theme.py ← SINGLE SOURCE OF TRUTH — all hex values live here
│
├── core/base_window.py ← ToolbeltWindow base class (auto-applies theme)
│ │
│ └── verse_device_graph._DeviceGraphWindow (example)
│ └── your_tool.MyToolWindow (your new window)
│
└── dashboard_pyside6.py ← imports QSS as _QSS from theme
To change the platform's appearance: edit PALETTE in core/theme.py.
One edit → dashboard, device graph, every tool window, every plugin updates automatically.
Never hard-code a hex color value anywhere else.
from ..core.base_window import ToolbeltWindow
class MyToolWindow(ToolbeltWindow):
def __init__(self):
super().__init__(title="UEFN Toolbelt — My Tool Name", width=1100, height=700)
self._build_ui()
def _build_ui(self):
root = QWidget()
self.setCentralWidget(root)
vl = QVBoxLayout(root)
vl.setContentsMargins(0, 0, 0, 0)
vl.setSpacing(0)
# Only add a topbar if it carries actual toolbar buttons.
# The OS title bar (set via title=) already shows the tool name —
# never add make_topbar() just to repeat that text.
bar, bl = self.make_topbar("MY TOOL")
bl.addWidget(self.make_btn("Run", accent=True, cb=self._run))
bl.addWidget(self.make_btn("Clear", cb=self._clear))
bl.addStretch()
vl.addWidget(bar)
# ... rest of UI
# In your tool function:
win = MyToolWindow()
win.show_in_uefn() # ← QSS + Slate tick, both handled automaticallyThat's it. No manual setStyleSheet, no Slate tick boilerplate, no color constants to copy.
Every ToolbeltWindow title must follow this exact format:
"UEFN Toolbelt — Tool Name"
- The prefix is always
UEFN Toolbelt(not "Toolbelt", not the project name) - The separator is
—(space + em dash + space) - The tool name is title-case, no trailing punctuation
Help dialogs append Help to the main window title:
"UEFN Toolbelt — Tool Name Help"
Sub-dialogs (edit note, confirm, etc.) use a descriptive suffix:
"UEFN Toolbelt — Edit Note"
"UEFN Toolbelt — Generated Verse Wiring"
This convention ensures every OS title bar, taskbar entry, and Alt+Tab preview reads consistently — users always know they are inside Toolbelt.
All values live in core/theme.PALETTE. Use the token name — never raw hex.
| Token | Hex | Use |
|---|---|---|
bg |
#181818 |
Window / root background |
panel |
#212121 |
Input fields, text areas, secondary panels |
card |
#1E1E1E |
Elevated surfaces, node bodies |
border |
#2A2A2A |
Subtle borders, dividers, splitters |
border2 |
#363636 |
Heavier borders, node outlines, button borders |
text |
#CCCCCC |
Primary text |
muted |
#555555 |
Secondary / hint / disabled text |
accent |
#3A3AFF |
Primary action, focus rings, pressed states |
brand |
#e94560 |
Toolbelt brand red — window titles only |
warn |
#f1c40f |
Warnings |
error |
#FF4444 |
Errors |
ok |
#44FF88 |
Success / healthy |
grid |
#1A1A1A |
Canvas grid lines, scrollbar track |
topbar |
#111111 |
Top bar / status bar background |
# Hex string (for inline stylesheets)
self.hex("accent") # → "#3A3AFF"
# QColor (for QPainter / QBrush / QPen)
self.P["accent"] # → QColor("#3A3AFF")
# Outside a ToolbeltWindow (e.g. QGraphicsItem subclasses)
from ..core.theme import PALETTE
_P = {k: QColor(v) for k, v in PALETTE.items()}
_P["accent"] # → QColor("#3A3AFF")Every tool that opens its own ToolbeltWindow must include a ? help button. Users should never have to guess what a tool does or how to use it.
Placement rule:
- If the window has a topbar with toolbar buttons → add
?as the last button on the right of the topbar - If the window has no topbar → add
?to the bottom action row, right-aligned after a stretch
# Window with topbar (e.g. verse_device_graph)
bar, bl = self.make_topbar("VERSE GRAPH")
bl.addWidget(self.make_btn("Scan", accent=True, cb=self._scan))
bl.addStretch()
bl.addWidget(self.make_btn("?", cb=self._do_help, width=28))
# Window without topbar (e.g. prefab_migrator)
# Add ? to the bottom action row:
al.addWidget(b_primary_action)
al.addStretch()
al.addWidget(self._status_label)
al.addWidget(b_help) # _btn("?"), fixedWidth=28, cb=self._do_helpThe help dialog pattern (use this every time — copy it verbatim):
class _HelpDialog(ToolbeltWindow):
_CONTENT = """\
WHAT IS THIS?
...
─────────────────────────────────────────────────────────────────────────
TYPICAL WORKFLOW
...
"""
def __init__(self):
super().__init__(title="UEFN Toolbelt — My Tool Help", width=700, height=760)
self._build_ui()
def _build_ui(self):
root = QWidget()
self.setCentralWidget(root)
vl = QVBoxLayout(root)
vl.setContentsMargins(0, 0, 0, 0)
vl.setSpacing(0)
editor = QTextEdit()
editor.setReadOnly(True)
editor.setPlainText(self._CONTENT)
editor.setFont(QFont("Consolas", 9))
editor.setLineWrapMode(QTextEdit.NoWrap) # ← always NoWrap
editor.setStyleSheet(
f"background:{self.hex('panel')}; color:{self.hex('text')}; border:none; padding:16px;"
)
vl.addWidget(editor)
# In the main window:
def _do_help(self):
self._help_dlg = _HelpDialog()
self._help_dlg.show_in_uefn()Help content must cover:
- What is this? (one paragraph)
- Why it was made / what problem it solves
- Typical workflow (numbered steps)
- Options/parameters explained
- Any known limitations
core/base_window.ToolbeltWindow provides these helpers. Call them from inside
any subclass — no imports or style arguments needed.
46px dark bar with brand-red title. Use this only when the bar carries actual toolbar buttons (scan, export, refresh, etc.). The OS title bar already shows the window name — do NOT add make_topbar() just to repeat it with a stretch-only layout. This applies to help dialogs and sub-windows too — the OS title bar already identifies them.
# ✅ Correct — topbar earns its place with real actions
bar, bl = self.make_topbar("VERSE GRAPH")
bl.addWidget(self.make_btn("Scan", accent=True, cb=self._scan))
bl.addWidget(self.make_btn("Export", cb=self._export))
bl.addStretch()
vl.addWidget(bar)
# ❌ Wrong — title-only topbar duplicates the OS window title
bar, bl = self.make_topbar("MY TOOL")
bl.addStretch() # no buttons = just visual noise, remove this entire block
vl.addWidget(bar)For help/reference dialogs with pre-formatted text content, always set NoWrap to prevent separator lines and fixed-width content from wrapping:
editor = QTextEdit()
editor.setReadOnly(True)
editor.setFont(QFont("Consolas", 9))
editor.setLineWrapMode(QTextEdit.NoWrap) # ← prevents separator lines from splitting
editor.setStyleSheet(f"background:{self.hex('panel')}; color:{self.hex('text')}; border:none; padding:16px;")accent=True → primary blue tint. cb wires clicked signal.
self.make_btn("Export", cb=self._export)
self.make_btn("Run", accent=True, cb=self._run, height=32)Roles: 'body' · 'header' · 'section' · 'muted' · 'brand'
self.make_label("SECTION TITLE", role="section")
self.make_label("Status: ok", color=self.hex("ok"))Horizontal separator at border2 color.
self.make_text_area(60, mono=True) # code output
self.make_text_area(52, mono=False, read_only=False) # editable notes
self.make_text_area(50, text_color=self.hex("warn")) # warning outputThin gradient health / progress bar.
self._bar = self.make_hbar(4)
vl.addWidget(self._bar)
# Later:
self.set_hbar_value(self._bar, 73) # green fill at 73%
self.set_hbar_value(self._bar, 35) # red fill at 35%Side-panel scroll area. Add content to the returned inner widget.
scroll, inner = self.make_scroll_panel(300)
inner_lay = QVBoxLayout(inner)Show the window AND register the Slate tick driver.
Always call this instead of show().
Close + unregister the Slate tick cleanly.
| Role | Font | Size | Weight |
|---|---|---|---|
| Window / tool title | Segoe UI | 12pt | Bold |
| Section header | Segoe UI | 9–10pt | Bold |
| Body / labels | Segoe UI | 9pt | Normal |
| Monospaced output | Consolas | 8pt | Normal |
| Status bar | Segoe UI | 11px | Normal |
| Situation | Token |
|---|---|
| Window / tool title | brand |
| Success, healthy, passing | ok |
| Warning, degraded | warn |
| Error, failed, broken | error |
| Primary action | accent |
| Body text | text |
| Hint / disabled / secondary | muted |
@editable / device refs |
brand |
Events / .Subscribe |
#2ecc71 (semantic green, not in palette) |
Functions / .calls |
#3498db (semantic blue, not in palette) |
UEFN runs Unreal's Slate event loop, not Qt's. Without driving
app.processEvents()from a Slate callback, Qt windows open but never render.
show_in_uefn() handles this automatically. If you ever need the raw pattern:
import unreal
from PySide6.QtWidgets import QApplication
app = QApplication.instance() or QApplication([])
win.show()
handle = [None]
def _tick(dt):
try:
if not win.isVisible():
unreal.unregister_slate_post_tick_callback(handle[0]); return
app.processEvents()
except Exception:
try: unreal.unregister_slate_post_tick_callback(handle[0])
except Exception: pass
handle[0] = unreal.register_slate_post_tick_callback(_tick)For tools with a node graph or canvas (like the Verse Device Graph):
from ..core.theme import PALETTE
from PySide6.QtGui import QColor
# Module-level palette dict for QGraphicsItem subclasses
_P = {k: QColor(v) for k, v in PALETTE.items()}
# Scene background
scene = QGraphicsScene()
scene.setBackgroundBrush(QBrush(_P["bg"]))
# View
view = QGraphicsView(scene)
view.setBackgroundBrush(QBrush(_P["bg"]))
view.setStyleSheet(f"background:{PALETTE['bg']}; border:none;")
# Grid lines (in drawBackground override)
p.setPen(QPen(_P["border"]))
# Node body gradient
grad = QLinearGradient(0, 0, 0, NODE_H)
grad.setColorAt(0, _P["card"])
grad.setColorAt(1, _P["bg"])
# Node outline
outline_color = _P["border2"] # default
outline_color = _P["accent"] # selectedThese rules apply when building tabs in dashboard_pyside6.py. Violations cause crowded
or broken layouts that are hard to fix after the fact.
Minimum width: 90px. The _spin() helper enforces max(width, 90) automatically.
Never pass a width below 90 — 3-digit numbers (e.g. 400, 100) will crowd the arrow
buttons and become unreadable.
# ✓ correct
count_s = _spin(6, 1, 200) # default width=90
size_s = _spin(100, 10, 2000) # default width=90
large_s = _spin(5000, 0, 99999, 0, 100) # explicit 100px for 5-digit values
# ✗ wrong — clips numbers against arrows
count_s = _spin(6, 1, 200, width=60)Width budget by digit count:
| Max value | Recommended width |
|---|---|
| 1–2 digits (0–99) | 90px |
| 3 digits (100–999) | 90px |
| 4 digits (1000–9999) | 90px |
| 5+ digits (10000+) | 100px |
| Decimal (e.g. 1.00) | 90px |
Do NOT style QSpinBox::up-arrow or QDoubleSpinBox::down-arrow in QSS.
Setting width/height on arrow subcontrols with no image source causes a null-paint
crash (EXCEPTION_ACCESS_VIOLATION) in UEFN's embedded Qt runtime when the user clicks
the spinbox. Arrow subcontrols must be left at Qt native defaults.
All _btn_inp calls in the same group must use widgets of the same width.
The button expands to fill remaining space — if one row has a 70px widget and another
has a 90px widget, the buttons have different widths and the group looks broken.
# ✓ correct — both widgets same width, buttons align
vol_sp = _spin(1.0, 0.0, 4.0, 2) # 90px (default)
radius_sp = _spin(2000.0, 0.0, 99999.0, 0) # 90px (default)
_btn_inp(g, "Set Volume", lambda *_: ..., vol_sp)
_btn_inp(g, "Set Radius", lambda *_: ..., radius_sp)
# ✗ wrong — mismatched widgets, unequal button widths
vol_sp = _spin(1.0, 0.0, 4.0, 2, 70) # 70px
radius_sp = _spin(2000.0, 0.0, 99999.0, 0, 90) # 90pxMax 2 label+widget pairs per QHBoxLayout row. More than 2 pairs causes overflow
on typical panel widths. Use multiple rows instead:
# ✓ correct
_lrow(("Type:", lt_combo), ("Intensity:", intensity_sp))
_lrow(("Color:", color_inp), ("Radius:", atten_sp))
# ✗ wrong — 4 pairs overflow
_lrow(("Type:", lt_combo), ("Intensity:", intensity_sp), ("Color:", color_inp), ("Radius:", atten_sp))Every Toolbelt window must use the canonical TB icon and a consistent title format.
This is handled automatically by ToolbeltWindow — you just need to pass the right title.
ToolbeltWindow.__init__ calls make_toolbelt_icon() from core/base_window.py automatically.
Never recreate the icon inline. If you need the icon outside a ToolbeltWindow (e.g. the
main dashboard, which extends QMainWindow directly), import from the same source:
from ..core.base_window import make_toolbelt_icon
self.setWindowIcon(make_toolbelt_icon())| Window type | Title string |
|---|---|
| Main dashboard | "UEFN Toolbelt" |
| Tool sub-window | "UEFN Toolbelt — Tool Name" |
# ✓ correct
super().__init__(title="UEFN Toolbelt — Verse Device Graph", width=1400, height=860)
# ✗ wrong — no brand prefix
super().__init__(title="Verse Device Graph")
# ✗ wrong — shape/emoji prefix in title (the icon already serves this purpose)
self.setWindowTitle("⬡ UEFN Toolbelt")The icon is a 64×64 blue hexagon with white "TB" text (#3A3AFF fill, Segoe UI 16pt Bold).
Do not use a different icon, a plain QIcon(), or no icon at all.
Never use
QDialog,QInputDialog,QMessageBox, or any other unstyled Qt dialog. Every window the user sees — confirmation prompts, text editors, progress indicators, error messages — must be aToolbeltWindowsubclass withshow_in_uefn(). Unstyled dialogs break the visual contract and are immediately obvious as unfinished.
class MyPromptDialog(ToolbeltWindow):
def __init__(self, initial_value: str = "") -> None:
super().__init__(title="UEFN Toolbelt — My Prompt", width=440, height=300)
self._value = initial_value
self._build_ui()
def _build_ui(self) -> None:
root = QWidget()
self.setCentralWidget(root)
vl = QVBoxLayout(root)
vl.setContentsMargins(0, 0, 0, 0)
vl.setSpacing(0)
bar, bl = self.make_topbar("MY PROMPT")
bl.addWidget(self.make_btn("Save", accent=True, cb=self._save))
bl.addWidget(self.make_btn("Cancel", cb=self.close))
bl.addStretch()
vl.addWidget(bar)
body = QWidget()
body.setStyleSheet(f"background:{self.hex('panel')};")
fl = QVBoxLayout(body)
fl.setContentsMargins(16, 14, 16, 14)
fl.setSpacing(8)
fl.addWidget(self.make_label("Value", role="muted"))
self._edit = QLineEdit(self._value)
self._edit.setFixedHeight(28)
self._edit.setStyleSheet(
f"background:#212121; color:{self.hex('text')}; "
f"border:1px solid #363636; border-radius:3px; padding:3px 8px;"
)
fl.addWidget(self._edit)
vl.addWidget(body)
def _save(self) -> None:
self._value = self._edit.text().strip()
# update whatever owns this dialog, then close
self.close()
# Open it — always show_in_uefn(), never show() alone:
self._dlg = MyPromptDialog(initial_value="hello") # store ref to prevent GC
self._dlg.show_in_uefn()- Always store the dialog reference on
self(e.g.self._dlg = ...) to prevent Python GC from destroying it while Qt still holds the window open. - Use
make_topbar+make_btn— the title bar must be consistent with every other window. - Body background:
self.hex('panel')(#212121). Input fields:background:#212121; border:1px solid #363636. - For multi-line text: use
QTextEditwith the same input QSS. show_in_uefn()registers the Slate tick driver automatically. Never callexec()orshow()alone.
- No
QDialog,QInputDialog, orQMessageBox— use aToolbeltWindowsubclass. See "Dialogs and Sub-Windows" above. - No calling
show()orexec()on dialogs — alwaysshow_in_uefn(). - No floating dialog reference — always assign to
self._somethingbefore callingshow_in_uefn()or the window will be garbage-collected. - No purple, navy, or blue-grey tints. Banned values:
#0a0a1a,#1a1a30,#2a2a45,#3a3a5a,#888899,#444466,#d0d0e0, or any hex where R/G/B are unequal in a blue/purple direction. The palette is strictly neutral dark grey + semantic accents. - No inline
setStyleSheetwith raw hex — useself.hex("token"). - No manual
_QSSimport fromdashboard_pyside6— useToolbeltWindow. - No subclassing
QMainWindowdirectly — always useToolbeltWindow. - No calling
show()alone — alwaysshow_in_uefn()or the window is invisible. - No custom button colors —
accent=Truefor primary, default QSS for secondary.
If you are an AI agent generating code for this project, apply these rules unconditionally:
- Subclass
ToolbeltWindowfrom..core.base_window— neverQMainWindowdirectly. - Call
win.show_in_uefn()— neverwin.show()alone. - Use
self.hex("token")for hex strings,self.P["token"]for QColor. - Use
self.make_topbar,self.make_btn,self.make_text_area, etc. — don't build widgets manually. - For
QGraphicsItemsubclasses:from ..core.theme import PALETTEand build a local_Pdict. - Never hard-code a hex color. If a semantic token doesn't exist, add it to
core/theme.PALETTE. - Never use
QDialog,QInputDialog, orQMessageBox— subclassToolbeltWindow. See "Dialogs and Sub-Windows". - Store dialog references on
selfbefore callingshow_in_uefn()to prevent GC. - Reference implementation:
tools/verse_device_graph.py—_NoteEditDialogfor dialogs,_DeviceGraphWindowfor full tool windows.
If you need a color that doesn't exist in PALETTE:
- Add it to
PALETTEincore/theme.pywith a semantic name. QSSrebuilds automatically on next reload — add a QSS rule to_build_qss()if needed.- Document it in the table above.
- Use
self.hex("your_new_token")in your tool.
Do not add a one-off color inline. If it's worth using, it's worth naming.
Content/Python/UEFN_Toolbelt/tools/verse_device_graph.py is the canonical example:
- Subclasses
ToolbeltWindow(search_DeviceGraphWindow) _Pbuilt fromPALETTEforQGraphicsItemusage (search_P = {k:)show_in_uefn()in the registered tool function (searchrun_verse_graph_open)- Full top bar, health bar, side scroll panel, QGraphicsScene canvas