diff options
| author | Lex Neva <lexelby@users.noreply.github.com> | 2023-10-21 12:16:34 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-10-21 18:16:34 +0200 |
| commit | 82f2edac1a348b274c177abf5581b089cdf2e527 (patch) | |
| tree | a8b4fed1e5106adc7e14457d00c6e1de0ade2050 | |
| parent | 526cd48a4f73d9b793b5a83ababd83069f4ad800 (diff) | |
attach params/lettering simulator window and allow detach (#2557)
| -rw-r--r-- | icons/detach_window.png | bin | 0 -> 2891 bytes | |||
| -rw-r--r-- | icons/detach_window.svg | 98 | ||||
| -rw-r--r-- | lib/extensions/lettering.py | 81 | ||||
| -rw-r--r-- | lib/extensions/params.py | 89 | ||||
| -rw-r--r-- | lib/gui/__init__.py | 2 | ||||
| -rw-r--r-- | lib/gui/element_info.py | 2 | ||||
| -rw-r--r-- | lib/gui/preferences.py | 2 | ||||
| -rw-r--r-- | lib/gui/simulator.py | 346 | ||||
| -rw-r--r-- | lib/gui/test_swatches.py | 2 | ||||
| -rw-r--r-- | lib/utils/settings.py | 3 | ||||
| -rw-r--r-- | lib/utils/threading.py | 1 |
11 files changed, 365 insertions, 261 deletions
diff --git a/icons/detach_window.png b/icons/detach_window.png Binary files differnew file mode 100644 index 00000000..5758bf88 --- /dev/null +++ b/icons/detach_window.png diff --git a/icons/detach_window.svg b/icons/detach_window.svg new file mode 100644 index 00000000..da7d63f5 --- /dev/null +++ b/icons/detach_window.svg @@ -0,0 +1,98 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="256" + height="256" + viewBox="0 0 256 256" + id="svg8375" + version="1.1" + inkscape:version="1.3 (1:1.3+202307231459+0e150ed6c4)" + sodipodi:docname="detach_window.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + <defs + id="defs8377"> + <marker + style="overflow:visible" + id="Triangle" + refX="0" + refY="0" + orient="auto-start-reverse" + inkscape:stockid="Triangle arrow" + markerWidth="0.5" + markerHeight="0.5" + viewBox="0 0 1 1" + inkscape:isstock="true" + inkscape:collect="always" + preserveAspectRatio="xMidYMid"> + <path + transform="scale(0.5)" + style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt" + d="M 5.77,0 -2.88,5 V -5 Z" + id="path135" /> + </marker> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.3183809" + inkscape:cx="193.41906" + inkscape:cy="284.81905" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + units="mm" + inkscape:window-width="1366" + inkscape:window-height="705" + inkscape:window-x="-4" + inkscape:window-y="-4" + inkscape:window-maximized="1" + inkscape:showpageshadow="2" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" /> + <metadata + id="metadata8380"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1"> + <rect + style="fill:#000000;stroke-width:22.3274" + id="rect1-3-7" + width="94.246475" + height="86.769928" + x="159.3401" + y="3.0987689" /> + <rect + style="fill:#000000;stroke-width:13.626" + id="rect1-3-7-5" + width="57.516991" + height="52.954185" + x="3.5270462" + y="199.60167" /> + <path + style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:17.0079;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Triangle)" + d="M 69.827955,184.17307 126.75642,123.7445" + id="path3" + sodipodi:nodetypes="cc" /> + </g> +</svg> diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 3bcebadc..6f79d9a7 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -14,36 +14,37 @@ import wx import wx.adv import wx.lib.agw.floatspin as fs +from .commands import CommandsExtension +from .lettering_custom_font_dir import get_custom_font_dir from ..elements import nodes_to_elements -from ..gui import PresetsPanel, SimulatorPreview, info_dialog +from ..gui import PresetsPanel, PreviewRenderer, info_dialog +from ..gui.simulator import SplitSimulatorWindow from ..i18n import _ from ..lettering import Font, FontError from ..lettering.categories import FONT_CATEGORIES, FontCategory +from ..stitch_plan import stitch_groups_to_stitch_plan from ..svg import get_correction_transform from ..svg.tags import (INKSCAPE_LABEL, INKSTITCH_LETTERING, SVG_GROUP_TAG, SVG_PATH_TAG) -from ..utils import DotDict, cache, get_bundled_dir, get_resource_dir -from ..utils.threading import ExitThread -from .commands import CommandsExtension -from .lettering_custom_font_dir import get_custom_font_dir +from ..utils import DotDict, cache, get_bundled_dir +from ..utils.threading import ExitThread, check_stop_flag -class LetteringFrame(wx.Frame): +class LetteringPanel(wx.Panel): DEFAULT_FONT = "small_font" - def __init__(self, *args, **kwargs): - self.group = kwargs.pop('group') - self.cancel_hook = kwargs.pop('on_cancel', None) - self.metadata = kwargs.pop('metadata', []) + def __init__(self, parent, simulator, group, on_cancel=None, metadata=None): + self.parent = parent + self.simulator = simulator + self.group = group + self.cancel_hook = on_cancel + self.metadata = metadata or dict() - # begin wxGlade: MyFrame.__init__ - wx.Frame.__init__(self, None, wx.ID_ANY, _("Ink/Stitch Lettering")) - self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT) + super().__init__(parent, wx.ID_ANY) - icon = wx.Icon(os.path.join(get_resource_dir("icons"), "inkstitch256x256.png")) - self.SetIcon(icon) + self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE) - self.preview = SimulatorPreview(self, target_duration=1) + self.preview_renderer = PreviewRenderer(self.render_stitch_plan, self.on_stitch_plan_rendered) self.presets_panel = PresetsPanel(self) # font @@ -257,11 +258,11 @@ class LetteringFrame(wx.Frame): self.settings[attribute] = event.GetEventObject().GetValue() if attribute == "text" and self.font_glyph_filter.GetValue() is True: self.on_filter_changed() - self.preview.update() + self.preview_renderer.update() def on_trim_option_change(self, event=None): self.settings.trim_option = self.trim_option_choice.GetCurrentSelection() - self.preview.update() + self.preview_renderer.update() def on_font_changed(self, event=None): font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) @@ -329,7 +330,7 @@ class LetteringFrame(wx.Frame): self.Layout() def update_preview(self, event=None): - self.preview.update() + self.preview_renderer.update() def update_lettering(self, raise_error=False): # return if there is no font in the font list (possibly due to a font size filter) @@ -367,19 +368,24 @@ class LetteringFrame(wx.Frame): if self.settings.scale != 100 and not destination_group.get('transform', None): destination_group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0) - def generate_patches(self, abort_early=None): - patches = [] + def render_stitch_plan(self): + stitch_groups = [] try: self.update_lettering() elements = nodes_to_elements(self.group.iterdescendants(SVG_PATH_TAG)) for element in elements: - if abort_early and abort_early.is_set(): - # cancel; settings were updated and we need to start over - return [] + check_stop_flag() + + stitch_groups.extend(element.embroider(None)) - patches.extend(element.embroider(None)) + 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: raise except ExitThread: @@ -390,7 +396,10 @@ class LetteringFrame(wx.Frame): # satins or division by zero caused by incorrect param values. pass - return patches + def on_stitch_plan_rendered(self, stitch_plan): + self.simulator.stop() + self.simulator.load(stitch_plan) + self.simulator.go() def get_preset_data(self): # called by self.presets_panel @@ -409,14 +418,12 @@ class LetteringFrame(wx.Frame): return "lettering" def apply(self, event): - self.preview.disable() self.update_lettering(True) self.save_settings() self.close() def close(self): - self.preview.close() - self.Destroy() + self.GetTopLevelParent().Close() def cancel(self, event): if self.cancel_hook: @@ -534,14 +541,14 @@ class Lettering(CommandsExtension): def effect(self): metadata = self.get_inkstitch_metadata() app = wx.App() - frame = LetteringFrame(group=self.get_or_create_group(), on_cancel=self.cancel, metadata=metadata) - - # position left, center - current_screen = wx.Display.GetFromPoint(wx.GetMousePosition()) - display = wx.Display(current_screen) - display_size = display.GetClientArea() - frame_size = frame.GetSize() - frame.SetPosition((int(display_size[0]), int(display_size[3] / 2 - frame_size[1] / 2))) + frame = SplitSimulatorWindow( + title=_("Ink/Stitch Lettering"), + panel_class=LetteringPanel, + group=self.get_or_create_group(), + on_cancel=self.cancel, + metadata=metadata, + target_duration=1 + ) frame.Show() app.MainLoop() diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 994cf3d6..81e4bd53 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -15,18 +15,20 @@ from secrets import randbelow import wx from wx.lib.scrolledpanel import ScrolledPanel +from .base import InkstitchExtension from ..commands import is_command, is_command_symbol from ..elements import (Clone, EmbroideryElement, FillStitch, Polyline, SatinColumn, Stroke) from ..elements.clone import is_clone from ..exceptions import InkstitchException, format_uncaught_exception -from ..gui import PresetsPanel, SimulatorPreview, WarningPanel +from ..gui import PresetsPanel, PreviewRenderer, WarningPanel +from ..gui.simulator import SplitSimulatorWindow from ..i18n import _ +from ..stitch_plan import stitch_groups_to_stitch_plan from ..svg.tags import SVG_POLYLINE_TAG from ..utils import get_resource_dir from ..utils.param import ParamOption from ..utils.threading import ExitThread, check_stop_flag -from .base import InkstitchExtension def grouper(iterable_obj, count, fillvalue=None): @@ -439,7 +441,7 @@ class ParamsTab(ScrolledPanel): for item in param.select_items: self.choice_widgets[item].extend([input, col4]) - self.settings_grid.Add(input, flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.EXPAND, border=40) + self.settings_grid.Add(input, flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.EXPAND, border=10) self.settings_grid.Add(col4, flag=wx.ALIGN_CENTER_VERTICAL) self.inputs_to_params = {v: k for k, v in self.param_inputs.items()} @@ -470,20 +472,17 @@ class ParamsTab(ScrolledPanel): # end of class SatinPane -class SettingsFrame(wx.Frame): - def __init__(self, *args, **kwargs): - self.tabs_factory = kwargs.pop('tabs_factory', []) - self.cancel_hook = kwargs.pop('on_cancel', None) - self.metadata = kwargs.pop('metadata', []) - - # begin wxGlade: MyFrame.__init__ - wx.Frame.__init__(self, None, wx.ID_ANY, _("Embroidery Params")) +class SettingsPanel(wx.Panel): + def __init__(self, parent, tabs_factory=None, on_cancel=None, metadata=None, simulator=None): + self.tabs_factory = tabs_factory + self.cancel_hook = on_cancel + self.metadata = metadata + self.simulator = simulator + self.parent = parent - self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT) + super().__init__(self.parent, wx.ID_ANY) - icon = wx.Icon(os.path.join( - get_resource_dir("icons"), "inkstitch256x256.png")) - self.SetIcon(icon) + self.preview_renderer = PreviewRenderer(self.render_stitch_plan, self.on_stitch_plan_rendered) self.notebook = wx.Notebook(self, wx.ID_ANY) self.tabs = self.tabs_factory(self.notebook) @@ -491,7 +490,6 @@ class SettingsFrame(wx.Frame): for tab in self.tabs: tab.on_change(self.update_preview) - self.preview = SimulatorPreview(self) self.presets_panel = PresetsPanel(self) self.warning_panel = WarningPanel(self) self.warning_panel.Hide() @@ -507,43 +505,45 @@ class SettingsFrame(wx.Frame): self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit")) self.apply_button.Bind(wx.EVT_BUTTON, self.apply) - self.notebook.SetMinSize((800, 600)) - self.__do_layout() - # end wxGlade + self.update_preview() - def update_preview(self, tab): - self.preview.update() + def update_preview(self, tab=None): + self.simulator.stop() + self.simulator.clear() + self.preview_renderer.update() - def generate_patches(self, abort_early): - # called by self.preview - - patches = [] + def render_stitch_plan(self): + stitch_groups = [] nodes = [] for tab in self.tabs: tab.apply() - if tab.enabled() and not tab.is_dependent_tab(): nodes.extend(tab.nodes) + check_stop_flag() + # sort nodes into the proper stacking order nodes.sort(key=lambda node: node.order) try: wx.CallAfter(self._hide_warning) for node in nodes: - if abort_early.is_set(): - # cancel; params were updated and we need to start over - return [] - # Making a copy of the embroidery element is an easy # way to drop the cache in the @cache decorators used # for many params in embroider.py. - patches.extend(copy(node).embroider(None)) + stitch_groups.extend(copy(node).embroider(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: @@ -551,7 +551,10 @@ class SettingsFrame(wx.Frame): except Exception: wx.CallAfter(self._show_warning, format_uncaught_exception()) - return patches + def on_stitch_plan_rendered(self, stitch_plan): + self.simulator.stop() + self.simulator.load(stitch_plan) + self.simulator.go() def _hide_warning(self): self.warning_panel.clear() @@ -589,7 +592,7 @@ class SettingsFrame(wx.Frame): for tab in self.tabs: tab.load_preset(preset_data) - self.preview.update() + self.preview_renderer.update() def _apply(self): for tab in self.tabs: @@ -601,13 +604,11 @@ class SettingsFrame(wx.Frame): self.close() def use_last(self, event): - self.preview.disable() self.presets_panel.load_preset("__LAST__") self.apply(event) def close(self): - self.preview.close() - self.Destroy() + self.GetTopLevelParent().Close() def cancel(self, event): if self.cancel_hook: @@ -781,18 +782,14 @@ class Params(InkstitchExtension): try: app = wx.App() metadata = self.get_inkstitch_metadata() - frame = SettingsFrame( + frame = SplitSimulatorWindow( + title=_("Embroidery Params"), + panel_class=SettingsPanel, tabs_factory=self.create_tabs, on_cancel=self.cancel, - metadata=metadata) - - # position left, center - current_screen = wx.Display.GetFromPoint(wx.GetMousePosition()) - display = wx.Display(current_screen) - display_size = display.GetClientArea() - frame_size = frame.GetSize() - frame.SetPosition((int(display_size[0]), int( - display_size[3]/2 - frame_size[1]/2))) + metadata=metadata, + target_duration=5 + ) frame.Show() app.MainLoop() diff --git a/lib/gui/__init__.py b/lib/gui/__init__.py index aac2319f..4343e4d1 100644 --- a/lib/gui/__init__.py +++ b/lib/gui/__init__.py @@ -6,5 +6,5 @@ from .dialogs import confirm_dialog, info_dialog from .electron import open_url from .presets import PresetsPanel -from .simulator import EmbroiderySimulator, SimulatorPreview, show_simulator +from .simulator import PreviewRenderer, show_simulator from .warnings import WarningPanel diff --git a/lib/gui/element_info.py b/lib/gui/element_info.py index c64ac1a7..5597013a 100644 --- a/lib/gui/element_info.py +++ b/lib/gui/element_info.py @@ -16,7 +16,7 @@ class ElementInfoFrame(wx.Frame): self.index = 0 wx.Frame.__init__(self, None, wx.ID_ANY, _("Element Info"), *args, **kwargs) - self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT) + self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE) self.main_panel = wx.Panel(self, wx.ID_ANY) diff --git a/lib/gui/preferences.py b/lib/gui/preferences.py index 882ddfac..a8a2dc7a 100644 --- a/lib/gui/preferences.py +++ b/lib/gui/preferences.py @@ -16,7 +16,7 @@ class PreferencesFrame(wx.Frame): wx.Frame.__init__(self, None, wx.ID_ANY, _("Preferences"), *args, **kwargs) self.SetTitle(_("Preferences")) - self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT) + self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE) metadata = self.extension.get_inkstitch_metadata() diff --git a/lib/gui/simulator.py b/lib/gui/simulator.py index 5cde0581..b6a8a6cc 100644 --- a/lib/gui/simulator.py +++ b/lib/gui/simulator.py @@ -12,9 +12,10 @@ from wx.lib.intctrl import IntCtrl from lib.debug import debug from lib.utils import get_resource_dir +from lib.utils.settings import global_settings from lib.utils.threading import ExitThread from ..i18n import _ -from ..stitch_plan import stitch_groups_to_stitch_plan, stitch_plan_from_file +from ..stitch_plan import stitch_plan_from_file from ..svg import PIXELS_PER_MM # L10N command label at bottom of simulator window @@ -34,7 +35,8 @@ class ControlPanel(wx.Panel): def __init__(self, parent, *args, **kwargs): """""" self.parent = parent - self.stitch_plan = kwargs.pop('stitch_plan') + self.stitch_plan = kwargs.pop('stitch_plan', None) + self.detach_callback = kwargs.pop('detach_callback', None) self.target_stitches_per_second = kwargs.pop('stitches_per_second') self.target_duration = kwargs.pop('target_duration') kwargs['style'] = wx.BORDER_SUNKEN @@ -97,16 +99,16 @@ class ControlPanel(wx.Panel): self.btnNpp.Bind(wx.EVT_TOGGLEBUTTON, self.toggle_npp) self.btnNpp.SetBitmap(self.load_icon('npp')) self.btnNpp.SetToolTip(_('Display needle penetration point (O)')) - self.slider = SimulatorSlider(self, -1, value=1, minValue=1, maxValue=self.stitch_plan.num_stitches) + self.slider = SimulatorSlider(self, -1, value=1, minValue=1, maxValue=2) self.slider.Bind(wx.EVT_SLIDER, self.on_slider) - self.stitchBox = IntCtrl(self, -1, value=1, min=1, max=self.stitch_plan.num_stitches, - size=((100, -1)), limited=True, allow_none=True, style=wx.TE_PROCESS_ENTER) + self.stitchBox = IntCtrl(self, -1, value=1, min=1, max=2, limited=True, allow_none=True, + size=((100, -1)), style=wx.TE_PROCESS_ENTER) self.stitchBox.Bind(wx.EVT_LEFT_DOWN, self.on_stitch_box_focus) self.stitchBox.Bind(wx.EVT_SET_FOCUS, self.on_stitch_box_focus) self.stitchBox.Bind(wx.EVT_TEXT_ENTER, self.on_stitch_box_focusout) self.stitchBox.Bind(wx.EVT_KILL_FOCUS, self.on_stitch_box_focusout) self.Bind(wx.EVT_LEFT_DOWN, self.on_stitch_box_focusout) - self.totalstitchText = wx.StaticText(self, -1, label=f"/ { self.stitch_plan.num_stitches }") + self.totalstitchText = wx.StaticText(self, -1, label="/ ________") self.btnJump = wx.BitmapToggleButton(self, -1, style=self.button_style) self.btnJump.SetToolTip(_('Show jump stitches')) self.btnJump.SetBitmap(self.load_icon('jump')) @@ -123,12 +125,17 @@ class ControlPanel(wx.Panel): self.btnColorChange.SetToolTip(_('Show color changes')) self.btnColorChange.SetBitmap(self.load_icon('color_change')) self.btnColorChange.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.on_marker_button('color_change', event)) + if self.detach_callback: + self.btnDetachSimulator = wx.BitmapButton(self, -1, style=self.button_style) + self.btnDetachSimulator.SetToolTip(_('Detach/attach simulator window')) + self.btnDetachSimulator.SetBitmap(self.load_icon('detach_window')) + self.btnDetachSimulator.Bind(wx.EVT_BUTTON, lambda event: self.detach_callback()) # Layout self.hbSizer1 = wx.BoxSizer(wx.HORIZONTAL) self.hbSizer1.Add(self.slider, 1, wx.EXPAND | wx.RIGHT, 10) - self.hbSizer1.Add(self.stitchBox, 0, wx.ALIGN_CENTER | wx.RIGHT, 10) - self.hbSizer1.Add(self.totalstitchText, 0, wx.ALIGN_CENTER | wx.RIGHT, 10) + self.hbSizer1.Add(self.stitchBox, 0, wx.ALIGN_CENTER | wx.Right, 10) + self.hbSizer1.Add(self.totalstitchText, 0, wx.ALIGN_CENTER | wx.LEFT, 10) self.controls_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Controls")), wx.HORIZONTAL) self.controls_inner_sizer = wx.BoxSizer(wx.HORIZONTAL) @@ -151,6 +158,8 @@ class ControlPanel(wx.Panel): self.show_inner_sizer.Add(self.btnTrim, 0, wx.ALL, 2) self.show_inner_sizer.Add(self.btnStop, 0, wx.ALL, 2) self.show_inner_sizer.Add(self.btnColorChange, 0, wx.ALL, 2) + if self.detach_callback: + self.show_inner_sizer.Add(self.btnDetachSimulator, 0, wx.ALL, 2) self.show_sizer.Add((1, 1), 1) self.show_sizer.Add(self.show_inner_sizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) self.show_sizer.Add((1, 1), 1) @@ -209,7 +218,6 @@ class ControlPanel(wx.Panel): (wx.ACCEL_NORMAL, ord('o'), self.on_toggle_npp_shortcut), (wx.ACCEL_NORMAL, ord('p'), self.play_or_pause), (wx.ACCEL_NORMAL, wx.WXK_SPACE, self.play_or_pause), - (wx.ACCEL_NORMAL, ord('q'), self.animation_quit), (wx.ACCEL_NORMAL, wx.WXK_PAGEDOWN, self.animation_one_command_backward), (wx.ACCEL_NORMAL, wx.WXK_PAGEUP, self.animation_one_command_forward), @@ -227,7 +235,8 @@ class ControlPanel(wx.Panel): self.SetFocus() # wait for layouts so that panel size is set - wx.CallLater(50, self.load, self.stitch_plan) + if self.stitch_plan: + wx.CallLater(50, self.load, self.stitch_plan) def set_drawing_panel(self, drawing_panel): self.drawing_panel = drawing_panel @@ -240,6 +249,7 @@ class ControlPanel(wx.Panel): self.num_stitches = num_stitches self.stitchBox.SetMax(num_stitches) self.slider.SetMax(num_stitches) + self.totalstitchText.SetLabel(f"/ { num_stitches }") self.choose_speed() def add_color(self, color, num_stitches): @@ -423,9 +433,6 @@ class ControlPanel(wx.Panel): stitch_number += 1 self.drawing_panel.set_current_stitch(stitch_number) - def animation_quit(self, event): - self.parent.quit() - def animation_restart(self, event): self.drawing_panel.restart() @@ -454,7 +461,7 @@ class DrawingPanel(wx.Panel): def __init__(self, *args, **kwargs): """""" - self.stitch_plan = kwargs.pop('stitch_plan') + self.stitch_plan = kwargs.pop('stitch_plan', None) self.control_panel = kwargs.pop('control_panel') kwargs['style'] = wx.BORDER_SUNKEN wx.Panel.__init__(self, *args, **kwargs) @@ -483,8 +490,11 @@ class DrawingPanel(wx.Panel): self.Bind(wx.EVT_LEFT_DOWN, self.on_left_mouse_button_down) self.Bind(wx.EVT_MOUSEWHEEL, self.on_mouse_wheel) + self.SetMinSize((400, 400)) + # wait for layouts so that panel size is set - wx.CallLater(50, self.load, self.stitch_plan) + if self.stitch_plan: + wx.CallLater(50, self.load, self.stitch_plan) def clamp_current_stitch(self): if self.current_stitch < 1: @@ -520,10 +530,12 @@ class DrawingPanel(wx.Panel): wx.CallLater(int(1000 * frame_time), self.animate) def OnPaint(self, e): + dc = wx.PaintDC(self) + if not self.loaded: + dc.Clear() return - dc = wx.PaintDC(self) canvas = wx.GraphicsContext.Create(dc) self.draw_stitches(canvas) @@ -616,8 +628,8 @@ class DrawingPanel(wx.Panel): canvas.StrokeLineSegments(stitches, [(stitch[0] + 0.001, stitch[1]) for stitch in stitches]) def clear(self): - dc = wx.ClientDC(self) - dc.Clear() + self.loaded = False + self.Refresh() def load(self, stitch_plan): self.current_stitch = 1 @@ -722,7 +734,7 @@ class DrawingPanel(wx.Panel): command = self.commands[self.current_stitch] self.control_panel.on_current_stitch(self.current_stitch, command) statusbar = self.GetTopLevelParent().statusbar - statusbar.SetStatusText(_("Command: %s") % COMMAND_NAMES[command]) + statusbar.SetStatusText(_("Command: %s") % COMMAND_NAMES[command], 1) self.stop_if_at_end() self.Refresh() @@ -1012,19 +1024,15 @@ class SimulatorSlider(wx.Panel): class SimulatorPanel(wx.Panel): """""" - def __init__(self, parent, *args, **kwargs): + def __init__(self, parent, stitch_plan=None, target_duration=5, stitches_per_second=16, detach_callback=None): """""" - self.parent = parent - stitch_plan = kwargs.pop('stitch_plan') - target_duration = kwargs.pop('target_duration') - stitches_per_second = kwargs.pop('stitches_per_second') - kwargs['style'] = wx.BORDER_SUNKEN - wx.Panel.__init__(self, parent, *args, **kwargs) + super().__init__(parent, style=wx.BORDER_SUNKEN) self.cp = ControlPanel(self, stitch_plan=stitch_plan, stitches_per_second=stitches_per_second, - target_duration=target_duration) + target_duration=target_duration, + detach_callback=detach_callback) self.dp = DrawingPanel(self, stitch_plan=stitch_plan, control_panel=self.cp) self.cp.set_drawing_panel(self.dp) @@ -1033,9 +1041,6 @@ class SimulatorPanel(wx.Panel): vbSizer.Add(self.cp, 0, wx.EXPAND | wx.ALL, 2) self.SetSizerAndFit(vbSizer) - def quit(self): - self.parent.quit() - def go(self): self.dp.go() @@ -1050,108 +1055,161 @@ class SimulatorPanel(wx.Panel): self.dp.clear() -class EmbroiderySimulator(wx.Frame): - def __init__(self, *args, **kwargs): - self.on_close_hook = kwargs.pop('on_close', None) - stitch_plan = kwargs.pop('stitch_plan', None) - stitches_per_second = kwargs.pop('stitches_per_second', 16) - target_duration = kwargs.pop('target_duration', None) - wx.Frame.__init__(self, *args, **kwargs) - - self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT) - - sizer = wx.BoxSizer(wx.HORIZONTAL) - self.simulator_panel = SimulatorPanel(self, - stitch_plan=stitch_plan, - target_duration=target_duration, - stitches_per_second=stitches_per_second) - sizer.Add(self.simulator_panel, 1, wx.EXPAND) - - self.statusbar = self.CreateStatusBar() - - # SetSizeHints seems to be ignored in macOS, so we have to adjust size manually - # self.SetSizeHints(sizer.CalcMin()) - frame_width, frame_height = self.GetSize() - sizer_width, sizer_height = sizer.CalcMin() - size_diff = frame_width - sizer_width - if size_diff < 0: - frame_x, frame_y = self.GetPosition() - self.SetPosition((frame_x + size_diff, frame_y)) - self.SetSize((sizer_width, frame_height)) +class SimulatorWindow(wx.Frame): + def __init__(self, panel=None, parent=None, **kwargs): + super().__init__(None, title=_("Embroidery Simulation"), **kwargs) - self.Bind(wx.EVT_CLOSE, self.on_close) + self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE) + + self.sizer = wx.BoxSizer(wx.VERTICAL) + + self.statusbar = self.CreateStatusBar(2) + self.statusbar.SetStatusWidths((0, -1)) - def quit(self): - self.Close() + if panel and parent: + self.is_child = True + self.panel = panel + self.parent = parent + self.panel.Reparent(self) + self.sizer.Add(self.panel, 1, wx.EXPAND) + self.panel.Show() + else: + self.is_child = False + self.simulator_panel = SimulatorPanel(self) + self.sizer.Add(self.simulator_panel, 1, wx.EXPAND) + + self.SetSizer(self.sizer) + self.Layout() + + self.SetMinSize(self.sizer.CalcMin()) + + if self.is_child: + self.Bind(wx.EVT_CLOSE, self.on_close) + + def detach_simulator_panel(self): + self.sizer.Detach(self.panel) def on_close(self, event): - self.simulator_panel.stop() + self.parent.attach_simulator() - if self.on_close_hook: - self.on_close_hook() - self.SetFocus() +class SplitSimulatorWindow(wx.Frame): + def __init__(self, panel_class, title, target_duration=None, **kwargs): + super().__init__(None, title=title) + + self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE) + + self.detached_simulator_frame = None + self.splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE) + self.simulator_panel = SimulatorPanel(self.splitter, target_duration=target_duration, detach_callback=self.toggle_detach_simulator) + self.settings_panel = panel_class(self.splitter, simulator=self.simulator_panel, **kwargs) + + self.splitter.SplitVertically(self.settings_panel, self.simulator_panel) + self.splitter.SetMinimumPaneSize(100) + + icon = wx.Icon(os.path.join(get_resource_dir("icons"), "inkstitch256x256.png")) + self.SetIcon(icon) + + self.statusbar = self.CreateStatusBar(2) + + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.sizer.Add(self.splitter, 1, wx.EXPAND) + self.SetSizer(self.sizer) + + self.SetMinSize(self.sizer.CalcMin()) + + self.Maximize() + self.Show() + wx.CallLater(100, self.set_sash_position) + + self.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGING, self.splitter_resize) + self.Bind(wx.EVT_CLOSE, self.on_close) + + if global_settings['pop_out_simulator']: + self.detach_simulator() + + def splitter_resize(self, event): + self.statusbar.SetStatusWidths((self.simulator_panel.GetScreenPosition()[0], -1)) + + def set_sash_position(self): + settings_panel_min_size = self.settings_panel.GetSizer().CalcMin() + debug.log(f"{settings_panel_min_size=}") + self.splitter.SetSashPosition(settings_panel_min_size.width) + self.statusbar.SetStatusWidths((settings_panel_min_size.width, -1)) + + def on_close(self, event): + if self.detached_simulator_frame: + self.detached_simulator_frame.Destroy() self.Destroy() - def go(self): - self.simulator_panel.go() + def toggle_detach_simulator(self): + if self.detached_simulator_frame: + self.attach_simulator() + else: + self.detach_simulator() - def stop(self): - self.simulator_panel.stop() + def attach_simulator(self): + self.detached_simulator_frame.detach_simulator_panel() + self.simulator_panel.Reparent(self.splitter) + self.splitter.SplitVertically(self.settings_panel, self.simulator_panel) - def load(self, stitch_plan): - self.simulator_panel.load(stitch_plan) + self.GetStatusBar().SetStatusText(self.detached_simulator_frame.GetStatusBar().GetStatusText(1), 1) - def clear(self): - self.simulator_panel.clear() + self.detached_simulator_frame.Destroy() + self.detached_simulator_frame = None + self.Maximize() + self.splitter.UpdateSize() + self.SetFocus() + self.Raise() + wx.CallLater(100, self.set_sash_position) + global_settings['pop_out_simulator'] = False + def detach_simulator(self): + self.splitter.Unsplit() + self.detached_simulator_frame = SimulatorWindow(panel=self.simulator_panel, parent=self) + self.splitter.SetMinimumPaneSize(100) -class SimulatorPreview(Thread): - """Manages a preview simulation and a background thread for generating patches.""" + current_screen = wx.Display.GetFromPoint(wx.GetMousePosition()) + display = wx.Display(current_screen) + screen_rect = display.GetClientArea() + settings_panel_size = self.settings_panel.GetSizer().CalcMin() + self.SetMinSize(settings_panel_size) + self.Maximize(False) + self.SetSize((settings_panel_size.width, screen_rect.height)) + self.SetPosition((screen_rect.left, screen_rect.top)) - def __init__(self, parent, *args, **kwargs): - """Construct a SimulatorPreview. + self.detached_simulator_frame.SetSize((screen_rect.width - settings_panel_size.width, screen_rect.height)) + self.detached_simulator_frame.SetPosition((settings_panel_size.width, screen_rect.top)) - The parent is expected to be a wx.Window and also implement the following methods: + self.detached_simulator_frame.GetStatusBar().SetStatusText(self.GetStatusBar().GetStatusText(1), 1) + self.GetStatusBar().SetStatusText("", 1) - def generate_patches(self, abort_event): - Produce an list of StitchGroup instances. This method will be - invoked in a background thread and it is expected that it may - take awhile. + self.detached_simulator_frame.Show() + + global_settings['pop_out_simulator'] = True - If possible, this method should periodically check - abort_event.is_set(), and if True, stop early. The return - value will be ignored in this case. - """ - self.parent = parent - self.target_duration = kwargs.pop('target_duration', 5) - super(SimulatorPreview, self).__init__(*args, **kwargs) - self.daemon = True - self.simulate_window = None +class PreviewRenderer(Thread): + """Render stitch plan in a background thread.""" + + def __init__(self, render_stitch_plan_hook, rendering_completed_hook): + super(PreviewRenderer, self).__init__() + self.daemon = True self.refresh_needed = Event() + self.render_stitch_plan_hook = render_stitch_plan_hook + self.rendering_completed_hook = rendering_completed_hook + # This is read by utils.threading.check_stop_flag() to abort stitch plan # generation. self.stop = Event() - # used when closing to avoid having the window reopen at the last second - self._disabled = False - - wx.CallLater(1000, self.update) - - def disable(self): - self._disabled = True - def update(self): - """Request an update of the simulator preview with freshly-generated patches.""" + """Request to render a new stitch plan. - if self.simulate_window: - self.simulate_window.stop() - self.simulate_window.clear() - - if self._disabled: - return + self.render_stitch_plan_hook() will be called in a background thread, and then + self.rendering_completed_hook() will be called with the resulting stitch plan. + """ if not self.is_alive(): self.start() @@ -1167,80 +1225,22 @@ class SimulatorPreview(Thread): try: debug.log("update_patches") - self.update_patches() + self.render_stitch_plan() except ExitThread: debug.log("ExitThread caught") self.stop.clear() - def update_patches(self): + def render_stitch_plan(self): try: - patches = self.parent.generate_patches(self.refresh_needed) + stitch_plan = self.render_stitch_plan_hook() + if stitch_plan: + # rendering_completed() will be called in the main thread. + wx.CallAfter(self.rendering_completed_hook, stitch_plan) except ExitThread: raise except: # noqa: E722 - # If something goes wrong when rendering patches, it's not great, - # but we don't really want the simulator thread to crash. Instead, - # just swallow the exception and abort. It'll show up when they - # try to actually embroider the shape. - return - - if patches and not self.refresh_needed.is_set(): - metadata = self.parent.metadata - collapse_len = metadata['collapse_len_mm'] - min_stitch_len = metadata['min_stitch_len_mm'] - stitch_plan = stitch_groups_to_stitch_plan(patches, collapse_len=collapse_len, min_stitch_len=min_stitch_len) - - # GUI stuff needs to happen in the main thread, so we ask the main - # thread to call refresh_simulator(). - wx.CallAfter(self.refresh_simulator, patches, stitch_plan) - - def refresh_simulator(self, patches, stitch_plan): - if self.simulate_window: - self.simulate_window.stop() - self.simulate_window.load(stitch_plan) - else: - params_rect = self.parent.GetScreenRect() - simulator_pos = params_rect.GetTopRight() - simulator_pos.x += 5 - - current_screen = wx.Display.GetFromPoint(wx.GetMousePosition()) - display = wx.Display(current_screen) - screen_rect = display.GetClientArea() - simulator_pos.y = screen_rect.GetTop() - - width = screen_rect.GetWidth() - params_rect.GetWidth() - height = screen_rect.GetHeight() - - try: - self.simulate_window = EmbroiderySimulator(None, -1, _("Preview"), - simulator_pos, - size=(width, height), - stitch_plan=stitch_plan, - on_close=self.simulate_window_closed, - target_duration=self.target_duration) - except Exception: - import traceback - print(traceback.format_exc(), file=sys.stderr) - try: - # a window may have been created, so we need to destroy it - # or the app will never exit - wx.Window.FindWindowByName(_("Preview")).Destroy() - except Exception: - pass - - self.simulate_window.Show() - wx.CallLater(10, self.parent.Raise) - - wx.CallAfter(self.simulate_window.go) - - def simulate_window_closed(self): - self.simulate_window = None - - def close(self): - self.disable() - if self.simulate_window: - self.simulate_window.stop() - self.simulate_window.Close() + import traceback + debug.log("unhandled exception in PreviewRenderer.render_stitch_plan(): " + traceback.format_exc()) def show_simulator(stitch_plan): @@ -1255,7 +1255,7 @@ def show_simulator(stitch_plan): width = screen_rect[2] - 1 height = screen_rect[3] - 1 - frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), pos=simulator_pos, size=(width, height), stitch_plan=stitch_plan) + frame = SimulatorWindow(pos=simulator_pos, size=(width, height), stitch_plan=stitch_plan) app.SetTopWindow(frame) frame.Show() app.MainLoop() diff --git a/lib/gui/test_swatches.py b/lib/gui/test_swatches.py index c857f6f8..e5a43149 100644 --- a/lib/gui/test_swatches.py +++ b/lib/gui/test_swatches.py @@ -20,7 +20,7 @@ class GenerateSwatchesFrame(wx.Frame): wx.Frame.__init__(self, *args, **kwargs) wx.Frame.__init__(self, None, wx.ID_ANY, _("Generate Swatches"), *args, **kwargs) - self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT) + self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE) self.panel = wx.Panel(self, wx.ID_ANY) diff --git a/lib/utils/settings.py b/lib/utils/settings.py index f2ce276d..51cfcdb8 100644 --- a/lib/utils/settings.py +++ b/lib/utils/settings.py @@ -12,7 +12,8 @@ DEFAULT_METADATA = { } DEFAULT_SETTINGS = { - "cache_size": 100 + "cache_size": 100, + "pop_out_simulator": False } diff --git a/lib/utils/threading.py b/lib/utils/threading.py index f0c22887..f774dcbe 100644 --- a/lib/utils/threading.py +++ b/lib/utils/threading.py @@ -17,6 +17,7 @@ _default_stop_flag = threading.Event() def check_stop_flag(): + # This getattr() actually looks at the PreviewRenderer instance's stop attribute. if getattr(threading.current_thread(), 'stop', _default_stop_flag).is_set(): debug.log("exiting thread") raise ExitThread() |
