diff options
Diffstat (limited to 'lib/gui/simulator/control_panel.py')
| -rw-r--r-- | lib/gui/simulator/control_panel.py | 343 |
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() |
