inkstitch/lib/gui/simulator/control_panel.py

345 wiersze
14 KiB
Python

# 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
from sys import platform
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() and platform != "win32":
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()