# 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()