summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLex Neva <lexelby@users.noreply.github.com>2023-10-21 12:16:34 -0400
committerGitHub <noreply@github.com>2023-10-21 18:16:34 +0200
commit82f2edac1a348b274c177abf5581b089cdf2e527 (patch)
treea8b4fed1e5106adc7e14457d00c6e1de0ade2050
parent526cd48a4f73d9b793b5a83ababd83069f4ad800 (diff)
attach params/lettering simulator window and allow detach (#2557)
-rw-r--r--icons/detach_window.pngbin0 -> 2891 bytes
-rw-r--r--icons/detach_window.svg98
-rw-r--r--lib/extensions/lettering.py81
-rw-r--r--lib/extensions/params.py89
-rw-r--r--lib/gui/__init__.py2
-rw-r--r--lib/gui/element_info.py2
-rw-r--r--lib/gui/preferences.py2
-rw-r--r--lib/gui/simulator.py346
-rw-r--r--lib/gui/test_swatches.py2
-rw-r--r--lib/utils/settings.py3
-rw-r--r--lib/utils/threading.py1
11 files changed, 365 insertions, 261 deletions
diff --git a/icons/detach_window.png b/icons/detach_window.png
new file mode 100644
index 00000000..5758bf88
--- /dev/null
+++ b/icons/detach_window.png
Binary files differ
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()