"""Export dialog -- modal UI for launching project exports.
Launched from File > Export Project... (Ctrl+E). Swaps between
target-specific sub-forms (Desktop / Web / Standalone Exe / Nuitka /
Android), persists settings in ``simvx.toml``, and drives
:func:`simvx.editor.export_controller.run_export`.
"""
import logging
import shutil
import sys
from pathlib import Path
from typing import TYPE_CHECKING
from simvx.core import (
Button,
CheckBox,
DropDown,
HBoxContainer,
Label,
Panel,
ProgressBar,
Signal,
TextEdit,
VBoxContainer,
Vec2,
)
from simvx.core.project import (
ProjectSettings,
find_project,
load_project,
save_project,
)
from simvx.web.export import DEFAULT_PYODIDE_VERSION
from . import export_controller
from .export_controller import (
ExportProgress,
ExportRequest,
ExportTarget,
detect_host_os,
load_request_from_toml,
save_request_to_toml,
)
if TYPE_CHECKING:
from .state import State
log = logging.getLogger(__name__)
__all__ = ["ExportDialog"]
# Display labels for targets (dropdown order mirrors ExportTarget enum logical order)
_TARGETS: list[tuple[ExportTarget, str]] = [
(ExportTarget.DESKTOP_WHEEL, "Desktop Wheel"),
(ExportTarget.DESKTOP_FOLDER, "Desktop Folder"),
(ExportTarget.WEB_HTML, "Web HTML"),
(ExportTarget.STANDALONE_EXE, "Standalone Executable"),
(ExportTarget.STANDALONE_NUITKA, "Nuitka (coming soon)"),
(ExportTarget.ANDROID_APK, "Android APK"),
]
def _labeled_field(label: str, widget, label_w: float = 130.0) -> HBoxContainer:
"""Build an inline label + widget row."""
row = HBoxContainer()
row.separation = 8
lbl = Label(label)
lbl.size = Vec2(label_w, 28)
lbl.text_colour = (0.75, 0.75, 0.78, 1.0)
row.add_child(lbl)
widget.size = Vec2(260, 28)
row.add_child(widget)
return row
[docs]
class ExportDialog(Panel):
"""Modal export dialog.
Signals:
closed: emitted when the dialog hides.
exported: emitted with the final :class:`ExportProgress` when
an export run completes.
"""
DIALOG_W = 620.0
DIALOG_H = 560.0
def __init__(self, state: State | None = None, **kwargs):
super().__init__(**kwargs)
self.state = state
self.bg_colour = (0.0, 0.0, 0.0, 0.6)
self.border_width = 0
self.visible = False
self.z_index = 1600
self.closed = Signal()
self.exported = Signal()
# Target form panels keyed by target
self._forms: dict[ExportTarget, Panel] = {}
self._target: ExportTarget = ExportTarget.DESKTOP_FOLDER
self._running: bool = False
self._current_request: ExportRequest | None = None
# UI refs
self._inner: Panel | None = None
self._target_dropdown: DropDown | None = None
self._package_edit: TextEdit | None = None
self._version_edit: TextEdit | None = None
self._output_edit: TextEdit | None = None
self._message_label: Label | None = None
self._progress_row: HBoxContainer | None = None
self._progress_bar: ProgressBar | None = None
self._progress_label: Label | None = None
self._validate_btn: Button | None = None
self._export_btn: Button | None = None
self._close_btn: Button | None = None
# Per-form widget refs (populated in _build_* helpers)
self._desktop_mode_dd: DropDown | None = None
self._desktop_os_dd: DropDown | None = None
self._desktop_build_wheel_cb: CheckBox | None = None
self._desktop_create_zip_cb: CheckBox | None = None
self._desktop_icon_edit: TextEdit | None = None
self._web_width_edit: TextEdit | None = None
self._web_height_edit: TextEdit | None = None
self._web_responsive_cb: CheckBox | None = None
self._web_pyodide_edit: TextEdit | None = None
self._web_packages_edit: TextEdit | None = None
self._web_root_edit: TextEdit | None = None
self._web_title_edit: TextEdit | None = None
self._exe_onefile_cb: CheckBox | None = None
self._exe_console_cb: CheckBox | None = None
self._exe_name_edit: TextEdit | None = None
self._exe_host_label: Label | None = None
self._exe_warning: Label | None = None
self._android_package_edit: TextEdit | None = None
self._android_min_sdk_edit: TextEdit | None = None
self._android_mode_dd: DropDown | None = None
self._android_warning: Label | None = None
self._build()
# ------------------------------------------------------------------
# Build
# ------------------------------------------------------------------
def _build(self):
inner = Panel(name="ExportDialogInner")
inner.bg_colour = (0.16, 0.16, 0.18, 1.0)
inner.border_colour = (0.35, 0.35, 0.40, 1.0)
inner.border_width = 1.0
inner.size = Vec2(self.DIALOG_W, self.DIALOG_H)
self._inner = inner
vbox = VBoxContainer(name="ExportDialogVBox")
vbox.separation = 8
vbox.position = Vec2(16, 16)
vbox.size = Vec2(self.DIALOG_W - 32, self.DIALOG_H - 32)
inner.add_child(vbox)
# Title
title = Label("Export Project", name="Title")
title.font_size = 16.0
title.text_colour = (0.95, 0.95, 0.97, 1.0)
title.size = Vec2(self.DIALOG_W - 32, 24)
vbox.add_child(title)
# Project name (informational)
self._project_label = Label("(no project)", name="ProjectName")
self._project_label.font_size = 11.0
self._project_label.text_colour = (0.55, 0.55, 0.58, 1.0)
self._project_label.size = Vec2(self.DIALOG_W - 32, 18)
vbox.add_child(self._project_label)
# Target row
target_row = HBoxContainer(name="TargetRow")
target_row.separation = 8
tlbl = Label("Target:")
tlbl.size = Vec2(130, 28)
tlbl.text_colour = (0.75, 0.75, 0.78, 1.0)
target_row.add_child(tlbl)
self._target_dropdown = DropDown(items=[lbl for _, lbl in _TARGETS], selected=1, name="TargetDropDown")
self._target_dropdown.size = Vec2(260, 28)
self._target_dropdown.item_selected.connect(self._on_target_selected)
target_row.add_child(self._target_dropdown)
vbox.add_child(target_row)
# Common fields
common = VBoxContainer(name="CommonFields")
common.separation = 6
common.size = Vec2(self.DIALOG_W - 32, 120)
self._package_edit = TextEdit(name="PackageNameEdit")
common.add_child(_labeled_field("Package name", self._package_edit))
self._version_edit = TextEdit(name="VersionEdit")
common.add_child(_labeled_field("Version", self._version_edit))
# Output dir row with Browse button
out_row = HBoxContainer(name="OutputRow")
out_row.separation = 8
out_lbl = Label("Output dir")
out_lbl.size = Vec2(130, 28)
out_lbl.text_colour = (0.75, 0.75, 0.78, 1.0)
out_row.add_child(out_lbl)
self._output_edit = TextEdit(name="OutputDirEdit")
self._output_edit.size = Vec2(200, 28)
out_row.add_child(self._output_edit)
browse = Button("Browse...", name="BrowseBtn")
browse.size = Vec2(80, 28)
browse.pressed.connect(self._on_browse_output)
out_row.add_child(browse)
common.add_child(out_row)
vbox.add_child(common)
# Target form holder
self._form_holder = Panel(name="TargetForm")
self._form_holder.bg_colour = (0.13, 0.13, 0.15, 1.0)
self._form_holder.border_width = 0
self._form_holder.size = Vec2(self.DIALOG_W - 32, 220)
self._build_forms()
vbox.add_child(self._form_holder)
# Message area
self._message_label = Label("", name="MessageArea")
self._message_label.font_size = 11.0
self._message_label.text_colour = (0.9, 0.4, 0.4, 1.0)
self._message_label.size = Vec2(self.DIALOG_W - 32, 40)
vbox.add_child(self._message_label)
# Progress row
self._progress_row = HBoxContainer(name="ProgressRow")
self._progress_row.separation = 8
self._progress_bar = ProgressBar(0, 100, name="ExportProgressBar")
self._progress_bar.size = Vec2(240, 18)
self._progress_row.add_child(self._progress_bar)
self._progress_label = Label("", name="ProgressLabel")
self._progress_label.size = Vec2(260, 18)
self._progress_label.text_colour = (0.75, 0.75, 0.78, 1.0)
self._progress_row.add_child(self._progress_label)
self._progress_row.visible = False
self._progress_row.size = Vec2(self.DIALOG_W - 32, 24)
vbox.add_child(self._progress_row)
# Button row
btns = HBoxContainer(name="ButtonRow")
btns.separation = 8
self._validate_btn = Button("Validate", name="ValidateBtn")
self._validate_btn.size = Vec2(100, 32)
self._validate_btn.pressed.connect(self._on_validate)
btns.add_child(self._validate_btn)
self._export_btn = Button("Export", name="ExportBtn")
self._export_btn.size = Vec2(100, 32)
self._export_btn.bg_colour = (0.20, 0.57, 0.92, 1.0)
self._export_btn.pressed.connect(self._on_export)
btns.add_child(self._export_btn)
self._close_btn = Button("Close", name="CloseBtn")
self._close_btn.size = Vec2(100, 32)
self._close_btn.bg_colour = (0.30, 0.30, 0.33, 1.0)
self._close_btn.pressed.connect(self._on_close)
btns.add_child(self._close_btn)
vbox.add_child(btns)
self.add_child(inner)
self._set_target(ExportTarget.DESKTOP_FOLDER)
# ---- Target-specific sub-forms -----------------------------------
def _build_forms(self):
self._forms[ExportTarget.DESKTOP_WHEEL] = self._build_desktop_form()
self._forms[ExportTarget.DESKTOP_FOLDER] = self._forms[ExportTarget.DESKTOP_WHEEL]
self._forms[ExportTarget.WEB_HTML] = self._build_web_form()
self._forms[ExportTarget.STANDALONE_EXE] = self._build_exe_form()
self._forms[ExportTarget.STANDALONE_NUITKA] = self._build_nuitka_form()
self._forms[ExportTarget.ANDROID_APK] = self._build_android_form()
for form in set(self._forms.values()):
form.visible = False
self._form_holder.add_child(form)
def _build_desktop_form(self) -> Panel:
form = Panel(name="DesktopForm")
form.border_width = 0
vbox = VBoxContainer()
vbox.separation = 6
vbox.position = Vec2(8, 8)
vbox.size = Vec2(self.DIALOG_W - 48, 200)
self._desktop_mode_dd = DropDown(items=["Folder", "Wheel"], selected=0, name="DesktopModeDD")
self._desktop_mode_dd.item_selected.connect(self._on_desktop_mode_changed)
vbox.add_child(_labeled_field("Mode", self._desktop_mode_dd))
self._desktop_os_dd = DropDown(items=["linux", "windows", "macos"], selected=0, name="DesktopOsDD")
vbox.add_child(_labeled_field("OS label", self._desktop_os_dd))
self._desktop_build_wheel_cb = CheckBox("Build wheel (uv build)", name="BuildWheelCheckBox")
vbox.add_child(_labeled_field("", self._desktop_build_wheel_cb))
self._desktop_create_zip_cb = CheckBox("Create ZIP", name="CreateZipCheckBox")
vbox.add_child(_labeled_field("", self._desktop_create_zip_cb))
self._desktop_icon_edit = TextEdit(name="IconPathEdit")
vbox.add_child(_labeled_field("Icon path", self._desktop_icon_edit))
form.add_child(vbox)
form.size = Vec2(self.DIALOG_W - 32, 200)
return form
def _build_web_form(self) -> Panel:
form = Panel(name="WebForm")
form.border_width = 0
vbox = VBoxContainer()
vbox.separation = 6
vbox.position = Vec2(8, 8)
vbox.size = Vec2(self.DIALOG_W - 48, 200)
self._web_width_edit = TextEdit(name="WebWidthEdit")
vbox.add_child(_labeled_field("Width", self._web_width_edit))
self._web_height_edit = TextEdit(name="WebHeightEdit")
vbox.add_child(_labeled_field("Height", self._web_height_edit))
self._web_responsive_cb = CheckBox("Responsive", name="ResponsiveCheckBox")
vbox.add_child(_labeled_field("", self._web_responsive_cb))
self._web_pyodide_edit = TextEdit(name="PyodideVersionEdit")
vbox.add_child(_labeled_field("Pyodide version", self._web_pyodide_edit))
self._web_packages_edit = TextEdit(name="ExtraPackagesEdit")
vbox.add_child(_labeled_field("Extra packages (,)", self._web_packages_edit))
self._web_root_edit = TextEdit(name="RootClassEdit")
vbox.add_child(_labeled_field("Root class", self._web_root_edit))
self._web_title_edit = TextEdit(name="WebTitleEdit")
vbox.add_child(_labeled_field("Title", self._web_title_edit))
info = Label("Output is a single .html file.", name="WebInfo")
info.font_size = 10.0
info.text_colour = (0.55, 0.55, 0.58, 1.0)
info.size = Vec2(self.DIALOG_W - 48, 18)
vbox.add_child(info)
form.add_child(vbox)
form.size = Vec2(self.DIALOG_W - 32, 220)
return form
def _build_exe_form(self) -> Panel:
form = Panel(name="ExeForm")
form.border_width = 0
vbox = VBoxContainer()
vbox.separation = 6
vbox.position = Vec2(8, 8)
vbox.size = Vec2(self.DIALOG_W - 48, 200)
self._exe_host_label = Label(f"Host OS: {detect_host_os()}", name="ExeHostLabel")
self._exe_host_label.font_size = 12.0
self._exe_host_label.text_colour = (0.85, 0.85, 0.88, 1.0)
self._exe_host_label.size = Vec2(self.DIALOG_W - 48, 20)
vbox.add_child(self._exe_host_label)
self._exe_onefile_cb = CheckBox("Onefile", name="ExeOnefileCheckBox", checked=True)
vbox.add_child(_labeled_field("", self._exe_onefile_cb))
self._exe_console_cb = CheckBox("Console", name="ExeConsoleCheckBox")
vbox.add_child(_labeled_field("", self._exe_console_cb))
self._exe_name_edit = TextEdit(name="ExeNameEdit")
vbox.add_child(_labeled_field("Executable name", self._exe_name_edit))
info = Label(
"PyInstaller cannot cross-compile. Build on Linux/Windows/macOS "
"respectively for that platform's binary.",
name="ExeInfo",
)
info.font_size = 10.0
info.text_colour = (0.55, 0.55, 0.58, 1.0)
info.size = Vec2(self.DIALOG_W - 48, 18)
vbox.add_child(info)
self._exe_warning = Label("", name="ExeWarning")
self._exe_warning.font_size = 11.0
self._exe_warning.text_colour = (0.95, 0.75, 0.30, 1.0)
self._exe_warning.size = Vec2(self.DIALOG_W - 48, 18)
vbox.add_child(self._exe_warning)
form.add_child(vbox)
form.size = Vec2(self.DIALOG_W - 32, 220)
return form
def _build_nuitka_form(self) -> Panel:
form = Panel(name="NuitkaForm")
form.border_width = 0
lbl = Label("Nuitka target not yet implemented.", name="NuitkaNotice")
lbl.font_size = 13.0
lbl.text_colour = (0.85, 0.45, 0.45, 1.0)
lbl.position = Vec2(16, 40)
lbl.size = Vec2(self.DIALOG_W - 64, 22)
form.add_child(lbl)
form.size = Vec2(self.DIALOG_W - 32, 220)
return form
def _build_android_form(self) -> Panel:
form = Panel(name="AndroidForm")
form.border_width = 0
vbox = VBoxContainer()
vbox.separation = 6
vbox.position = Vec2(8, 8)
vbox.size = Vec2(self.DIALOG_W - 48, 200)
self._android_package_edit = TextEdit(name="AndroidPackageEdit")
vbox.add_child(_labeled_field("Package (reverse DNS)", self._android_package_edit))
self._android_min_sdk_edit = TextEdit(name="AndroidMinSdkEdit")
vbox.add_child(_labeled_field("Min SDK", self._android_min_sdk_edit))
self._android_mode_dd = DropDown(items=["debug", "release"], selected=0, name="AndroidModeDD")
vbox.add_child(_labeled_field("Build mode", self._android_mode_dd))
info = Label(
"Requires buildozer + Android SDK/NDK + glslc.",
name="AndroidInfo",
)
info.font_size = 10.0
info.text_colour = (0.55, 0.55, 0.58, 1.0)
info.size = Vec2(self.DIALOG_W - 48, 18)
vbox.add_child(info)
self._android_warning = Label("", name="AndroidWarning")
self._android_warning.font_size = 11.0
self._android_warning.text_colour = (0.95, 0.75, 0.30, 1.0)
self._android_warning.size = Vec2(self.DIALOG_W - 48, 18)
vbox.add_child(self._android_warning)
form.add_child(vbox)
form.size = Vec2(self.DIALOG_W - 32, 220)
return form
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
[docs]
def show_dialog(self, parent_size: Vec2 | None = None):
"""Show and center the dialog; populate fields from simvx.toml."""
self.visible = True
if parent_size:
self.size = parent_size
if self._inner:
pw = self.size.x if self.size.x > 0 else 1280
ph = self.size.y if self.size.y > 0 else 720
self._inner.position = Vec2((pw - self.DIALOG_W) / 2, (ph - self.DIALOG_H) / 2)
self._populate_from_settings()
self._update_button_states()
self._progress_row.visible = False
self._message_label.text = ""
self.queue_redraw()
[docs]
def hide_dialog(self):
self.visible = False
self._running = False
self.closed.emit()
# ------------------------------------------------------------------
# Populate / serialise
# ------------------------------------------------------------------
def _project_dir(self) -> Path:
if self.state is not None and getattr(self.state, "project_path", None):
return Path(self.state.project_path)
return Path.cwd()
def _toml_path(self) -> Path:
project = self._project_dir()
toml = project / "simvx.toml"
if toml.is_file():
return toml
found = find_project(project)
return found if found is not None else toml
def _load_settings(self) -> ProjectSettings:
toml = self._toml_path()
if toml.is_file():
try:
return load_project(toml)
except Exception:
log.exception("Failed to load %s", toml)
s = ProjectSettings()
s.project_path = str(toml)
return s
def _populate_from_settings(self):
settings = self._load_settings()
project = self._project_dir()
self._project_label.text = f"Project: {settings.name} ({project})"
req = load_request_from_toml(self._target, settings, project)
self._apply_request_to_form(req)
self._update_warnings()
def _apply_request_to_form(self, req: ExportRequest):
self._package_edit.text = req.package_name
self._version_edit.text = req.version
self._output_edit.text = str(req.output_dir)
# Desktop
self._desktop_mode_dd.selected_index = 1 if self._target == ExportTarget.DESKTOP_WHEEL else 0
os_idx = ["linux", "windows", "macos"].index(req.os_label) if req.os_label in ("linux", "windows", "macos") else 0
self._desktop_os_dd.selected_index = os_idx
self._desktop_build_wheel_cb.checked = req.build_wheel
self._desktop_create_zip_cb.checked = req.create_zip
self._desktop_icon_edit.text = req.icon_path or ""
# Web
self._web_width_edit.text = str(req.web_width)
self._web_height_edit.text = str(req.web_height)
self._web_responsive_cb.checked = req.web_responsive
self._web_pyodide_edit.text = req.pyodide_version
self._web_packages_edit.text = ",".join(req.extra_packages)
self._web_root_edit.text = req.root_class or ""
self._web_title_edit.text = req.title
# Exe
self._exe_onefile_cb.checked = req.exe_onefile
self._exe_console_cb.checked = req.exe_console
self._exe_name_edit.text = req.exe_name
self._exe_host_label.text = f"Host OS: {req.exe_host_os}"
# Android
self._android_package_edit.text = req.android_package
self._android_min_sdk_edit.text = str(req.android_min_sdk)
a_idx = 1 if req.android_mode == "release" else 0
self._android_mode_dd.selected_index = a_idx
def _build_request(self) -> ExportRequest:
project = self._project_dir()
target = self._target
# Desktop mode dropdown can override explicit target
if target in (ExportTarget.DESKTOP_WHEEL, ExportTarget.DESKTOP_FOLDER):
target = (
ExportTarget.DESKTOP_WHEEL
if self._desktop_mode_dd.selected_index == 1
else ExportTarget.DESKTOP_FOLDER
)
self._target = target
extra = [s.strip() for s in self._web_packages_edit.text.split(",") if s.strip()]
try:
web_w = int(self._web_width_edit.text or "0")
except ValueError:
web_w = 0
try:
web_h = int(self._web_height_edit.text or "0")
except ValueError:
web_h = 0
try:
min_sdk = int(self._android_min_sdk_edit.text or "26")
except ValueError:
min_sdk = 26
out_text = self._output_edit.text.strip() or "dist"
out_dir = Path(out_text)
if not out_dir.is_absolute():
out_dir = project / out_dir
return ExportRequest(
target=target,
project_dir=project,
output_dir=out_dir,
package_name=self._package_edit.text.strip(),
version=self._version_edit.text.strip(),
entry_point=(self.state.main if self.state and getattr(self.state, "main", None) else "main.py"),
os_label=["linux", "windows", "macos"][self._desktop_os_dd.selected_index],
build_wheel=self._desktop_build_wheel_cb.checked,
create_zip=self._desktop_create_zip_cb.checked,
icon_path=(self._desktop_icon_edit.text.strip() or None),
web_width=web_w,
web_height=web_h,
web_responsive=self._web_responsive_cb.checked,
pyodide_version=self._web_pyodide_edit.text.strip() or DEFAULT_PYODIDE_VERSION,
extra_packages=extra,
root_class=(self._web_root_edit.text.strip() or None),
title=self._web_title_edit.text.strip() or "SimVX",
exe_onefile=self._exe_onefile_cb.checked,
exe_console=self._exe_console_cb.checked,
exe_name=self._exe_name_edit.text.strip(),
exe_host_os=detect_host_os(),
android_package=self._android_package_edit.text.strip(),
android_min_sdk=min_sdk,
android_mode=["debug", "release"][self._android_mode_dd.selected_index],
)
def _persist_request(self, req: ExportRequest):
toml = self._toml_path()
settings = self._load_settings()
save_request_to_toml(req, settings)
try:
save_project(settings, toml)
except Exception:
log.exception("Failed to save %s", toml)
# ------------------------------------------------------------------
# Target switching
# ------------------------------------------------------------------
def _on_target_selected(self, idx: int):
if 0 <= idx < len(_TARGETS):
self._set_target(_TARGETS[idx][0])
self._populate_from_settings()
def _set_target(self, target: ExportTarget):
self._target = target
# Hide all forms then show the active one
for form in set(self._forms.values()):
form.visible = False
active_form = self._forms.get(target)
if active_form is not None:
active_form.visible = True
# Keep dropdown in sync
for i, (t, _) in enumerate(_TARGETS):
if t == target:
if self._target_dropdown:
self._target_dropdown.selected_index = i
break
# Desktop: ensure mode dropdown reflects target
if target == ExportTarget.DESKTOP_WHEEL and self._desktop_mode_dd:
self._desktop_mode_dd.selected_index = 1
elif target == ExportTarget.DESKTOP_FOLDER and self._desktop_mode_dd:
self._desktop_mode_dd.selected_index = 0
self._update_button_states()
self._update_warnings()
self._message_label.text = ""
def _on_desktop_mode_changed(self, idx: int):
self._target = ExportTarget.DESKTOP_WHEEL if idx == 1 else ExportTarget.DESKTOP_FOLDER
for i, (t, _) in enumerate(_TARGETS):
if t == self._target and self._target_dropdown:
self._target_dropdown.selected_index = i
break
def _update_button_states(self):
if self._export_btn:
self._export_btn.disabled = (
self._running or self._target == ExportTarget.STANDALONE_NUITKA
)
if self._close_btn:
self._close_btn.text = "Cancel" if self._running else "Close"
def _update_warnings(self):
if self._exe_warning:
self._exe_warning.text = (
"Warning: pyinstaller not found on PATH."
if shutil.which("pyinstaller") is None
else ""
)
if self._android_warning:
msgs: list[str] = []
if shutil.which("buildozer") is None:
msgs.append("buildozer not found.")
if sys.platform.startswith("win"):
msgs.append("Android builds require a Linux or macOS host.")
self._android_warning.text = " ".join(msgs)
# ------------------------------------------------------------------
# Actions
# ------------------------------------------------------------------
def _on_validate(self):
req = self._build_request()
errs = req.validate()
if errs:
self._message_label.text_colour = (0.9, 0.4, 0.4, 1.0)
self._message_label.text = "; ".join(errs)
if self._export_btn:
self._export_btn.disabled = True
else:
self._message_label.text_colour = (0.45, 0.85, 0.45, 1.0)
self._message_label.text = "Validation OK."
if self._export_btn and self._target != ExportTarget.STANDALONE_NUITKA:
self._export_btn.disabled = False
def _on_browse_output(self):
# FileDialog access via state shared dialog (open_folder mode)
dlg = getattr(self.state, "_file_dialog", None) if self.state else None
if dlg is None:
return
def _on_chosen(path):
try:
dlg.file_selected.disconnect(_on_chosen)
except (RuntimeError, ValueError):
log.debug("Signal already disconnected during export dialog teardown")
if path:
self._output_edit.text = str(path)
dlg.file_selected.connect(_on_chosen)
dlg.show(mode="open", path=self._output_edit.text or str(self._project_dir()))
def _on_progress(self, progress: ExportProgress):
self._progress_row.visible = True
if progress.fraction < 0:
# indeterminate — show bar at 50%
self._progress_bar.value = 50
else:
self._progress_bar.value = max(0.0, min(1.0, progress.fraction)) * 100
self._progress_label.text = progress.status
self.queue_redraw()
def _on_export(self):
if self._running:
return
req = self._build_request()
errs = req.validate()
if errs:
self._message_label.text_colour = (0.9, 0.4, 0.4, 1.0)
self._message_label.text = "; ".join(errs)
return
# Persist config before running
self._persist_request(req)
self._current_request = req
self._running = True
self._update_button_states()
self._progress_row.visible = True
self._progress_bar.value = 0
self._progress_label.text = "Starting..."
self._message_label.text = ""
result = export_controller.run_export(req, self._on_progress)
self._running = False
self._update_button_states()
if result.success:
self._message_label.text_colour = (0.45, 0.85, 0.45, 1.0)
self._message_label.text = f"Success: {result.result_path}"
else:
self._message_label.text_colour = (0.9, 0.4, 0.4, 1.0)
self._message_label.text = f"Failed: {result.error}"
self.exported.emit(result)
def _on_close(self):
self.hide_dialog()
# ------------------------------------------------------------------
# Input
# ------------------------------------------------------------------
def _on_gui_input(self, event):
if hasattr(event, "key") and event.key == "escape" and event.pressed:
self._on_close()
return
super()._on_gui_input(event)