summaryrefslogtreecommitdiff
path: root/lib/gui/simulator/control_panel.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gui/simulator/control_panel.py')
-rw-r--r--lib/gui/simulator/control_panel.py343
1 files changed, 343 insertions, 0 deletions
diff --git a/lib/gui/simulator/control_panel.py b/lib/gui/simulator/control_panel.py
new file mode 100644
index 00000000..a359fe64
--- /dev/null
+++ b/lib/gui/simulator/control_panel.py
@@ -0,0 +1,343 @@
+# Authors: see git history
+#
+# Copyright (c) 2024 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+import os
+
+import wx
+from wx.lib.intctrl import IntCtrl
+
+from ...debug.debug import debug
+from ...i18n import _
+from ...utils import get_resource_dir
+from . import SimulatorSlider
+
+
+class ControlPanel(wx.Panel):
+ """"""
+
+ @debug.time
+ def __init__(self, parent, *args, **kwargs):
+ """"""
+ self.parent = parent
+ 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
+ wx.Panel.__init__(self, parent, *args, **kwargs)
+
+ self.drawing_panel = None
+ self.num_stitches = 0
+ self.current_stitch = 0
+ self.speed = 1
+ self.direction = 1
+ self._last_color_block_end = 0
+
+ self.icons_dir = get_resource_dir("icons")
+
+ # Widgets
+ self.button_size = self.GetTextExtent("M").y * 2
+ self.button_style = wx.BU_EXACTFIT | wx.BU_NOTEXT
+ self.btnMinus = wx.Button(self, -1, style=self.button_style)
+ self.btnMinus.Bind(wx.EVT_BUTTON, self.animation_slow_down)
+ self.btnMinus.SetBitmap(self.load_icon('slower'))
+ self.btnMinus.SetToolTip(_('Slow down (arrow down)'))
+ self.btnPlus = wx.Button(self, -1, style=self.button_style)
+ self.btnPlus.Bind(wx.EVT_BUTTON, self.animation_speed_up)
+ self.btnPlus.SetBitmap(self.load_icon('faster'))
+ self.btnPlus.SetToolTip(_('Speed up (arrow up)'))
+ self.btnBackwardStitch = wx.Button(self, -1, style=self.button_style)
+ self.btnBackwardStitch.Bind(wx.EVT_BUTTON, self.animation_one_stitch_backward)
+ self.btnBackwardStitch.SetBitmap(self.load_icon('backward_stitch'))
+ self.btnBackwardStitch.SetToolTip(_('Go backward one stitch (-)'))
+ self.btnForwardStitch = wx.Button(self, -1, style=self.button_style)
+ self.btnForwardStitch.Bind(wx.EVT_BUTTON, self.animation_one_stitch_forward)
+ self.btnForwardStitch.SetBitmap(self.load_icon('forward_stitch'))
+ self.btnForwardStitch.SetToolTip(_('Go forward one stitch (+)'))
+ self.btnBackwardCommand = wx.Button(self, -1, style=self.button_style)
+ self.btnBackwardCommand.Bind(wx.EVT_BUTTON, self.animation_one_command_backward)
+ self.btnBackwardCommand.SetBitmap(self.load_icon('backward_command'))
+ self.btnBackwardCommand.SetToolTip(_('Go backward one command (page-down)'))
+ self.btnForwardCommand = wx.Button(self, -1, style=self.button_style)
+ self.btnForwardCommand.Bind(wx.EVT_BUTTON, self.animation_one_command_forward)
+ self.btnForwardCommand.SetBitmap(self.load_icon('forward_command'))
+ self.btnForwardCommand.SetToolTip(_('Go forward one command (page-up)'))
+ self.btnDirection = wx.Button(self, -1, style=self.button_style)
+ self.btnDirection.Bind(wx.EVT_BUTTON, self.on_direction_button)
+ self.btnDirection.SetBitmap(self.load_icon('direction'))
+ self.btnDirection.SetToolTip(_('Switch animation direction (arrow left, arrow right)'))
+ self.btnPlay = wx.BitmapToggleButton(self, -1, style=self.button_style)
+ self.btnPlay.Bind(wx.EVT_TOGGLEBUTTON, self.on_play_button)
+ self.btnPlay.SetBitmap(self.load_icon('play'))
+ self.btnPlay.SetToolTip(_('Play (P)'))
+ self.btnRestart = wx.Button(self, -1, style=self.button_style)
+ self.btnRestart.Bind(wx.EVT_BUTTON, self.animation_restart)
+ self.btnRestart.SetBitmap(self.load_icon('restart'))
+ self.btnRestart.SetToolTip(_('Restart (R)'))
+ 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=2, limited=True, allow_none=True,
+ size=((100, -1)), style=wx.TE_PROCESS_ENTER)
+ self.stitchBox.Clear()
+ 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="")
+ extent = self.totalstitchText.GetTextExtent("0000000")
+ self.totalstitchText.SetMinSize(extent)
+
+ # 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_TOP | wx.TOP, 25)
+ self.hbSizer1.Add((1, 1), 0, wx.RIGHT, 10)
+ self.hbSizer1.Add(self.totalstitchText, 0, wx.ALIGN_TOP | wx.TOP, 25)
+ self.hbSizer1.Add((1, 1), 0, wx.RIGHT, 10)
+
+ self.controls_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Controls")), wx.HORIZONTAL)
+ self.controls_inner_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.controls_inner_sizer.Add(self.btnBackwardCommand, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnBackwardStitch, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnForwardStitch, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnForwardCommand, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnDirection, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnPlay, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnRestart, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_sizer.Add((1, 1), 1)
+ self.controls_sizer.Add(self.controls_inner_sizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10)
+ self.controls_sizer.Add((1, 1), 1)
+
+ self.speed_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Speed")), wx.VERTICAL)
+
+ self.speed_buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.speed_buttons_sizer.Add((1, 1), 1)
+ self.speed_buttons_sizer.Add(self.btnMinus, 0, wx.ALL, 2)
+ self.speed_buttons_sizer.Add(self.btnPlus, 0, wx.ALL, 2)
+ self.speed_buttons_sizer.Add((1, 1), 1)
+ self.speed_sizer.Add(self.speed_buttons_sizer, 0, wx.EXPAND | wx.ALL)
+ self.speed_text = wx.StaticText(self, wx.ID_ANY, label="", style=wx.ALIGN_CENTRE_HORIZONTAL | wx.ST_NO_AUTORESIZE)
+ self.speed_text.SetFont(wx.Font(wx.FontInfo(10).Bold()))
+ extent = self.speed_text.GetTextExtent(self.format_speed_text(100000))
+ self.speed_text.SetMinSize(extent)
+ self.speed_sizer.Add(self.speed_text, 0, wx.EXPAND | wx.ALL, 5)
+
+ # A normal BoxSizer can only make child components the same or
+ # proportional size. A FlexGridSizer can split up the available extra
+ # space evenly among all growable columns.
+ self.control_row2_sizer = wx.FlexGridSizer(cols=3, vgap=0, hgap=5)
+ self.control_row2_sizer.AddGrowableCol(0)
+ self.control_row2_sizer.AddGrowableCol(1)
+ self.control_row2_sizer.AddGrowableCol(2)
+ self.control_row2_sizer.Add(self.controls_sizer, 0, wx.EXPAND)
+ self.control_row2_sizer.Add(self.speed_sizer, 0, wx.EXPAND)
+
+ self.vbSizer = vbSizer = wx.BoxSizer(wx.VERTICAL)
+ vbSizer.Add(self.hbSizer1, 1, wx.EXPAND | wx.ALL, 10)
+ vbSizer.Add(self.control_row2_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
+ self.SetSizerAndFit(vbSizer)
+
+ # wait for layouts so that panel size is set
+ if self.stitch_plan:
+ wx.CallLater(50, self.load, self.stitch_plan)
+
+ def set_drawing_panel(self, drawing_panel):
+ self.drawing_panel = drawing_panel
+ self.drawing_panel.set_speed(self.speed)
+
+ def _set_num_stitches(self, num_stitches):
+ if num_stitches < 2:
+ # otherwise the slider and intctrl get mad
+ num_stitches = 2
+ self.num_stitches = num_stitches
+ self.stitchBox.SetValue(1)
+ self.stitchBox.SetMax(num_stitches)
+ self.slider.SetMax(num_stitches)
+ self.totalstitchText.SetLabel(f"/ { num_stitches }")
+ self.choose_speed()
+
+ def clear(self):
+ self.stitches = []
+ self._set_num_stitches(0)
+ self.slider.clear()
+ self.stitchBox.Clear()
+ self.totalstitchText.SetLabel("")
+
+ def load(self, stitch_plan):
+ self.clear()
+ self.stitches = []
+ self._set_num_stitches(stitch_plan.num_stitches)
+
+ stitch_num = 0
+ last_block_end = 1
+ for color_block in stitch_plan.color_blocks:
+ self.stitches.extend(color_block.stitches)
+
+ start = stitch_num + 1
+ end = start + color_block.num_stitches - 1
+ self.slider.add_color_section(color_block.color.rgb, last_block_end, end)
+ last_block_end = end
+
+ for stitch_num, stitch in enumerate(color_block.stitches, start):
+ if stitch.trim:
+ self.slider.add_marker("trim", stitch_num)
+ elif stitch.stop:
+ self.slider.add_marker("stop", stitch_num)
+ elif stitch.jump:
+ self.slider.add_marker("jump", stitch_num)
+ elif stitch.color_change:
+ self.slider.add_marker("color_change", stitch_num)
+
+ def is_dark_theme(self):
+ return wx.SystemSettings().GetAppearance().IsDark()
+
+ def load_icon(self, icon_name):
+ if self.is_dark_theme():
+ icon = wx.Image(os.path.join(self.icons_dir, f"{icon_name}_dark.png"))
+ else:
+ icon = wx.Image(os.path.join(self.icons_dir, f"{icon_name}.png"))
+ icon.Rescale(self.button_size, self.button_size, wx.IMAGE_QUALITY_HIGH)
+ return icon.ConvertToBitmap()
+
+ def choose_speed(self):
+ if self.target_duration:
+ self.set_speed(int(self.num_stitches / float(self.target_duration)))
+ else:
+ self.set_speed(self.target_stitches_per_second)
+
+ def animation_forward(self, event=None):
+ self.drawing_panel.forward()
+ self.direction = 1
+ self.update_speed_text()
+
+ def animation_reverse(self, event=None):
+ self.drawing_panel.reverse()
+ self.direction = -1
+ self.update_speed_text()
+
+ def on_direction_button(self, event):
+ if self.direction == -1:
+ self.animation_forward()
+ else:
+ self.animation_reverse()
+
+ def set_speed(self, speed):
+ self.speed = int(max(speed, 1))
+ self.update_speed_text()
+
+ if self.drawing_panel:
+ self.drawing_panel.set_speed(self.speed)
+
+ def format_speed_text(self, speed):
+ return _('%d stitches/sec') % speed
+
+ def update_speed_text(self):
+ self.speed_text.SetLabel(self.format_speed_text(self.speed * self.direction))
+
+ def on_slider(self, event):
+ self.animation_pause()
+ stitch = event.GetEventObject().GetValue()
+ self.stitchBox.SetValue(stitch)
+
+ if self.drawing_panel:
+ self.drawing_panel.set_current_stitch(stitch)
+
+ self.parent.SetFocus()
+
+ def on_current_stitch(self, stitch, command):
+ if self.current_stitch != stitch:
+ self.current_stitch = stitch
+ self.slider.SetValue(stitch)
+ self.stitchBox.SetValue(stitch)
+
+ def on_stitch_box_focus(self, event):
+ self.animation_pause()
+ self.parent.SetAcceleratorTable(wx.AcceleratorTable([]))
+ event.Skip()
+
+ def on_stitch_box_focusout(self, event):
+ self.parent.SetAcceleratorTable(self.parent.accel_table)
+ stitch = self.stitchBox.GetValue()
+ # We now want to remove the focus from the stitchBox.
+ # In Windows it won't work if we set focus to self.parent, while setting the focus to the
+ # top level would work. This in turn would activate the trim button in Linux. So let's
+ # set the focus on the slider instead where it doesn't cause any harm in any of the operating systems
+ self.slider.SetFocus()
+
+ if stitch is None:
+ stitch = 1
+ self.stitchBox.SetValue(1)
+
+ self.slider.SetValue(stitch)
+
+ if self.drawing_panel:
+ self.drawing_panel.set_current_stitch(stitch)
+ event.Skip()
+
+ def animation_slow_down(self, event):
+ """"""
+ self.set_speed(self.speed / 2.0)
+
+ def animation_speed_up(self, event):
+ """"""
+ self.set_speed(self.speed * 2.0)
+
+ def animation_pause(self, event=None):
+ self.drawing_panel.stop()
+
+ def animation_start(self, event=None):
+ self.drawing_panel.go()
+
+ def on_start(self):
+ self.btnPlay.SetValue(True)
+
+ def on_stop(self):
+ self.btnPlay.SetValue(False)
+
+ def on_play_button(self, event):
+ play = self.btnPlay.GetValue()
+ if play:
+ self.animation_start()
+ else:
+ self.animation_pause()
+
+ def play_or_pause(self, event):
+ if self.drawing_panel.animating:
+ self.animation_pause()
+ else:
+ self.animation_start()
+
+ def animation_one_stitch_forward(self, event):
+ self.animation_pause()
+ self.drawing_panel.one_stitch_forward()
+
+ def animation_one_stitch_backward(self, event):
+ self.animation_pause()
+ self.drawing_panel.one_stitch_backward()
+
+ def animation_one_command_backward(self, event):
+ self.animation_pause()
+ stitch_number = self.current_stitch - 1
+ while stitch_number >= 1:
+ # stitch number shown to the user starts at 1
+ stitch = self.stitches[stitch_number - 1]
+ if stitch.jump or stitch.trim or stitch.stop or stitch.color_change:
+ break
+ stitch_number -= 1
+ self.drawing_panel.set_current_stitch(stitch_number)
+
+ def animation_one_command_forward(self, event):
+ self.animation_pause()
+ stitch_number = self.current_stitch + 1
+ while stitch_number <= self.num_stitches:
+ # stitch number shown to the user starts at 1
+ stitch = self.stitches[stitch_number - 1]
+ if stitch.jump or stitch.trim or stitch.stop or stitch.color_change:
+ break
+ stitch_number += 1
+ self.drawing_panel.set_current_stitch(stitch_number)
+
+ def animation_restart(self, event):
+ self.drawing_panel.restart()