summaryrefslogtreecommitdiff
path: root/lib/extensions/sew_stack_editor.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/extensions/sew_stack_editor.py')
-rwxr-xr-xlib/extensions/sew_stack_editor.py564
1 files changed, 564 insertions, 0 deletions
diff --git a/lib/extensions/sew_stack_editor.py b/lib/extensions/sew_stack_editor.py
new file mode 100755
index 00000000..6c7cb543
--- /dev/null
+++ b/lib/extensions/sew_stack_editor.py
@@ -0,0 +1,564 @@
+# Authors: see git history
+#
+# Copyright (c) 2010 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+import sys
+
+import wx
+from wx.lib.agw import ultimatelistctrl as ulc
+from wx.lib.checkbox import GenCheckBox
+from wx.lib.splitter import MultiSplitterWindow
+
+from .base import InkstitchExtension
+from ..debug.debug import debug
+from ..exceptions import InkstitchException, format_uncaught_exception
+from ..gui import PreviewRenderer, WarningPanel, confirm_dialog
+from ..gui.simulator import SplitSimulatorWindow
+from ..i18n import _
+from ..sew_stack import SewStack
+from ..sew_stack.stitch_layers import RunningStitchLayer
+from ..stitch_plan import stitch_groups_to_stitch_plan
+from ..utils.icons import load_icon
+from ..utils.svg_data import get_pagecolor
+from ..utils.threading import ExitThread, check_stop_flag
+
+
+# -*- coding: UTF-8 -*-
+
+
+class VisibleCheckBox(GenCheckBox):
+ def __init__(self, parent, *args, **kwargs):
+ render = wx.RendererNative.Get()
+ width, height = render.GetCheckBoxSize(parent)
+
+ self.checked_bitmap = load_icon("visible", width=width, height=height)
+ self.unchecked_bitmap = load_icon("invisible", width=width, height=height)
+
+ super().__init__(parent, *args, **kwargs)
+
+ def GetBitmap(self):
+ if self.IsChecked():
+ return self.checked_bitmap
+ else:
+ return self.unchecked_bitmap
+
+
+class SewStackPanel(wx.Panel):
+ """An editing UI to modify the sew stacks on multiple objects.
+
+ Each object has a Sew Stack, and every Sew Stack has one or more Stitch
+ Layers in it. This GUI will present the layers to the user and let them
+ edit each layer's properties. The user can also reorder the layers and add
+ and remove layers.
+
+ When editing multiple objects' Sew Stacks at once, all Sew Stacks must be
+ compatible. That means each one must have the same types of layers in the
+ same order.
+
+ When the user changes a property in a layer, the property is bolded to
+ indicate that it has changed. When saving changes, only properties that the
+ user changed are saved into the corresponding layers in all objects' Sew
+ Stacks. Ditto if layers are added, removed, or reordered: the layers will be
+ added, removed, or reordered in all objects' Sew Stacks.
+ """
+
+ def __init__(self, parent, sew_stacks=None, metadata=None, background_color='white', simulator=None):
+ super().__init__(parent, wx.ID_ANY)
+
+ self.metadata = metadata
+ self.background_color = background_color
+ self.simulator = simulator
+ self.parent = parent
+
+ self.sew_stacks = sew_stacks
+ self.layer_editors = self.get_layer_editors()
+
+ self.splitter = MultiSplitterWindow(self, wx.ID_ANY, style=wx.SP_LIVE_UPDATE)
+ self.splitter.SetOrientation(wx.VERTICAL)
+ self.splitter.SetMinimumPaneSize(50)
+ self.splitter.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGING, self.on_splitter_sash_pos_changing)
+
+ self.layer_config_panel = None
+ self.layer_list_wrapper = wx.Panel(self.splitter, wx.ID_ANY)
+ layer_list_sizer = wx.BoxSizer(wx.VERTICAL)
+ self.layer_list = ulc.UltimateListCtrl(
+ parent=self.layer_list_wrapper,
+ size=(300, 200),
+ agwStyle=ulc.ULC_REPORT | ulc.ULC_SINGLE_SEL | ulc.ULC_VRULES | ulc.ULC_HAS_VARIABLE_ROW_HEIGHT
+ )
+ self._checkbox_to_row = {}
+ self.update_layer_list()
+ layer_list_sizer.Add(self.layer_list, 1, wx.BOTTOM | wx.EXPAND, 2)
+ layer_list_sizer.Add(self.create_layer_buttons(), 0, wx.EXPAND | wx.BOTTOM, 10)
+ self.sew_stack_only_checkbox = wx.CheckBox(self.layer_list_wrapper, label=_("Sew stack only"), style=wx.CHK_3STATE)
+ self.sew_stack_only_checkbox.Set3StateValue(self.get_sew_stack_only_checkbox_value())
+ self.sew_stack_only_checkbox.SetToolTip(_("Only sew the Sew Stack layers, and ignore settings from Params"))
+ layer_list_sizer.Add(self.sew_stack_only_checkbox, 0, wx.EXPAND | wx.BOTTOM, 10)
+ self.layer_list_wrapper.SetSizer(layer_list_sizer)
+ self.splitter.AppendWindow(self.layer_list_wrapper, 300)
+
+ self.splitter.SizeWindows()
+
+ self._dragging_row = None
+ self._editing_row = None
+ self._name_editor = None
+
+ self.layer_list.Bind(ulc.EVT_LIST_BEGIN_DRAG, self.on_begin_drag)
+ self.layer_list.Bind(ulc.EVT_LIST_END_DRAG, self.on_end_drag)
+ self.layer_list.Bind(ulc.EVT_LIST_ITEM_ACTIVATED, self.on_double_click)
+ self.layer_list.Bind(ulc.EVT_LIST_ITEM_SELECTED, self.on_layer_selection_changed)
+ # self.layer_list.Bind(ulc.EVT_LIST_ITEM_DESELECTED, self.on_layer_selection_changed)
+ self.Bind(wx.EVT_CHECKBOX, self.on_checkbox)
+
+ self.preview_renderer = PreviewRenderer(self.render_stitch_plan, self.on_stitch_plan_rendered)
+
+ self.warning_panel = WarningPanel(self)
+ self.warning_panel.Hide()
+
+ self.cancel_button = wx.Button(self, wx.ID_ANY, _("Cancel"))
+ self.cancel_button.Bind(wx.EVT_BUTTON, self.on_cancel)
+ self.Bind(wx.EVT_CLOSE, self.on_close)
+
+ self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit"))
+ self.apply_button.Bind(wx.EVT_BUTTON, self.apply)
+
+ self.__do_layout()
+ self.update_preview()
+
+ def get_sew_stack_only_checkbox_value(self):
+ values = [sew_stack.sew_stack_only for sew_stack in self.sew_stacks]
+ if all(values):
+ return wx.CHK_CHECKED
+ elif all(value is False for value in values):
+ return wx.CHK_UNCHECKED
+ else:
+ return wx.CHK_UNDETERMINED
+
+ def get_layer_types(self):
+ sew_stacks_layer_types = []
+
+ for sew_stack in self.sew_stacks:
+ sew_stacks_layer_types.append(tuple(type(layer) for layer in sew_stack.layers))
+
+ if len(set(sew_stacks_layer_types)) > 1:
+ raise ValueError("SewStackPanel: internal error: sew stacks do not all have the same layer types")
+
+ return sew_stacks_layer_types[0]
+
+ def get_layer_editors(self):
+ layer_types = self.get_layer_types()
+ editors = []
+ for i, layer_type in enumerate(layer_types):
+ layers = [sew_stack.layers[i] for sew_stack in self.sew_stacks]
+ editors.append(layer_type.editor_class(layers, change_callback=self.on_property_changed))
+
+ return editors
+
+ def __do_layout(self):
+ main_sizer = wx.BoxSizer(wx.VERTICAL)
+ main_sizer.Add(self.warning_panel, 0, flag=wx.ALL, border=10)
+ main_sizer.Add(self.splitter, 1, wx.LEFT | wx.TOP | wx.RIGHT | wx.EXPAND, 10)
+ buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ buttons_sizer.Add(self.cancel_button, 0, wx.RIGHT, 5)
+ buttons_sizer.Add(self.apply_button, 0, wx.BOTTOM, 5)
+ main_sizer.Add(buttons_sizer, 0, wx.ALIGN_RIGHT | wx.TOP | wx.LEFT | wx.RIGHT, 10)
+ self.SetSizer(main_sizer)
+ main_sizer.Fit(self)
+ self.Layout()
+
+ def create_layer_buttons(self):
+ self.layer_buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
+
+ self.add_layer_button = wx.Button(self.layer_list_wrapper, wx.ID_ANY, style=wx.BU_EXACTFIT)
+ self.add_layer_button.SetBitmapLabel(wx.ArtProvider.GetBitmap(wx.ART_PLUS, wx.ART_MENU))
+ self.layer_buttons_sizer.Add(self.add_layer_button, 0, 0, 0)
+ self.add_layer_button.Bind(wx.EVT_BUTTON, self.on_add_layer_button)
+
+ self.delete_layer_button = wx.Button(self.layer_list_wrapper, wx.ID_ANY, style=wx.BU_EXACTFIT)
+ self.delete_layer_button.SetBitmapLabel(wx.ArtProvider.GetBitmap(wx.ART_DELETE, wx.ART_MENU))
+ self.layer_buttons_sizer.Add(self.delete_layer_button, 0, wx.LEFT, 5)
+ self.delete_layer_button.Bind(wx.EVT_BUTTON, self.on_delete_layer_button)
+
+ self.move_layer_up_button = wx.Button(self.layer_list_wrapper, wx.ID_ANY, style=wx.BU_EXACTFIT)
+ self.move_layer_up_button.SetBitmapLabel(wx.ArtProvider.GetBitmap(wx.ART_GO_UP, wx.ART_MENU))
+ self.layer_buttons_sizer.Add(self.move_layer_up_button, 0, wx.LEFT, 5)
+ self.move_layer_up_button.Bind(wx.EVT_BUTTON, self.on_move_layer_up_button)
+
+ self.move_layer_down_button = wx.Button(self.layer_list_wrapper, wx.ID_ANY, style=wx.BU_EXACTFIT)
+ self.move_layer_down_button.SetBitmapLabel(wx.ArtProvider.GetBitmap(wx.ART_GO_DOWN, wx.ART_MENU))
+ self.layer_buttons_sizer.Add(self.move_layer_down_button, 0, wx.LEFT, 5)
+ self.move_layer_down_button.Bind(wx.EVT_BUTTON, self.on_move_layer_down_button)
+
+ self.layer_buttons_sizer.Add(0, 0, 1, wx.EXPAND)
+
+ self.single_layer_preview_button = wx.BitmapToggleButton(self.layer_list_wrapper, wx.ID_ANY, style=wx.BU_EXACTFIT | wx.BU_NOTEXT)
+ self.single_layer_preview_button.SetToolTip(_("Preview selected layer"))
+ self.single_layer_preview_button.SetBitmap(load_icon('layer', self))
+ self.single_layer_preview_button.Bind(wx.EVT_TOGGLEBUTTON, self.on_single_layer_preview_button)
+ self.layer_buttons_sizer.Add(self.single_layer_preview_button, 0, wx.LEFT, 0)
+
+ self.all_layers_preview_button = wx.BitmapToggleButton(self.layer_list_wrapper, wx.ID_ANY, style=wx.BU_EXACTFIT | wx.BU_NOTEXT)
+ self.all_layers_preview_button.SetToolTip(_("Preview all layers"))
+ self.all_layers_preview_button.SetBitmap(load_icon('layers', self))
+ self.all_layers_preview_button.SetValue(True)
+ self.all_layers_preview_button.Bind(wx.EVT_TOGGLEBUTTON, self.on_all_layers_preview_button)
+ self.layer_buttons_sizer.Add(self.all_layers_preview_button, 0, wx.LEFT, 1)
+
+ return self.layer_buttons_sizer
+
+ def update_layer_list(self):
+ self.layer_list.Freeze()
+
+ if self.layer_list.GetColumnCount() == 3:
+ # Save and restore column widths to work around an UltimateListCtrl bug.
+ # If we don't do this, ULC_AUTOSIZE_FILL stops working and the layer name
+ # column shrinks.
+ column_sizes = [self.layer_list.GetColumn(i).GetWidth() for i in range(self.layer_list.GetColumnCount())]
+ else:
+ column_sizes = (24, wx.LIST_AUTOSIZE, ulc.ULC_AUTOSIZE_FILL)
+
+ self.layer_list.ClearAll()
+
+ self.layer_list.InsertColumn(0, "", format=ulc.ULC_FORMAT_RIGHT)
+ self.layer_list.InsertColumn(1, _("Type"), format=ulc.ULC_FORMAT_CENTER)
+ self.layer_list.InsertColumn(2, _("Layer Name"))
+
+ self._checkbox_to_row.clear()
+
+ for i in range(len(self.layer_editors)):
+ is_checked = any(sew_stack.layers[i].enabled for sew_stack in self.sew_stacks)
+ item = ulc.UltimateListItem()
+ item.SetMask(ulc.ULC_MASK_WINDOW | ulc.ULC_MASK_CHECK | ulc.ULC_MASK_FORMAT)
+ checkbox = VisibleCheckBox(self.layer_list)
+ self._checkbox_to_row[checkbox] = i
+ checkbox.SetValue(is_checked)
+ item.SetWindow(checkbox)
+ item.SetAlign(ulc.ULC_FORMAT_RIGHT)
+ item.Check(is_checked)
+ item.SetId(i)
+ item.SetColumn(0)
+ self.layer_list.InsertItem(item)
+
+ self.layer_list.SetStringItem(i, 1, self.sew_stacks[0].layers[i].layer_type_name)
+ self.layer_list.SetStringItem(i, 2, self.sew_stacks[0].layers[i].name)
+
+ # insert one more row so that the UltimateListCtrl allows dragging items to the very
+ # end of the list
+ self.layer_list.InsertStringItem(len(self.layer_editors), "")
+ self.layer_list.EnableItem(len(self.layer_editors), enable=False)
+
+ for i, size in enumerate(column_sizes):
+ self.layer_list.SetColumnWidth(i, size)
+
+ if self.layer_config_panel is not None:
+ self.layer_config_panel.Hide()
+ self.splitter.DetachWindow(self.layer_config_panel)
+ self.layer_config_panel = None
+
+ self.layer_list.Thaw()
+
+ def on_begin_drag(self, event):
+ self.stop_editing()
+ self._dragging_row = event.Index
+
+ def on_end_drag(self, event):
+ self.stop_editing()
+ if self._dragging_row is not None:
+ destination = event.Index
+ if destination > self._dragging_row:
+ destination -= 1
+ self.move_layer(self._dragging_row, destination)
+ self._dragging_row = None
+ self.update_preview()
+
+ def move_layer(self, from_index, to_index):
+ debug.log(f"move_layer({from_index=}, {to_index=})")
+ if 0 <= from_index < len(self.layer_editors):
+ if 0 <= to_index < len(self.layer_editors):
+ debug.log(f"before move: {self.layer_editors}")
+ layer_editor = self.layer_editors.pop(from_index)
+ self.layer_editors.insert(to_index, layer_editor)
+
+ for sew_stack in self.sew_stacks:
+ sew_stack.move_layer(from_index, to_index)
+
+ debug.log(f"after move: {self.layer_editors}")
+
+ self.update_layer_list()
+ self.update_preview()
+ return True
+ return False
+
+ def on_double_click(self, event):
+ debug.log(f"double-click {event.Index}")
+
+ if event.GetColumn() != 2:
+ event.Veto()
+ return
+
+ self.stop_editing()
+
+ self._editing_row = event.Index
+ self._name_editor = wx.TextCtrl(self.layer_list, wx.ID_ANY, value=self.sew_stacks[0].layers[event.Index].name,
+ style=wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB | wx.TE_LEFT)
+ self._name_editor.Bind(wx.EVT_TEXT_ENTER, self.on_name_editor_end)
+ self._name_editor.Bind(wx.EVT_KEY_UP, self.on_name_editor_key_up)
+ self.layer_list.SetItemWindow(event.Index, 2, self._name_editor, expand=True)
+
+ def on_name_editor_key_up(self, event):
+ keyCode = event.GetKeyCode()
+ if keyCode == wx.WXK_ESCAPE:
+ self.stop_editing(cancel=True)
+ else:
+ event.Skip()
+
+ def on_name_editor_end(self, event):
+ self.stop_editing()
+
+ def on_layer_selection_changed(self, event):
+ self.stop_editing()
+ debug.log(f"layer selection changed: {event.Index} {self.layer_list.GetFirstSelected()}")
+ if -1 < event.Index < len(self.layer_editors):
+ selected_layer = self.layer_editors[event.Index]
+ new_layer_config_panel = selected_layer.get_panel(parent=self.splitter)
+
+ if self.layer_config_panel is not None:
+ self.layer_config_panel.Hide()
+ self.splitter.ReplaceWindow(self.layer_config_panel, new_layer_config_panel)
+ else:
+ self.splitter.AppendWindow(new_layer_config_panel)
+ self.layer_config_panel = new_layer_config_panel
+ self.splitter.SizeWindows()
+
+ self.Layout()
+
+ if self.single_layer_preview_button.GetValue():
+ self.update_preview()
+
+ def on_checkbox(self, event):
+ checkbox = event.GetEventObject()
+ if checkbox is self.sew_stack_only_checkbox:
+ for sew_stack in self.sew_stacks:
+ sew_stack.sew_stack_only = event.IsChecked()
+ else:
+ row = self._checkbox_to_row.get(checkbox)
+ if row is not None:
+ for sew_stack in self.sew_stacks:
+ sew_stack.layers[row].enable(event.IsChecked())
+ self.update_preview()
+
+ def on_splitter_sash_pos_changing(self, event):
+ # MultiSplitterWindow doesn't enforce the minimum pane size on the lower
+ # pane for some reason, so we'll have to. Setting the sash position on
+ # the event overrides whatever the user is trying to do.
+ size = self.splitter.GetSize()
+ sash_position = event.GetSashPosition()
+ sash_position = min(sash_position, size.y - 50)
+ event.SetSashPosition(sash_position)
+
+ def on_add_layer_button(self, event):
+ # TODO: pop up a dialog to select layer type. Also support pre-set
+ # groups of layers (for example contour underlay, zig-zag underlay, and
+ # satin) and saved "favorite" layers.
+ new_layers = []
+ for sew_stack in self.sew_stacks:
+ new_layers.append(sew_stack.append_layer(RunningStitchLayer))
+ self.layer_editors.append(RunningStitchLayer.editor_class(new_layers, change_callback=self.on_property_changed))
+ self.update_layer_list()
+ self.layer_list.Select(len(self.layer_editors) - 1)
+ self.update_preview()
+
+ def on_delete_layer_button(self, event):
+ index = self.layer_list.GetFirstSelected()
+ if 0 <= index < len(self.layer_editors):
+ if confirm_dialog(self, _("Are you sure you want to delete this layer?") + "\n\n" + self.sew_stacks[0].layers[index].name):
+ del self.layer_editors[index]
+
+ for sew_stack in self.sew_stacks:
+ sew_stack.delete_layer(index)
+
+ self.update_layer_list()
+ self.update_preview()
+
+ def on_move_layer_up_button(self, event):
+ index = self.layer_list.GetFirstSelected()
+ destination = index - 1
+ if self.move_layer(index, destination):
+ self.layer_list.Select(destination)
+
+ def on_move_layer_down_button(self, event):
+ index = self.layer_list.GetFirstSelected()
+ destination = index + 1
+ if self.move_layer(index, destination):
+ self.layer_list.Select(destination)
+
+ def stop_editing(self, cancel=False):
+ if self._name_editor is None or self._editing_row is None:
+ return
+
+ if not cancel:
+ new_name = self._name_editor.GetValue()
+ for sew_stack in self.sew_stacks:
+ sew_stack.layers[self._editing_row].name = new_name
+
+ self.layer_list.DeleteItemWindow(self._editing_row, 2)
+ item = self.layer_list.GetItem(self._editing_row, 2)
+ item.SetMask(ulc.ULC_MASK_TEXT)
+ item.SetText(new_name)
+ self.layer_list.SetItem(item)
+
+ self._name_editor.Hide()
+ self._name_editor.Destroy()
+ self._name_editor = None
+ self._editing_row = None
+
+ def on_property_changed(self, property_name, property_value):
+ self.update_preview()
+
+ def on_single_layer_preview_button(self, event):
+ if not event.GetInt():
+ # don't allow them to unselect this button, they're supposed to select the other one
+ self.single_layer_preview_button.SetValue(True)
+ return
+
+ self.all_layers_preview_button.SetValue(False)
+
+ # ensure a layer is selected so that it's clear which one is being previewed
+ if self.layer_list.GetFirstSelected() == -1:
+ self.layer_list.Select(0)
+
+ self.update_preview()
+
+ def on_all_layers_preview_button(self, event):
+ if not event.GetInt():
+ # don't allow them to unselect this button, they're supposed to select the other one
+ self.all_layers_preview_button.SetValue(True)
+ return
+
+ self.single_layer_preview_button.SetValue(False)
+ self.update_preview()
+
+ def update_preview(self):
+ self.simulator.stop()
+ self.simulator.clear()
+ self.preview_renderer.update()
+
+ def render_stitch_plan(self):
+ try:
+ if not self.layer_editors:
+ return
+
+ wx.CallAfter(self._hide_warning)
+ self._update_layers()
+
+ stitch_groups = []
+ for sew_stack in self.sew_stacks:
+ if self.single_layer_preview_button.GetValue():
+ layer = sew_stack.layers[self.layer_list.GetFirstSelected()]
+ stitch_groups.extend(layer.embroider(None))
+ else:
+ stitch_groups.extend(sew_stack.embroider(stitch_groups[-1] if stitch_groups else None))
+
+ check_stop_flag()
+
+ if stitch_groups:
+ return stitch_groups_to_stitch_plan(
+ stitch_groups,
+ collapse_len=self.metadata['collapse_len_mm'],
+ min_stitch_len=self.metadata['min_stitch_len_mm']
+ )
+ except (SystemExit, ExitThread):
+ raise
+ except InkstitchException as exc:
+ wx.CallAfter(self._show_warning, str(exc))
+ except Exception:
+ wx.CallAfter(self._show_warning, format_uncaught_exception())
+
+ def on_stitch_plan_rendered(self, stitch_plan):
+ try:
+ self.simulator.stop()
+ self.simulator.load(stitch_plan)
+ self.simulator.go()
+ except RuntimeError:
+ # this can happen when they close the window at a bad time
+ pass
+
+ def _hide_warning(self):
+ self.warning_panel.clear()
+ self.warning_panel.Hide()
+ self.Layout()
+
+ def _show_warning(self, warning_text):
+ self.warning_panel.set_warning_text(warning_text)
+ self.warning_panel.Show()
+ self.Layout()
+
+ def _update_layers(self):
+ for sew_stack in self.sew_stacks:
+ for layer_num in range(len(self.layer_editors)):
+ layer = sew_stack.layers[layer_num]
+ self.layer_editors[layer_num].update_layer(layer)
+
+ def _apply(self):
+ self._update_layers()
+ for sew_stack in self.sew_stacks:
+ sew_stack.save()
+
+ def apply(self, event):
+ self._apply()
+ self.close()
+
+ def confirm_close(self):
+ self.simulator.stop()
+ if any(layer_editor.has_changes() for layer_editor in self.layer_editors):
+ return confirm_dialog(self, _("Are you sure you want to quit without saving changes?"))
+ else:
+ # They made no changes, so it's safe to close.
+ return True
+
+ def close(self):
+ wx.CallAfter(self.GetTopLevelParent().close)
+
+ def on_close(self, event):
+ if self.confirm_close():
+ self.close()
+ else:
+ event.Veto()
+
+ def on_cancel(self, event):
+ if self.confirm_close():
+ self.close()
+
+
+class SewStackEditor(InkstitchExtension):
+ def __init__(self, *args, **kwargs):
+ self.cancelled = False
+ InkstitchExtension.__init__(self, *args, **kwargs)
+
+ def get_sew_stacks(self):
+ nodes = self.get_nodes()
+ if nodes:
+ return [SewStack(node) for node in nodes]
+ else:
+ self.no_elements_error()
+
+ def effect(self):
+ app = wx.App()
+ metadata = self.get_inkstitch_metadata()
+ background_color = get_pagecolor(self.svg.namedview)
+ frame = SplitSimulatorWindow(
+ title=_("Embroidery Params"),
+ panel_class=SewStackPanel,
+ sew_stacks=self.get_sew_stacks(),
+ metadata=metadata,
+ background_color=background_color,
+ target_duration=5
+ )
+
+ frame.Show()
+ app.MainLoop()
+
+ if self.cancelled:
+ # This prevents the superclass from outputting the SVG, because we
+ # may have modified the DOM.
+ sys.exit(0)