diff options
| author | Lex Neva <lexelby@users.noreply.github.com> | 2025-01-29 12:04:07 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-01-29 12:04:07 -0500 |
| commit | 913c2700d1486284dba0583ae1b280b1aa237570 (patch) | |
| tree | c165b29d0794981b5e44ab46f9838baab16b06a4 /lib/extensions/sew_stack_editor.py | |
| parent | efe3b27f17686094f74462bd81763a8197b54c6e (diff) | |
Sew Stack first steps (#3133)
* handle more recursive cases
* scaffolding for stitch layers
* scaffolding for SewStack
* always use DotDict when parsing json params
* add DefaultDotDict + DotDict fixes
* first working SewStack (no UI yet)
* ignore inkstitch_debug.log and .svg
* refactor
* early WIP: property grid display temporarily in stitch plan preview
* start of sew stack editor extension
* add layer properties panel and splitter
* spacing and better icon
* handle checkbox
* add layer action buttons
* show selected property help text in an HtmlWindow
* rename
* rephrase help text for tolerance
* refactor into separate file
* simplify structure
* better property type handling
* add randomization button
* add random seed re-roll button
* simulator preview
* update preview in a few more cases
* always DotDict
* avoid ridiculously slow simulations
* preview selected layer or all layers
* edit multiple objects and save only modified properties into the SVG
* better preview handling
* add reverse and jitter
* add stitch path jitter
* fix types
* fix random shuffle button
* fixes
* fix repeats
* type hinting to please pycharm
* show layer description
* avoid exception in properties with multiple values
* fix typing
* fix new layer
* draw a box around property grid and help box
* confirm before closing
* rename properties and fix seed
* fix close/cancel logic
* add buttons to undo changes and reset to default value
* set not modified if default is original setting
* fix invisible icon
* more space for properties
* fix random properties
* better regulation of simulator rendering speed
* Fixed timer being passed a float
* fix get_json_param() default handling
* fix tests
* add checkbox for sew stack only
* fix property help
* adjustable stitch layer editor help box size, with persistence
* repeat exact stitches
* "fix" style
* adjust for new next_element stuff
---------
Co-authored-by: CapellanCitizen <thecapellancitizen@gmail.com>
Diffstat (limited to 'lib/extensions/sew_stack_editor.py')
| -rwxr-xr-x | lib/extensions/sew_stack_editor.py | 564 |
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) |
