"""Export Panel -- Editor UI for managing export presets and building game packages.
Wraps the ``simvx.core.export`` module (ExportPreset, ProjectExporter) with
a two-pane editor: preset list on the left, preset property editor on the right.
Includes validation feedback, size estimation, and a progress dialog for builds.
Layout:
+---------------------------------------------------------------+
| Export Project [Validate] [Export] |
|---------------------------------------------------------------|
| Presets | Preset Properties |
| [+ MyGame-Linux] | Name [MyGame-Linux ] |
| [ ] MyGame-Win | Platform [ linux v] |
| | Entry [ main.py ] [...] |
| [+] [Dup] [-] | Version [ 0.1.0 ] |
| | Include [**/*.py, assets/**/* ] |
| | Exclude [ ] |
| | Icon [ ] [...] |
| | Wheel [ ] |
| | Description [A SimVX game. ] |
| |-------------------------------------------|
| | Size estimate: ~42.5 KB |
| | Status: Ready |
+---------------------------------------------------------------+
"""
from __future__ import annotations
import logging
import threading
from pathlib import Path
from simvx.core import (
Control,
Signal,
Vec2,
)
from simvx.core.export import PLATFORMS, ExportError, ExportMode, ExportPreset, ProjectExporter
from simvx.core.ui.theme import get_theme
log = logging.getLogger(__name__)
__all__ = ["ExportPanel", "ExportProgressDialog"]
# ============================================================================
# Theme-derived colour functions (call at draw time for live theme support)
# ============================================================================
def _BG():
return get_theme().bg
def _HEADER_BG():
return get_theme().bg_darker
def _LIST_BG():
return get_theme().bg
def _LIST_SEL():
return get_theme().selection_bg
def _LIST_HOVER():
return get_theme().hover_bg
def _EDITOR_BG():
return get_theme().bg_light
def _ROW_BG():
return get_theme().bg_light
def _ROW_ALT():
return get_theme().bg_lighter
def _TEXT():
return get_theme().text
def _TEXT_DIM():
return get_theme().text_dim
def _ACCENT():
return get_theme().accent
def _SUCCESS():
return get_theme().success
def _ERROR():
return get_theme().error
def _WARNING():
return get_theme().warning
def _BTN():
return get_theme().btn_bg
def _BTN_HOVER():
return get_theme().btn_hover
def _BTN_ACCENT():
return get_theme().accent
def _SEPARATOR():
return get_theme().border
_HEADER_HEIGHT = 32.0
_ROW_HEIGHT = 26.0
_PADDING = 6.0
_LABEL_WIDTH = 90.0
_LIST_WIDTH_RATIO = 0.28
_FONT = 11.0 / 14.0
_SMALL_FONT = 10.0 / 14.0
_PLATFORM_OPTIONS = sorted(PLATFORMS)
_EXPORT_MODE_OPTIONS = ["Folder", "Wheel"]
# ============================================================================
# ExportProgressDialog
# ============================================================================
[docs]
class ExportProgressDialog(Control):
"""Modal dialog showing export progress with status text and progress bar.
Attributes:
on_cancel: Signal emitted when the user clicks Cancel.
on_complete: Signal emitted when the export finishes (success: bool, message: str).
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bg_colour = (0.0, 0.0, 0.0, 0.5)
self.size = Vec2(400, 180)
self.visible = False
self.on_cancel = Signal()
self.on_complete = Signal()
self._progress: float = 0.0
self._status: str = ""
self._finished = False
self._success = False
self._result_message: str = ""
self._cancelled = False
# ------------------------------------------------------------------ #
# Public API
# ------------------------------------------------------------------ #
[docs]
def show_progress(self):
"""Show the dialog and reset state."""
self._progress = 0.0
self._status = "Preparing export..."
self._finished = False
self._success = False
self._result_message = ""
self._cancelled = False
self.visible = True
[docs]
def set_progress(self, fraction: float, status: str):
"""Update progress (0.0-1.0) and status text."""
self._progress = max(0.0, min(1.0, fraction))
self._status = status
[docs]
def finish(self, success: bool, message: str):
"""Mark the export as finished."""
self._finished = True
self._success = success
self._progress = 1.0 if success else self._progress
self._result_message = message
self._status = message
self.on_complete.emit(success, message)
[docs]
def dismiss(self):
"""Hide the dialog."""
self.visible = False
# ------------------------------------------------------------------ #
# Drawing
# ------------------------------------------------------------------ #
[docs]
def draw(self, renderer):
if not self.visible:
return
gx, gy, gw, gh = self.get_global_rect()
# Overlay backdrop
renderer.draw_filled_rect(gx, gy, gw, gh, self.bg_colour)
# Dialog box
dw, dh = 380.0, 160.0
dx = gx + (gw - dw) / 2
dy = gy + (gh - dh) / 2
renderer.draw_filled_rect(dx, dy, dw, dh, (0.16, 0.16, 0.16, 1.0))
renderer.draw_rect(dx, dy, dw, dh, _SEPARATOR())
# Title
title = "Export Complete" if self._finished else "Exporting..."
renderer.draw_text(title, dx + _PADDING, dy + 8, _TEXT(), _FONT)
renderer.draw_filled_rect(dx, dy + 28, dw, 1, _SEPARATOR())
# Status text
colour = _TEXT_DIM()
if self._finished:
colour = _SUCCESS() if self._success else _ERROR()
renderer.draw_text(self._status, dx + _PADDING, dy + 40, colour, _SMALL_FONT)
# Progress bar background
bar_x, bar_y, bar_w, bar_h = dx + _PADDING, dy + 65, dw - 2 * _PADDING, 16.0
renderer.draw_filled_rect(bar_x, bar_y, bar_w, bar_h, (0.10, 0.10, 0.10, 1.0))
# Progress bar fill
fill_colour = _SUCCESS() if (self._finished and self._success) else _ACCENT()
if self._progress > 0:
renderer.draw_filled_rect(bar_x, bar_y, bar_w * self._progress, bar_h, fill_colour)
# Progress percentage
pct_text = f"{self._progress * 100:.0f}%"
renderer.draw_text(pct_text, bar_x + bar_w / 2 - 10, bar_y + 2, _TEXT(), _SMALL_FONT)
# Buttons
btn_y = dy + dh - 36
if self._finished:
# Close button
self._draw_button(renderer, dx + dw - 80, btn_y, 70, 24, "Close", _BTN())
else:
# Cancel button
self._draw_button(renderer, dx + dw - 80, btn_y, 70, 24, "Cancel", _ERROR())
# Result details (if finished)
if self._finished and self._result_message:
detail = self._result_message[:60]
renderer.draw_text(detail, dx + _PADDING, dy + 95, _TEXT_DIM(), _SMALL_FONT)
@staticmethod
def _draw_button(renderer, x, y, w, h, text, bg):
renderer.draw_filled_rect(x, y, w, h, bg)
renderer.draw_rect(x, y, w, h, _SEPARATOR())
renderer.draw_text(text, x + 8, y + 5, _TEXT(), _SMALL_FONT)
# ------------------------------------------------------------------ #
# Input
# ------------------------------------------------------------------ #
def _on_gui_input(self, event):
if not self.visible:
return
if not (hasattr(event, "pressed") and event.pressed and getattr(event, "button", 0) == 1):
return
gx, gy, gw, gh = self.get_global_rect()
dw, dh = 380.0, 160.0
dx = gx + (gw - dw) / 2
dy = gy + (gh - dh) / 2
ex, ey = event.position
btn_y = dy + dh - 36
btn_x = dx + dw - 80
if btn_x <= ex <= btn_x + 70 and btn_y <= ey <= btn_y + 24:
if self._finished:
self.dismiss()
else:
self._cancelled = True
self.on_cancel.emit()
self.finish(False, "Export cancelled by user.")
# ============================================================================
# ExportPanel
# ============================================================================
[docs]
class ExportPanel(Control):
"""Editor panel for managing export presets and building game packages.
Two-pane layout: preset list (left), preset property editor (right).
Wraps ``simvx.core.export.ProjectExporter`` for validation and export.
Args:
editor_state: The central EditorState (optional, used for project path).
"""
def __init__(self, editor_state=None, **kwargs):
super().__init__(**kwargs)
self.state = editor_state
self.bg_colour = _BG()
self.size = Vec2(700, 500)
# Preset management
self._presets: list[ExportPreset] = [ExportPreset()]
self._selected_idx: int = 0
# Exporter
self._exporter = ProjectExporter()
# Validation
self._validation_errors: list[str] = []
self._validation_warnings: list[str] = []
self._size_estimate: int | None = None
# Scroll state
self._scroll_y: float = 0.0
# Progress dialog
self._progress_dialog = ExportProgressDialog(name="ExportProgress")
self._export_thread: threading.Thread | None = None
# Signals
self.export_started = Signal()
self.export_finished = Signal()
# ====================================================================
# Preset management
# ====================================================================
@property
def selected_preset(self) -> ExportPreset | None:
if 0 <= self._selected_idx < len(self._presets):
return self._presets[self._selected_idx]
return None
[docs]
def add_preset(self, preset: ExportPreset | None = None):
"""Add a new export preset."""
p = preset or ExportPreset(name=f"Preset-{len(self._presets) + 1}")
self._presets.append(p)
self._selected_idx = len(self._presets) - 1
self._validate()
[docs]
def duplicate_preset(self):
"""Duplicate the currently selected preset."""
src = self.selected_preset
if not src:
return
from dataclasses import replace
dup = replace(src, name=f"{src.name}-copy")
self._presets.append(dup)
self._selected_idx = len(self._presets) - 1
self._validate()
[docs]
def remove_preset(self):
"""Remove the currently selected preset."""
if len(self._presets) <= 1:
return # Keep at least one preset
self._presets.pop(self._selected_idx)
self._selected_idx = min(self._selected_idx, len(self._presets) - 1)
self._validate()
[docs]
def select_preset(self, index: int):
"""Select a preset by index."""
if 0 <= index < len(self._presets):
self._selected_idx = index
self._size_estimate = None
self._validate()
# ====================================================================
# Validation / estimation
# ====================================================================
def _validate(self):
"""Validate the selected preset and update error/warning lists."""
preset = self.selected_preset
self._validation_errors.clear()
self._validation_warnings.clear()
if not preset:
return
try:
project_dir = self._get_project_dir()
warnings = self._exporter.validate_preset(preset, project_dir)
self._validation_warnings = warnings
except ExportError as exc:
self._validation_errors.append(str(exc))
def _estimate_size(self):
"""Calculate approximate export size for the selected preset."""
preset = self.selected_preset
project_dir = self._get_project_dir()
if not preset or not project_dir:
self._size_estimate = None
return
try:
self._size_estimate = self._exporter.estimate_size(project_dir, preset)
except ExportError:
self._size_estimate = None
def _get_project_dir(self) -> Path | None:
"""Resolve the project directory from editor state or cwd."""
if self.state and self.state.project_path:
return Path(self.state.project_path)
if self.state and self.state.current_scene_path:
return Path(self.state.current_scene_path).parent
return Path.cwd()
# ====================================================================
# Export execution
# ====================================================================
[docs]
def start_export(self):
"""Begin exporting the selected preset in a background thread."""
preset = self.selected_preset
project_dir = self._get_project_dir()
if not preset or not project_dir:
return
self._validate()
if self._validation_errors:
return
output_dir = project_dir / "dist"
self._progress_dialog.show_progress()
self.export_started.emit()
def _run():
try:
self._progress_dialog.set_progress(0.1, "Validating preset...")
self._exporter.validate_preset(preset, project_dir)
self._progress_dialog.set_progress(0.3, "Collecting assets...")
# The exporter does collection internally, we just call export
self._progress_dialog.set_progress(0.5, "Copying files and generating package...")
result_path = self._exporter.export(project_dir, preset, output_dir)
if preset.build_wheel:
self._progress_dialog.set_progress(0.8, "Building wheel...")
self._progress_dialog.finish(True, f"Exported to {result_path}")
except ExportError as exc:
self._progress_dialog.finish(False, str(exc))
except Exception as exc:
log.exception("Export failed")
self._progress_dialog.finish(False, f"Unexpected error: {exc}")
self._export_thread = threading.Thread(target=_run, daemon=True)
self._export_thread.start()
# ====================================================================
# Drawing
# ====================================================================
[docs]
def draw(self, renderer):
gx, gy, gw, gh = self.get_global_rect()
renderer.draw_filled_rect(gx, gy, gw, gh, _BG())
renderer.push_clip(gx, gy, gw, gh)
y = gy
y = self._draw_header(renderer, gx, y, gw)
content_h = gh - (y - gy)
list_w = gw * _LIST_WIDTH_RATIO
editor_w = gw - list_w
# Left: preset list
self._draw_preset_list(renderer, gx, y, list_w, content_h)
# Separator
renderer.draw_filled_rect(gx + list_w, y, 1, content_h, _SEPARATOR())
# Right: preset editor
renderer.push_clip(gx + list_w + 1, y, editor_w - 1, content_h)
self._draw_preset_editor(renderer, gx + list_w + 1, y, editor_w - 1, content_h)
renderer.pop_clip()
# Progress dialog overlay
if self._progress_dialog.visible:
self._progress_dialog.size = Vec2(gw, gh)
self._progress_dialog.position = Vec2(gx, gy)
self._progress_dialog.draw(renderer)
renderer.pop_clip()
def _draw_header(self, renderer, x, y, w) -> float:
"""Draw the title bar with Validate and Export buttons."""
renderer.draw_filled_rect(x, y, w, _HEADER_HEIGHT, _HEADER_BG())
renderer.draw_text("Export Project", x + _PADDING, y + 9, _TEXT(), _FONT)
# Validate button
vbtn_w = 65.0
vbtn_x = x + w - 2 * (vbtn_w + _PADDING) - _PADDING
self._draw_button(renderer, vbtn_x, y + 4, vbtn_w, 24, "Validate", _BTN())
# Export button
ebtn_x = x + w - vbtn_w - _PADDING
has_errors = bool(self._validation_errors)
ebtn_bg = _BTN() if has_errors else _BTN_ACCENT()
self._draw_button(renderer, ebtn_x, y + 4, vbtn_w, 24, "Export", ebtn_bg)
renderer.draw_filled_rect(x, y + _HEADER_HEIGHT - 1, w, 1, _SEPARATOR())
return y + _HEADER_HEIGHT
def _draw_preset_list(self, renderer, x, y, w, h):
"""Draw the preset list panel."""
renderer.draw_filled_rect(x, y, w, h, _LIST_BG())
renderer.push_clip(x, y, w, h)
cy = y + _PADDING
renderer.draw_text("Presets", x + _PADDING, cy, _TEXT_DIM(), _SMALL_FONT)
cy += 18
for i, preset in enumerate(self._presets):
row_bg = _LIST_SEL() if i == self._selected_idx else _LIST_BG()
renderer.draw_filled_rect(x + 2, cy, w - 4, _ROW_HEIGHT, row_bg)
text_colour = _TEXT() if i == self._selected_idx else _TEXT_DIM()
renderer.draw_text(preset.name, x + _PADDING + 4, cy + 5, text_colour, _SMALL_FONT)
# Platform badge
plat_text = preset.platform[:3].upper()
renderer.draw_text(plat_text, x + w - 36, cy + 5, _ACCENT(), _SMALL_FONT)
cy += _ROW_HEIGHT
# Add / Duplicate / Remove buttons
btn_y = y + h - 30
btn_w = 28.0
bx = x + _PADDING
self._draw_button(renderer, bx, btn_y, btn_w, 22, "+", _BTN())
bx += btn_w + 4
self._draw_button(renderer, bx, btn_y, 36, 22, "Dup", _BTN())
bx += 40
self._draw_button(renderer, bx, btn_y, btn_w, 22, "-", _ERROR() if len(self._presets) > 1 else _BTN())
renderer.pop_clip()
def _draw_preset_editor(self, renderer, x, y, w, h):
"""Draw the property editor for the selected preset."""
preset = self.selected_preset
if not preset:
renderer.draw_text("No preset selected", x + _PADDING, y + 20, _TEXT_DIM(), _FONT)
return
cy = y + _PADDING
# Export Mode dropdown row
mode_label = _EXPORT_MODE_OPTIONS[int(preset.export_mode)]
cy = self._draw_field_row(renderer, x, cy, w, "Export Mode", mode_label, False)
cy = self._draw_field_row(renderer, x, cy, w, "Name", preset.name, True)
cy = self._draw_field_row(renderer, x, cy, w, "Platform", preset.platform, False)
cy = self._draw_field_row(renderer, x, cy, w, "Entry Point", preset.entry_point, True)
cy = self._draw_field_row(renderer, x, cy, w, "Version", preset.version, False)
cy = self._draw_field_row(renderer, x, cy, w, "Include", ", ".join(preset.include_patterns), True)
cy = self._draw_field_row(renderer, x, cy, w, "Exclude", ", ".join(preset.exclude_patterns) or "(none)", False)
cy = self._draw_field_row(renderer, x, cy, w, "Icon", preset.icon_path or "(none)", True)
cy = self._draw_field_row(renderer, x, cy, w, "Description", preset.description, False)
# Build wheel checkbox row (only visible in wheel mode)
if preset.export_mode == ExportMode.WHEEL:
bg = _ROW_ALT()
renderer.draw_filled_rect(x, cy, w, _ROW_HEIGHT, bg)
renderer.draw_text("Build Wheel", x + _PADDING, cy + 6, _TEXT_DIM(), _SMALL_FONT)
chk_text = "[x]" if preset.build_wheel else "[ ]"
renderer.draw_text(chk_text, x + _LABEL_WIDTH, cy + 6, _TEXT(), _SMALL_FONT)
cy += _ROW_HEIGHT
# Create ZIP checkbox row (only visible in folder mode)
if preset.export_mode == ExportMode.FOLDER:
bg = _ROW_ALT()
renderer.draw_filled_rect(x, cy, w, _ROW_HEIGHT, bg)
renderer.draw_text("Create ZIP", x + _PADDING, cy + 6, _TEXT_DIM(), _SMALL_FONT)
chk_text = "[x]" if preset.create_zip else "[ ]"
renderer.draw_text(chk_text, x + _LABEL_WIDTH, cy + 6, _TEXT(), _SMALL_FONT)
cy += _ROW_HEIGHT
# Scene to Code checkbox row
bg = _ROW_BG()
renderer.draw_filled_rect(x, cy, w, _ROW_HEIGHT, bg)
renderer.draw_text("Scene to Code", x + _PADDING, cy + 6, _TEXT_DIM(), _SMALL_FONT)
chk_text = "[x]" if preset.scene_to_code else "[ ]"
renderer.draw_text(chk_text, x + _LABEL_WIDTH, cy + 6, _TEXT(), _SMALL_FONT)
cy += _ROW_HEIGHT
# Separator
renderer.draw_filled_rect(x, cy, w, 1, _SEPARATOR())
cy += _PADDING + 1
# Size estimate
if self._size_estimate is not None:
size_kb = self._size_estimate / 1024
if size_kb > 1024:
size_text = f"~{size_kb / 1024:.1f} MB"
else:
size_text = f"~{size_kb:.1f} KB"
renderer.draw_text(f"Estimated size: {size_text}", x + _PADDING, cy, _TEXT_DIM(), _SMALL_FONT)
else:
renderer.draw_text("Size: (click Validate to estimate)", x + _PADDING, cy, _TEXT_DIM(), _SMALL_FONT)
cy += 16
# Package name
renderer.draw_text(f"Package: {preset.package_name}", x + _PADDING, cy, _TEXT_DIM(), _SMALL_FONT)
cy += 20
# Validation status
if self._validation_errors:
renderer.draw_text("Errors:", x + _PADDING, cy, _ERROR(), _SMALL_FONT)
cy += 14
for err in self._validation_errors:
renderer.draw_text(f" {err[:80]}", x + _PADDING, cy, _ERROR(), _SMALL_FONT)
cy += 14
elif self._validation_warnings:
renderer.draw_text("Warnings:", x + _PADDING, cy, _WARNING(), _SMALL_FONT)
cy += 14
for warn in self._validation_warnings:
renderer.draw_text(f" {warn[:80]}", x + _PADDING, cy, _WARNING(), _SMALL_FONT)
cy += 14
else:
renderer.draw_text("Status: Ready to export", x + _PADDING, cy, _SUCCESS(), _SMALL_FONT)
def _draw_field_row(self, renderer, x, y, w, label: str, value: str, alt: bool) -> float:
"""Draw a single label-value row."""
bg = _ROW_ALT() if alt else _ROW_BG()
renderer.draw_filled_rect(x, y, w, _ROW_HEIGHT, bg)
renderer.draw_text(label, x + _PADDING, y + 6, _TEXT_DIM(), _SMALL_FONT)
# Truncate long values to fit
max_chars = int((w - _LABEL_WIDTH - _PADDING) / 6)
display = value[:max_chars] + "..." if len(value) > max_chars else value
renderer.draw_text(display, x + _LABEL_WIDTH, y + 6, _TEXT(), _SMALL_FONT)
return y + _ROW_HEIGHT
@staticmethod
def _draw_button(renderer, x, y, w, h, text, bg):
renderer.draw_filled_rect(x, y, w, h, bg)
renderer.draw_rect(x, y, w, h, _SEPARATOR())
renderer.draw_text(text, x + 6, y + 4, _TEXT(), _SMALL_FONT)
# ====================================================================
# Input handling
# ====================================================================
def _on_gui_input(self, event):
# Delegate to progress dialog first if visible
if self._progress_dialog.visible:
self._progress_dialog._on_gui_input(event)
return
if not (hasattr(event, "pressed") and event.pressed and getattr(event, "button", 0) == 1):
# Handle scroll
if hasattr(event, "delta"):
_, dy = event.delta if isinstance(event.delta, tuple) else (0, event.delta)
self._scroll_y = max(0.0, self._scroll_y - dy * 20.0)
return
gx, gy, gw, gh = self.get_global_rect()
ex, ey = event.position
if not (gx <= ex <= gx + gw and gy <= ey <= gy + gh):
return
# Header buttons
vbtn_w = 65.0
header_bottom = gy + _HEADER_HEIGHT
if gy + 4 <= ey <= gy + 28:
# Validate button
vbtn_x = gx + gw - 2 * (vbtn_w + _PADDING) - _PADDING
if vbtn_x <= ex <= vbtn_x + vbtn_w:
self._validate()
self._estimate_size()
return
# Export button
ebtn_x = gx + gw - vbtn_w - _PADDING
if ebtn_x <= ex <= ebtn_x + vbtn_w:
self.start_export()
return
list_w = gw * _LIST_WIDTH_RATIO
# Preset list clicks
if gx <= ex <= gx + list_w and ey >= header_bottom:
list_top = header_bottom + _PADDING + 18 # Below "Presets" label
btn_y = header_bottom + gh - _HEADER_HEIGHT - 30
# Preset item selection
if list_top <= ey < btn_y:
clicked_idx = int((ey - list_top) / _ROW_HEIGHT)
if 0 <= clicked_idx < len(self._presets):
self.select_preset(clicked_idx)
return
# Bottom buttons: Add / Dup / Remove
if btn_y <= ey <= btn_y + 22:
bx = gx + _PADDING
if bx <= ex <= bx + 28:
self.add_preset()
return
bx += 32
if bx <= ex <= bx + 36:
self.duplicate_preset()
return
bx += 40
if bx <= ex <= bx + 28:
self.remove_preset()
return
# Right side: build wheel checkbox toggle
editor_x = gx + list_w + 1
if editor_x <= ex and ey >= header_bottom:
# The build wheel row is the 9th row (0-indexed: 8)
wheel_row_y = header_bottom + _PADDING + 8 * _ROW_HEIGHT
if wheel_row_y <= ey <= wheel_row_y + _ROW_HEIGHT:
preset = self.selected_preset
if preset:
preset.build_wheel = not preset.build_wheel
# ====================================================================
# Serialization
# ====================================================================
[docs]
def get_presets_data(self) -> list[dict]:
"""Serialize all presets to a list of dicts for saving."""
from dataclasses import asdict
result = []
for p in self._presets:
d = asdict(p)
d["export_mode"] = int(p.export_mode)
result.append(d)
return result
[docs]
def load_presets_data(self, data: list[dict]):
"""Load presets from serialized data."""
self._presets.clear()
for d in data:
p = ExportPreset(
name=d.get("name", "Unnamed"),
platform=d.get("platform", "linux"),
entry_point=d.get("entry_point", "main.py"),
include_patterns=d.get("include_patterns", ["**/*.py", "assets/**/*", "scenes/**/*"]),
exclude_patterns=d.get("exclude_patterns", []),
icon_path=d.get("icon_path"),
version=d.get("version", "0.1.0"),
description=d.get("description", "A SimVX game."),
custom_features=d.get("custom_features", {}),
build_wheel=d.get("build_wheel", False),
export_mode=ExportMode(d.get("export_mode", ExportMode.FOLDER)),
create_zip=d.get("create_zip", False),
scene_to_code=d.get("scene_to_code", False),
)
self._presets.append(p)
if not self._presets:
self._presets.append(ExportPreset())
self._selected_idx = 0
self._validate()