From 1f57763e797b933d0616495a32051ec74ec516bd Mon Sep 17 00:00:00 2001 From: Kaalleen <36401965+kaalleen@users.noreply.github.com> Date: Wed, 29 May 2024 14:38:17 +0200 Subject: add help tab to lettering (#2948) --- lib/extensions/satin_multicolor.py | 2 +- lib/gui/__init__.py | 1 - lib/gui/lettering.py | 470 ----------------------------- lib/gui/lettering/__init__.py | 8 + lib/gui/lettering/help_panel.py | 51 ++++ lib/gui/lettering/main_panel.py | 375 +++++++++++++++++++++++ lib/gui/lettering/option_panel.py | 105 +++++++ lib/gui/satin_multicolor/__init__.py | 8 + lib/gui/satin_multicolor/colorize.py | 273 ----------------- lib/gui/satin_multicolor/colorize_panel.py | 273 +++++++++++++++++ lib/gui/satin_multicolor/main_panel.py | 3 +- 11 files changed, 822 insertions(+), 747 deletions(-) delete mode 100644 lib/gui/lettering.py create mode 100644 lib/gui/lettering/__init__.py create mode 100644 lib/gui/lettering/help_panel.py create mode 100644 lib/gui/lettering/main_panel.py create mode 100644 lib/gui/lettering/option_panel.py create mode 100644 lib/gui/satin_multicolor/__init__.py delete mode 100644 lib/gui/satin_multicolor/colorize.py create mode 100644 lib/gui/satin_multicolor/colorize_panel.py diff --git a/lib/extensions/satin_multicolor.py b/lib/extensions/satin_multicolor.py index 5657bdbf..a6c1983e 100644 --- a/lib/extensions/satin_multicolor.py +++ b/lib/extensions/satin_multicolor.py @@ -10,7 +10,7 @@ import wx.adv from inkex import errormsg from ..elements import SatinColumn -from ..gui import MultiColorSatinPanel +from ..gui.satin_multicolor import MultiColorSatinPanel from ..gui.simulator import SplitSimulatorWindow from ..i18n import _ from ..utils.svg_data import get_pagecolor diff --git a/lib/gui/__init__.py b/lib/gui/__init__.py index 53fb7e65..09f5d3bb 100644 --- a/lib/gui/__init__.py +++ b/lib/gui/__init__.py @@ -7,4 +7,3 @@ from .dialogs import confirm_dialog, info_dialog from .presets import PresetsPanel from .simulator import PreviewRenderer from .warnings import WarningPanel -from .satin_multicolor.main_panel import MultiColorSatinPanel diff --git a/lib/gui/lettering.py b/lib/gui/lettering.py deleted file mode 100644 index bce39ff8..00000000 --- a/lib/gui/lettering.py +++ /dev/null @@ -1,470 +0,0 @@ -# Authors: see git history -# -# Copyright (c) 2010 Authors -# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. - -import json -from base64 import b64decode - -import inkex -import wx -import wx.adv -import wx.lib.agw.floatspin as fs - -from ..elements import nodes_to_elements -from ..i18n import _ -from ..lettering import FontError, get_font_list -from ..lettering.categories import FONT_CATEGORIES, FontCategory -from ..stitch_plan import stitch_groups_to_stitch_plan -from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_LETTERING, SVG_PATH_TAG -from ..utils import DotDict, cache -from ..utils.threading import ExitThread, check_stop_flag -from . import PresetsPanel, PreviewRenderer, info_dialog - - -class LetteringPanel(wx.Panel): - DEFAULT_FONT = "small_font" - - def __init__(self, parent, simulator, group, metadata=None, background_color='white'): - self.parent = parent - self.simulator = simulator - self.group = group - self.metadata = metadata or dict() - self.background_color = background_color - - super().__init__(parent, wx.ID_ANY) - - self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE) - - self.preview_renderer = PreviewRenderer(self.render_stitch_plan, self.on_stitch_plan_rendered) - self.presets_panel = PresetsPanel(self) - - # font - self.font_selector_box = wx.StaticBox(self, wx.ID_ANY, label=_("Font")) - - self.font_chooser = wx.adv.BitmapComboBox(self, wx.ID_ANY, style=wx.CB_READONLY | wx.CB_SORT) - self.font_chooser.Bind(wx.EVT_COMBOBOX, self.on_font_changed) - - self.font_size_filter = fs.FloatSpin(self, min_val=0, max_val=None, increment=1, value="0") - self.font_size_filter.SetFormat("%f") - self.font_size_filter.SetDigits(2) - self.font_size_filter.Bind(fs.EVT_FLOATSPIN, self.on_filter_changed) - self.font_size_filter.SetToolTip(_("Font size filter (mm). 0 for all sizes.")) - - self.font_glyph_filter = wx.CheckBox(self, label=_("Glyphs")) - self.font_glyph_filter.Bind(wx.EVT_CHECKBOX, self.on_filter_changed) - self.font_glyph_filter.SetToolTip(_("Filter fonts by available glyphs.")) - - self.font_category_filter = wx.ComboBox(self, wx.ID_ANY, choices=[], style=wx.CB_DROPDOWN | wx.CB_READONLY) - unfiltered = FontCategory('unfiltered', "---") - self.font_category_filter.Append(unfiltered.name, unfiltered) - for category in FONT_CATEGORIES: - self.font_category_filter.Append(category.name, category) - self.font_category_filter.SetToolTip(_("Filter fonts by category.")) - self.font_category_filter.SetSelection(0) - self.font_category_filter.Bind(wx.EVT_COMBOBOX, self.on_filter_changed) - - # font details - self.font_description = wx.StaticText(self, wx.ID_ANY) - self.Bind(wx.EVT_SIZE, self.resize) - - # font filter - self.filter_box = wx.StaticBox(self, wx.ID_ANY, label=_("Font Filter")) - - # options - self.options_box = wx.StaticBox(self, wx.ID_ANY, label=_("Options")) - - self.scale_spinner = wx.SpinCtrl(self, wx.ID_ANY, min=0, max=1000, initial=100) - self.scale_spinner.Bind(wx.EVT_SPINCTRL, lambda event: self.on_change("scale", event)) - - self.back_and_forth_checkbox = wx.CheckBox(self, label=_("Stitch lines of text back and forth")) - self.back_and_forth_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("back_and_forth", event)) - - self.trim_option_choice = wx.Choice(self, choices=[_("Never"), _("after each line"), _("after each word"), _("after each letter")], - name=_("Add trim command")) - self.trim_option_choice.Bind(wx.EVT_CHOICE, lambda event: self.on_trim_option_change(event)) - - self.use_trim_symbols = wx.CheckBox(self, label=_("Use command symbols")) - self.use_trim_symbols.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("use_trim_symbols", event)) - self.use_trim_symbols.SetToolTip(_('Uses command symbols if enabled. When disabled inserts trim commands as params.')) - - # text editor - self.text_input_box = wx.StaticBox(self, wx.ID_ANY, label=_("Text")) - - self.text_editor = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_DONTWRAP) - self.text_editor.Bind(wx.EVT_TEXT, lambda event: self.on_change("text", event)) - - self.cancel_button = wx.Button(self, wx.ID_ANY, _("Cancel")) - self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel) - self.Bind(wx.EVT_CLOSE, self.cancel) - - self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit")) - self.apply_button.Bind(wx.EVT_BUTTON, self.apply) - - # set font list - self.update_font_list() - self.set_font_list() - - self.__do_layout() - - self.load_settings() - self.apply_settings() - - def load_settings(self): - """Load the settings saved into the SVG group element""" - - self.settings = DotDict({ - "text": "", - "back_and_forth": False, - "font": None, - "scale": 100, - "trim_option": 0, - "use_trim_symbols": False - }) - - if INKSTITCH_LETTERING in self.group.attrib: - try: - self.settings.update(json.loads(self.group.get(INKSTITCH_LETTERING))) - except json.decoder.JSONDecodeError: - # legacy base64 encoded (changed in v2.0) - try: - self.settings.update(json.loads(b64decode(self.group.get(INKSTITCH_LETTERING)))) - except (TypeError, ValueError): - pass - except (TypeError, ValueError): - pass - - def apply_settings(self): - """Make the settings in self.settings visible in the UI.""" - self.back_and_forth_checkbox.SetValue(bool(self.settings.back_and_forth)) - self.trim_option_choice.SetSelection(self.settings.trim_option) - self.use_trim_symbols.SetValue(bool(self.settings.use_trim_symbols)) - self.text_editor.SetValue(self.settings.text) - self.scale_spinner.SetValue(self.settings.scale) - self.set_initial_font(self.settings.font) - - def save_settings(self): - """Save the settings into the SVG group element.""" - self.group.set(INKSTITCH_LETTERING, json.dumps(self.settings)) - - @property - @cache - def font_list(self): - return get_font_list() - - def update_font_list(self): - self.fonts = {} - self.fonts_by_id = {} - - # font size filter value - filter_size = self.font_size_filter.GetValue() - filter_glyph = self.font_glyph_filter.GetValue() - filter_category = self.font_category_filter.GetSelection() - 1 - - # glyph filter string without spaces - glyphs = [*self.text_editor.GetValue().replace(" ", "").replace("\n", "")] - - for font in self.font_list: - if filter_glyph and glyphs and not set(glyphs).issubset(font.available_glyphs): - continue - - if filter_category != -1: - category = FONT_CATEGORIES[filter_category].id - if category not in font.keywords: - continue - - if filter_size != 0 and (filter_size < font.size * font.min_scale or filter_size > font.size * font.max_scale): - continue - - self.fonts[font.marked_custom_font_name] = font - self.fonts_by_id[font.marked_custom_font_id] = font - - def set_font_list(self): - self.font_chooser.Clear() - for font in self.fonts.values(): - image = font.preview_image - - if image is not None: - image = wx.Image(image) - """ - # I would like to do this but Windows requires all images to be the exact same size - # It might work with an updated wxpython version - so let's keep it here - - # Scale to max 20 height - img_height = 20 - width, height = image.GetSize() - scale_factor = height / img_height - width = int(width / scale_factor) - image.Rescale(width, img_height, quality=wx.IMAGE_QUALITY_HIGH) - """ - # Windows requires all images to have the exact same size - image.Rescale(300, 20, quality=wx.IMAGE_QUALITY_HIGH) - self.font_chooser.Append(font.marked_custom_font_name, wx.Bitmap(image)) - else: - self.font_chooser.Append(font.marked_custom_font_name) - - def get_font_descriptions(self): - return {font.name: font.description for font in self.fonts.values()} - - def set_initial_font(self, font_id): - if font_id: - if font_id not in self.fonts_by_id: - message = '''This text was created using the font "%s", but Ink/Stitch can't find that font. ''' \ - '''A default font will be substituted.''' - info_dialog(self, _(message) % font_id) - try: - font = self.fonts_by_id[font_id].marked_custom_font_name - except KeyError: - font = self.default_font.marked_custom_font_name - self.font_chooser.SetValue(font) - - self.on_font_changed() - - @property - def default_font(self): - try: - return self.fonts_by_id[self.DEFAULT_FONT] - except KeyError: - return list(self.fonts.values())[0] - - def on_change(self, attribute, event): - self.settings[attribute] = event.GetEventObject().GetValue() - if attribute == "text" and self.font_glyph_filter.GetValue() is True: - self.on_filter_changed() - self.preview_renderer.update() - - def on_trim_option_change(self, event=None): - self.settings.trim_option = self.trim_option_choice.GetCurrentSelection() - self.preview_renderer.update() - - def on_font_changed(self, event=None): - font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) - self.settings.font = font.marked_custom_font_id - - filter_size = self.font_size_filter.GetValue() - self.scale_spinner.SetRange(int(font.min_scale * 100), int(font.max_scale * 100)) - if filter_size != 0: - self.scale_spinner.SetValue(int(filter_size / font.size * 100)) - self.settings['scale'] = self.scale_spinner.GetValue() - - font_variants = [] - try: - font_variants = font.has_variants() - except FontError: - pass - - # Update font description - color = wx.NullColour - description = font.description - if len(font_variants) == 0: - color = (255, 0, 0) - description = _('This font has no available font variant. Please update or remove the font.') - self.font_description.SetLabel(description) - self.font_description.SetForegroundColour(color) - self.font_description.Wrap(self.GetSize().width - 35) - - if font.reversible: - self.back_and_forth_checkbox.Enable() - self.back_and_forth_checkbox.SetValue(bool(self.settings.back_and_forth)) - else: - # The creator of the font banned the possibility of writing in reverse with json file: "reversible": false - self.back_and_forth_checkbox.Disable() - self.back_and_forth_checkbox.SetValue(False) - - self.update_preview() - self.Layout() - - def on_filter_changed(self, event=None): - self.update_font_list() - - if not self.fonts: - # No fonts for filtered size - self.font_chooser.Clear() - self.filter_box.SetForegroundColour("red") - return - else: - self.filter_box.SetForegroundColour(wx.NullColour) - - filter_size = self.font_size_filter.GetValue() - previous_font = self.font_chooser.GetValue() - self.set_font_list() - font = self.fonts.get(previous_font, self.default_font) - self.font_chooser.SetValue(font.marked_custom_font_name) - if font.marked_custom_font_name != previous_font: - self.on_font_changed() - elif filter_size != 0: - self.scale_spinner.SetValue(int(filter_size / font.size * 100)) - self.settings['scale'] = self.scale_spinner.GetValue() - - def resize(self, event=None): - description = self.font_description.GetLabel().replace("\n", " ") - self.font_description.SetLabel(description) - self.font_description.Wrap(self.GetSize().width - 35) - self.Layout() - - def update_preview(self, event=None): - 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) - if not self.font_chooser.GetValue(): - return - - del self.group[:] - - if self.settings.scale == 100: - destination_group = self.group - else: - destination_group = inkex.Group(attrib={ - # L10N The user has chosen to scale the text by some percentage - # (50%, 200%, etc). If you need to use the percentage symbol, - # make sure to double it (%%). - INKSCAPE_LABEL: _("Text scale %s%%") % self.settings.scale - }) - self.group.append(destination_group) - - font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) - try: - font.render_text(self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth, - trim_option=self.settings.trim_option, use_trim_symbols=self.settings.use_trim_symbols) - - except FontError as e: - if raise_error: - inkex.errormsg(_("Error: Text cannot be applied to the document.\n%s") % e) - return - else: - pass - - # destination_group isn't always the text scaling group (but also the parent group) - # the text scaling group label is dependend on the user language, so it would break in international file exchange if we used it - # scaling (correction transform) on the parent group is already applied, so let's use that for recognition - if self.settings.scale != 100 and not destination_group.get('transform', None): - destination_group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0) - - 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: - check_stop_flag() - - stitch_groups.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: - raise - except Exception: - raise - # Ignore errors. This can be things like incorrect paths for - # satins or division by zero caused by incorrect param values. - pass - - 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 - settings = dict(self.settings) - del settings["text"] - return settings - - def apply_preset_data(self, preset_data): - settings = DotDict(preset_data) - settings["text"] = self.settings.text - self.settings = settings - self.apply_settings() - - def get_preset_suite_name(self): - # called by self.presets_panel - return "lettering" - - def apply(self, event): - self.update_lettering(True) - self.save_settings() - self.close() - - def close(self): - self.simulator.stop() - wx.CallAfter(self.GetTopLevelParent().close) - - def cancel(self, event): - self.simulator.stop() - wx.CallAfter(self.GetTopLevelParent().cancel) - - def __do_layout(self): - outer_sizer = wx.BoxSizer(wx.VERTICAL) - - # font selection - font_selector_sizer = wx.StaticBoxSizer(self.font_selector_box, wx.VERTICAL) - font_selector_box = wx.BoxSizer(wx.HORIZONTAL) - font_selector_box.Add(self.font_chooser, 4, wx.EXPAND | wx.TOP | wx.BOTTOM | wx.RIGHT, 10) - font_selector_sizer.Add(font_selector_box, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) - font_selector_sizer.Add(self.font_description, 1, wx.EXPAND | wx.ALL, 10) - outer_sizer.Add(font_selector_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) - - # filter fon list - filter_sizer = wx.StaticBoxSizer(self.filter_box, wx.HORIZONTAL) - filter_size_label = wx.StaticText(self, wx.ID_ANY, _("Size")) - filter_sizer.Add(filter_size_label, 0, wx.LEFT | wx.TOP | wx.BOTTOM, 10) - filter_sizer.AddSpacer(5) - filter_sizer.Add(self.font_size_filter, 1, wx.RIGHT | wx.TOP | wx.BOTTOM, 10) - filter_sizer.AddSpacer(5) - filter_sizer.Add(self.font_glyph_filter, 1, wx.RIGHT | wx.TOP | wx.BOTTOM, 10) - filter_sizer.Add(self.font_category_filter, 1, wx.RIGHT | wx.TOP | wx.BOTTOM, 10) - outer_sizer.Add(filter_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) - - # options - left_option_sizer = wx.BoxSizer(wx.VERTICAL) - left_option_sizer.Add(self.back_and_forth_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 5) - - trim_option_sizer = wx.BoxSizer(wx.HORIZONTAL) - trim_option_sizer.Add(wx.StaticText(self, wx.ID_ANY, _("Add trims")), 0, wx.LEFT | wx.ALIGN_TOP, 5) - trim_option_sizer.Add(self.trim_option_choice, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5) - trim_option_sizer.Add(self.use_trim_symbols, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5) - left_option_sizer.Add(trim_option_sizer, 0, wx.ALIGN_LEFT, 5) - - font_scale_sizer = wx.BoxSizer(wx.HORIZONTAL) - font_scale_sizer.Add(wx.StaticText(self, wx.ID_ANY, _("Scale")), 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 0) - font_scale_sizer.Add(self.scale_spinner, 0, wx.LEFT, 10) - font_scale_sizer.Add(wx.StaticText(self, wx.ID_ANY, "%"), 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 3) - - options_sizer = wx.StaticBoxSizer(self.options_box, wx.HORIZONTAL) - options_sizer.Add(left_option_sizer, 1, wx.EXPAND, 10) - options_sizer.Add(font_scale_sizer, 0, wx.RIGHT, 10) - - outer_sizer.Add(options_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) - - # text input - text_input_sizer = wx.StaticBoxSizer(self.text_input_box, wx.VERTICAL) - text_input_sizer.Add(self.text_editor, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10) - outer_sizer.Add(text_input_sizer, 2, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) - - # presets - outer_sizer.Add(self.presets_panel, 0, wx.EXPAND | wx.EXPAND | wx.ALL, 10) - buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) - buttons_sizer.Add(self.cancel_button, 0, wx.RIGHT, 10) - buttons_sizer.Add(self.apply_button, 0, wx.RIGHT | wx.BOTTOM, 10) - outer_sizer.Add(buttons_sizer, 0, wx.ALIGN_RIGHT, 10) - - self.SetSizerAndFit(outer_sizer) - self.Layout() - - # SetSizerAndFit determined the minimum size that fits all the controls - # and set the window's minimum size so that the user can't make it - # smaller. It also set the window to that size. We'd like to give the - # user a bit more room for text, so we'll add some height. - size = self.GetSize() - size.height = size.height + 200 - self.SetSize(size) diff --git a/lib/gui/lettering/__init__.py b/lib/gui/lettering/__init__.py new file mode 100644 index 00000000..970ebca8 --- /dev/null +++ b/lib/gui/lettering/__init__.py @@ -0,0 +1,8 @@ +# Authors: see git history +# +# Copyright (c) 2024 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +from .help_panel import LetteringHelpPanel +from .option_panel import LetteringOptionsPanel +from .main_panel import LetteringPanel diff --git a/lib/gui/lettering/help_panel.py b/lib/gui/lettering/help_panel.py new file mode 100644 index 00000000..648da180 --- /dev/null +++ b/lib/gui/lettering/help_panel.py @@ -0,0 +1,51 @@ +# 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 wx + +from ...i18n import _ + + +class LetteringHelpPanel(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + help_sizer = wx.BoxSizer(wx.VERTICAL) + + help_text = wx.StaticText( + self, + wx.ID_ANY, + _("Add text to your design."), + style=wx.ALIGN_LEFT + ) + help_text.Wrap(500) + help_sizer.Add(help_text, 0, wx.ALL, 8) + + website_link = wx.adv.HyperlinkCtrl( + self, + wx.ID_ANY, + _("https://inkstitch.org/docs/lettering/#lettering-tool"), + _("https://inkstitch.org/docs/lettering/#lettering-tool") + ) + website_link.Bind(wx.adv.EVT_HYPERLINK, self.on_link_clicked) + help_sizer.Add(website_link, 0, wx.ALL, 8) + + help_sizer.Add((20, 20), 0, 0, 0) + + website_info = wx.StaticText(self, wx.ID_ANY, _("A font library with full description and embroidered examples can be found on our website:")) + help_sizer.Add(website_info, 0, wx.ALL, 8) + + fontlibrary_link = wx.adv.HyperlinkCtrl( + self, + wx.ID_ANY, + _("https://inkstitch.org/fonts/font-library/"), + _("https://inkstitch.org/fonts/font-library/") + ) + fontlibrary_link.Bind(wx.adv.EVT_HYPERLINK, self.on_link_clicked) + help_sizer.Add(fontlibrary_link, 0, wx.ALL, 8) + + self.SetSizer(help_sizer) + + def on_link_clicked(self, event): + event.Skip() diff --git a/lib/gui/lettering/main_panel.py b/lib/gui/lettering/main_panel.py new file mode 100644 index 00000000..eb3d61a4 --- /dev/null +++ b/lib/gui/lettering/main_panel.py @@ -0,0 +1,375 @@ +# Authors: see git history +# +# Copyright (c) 2010 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import json +from base64 import b64decode + +import inkex +import wx +import wx.adv + +from ...elements import nodes_to_elements +from ...i18n import _ +from ...lettering import FontError, get_font_list +from ...lettering.categories import FONT_CATEGORIES +from ...stitch_plan import stitch_groups_to_stitch_plan +from ...svg.tags import INKSCAPE_LABEL, INKSTITCH_LETTERING, SVG_PATH_TAG +from ...utils import DotDict, cache +from ...utils.threading import ExitThread, check_stop_flag +from .. import PresetsPanel, PreviewRenderer, info_dialog +from . import LetteringHelpPanel, LetteringOptionsPanel + + +class LetteringPanel(wx.Panel): + DEFAULT_FONT = "small_font" + + def __init__(self, parent, simulator, group, metadata=None, background_color='white'): + self.parent = parent + self.simulator = simulator + self.group = group + self.metadata = metadata or dict() + self.background_color = background_color + + super().__init__(parent, wx.ID_ANY) + + self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE) + + outer_sizer = wx.BoxSizer(wx.VERTICAL) + + self.preview_renderer = PreviewRenderer(self.render_stitch_plan, self.on_stitch_plan_rendered) + + # notebook + self.notebook = wx.Notebook(self, wx.ID_ANY) + self.options_panel = LetteringOptionsPanel(self.notebook, self) + self.notebook.AddPage(self.options_panel, _("Options")) + help_panel = LetteringHelpPanel(self.notebook) + self.notebook.AddPage(help_panel, _("Help")) + outer_sizer.Add(self.notebook, 1, wx.EXPAND | wx.ALL, 10) + + # presets + self.presets_panel = PresetsPanel(self) + outer_sizer.Add(self.presets_panel, 0, wx.EXPAND | wx.ALL, 10) + + # buttons + self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit")) + self.apply_button.Bind(wx.EVT_BUTTON, self.apply) + self.cancel_button = wx.Button(self, wx.ID_ANY, _("Cancel")) + self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel) + self.Bind(wx.EVT_CLOSE, self.cancel) + buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) + buttons_sizer.Add(self.cancel_button, 0, wx.RIGHT, 10) + buttons_sizer.Add(self.apply_button, 0, wx.RIGHT | wx.BOTTOM, 10) + outer_sizer.Add(buttons_sizer, 0, wx.ALIGN_RIGHT, 10) + + # set font list + self.update_font_list() + self.set_font_list() + + self.SetSizer(outer_sizer) + self.Layout() + + # SetSizerAndFit determined the minimum size that fits all the controls + # and set the window's minimum size so that the user can't make it + # smaller. It also set the window to that size. We'd like to give the + # user a bit more room for text, so we'll add some height. + size = self.options_panel.GetSize() + size.height = size.height + 200 + self.options_panel.SetSize(size) + + self.load_settings() + self.apply_settings() + + def load_settings(self): + """Load the settings saved into the SVG group element""" + + self.settings = DotDict({ + "text": "", + "back_and_forth": False, + "font": None, + "scale": 100, + "trim_option": 0, + "use_trim_symbols": False + }) + + if INKSTITCH_LETTERING in self.group.attrib: + try: + self.settings.update(json.loads(self.group.get(INKSTITCH_LETTERING))) + except json.decoder.JSONDecodeError: + # legacy base64 encoded (changed in v2.0) + try: + self.settings.update(json.loads(b64decode(self.group.get(INKSTITCH_LETTERING)))) + except (TypeError, ValueError): + pass + except (TypeError, ValueError): + pass + + def apply_settings(self): + """Make the settings in self.settings visible in the UI.""" + self.options_panel.back_and_forth_checkbox.SetValue(bool(self.settings.back_and_forth)) + self.options_panel.trim_option_choice.SetSelection(self.settings.trim_option) + self.options_panel.use_trim_symbols.SetValue(bool(self.settings.use_trim_symbols)) + self.options_panel.text_editor.SetValue(self.settings.text) + self.options_panel.scale_spinner.SetValue(self.settings.scale) + self.set_initial_font(self.settings.font) + + def save_settings(self): + """Save the settings into the SVG group element.""" + self.group.set(INKSTITCH_LETTERING, json.dumps(self.settings)) + + @property + @cache + def font_list(self): + return get_font_list() + + def update_font_list(self): + self.fonts = {} + self.fonts_by_id = {} + + # font size filter value + filter_size = self.options_panel.font_size_filter.GetValue() + filter_glyph = self.options_panel.font_glyph_filter.GetValue() + filter_category = self.options_panel.font_category_filter.GetSelection() - 1 + + # glyph filter string without spaces + glyphs = [*self.options_panel.text_editor.GetValue().replace(" ", "").replace("\n", "")] + + for font in self.font_list: + if filter_glyph and glyphs and not set(glyphs).issubset(font.available_glyphs): + continue + + if filter_category != -1: + category = FONT_CATEGORIES[filter_category].id + if category not in font.keywords: + continue + + if filter_size != 0 and (filter_size < font.size * font.min_scale or filter_size > font.size * font.max_scale): + continue + + self.fonts[font.marked_custom_font_name] = font + self.fonts_by_id[font.marked_custom_font_id] = font + + def set_font_list(self): + self.options_panel.font_chooser.Clear() + for font in self.fonts.values(): + image = font.preview_image + + if image is not None: + image = wx.Image(image) + """ + # I would like to do this but Windows requires all images to be the exact same size + # It might work with an updated wxpython version - so let's keep it here + + # Scale to max 20 height + img_height = 20 + width, height = image.GetSize() + scale_factor = height / img_height + width = int(width / scale_factor) + image.Rescale(width, img_height, quality=wx.IMAGE_QUALITY_HIGH) + """ + # Windows requires all images to have the exact same size + image.Rescale(300, 20, quality=wx.IMAGE_QUALITY_HIGH) + self.options_panel.font_chooser.Append(font.marked_custom_font_name, wx.Bitmap(image)) + else: + self.options_panel.font_chooser.Append(font.marked_custom_font_name) + + def get_font_descriptions(self): + return {font.name: font.description for font in self.fonts.values()} + + def set_initial_font(self, font_id): + if font_id: + if font_id not in self.fonts_by_id: + message = '''This text was created using the font "%s", but Ink/Stitch can't find that font. ''' \ + '''A default font will be substituted.''' + info_dialog(self, _(message) % font_id) + try: + font = self.fonts_by_id[font_id].marked_custom_font_name + except KeyError: + font = self.default_font.marked_custom_font_name + self.options_panel.font_chooser.SetValue(font) + + self.on_font_changed() + + @property + def default_font(self): + try: + return self.fonts_by_id[self.DEFAULT_FONT] + except KeyError: + return list(self.fonts.values())[0] + + def on_change(self, attribute, event): + self.settings[attribute] = event.GetEventObject().GetValue() + if attribute == "text" and self.options_panel.font_glyph_filter.GetValue() is True: + self.on_filter_changed() + self.preview_renderer.update() + + def on_trim_option_change(self, event=None): + self.settings.trim_option = self.options_panel.trim_option_choice.GetCurrentSelection() + self.preview_renderer.update() + + def on_font_changed(self, event=None): + font = self.fonts.get(self.options_panel.font_chooser.GetValue(), self.default_font) + self.settings.font = font.marked_custom_font_id + + filter_size = self.options_panel.font_size_filter.GetValue() + self.options_panel.scale_spinner.SetRange(int(font.min_scale * 100), int(font.max_scale * 100)) + if filter_size != 0: + self.options_panel.scale_spinner.SetValue(int(filter_size / font.size * 100)) + self.settings['scale'] = self.options_panel.scale_spinner.GetValue() + + font_variants = [] + try: + font_variants = font.has_variants() + except FontError: + pass + + # Update font description + color = wx.NullColour + description = font.description + if len(font_variants) == 0: + color = (255, 0, 0) + description = _('This font has no available font variant. Please update or remove the font.') + self.options_panel.font_description.SetLabel(description) + self.options_panel.font_description.SetForegroundColour(color) + self.options_panel.font_description.Wrap(self.options_panel.GetSize().width - 50) + + if font.reversible: + self.options_panel.back_and_forth_checkbox.Enable() + self.options_panel.back_and_forth_checkbox.SetValue(bool(self.settings.back_and_forth)) + else: + # The creator of the font banned the possibility of writing in reverse with json file: "reversible": false + self.options_panel.back_and_forth_checkbox.Disable() + self.options_panel.back_and_forth_checkbox.SetValue(False) + + self.update_preview() + self.Layout() + + def on_filter_changed(self, event=None): + self.update_font_list() + + if not self.fonts: + # No fonts for filtered size + self.options_panel.font_chooser.Clear() + self.options_panel.filter_box.SetForegroundColour("red") + return + else: + self.options_panel.filter_box.SetForegroundColour(wx.NullColour) + + filter_size = self.options_panel.font_size_filter.GetValue() + previous_font = self.options_panel.font_chooser.GetValue() + self.set_font_list() + font = self.fonts.get(previous_font, self.default_font) + self.options_panel.font_chooser.SetValue(font.marked_custom_font_name) + if font.marked_custom_font_name != previous_font: + self.on_font_changed() + elif filter_size != 0: + self.options_panel.scale_spinner.SetValue(int(filter_size / font.size * 100)) + self.settings['scale'] = self.options_panel.scale_spinner.GetValue() + + def resize(self, event=None): + description = self.options_panel.font_description.GetLabel().replace("\n", " ") + self.options_panel.font_description.SetLabel(description) + self.options_panel.font_description.Wrap(self.options_panel.GetSize().width - 50) + self.Layout() + + def update_preview(self, event=None): + 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) + if not self.options_panel.font_chooser.GetValue(): + return + + del self.group[:] + + if self.settings.scale == 100: + destination_group = self.group + else: + destination_group = inkex.Group(attrib={ + # L10N The user has chosen to scale the text by some percentage + # (50%, 200%, etc). If you need to use the percentage symbol, + # make sure to double it (%%). + INKSCAPE_LABEL: _("Text scale %s%%") % self.settings.scale + }) + self.group.append(destination_group) + + font = self.fonts.get(self.options_panel.font_chooser.GetValue(), self.default_font) + try: + font.render_text(self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth, + trim_option=self.settings.trim_option, use_trim_symbols=self.settings.use_trim_symbols) + + except FontError as e: + if raise_error: + inkex.errormsg(_("Error: Text cannot be applied to the document.\n%s") % e) + return + else: + pass + + # destination_group isn't always the text scaling group (but also the parent group) + # the text scaling group label is dependend on the user language, so it would break in international file exchange if we used it + # scaling (correction transform) on the parent group is already applied, so let's use that for recognition + if self.settings.scale != 100 and not destination_group.get('transform', None): + destination_group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0) + + 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: + check_stop_flag() + + stitch_groups.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: + raise + except Exception: + raise + # Ignore errors. This can be things like incorrect paths for + # satins or division by zero caused by incorrect param values. + pass + + 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 + settings = dict(self.settings) + del settings["text"] + return settings + + def apply_preset_data(self, preset_data): + settings = DotDict(preset_data) + settings["text"] = self.settings.text + self.settings = settings + self.apply_settings() + + def get_preset_suite_name(self): + # called by self.presets_panel + return "lettering" + + def apply(self, event): + self.update_lettering(True) + self.save_settings() + self.close() + + def close(self): + self.simulator.stop() + wx.CallAfter(self.GetTopLevelParent().close) + + def cancel(self, event): + self.simulator.stop() + wx.CallAfter(self.GetTopLevelParent().cancel) diff --git a/lib/gui/lettering/option_panel.py b/lib/gui/lettering/option_panel.py new file mode 100644 index 00000000..ab123f83 --- /dev/null +++ b/lib/gui/lettering/option_panel.py @@ -0,0 +1,105 @@ +import wx + +from ...i18n import _ +from ...lettering.categories import FONT_CATEGORIES, FontCategory + + +class LetteringOptionsPanel(wx.Panel): + def __init__(self, parent, panel): + self.panel = panel + wx.Panel.__init__(self, parent) + + outer_sizer = wx.BoxSizer(wx.VERTICAL) + + # font selection + self.font_chooser = wx.adv.BitmapComboBox(self, wx.ID_ANY, style=wx.CB_READONLY | wx.CB_SORT) + self.font_chooser.Bind(wx.EVT_COMBOBOX, self.panel.on_font_changed) + + self.font_description = wx.StaticText(self, wx.ID_ANY) + self.panel.Bind(wx.EVT_SIZE, self.panel.resize) + + self.font_selector_box = wx.StaticBox(self, wx.ID_ANY, label=_("Font")) + font_selector_sizer = wx.StaticBoxSizer(self.font_selector_box, wx.VERTICAL) + font_selector_box = wx.BoxSizer(wx.HORIZONTAL) + font_selector_box.Add(self.font_chooser, 4, wx.EXPAND | wx.TOP | wx.BOTTOM | wx.RIGHT, 10) + font_selector_sizer.Add(font_selector_box, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + font_selector_sizer.Add(self.font_description, 1, wx.EXPAND | wx.ALL, 10) + outer_sizer.Add(font_selector_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + + # filter font list + self.font_size_filter = wx.SpinCtrlDouble(self, min=0, max=100, inc=0.1, initial=0, style=wx.SP_WRAP) + self.font_size_filter.SetDigits(2) + self.font_size_filter.Bind(wx.EVT_SPINCTRLDOUBLE, self.panel.on_filter_changed) + self.font_size_filter.SetToolTip(_("Font size filter (mm). 0 for all sizes.")) + + self.font_glyph_filter = wx.CheckBox(self, label=_("Glyphs")) + self.font_glyph_filter.Bind(wx.EVT_CHECKBOX, self.panel.on_filter_changed) + self.font_glyph_filter.SetToolTip(_("Filter fonts by available glyphs.")) + + self.font_category_filter = wx.ComboBox(self, wx.ID_ANY, choices=[], style=wx.CB_DROPDOWN | wx.CB_READONLY) + unfiltered = FontCategory('unfiltered', "---") + self.font_category_filter.Append(unfiltered.name, unfiltered) + for category in FONT_CATEGORIES: + self.font_category_filter.Append(category.name, category) + self.font_category_filter.SetToolTip(_("Filter fonts by category.")) + self.font_category_filter.SetSelection(0) + self.font_category_filter.Bind(wx.EVT_COMBOBOX, self.panel.on_filter_changed) + + self.filter_box = wx.StaticBox(self, wx.ID_ANY, label=_("Font Filter")) + filter_sizer = wx.StaticBoxSizer(self.filter_box, wx.HORIZONTAL) + filter_size_label = wx.StaticText(self, wx.ID_ANY, _("Size")) + filter_sizer.Add(filter_size_label, 0, wx.LEFT | wx.TOP | wx.BOTTOM, 10) + filter_sizer.AddSpacer(5) + filter_sizer.Add(self.font_size_filter, 1, wx.RIGHT | wx.TOP | wx.BOTTOM, 10) + filter_sizer.AddSpacer(5) + filter_sizer.Add(self.font_glyph_filter, 1, wx.RIGHT | wx.TOP | wx.BOTTOM, 10) + filter_sizer.Add(self.font_category_filter, 1, wx.RIGHT | wx.TOP | wx.BOTTOM, 10) + outer_sizer.Add(filter_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + + # options + self.options_box = wx.StaticBox(self, wx.ID_ANY, label=_("Options")) + + self.scale_spinner = wx.SpinCtrl(self, wx.ID_ANY, min=0, max=1000, initial=100) + self.scale_spinner.Bind(wx.EVT_SPINCTRL, lambda event: self.panel.on_change("scale", event)) + + self.back_and_forth_checkbox = wx.CheckBox(self, label=_("Stitch lines of text back and forth")) + self.back_and_forth_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.panel.on_change("back_and_forth", event)) + + self.trim_option_choice = wx.Choice(self, choices=[_("Never"), _("after each line"), _("after each word"), _("after each letter")], + name=_("Add trim command")) + self.trim_option_choice.Bind(wx.EVT_CHOICE, lambda event: self.panel.on_trim_option_change(event)) + + self.use_trim_symbols = wx.CheckBox(self, label=_("Use command symbols")) + self.use_trim_symbols.Bind(wx.EVT_CHECKBOX, lambda event: self.panel.on_change("use_trim_symbols", event)) + self.use_trim_symbols.SetToolTip(_('Uses command symbols if enabled. When disabled inserts trim commands as params.')) + + left_option_sizer = wx.BoxSizer(wx.VERTICAL) + left_option_sizer.Add(self.back_and_forth_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 5) + + trim_option_sizer = wx.BoxSizer(wx.HORIZONTAL) + trim_option_sizer.Add(wx.StaticText(self, wx.ID_ANY, _("Add trims")), 0, wx.LEFT | wx.ALIGN_TOP, 5) + trim_option_sizer.Add(self.trim_option_choice, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5) + trim_option_sizer.Add(self.use_trim_symbols, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5) + left_option_sizer.Add(trim_option_sizer, 0, wx.ALIGN_LEFT, 5) + + font_scale_sizer = wx.BoxSizer(wx.HORIZONTAL) + font_scale_sizer.Add(wx.StaticText(self, wx.ID_ANY, _("Scale")), 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 0) + font_scale_sizer.Add(self.scale_spinner, 0, wx.LEFT, 10) + font_scale_sizer.Add(wx.StaticText(self, wx.ID_ANY, "%"), 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 3) + + options_sizer = wx.StaticBoxSizer(self.options_box, wx.HORIZONTAL) + options_sizer.Add(left_option_sizer, 1, wx.EXPAND, 10) + options_sizer.Add(font_scale_sizer, 0, wx.RIGHT, 10) + outer_sizer.Add(options_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + + # text input + self.text_input_box = wx.StaticBox(self, wx.ID_ANY, label=_("Text")) + self.text_editor = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_DONTWRAP) + self.text_editor.Bind(wx.EVT_TEXT, lambda event: self.panel.on_change("text", event)) + + text_input_sizer = wx.StaticBoxSizer(self.text_input_box, wx.VERTICAL) + text_input_sizer.Add(self.text_editor, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10) + outer_sizer.Add(text_input_sizer, 2, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + + # set panel sizer + self.SetSizer(outer_sizer) diff --git a/lib/gui/satin_multicolor/__init__.py b/lib/gui/satin_multicolor/__init__.py new file mode 100644 index 00000000..84406f22 --- /dev/null +++ b/lib/gui/satin_multicolor/__init__.py @@ -0,0 +1,8 @@ +# Authors: see git history +# +# Copyright (c) 2024 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +from .colorize_panel import ColorizePanel +from .help_panel import HelpPanel +from .main_panel import MultiColorSatinPanel diff --git a/lib/gui/satin_multicolor/colorize.py b/lib/gui/satin_multicolor/colorize.py deleted file mode 100644 index 87243145..00000000 --- a/lib/gui/satin_multicolor/colorize.py +++ /dev/null @@ -1,273 +0,0 @@ -# Authors: see git history -# -# Copyright (c) 2023 Authors -# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. - -from time import time - -import wx -from wx.lib.scrolledpanel import ScrolledPanel - -from ...i18n import _ - - -class ColorizePanel(ScrolledPanel): - - def __init__(self, parent, panel): - self.panel = panel - ScrolledPanel.__init__(self, parent) - - self.colorize_sizer = wx.BoxSizer(wx.VERTICAL) - general_settings_sizer = wx.FlexGridSizer(8, 2, 10, 20) - color_header_sizer = wx.BoxSizer(wx.HORIZONTAL) - self.color_sizer = wx.BoxSizer(wx.VERTICAL) - - # general settings - general_settings_headline = wx.StaticText(self, label=_("General Settings")) - general_settings_headline.SetFont(wx.Font().Bold()) - - equististance_label = wx.StaticText(self, label=_("Equidistant colors")) - equististance_label.SetToolTip(_("Whether colors should be equidistant or have varying widths.")) - self.equististance = wx.CheckBox(self) - self.equististance.SetValue(True) - self.equististance.Bind(wx.EVT_CHECKBOX, self._on_update_equidistance) - - self.monochrome_width_label = wx.StaticText(self, label=_("Monochrome color width")) - self.monochrome_width_label.SetToolTip(_("Adapt color width here when equidistance is enabled.")) - self.monochrome_width = wx.SpinCtrlDouble(self, min=0, max=100, initial=100, inc=1, style=wx.SP_WRAP) - self.monochrome_width.SetDigits(2) - self.monochrome_width.Bind(wx.EVT_SPINCTRLDOUBLE, self._on_update_monochrome_width) - - overflow_left_label = wx.StaticText(self, label=_("Overflow left")) - self.overflow_left = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, inc=0.1, style=wx.SP_WRAP) - self.overflow_left.SetDigits(2) - self.overflow_left.Bind(wx.EVT_SPINCTRLDOUBLE, self._update) - - overflow_right_label = wx.StaticText(self, label=_("Overflow right")) - self.overflow_right = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, inc=0.1, style=wx.SP_WRAP) - self.overflow_right.SetDigits(2) - self.overflow_right.Bind(wx.EVT_SPINCTRLDOUBLE, self._update) - - pull_compensation_label = wx.StaticText(self, label=_("Pull compensation (mm)")) - self.pull_compensation = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, inc=0.1, style=wx.SP_WRAP) - self.pull_compensation.SetDigits(2) - self.pull_compensation.Bind(wx.EVT_SPINCTRLDOUBLE, self._update) - - seed_label = wx.StaticText(self, label=_("Random seed")) - self.seed = wx.TextCtrl(self) - self.seed.SetValue(str(time())) - self.seed.Bind(wx.EVT_TEXT, self._update) - - # embroidery settings - keep_original_label = wx.StaticText(self, label=_("Keep original satin")) - self.keep_original = wx.CheckBox(self) - self.keep_original.SetValue(True) - - # Colors - color_settings_headline = wx.StaticText(self, label=_("Colors")) - color_settings_headline.SetFont(wx.Font().Bold()) - - self.total_width = wx.StaticText(self) - self.total_width.SetToolTip(_("Overflow excluded")) - - self.add_color_button = wx.Button(self, label=_("Add")) - self.add_color_button.Bind(wx.EVT_BUTTON, self._add_color_event) - - # Add to sizers - general_settings_sizer.Add(equististance_label, 0, wx.ALL, 0) - general_settings_sizer.Add(self.equististance, 0, wx.ALL | wx.EXPAND, 0) - general_settings_sizer.Add(self.monochrome_width_label, 0, wx.LEFT, 30) - general_settings_sizer.Add(self.monochrome_width, 0, wx.ALL | wx.EXPAND, 0) - general_settings_sizer.Add(overflow_left_label, 0, wx.ALL, 0) - general_settings_sizer.Add(self.overflow_left, 0, wx.ALL | wx.EXPAND, 0) - general_settings_sizer.Add(overflow_right_label, 0, wx.ALL, 0) - general_settings_sizer.Add(self.overflow_right, 0, wx.ALL | wx.EXPAND, 0) - general_settings_sizer.Add(pull_compensation_label, 0, wx.ALL, 0) - general_settings_sizer.Add(self.pull_compensation, 0, wx.ALL | wx.EXPAND, 0) - general_settings_sizer.Add(seed_label, 0, wx.ALL, 0) - general_settings_sizer.Add(self.seed, 0, wx.ALL | wx.EXPAND, 0) - general_settings_sizer.Add(keep_original_label, 0, wx.TOP, 30) - general_settings_sizer.Add(self.keep_original, 0, wx.TOP | wx.EXPAND, 30) - general_settings_sizer.AddGrowableCol(1) - - color_header_sizer.Add(color_settings_headline, 0, wx.ALL, 10) - color_header_sizer.Add((0, 0), 1, wx.ALL | wx.EXPAND, 10) - color_header_sizer.Add(self.total_width, 0, wx.ALL, 10) - - self.colorize_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10) - self.colorize_sizer.Add(general_settings_headline, 0, wx.ALL, 10) - self.colorize_sizer.Add(general_settings_sizer, 0, wx.ALL | wx.EXPAND, 20) - self.colorize_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10) - self.colorize_sizer.Add(color_header_sizer, 0, wx.EXPAND | wx.ALL, 10) - self.colorize_sizer.Add(self.color_sizer, 0, wx.EXPAND | wx.ALL, 10) - self.colorize_sizer.Add(self.add_color_button, 0, wx.ALIGN_RIGHT | wx.ALL, 10) - - self.SetSizer(self.colorize_sizer) - - def _on_update_monochrome_width(self, event): - equidistance = self.equististance.GetValue() - if not equidistance: - return - width = self.monochrome_width.GetValue() - num_colors = len(self.color_sizer.GetChildren()) - margin = (100 - width * num_colors) / max(num_colors - 1, 1) - self._set_widget_width_value(width, margin) - self._update() - - def _add_color_event(self, event): - self.add_color() - - def add_color(self, color='black'): - colorsizer = wx.BoxSizer(wx.HORIZONTAL) - - position = wx.Button(self, label='↑', style=wx.BU_EXACTFIT) - position.SetToolTip(_("Click to move color up.")) - position.Bind(wx.EVT_BUTTON, self._move_color_up) - - colorpicker = wx.ColourPickerCtrl(self, colour=wx.Colour(color)) - colorpicker.SetToolTip(_("Select color")) - colorpicker.Bind(wx.EVT_COLOURPICKER_CHANGED, self._update) - - color_width = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, style=wx.SP_WRAP) - color_width.SetDigits(2) - color_width.SetToolTip(_("Monochrome width. Can be changed individually when equidistance is disabled.")) - color_width.Bind(wx.EVT_SPINCTRLDOUBLE, self._update) - - color_margin_right = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, style=wx.SP_WRAP) - color_margin_right.SetDigits(2) - color_margin_right.SetToolTip(_("Margin right (bicolor section). Can be changed individually when equidistance is disabled.")) - color_margin_right.Bind(wx.EVT_SPINCTRLDOUBLE, self._update) - - remove_button = wx.Button(self, label='X') - remove_button.SetToolTip(_("Remove color")) - remove_button.Bind(wx.EVT_BUTTON, self._remove_color) - - colorsizer.Add(position, 0, wx.CENTER | wx.RIGHT | wx.TOP | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, 5) - colorsizer.Add(colorpicker, 0, wx.RIGHT | wx.TOP, 5) - colorsizer.Add(color_width, 1, wx.RIGHT | wx.TOP, 5) - colorsizer.Add(color_margin_right, 1, wx.RIGHT | wx.TOP | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, 5) - colorsizer.Add(remove_button, 0, wx.CENTER | wx.TOP, 5) - - self.color_sizer.Add(colorsizer, 0, wx.EXPAND | wx.ALL, 10) - - if self.equististance.GetValue(): - color_margin_right.Enable(False) - color_width.Enable(False) - else: - color_margin_right.Enable(True) - color_width.Enable(True) - - self._update_colors() - - color_margin_right.Show(False) - if len(self.color_sizer.GetChildren()) > 1: - self.color_sizer.GetChildren()[-2].GetSizer().GetChildren()[3].GetWindow().Show() - - self._update() - - self.FitInside() - self.Layout() - - def _move_color_up(self, event): - color = event.GetEventObject() - sizer = color.GetContainingSizer() - main_sizer = self.color_sizer - for i, item in enumerate(main_sizer.GetChildren()): - if item.GetSizer() == sizer: - index = i - break - if index == len(main_sizer.GetChildren()) - 1: - last_sizer = main_sizer.GetChildren()[-2].GetSizer().GetChildren() - last_sizer[2].GetWindow().Show(False) - sizer.GetChildren()[2].GetWindow().Show() - index = max(0, (index - 1)) - if index == 0: - previous_first = main_sizer.GetChildren()[0].GetSizer().GetChildren() - previous_first[0].GetWindow().Show() - sizer.GetChildren()[0].GetWindow().Show(False) - - main_sizer.Detach(sizer) - main_sizer.Insert(index, sizer, 0, wx.EXPAND | wx.ALL, 10) - self.FitInside() - self._update() - self.Layout() - - def _remove_color(self, event): - sizer = event.GetEventObject().GetContainingSizer() - sizer.Clear(True) - self.color_sizer.Remove(sizer) - self.FitInside() - - self._update_colors() - self._update() - - def _on_update_equidistance(self, event=None): - if self.equististance.GetValue(): - self.monochrome_width_label.Enable(True) - self.monochrome_width.Enable(True) - self._set_widget_status(False) - self._update_colors() - else: - self.monochrome_width_label.Enable(False) - self.monochrome_width.Enable(False) - self._set_widget_status(True) - self._update() - - def _set_widget_status(self, status): - for color in self.color_sizer.GetChildren(): - inner_sizer = color.GetSizer() - for color_widget in inner_sizer: - widget = color_widget.GetWindow() - if isinstance(widget, wx.SpinCtrlDouble): - widget.Enable(status) - - def _set_widget_width_value(self, value, margin=0): - first = True - for color in self.color_sizer.GetChildren(): - inner_sizer = color.GetSizer() - for color_widget in inner_sizer: - widget = color_widget.GetWindow() - if first and widget.Label == "↑": - inner_sizer.Hide(widget) - first = False - if isinstance(widget, wx.SpinCtrlDouble): - widget.SetValue(value) - widget.GetNextSibling().SetValue(margin) - break - - def get_total_width(self): - width = 0 - colors = self.color_sizer.GetChildren() - for color in colors: - inner_sizer = color.GetSizer() - for color_widget in inner_sizer: - widget = color_widget.GetWindow() - if isinstance(widget, wx.SpinCtrlDouble): - width += widget.GetValue() - last_margin = inner_sizer.GetChildren()[3].GetWindow().GetValue() - width -= last_margin - return round(width, 2) - - def _update(self, event=None): - width = self.get_total_width() - self.total_width.SetLabel(_("Total width: {width}%").format(width=width)) - if width > 100: - self.total_width.SetForegroundColour("red") - else: - self.total_width.SetForegroundColour(wx.NullColour) - self.panel.update_preview() - - def _update_colors(self): - equidistance = self.equististance.GetValue() - num_colors = len(self.color_sizer.GetChildren()) - if equidistance: - max_width = 100 / max(1, num_colors) - monochrome_value = self.monochrome_width.GetValue() - if monochrome_value > max_width: - self._set_widget_width_value(max_width) - else: - margin = (100 - monochrome_value * num_colors) / max(1, num_colors - 1) - self._set_widget_width_value(monochrome_value, margin) - self.monochrome_width.SetMax(max_width) - self.Refresh() - self._update() diff --git a/lib/gui/satin_multicolor/colorize_panel.py b/lib/gui/satin_multicolor/colorize_panel.py new file mode 100644 index 00000000..87243145 --- /dev/null +++ b/lib/gui/satin_multicolor/colorize_panel.py @@ -0,0 +1,273 @@ +# Authors: see git history +# +# Copyright (c) 2023 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +from time import time + +import wx +from wx.lib.scrolledpanel import ScrolledPanel + +from ...i18n import _ + + +class ColorizePanel(ScrolledPanel): + + def __init__(self, parent, panel): + self.panel = panel + ScrolledPanel.__init__(self, parent) + + self.colorize_sizer = wx.BoxSizer(wx.VERTICAL) + general_settings_sizer = wx.FlexGridSizer(8, 2, 10, 20) + color_header_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.color_sizer = wx.BoxSizer(wx.VERTICAL) + + # general settings + general_settings_headline = wx.StaticText(self, label=_("General Settings")) + general_settings_headline.SetFont(wx.Font().Bold()) + + equististance_label = wx.StaticText(self, label=_("Equidistant colors")) + equististance_label.SetToolTip(_("Whether colors should be equidistant or have varying widths.")) + self.equististance = wx.CheckBox(self) + self.equististance.SetValue(True) + self.equististance.Bind(wx.EVT_CHECKBOX, self._on_update_equidistance) + + self.monochrome_width_label = wx.StaticText(self, label=_("Monochrome color width")) + self.monochrome_width_label.SetToolTip(_("Adapt color width here when equidistance is enabled.")) + self.monochrome_width = wx.SpinCtrlDouble(self, min=0, max=100, initial=100, inc=1, style=wx.SP_WRAP) + self.monochrome_width.SetDigits(2) + self.monochrome_width.Bind(wx.EVT_SPINCTRLDOUBLE, self._on_update_monochrome_width) + + overflow_left_label = wx.StaticText(self, label=_("Overflow left")) + self.overflow_left = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, inc=0.1, style=wx.SP_WRAP) + self.overflow_left.SetDigits(2) + self.overflow_left.Bind(wx.EVT_SPINCTRLDOUBLE, self._update) + + overflow_right_label = wx.StaticText(self, label=_("Overflow right")) + self.overflow_right = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, inc=0.1, style=wx.SP_WRAP) + self.overflow_right.SetDigits(2) + self.overflow_right.Bind(wx.EVT_SPINCTRLDOUBLE, self._update) + + pull_compensation_label = wx.StaticText(self, label=_("Pull compensation (mm)")) + self.pull_compensation = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, inc=0.1, style=wx.SP_WRAP) + self.pull_compensation.SetDigits(2) + self.pull_compensation.Bind(wx.EVT_SPINCTRLDOUBLE, self._update) + + seed_label = wx.StaticText(self, label=_("Random seed")) + self.seed = wx.TextCtrl(self) + self.seed.SetValue(str(time())) + self.seed.Bind(wx.EVT_TEXT, self._update) + + # embroidery settings + keep_original_label = wx.StaticText(self, label=_("Keep original satin")) + self.keep_original = wx.CheckBox(self) + self.keep_original.SetValue(True) + + # Colors + color_settings_headline = wx.StaticText(self, label=_("Colors")) + color_settings_headline.SetFont(wx.Font().Bold()) + + self.total_width = wx.StaticText(self) + self.total_width.SetToolTip(_("Overflow excluded")) + + self.add_color_button = wx.Button(self, label=_("Add")) + self.add_color_button.Bind(wx.EVT_BUTTON, self._add_color_event) + + # Add to sizers + general_settings_sizer.Add(equististance_label, 0, wx.ALL, 0) + general_settings_sizer.Add(self.equististance, 0, wx.ALL | wx.EXPAND, 0) + general_settings_sizer.Add(self.monochrome_width_label, 0, wx.LEFT, 30) + general_settings_sizer.Add(self.monochrome_width, 0, wx.ALL | wx.EXPAND, 0) + general_settings_sizer.Add(overflow_left_label, 0, wx.ALL, 0) + general_settings_sizer.Add(self.overflow_left, 0, wx.ALL | wx.EXPAND, 0) + general_settings_sizer.Add(overflow_right_label, 0, wx.ALL, 0) + general_settings_sizer.Add(self.overflow_right, 0, wx.ALL | wx.EXPAND, 0) + general_settings_sizer.Add(pull_compensation_label, 0, wx.ALL, 0) + general_settings_sizer.Add(self.pull_compensation, 0, wx.ALL | wx.EXPAND, 0) + general_settings_sizer.Add(seed_label, 0, wx.ALL, 0) + general_settings_sizer.Add(self.seed, 0, wx.ALL | wx.EXPAND, 0) + general_settings_sizer.Add(keep_original_label, 0, wx.TOP, 30) + general_settings_sizer.Add(self.keep_original, 0, wx.TOP | wx.EXPAND, 30) + general_settings_sizer.AddGrowableCol(1) + + color_header_sizer.Add(color_settings_headline, 0, wx.ALL, 10) + color_header_sizer.Add((0, 0), 1, wx.ALL | wx.EXPAND, 10) + color_header_sizer.Add(self.total_width, 0, wx.ALL, 10) + + self.colorize_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10) + self.colorize_sizer.Add(general_settings_headline, 0, wx.ALL, 10) + self.colorize_sizer.Add(general_settings_sizer, 0, wx.ALL | wx.EXPAND, 20) + self.colorize_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10) + self.colorize_sizer.Add(color_header_sizer, 0, wx.EXPAND | wx.ALL, 10) + self.colorize_sizer.Add(self.color_sizer, 0, wx.EXPAND | wx.ALL, 10) + self.colorize_sizer.Add(self.add_color_button, 0, wx.ALIGN_RIGHT | wx.ALL, 10) + + self.SetSizer(self.colorize_sizer) + + def _on_update_monochrome_width(self, event): + equidistance = self.equististance.GetValue() + if not equidistance: + return + width = self.monochrome_width.GetValue() + num_colors = len(self.color_sizer.GetChildren()) + margin = (100 - width * num_colors) / max(num_colors - 1, 1) + self._set_widget_width_value(width, margin) + self._update() + + def _add_color_event(self, event): + self.add_color() + + def add_color(self, color='black'): + colorsizer = wx.BoxSizer(wx.HORIZONTAL) + + position = wx.Button(self, label='↑', style=wx.BU_EXACTFIT) + position.SetToolTip(_("Click to move color up.")) + position.Bind(wx.EVT_BUTTON, self._move_color_up) + + colorpicker = wx.ColourPickerCtrl(self, colour=wx.Colour(color)) + colorpicker.SetToolTip(_("Select color")) + colorpicker.Bind(wx.EVT_COLOURPICKER_CHANGED, self._update) + + color_width = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, style=wx.SP_WRAP) + color_width.SetDigits(2) + color_width.SetToolTip(_("Monochrome width. Can be changed individually when equidistance is disabled.")) + color_width.Bind(wx.EVT_SPINCTRLDOUBLE, self._update) + + color_margin_right = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, style=wx.SP_WRAP) + color_margin_right.SetDigits(2) + color_margin_right.SetToolTip(_("Margin right (bicolor section). Can be changed individually when equidistance is disabled.")) + color_margin_right.Bind(wx.EVT_SPINCTRLDOUBLE, self._update) + + remove_button = wx.Button(self, label='X') + remove_button.SetToolTip(_("Remove color")) + remove_button.Bind(wx.EVT_BUTTON, self._remove_color) + + colorsizer.Add(position, 0, wx.CENTER | wx.RIGHT | wx.TOP | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, 5) + colorsizer.Add(colorpicker, 0, wx.RIGHT | wx.TOP, 5) + colorsizer.Add(color_width, 1, wx.RIGHT | wx.TOP, 5) + colorsizer.Add(color_margin_right, 1, wx.RIGHT | wx.TOP | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, 5) + colorsizer.Add(remove_button, 0, wx.CENTER | wx.TOP, 5) + + self.color_sizer.Add(colorsizer, 0, wx.EXPAND | wx.ALL, 10) + + if self.equististance.GetValue(): + color_margin_right.Enable(False) + color_width.Enable(False) + else: + color_margin_right.Enable(True) + color_width.Enable(True) + + self._update_colors() + + color_margin_right.Show(False) + if len(self.color_sizer.GetChildren()) > 1: + self.color_sizer.GetChildren()[-2].GetSizer().GetChildren()[3].GetWindow().Show() + + self._update() + + self.FitInside() + self.Layout() + + def _move_color_up(self, event): + color = event.GetEventObject() + sizer = color.GetContainingSizer() + main_sizer = self.color_sizer + for i, item in enumerate(main_sizer.GetChildren()): + if item.GetSizer() == sizer: + index = i + break + if index == len(main_sizer.GetChildren()) - 1: + last_sizer = main_sizer.GetChildren()[-2].GetSizer().GetChildren() + last_sizer[2].GetWindow().Show(False) + sizer.GetChildren()[2].GetWindow().Show() + index = max(0, (index - 1)) + if index == 0: + previous_first = main_sizer.GetChildren()[0].GetSizer().GetChildren() + previous_first[0].GetWindow().Show() + sizer.GetChildren()[0].GetWindow().Show(False) + + main_sizer.Detach(sizer) + main_sizer.Insert(index, sizer, 0, wx.EXPAND | wx.ALL, 10) + self.FitInside() + self._update() + self.Layout() + + def _remove_color(self, event): + sizer = event.GetEventObject().GetContainingSizer() + sizer.Clear(True) + self.color_sizer.Remove(sizer) + self.FitInside() + + self._update_colors() + self._update() + + def _on_update_equidistance(self, event=None): + if self.equististance.GetValue(): + self.monochrome_width_label.Enable(True) + self.monochrome_width.Enable(True) + self._set_widget_status(False) + self._update_colors() + else: + self.monochrome_width_label.Enable(False) + self.monochrome_width.Enable(False) + self._set_widget_status(True) + self._update() + + def _set_widget_status(self, status): + for color in self.color_sizer.GetChildren(): + inner_sizer = color.GetSizer() + for color_widget in inner_sizer: + widget = color_widget.GetWindow() + if isinstance(widget, wx.SpinCtrlDouble): + widget.Enable(status) + + def _set_widget_width_value(self, value, margin=0): + first = True + for color in self.color_sizer.GetChildren(): + inner_sizer = color.GetSizer() + for color_widget in inner_sizer: + widget = color_widget.GetWindow() + if first and widget.Label == "↑": + inner_sizer.Hide(widget) + first = False + if isinstance(widget, wx.SpinCtrlDouble): + widget.SetValue(value) + widget.GetNextSibling().SetValue(margin) + break + + def get_total_width(self): + width = 0 + colors = self.color_sizer.GetChildren() + for color in colors: + inner_sizer = color.GetSizer() + for color_widget in inner_sizer: + widget = color_widget.GetWindow() + if isinstance(widget, wx.SpinCtrlDouble): + width += widget.GetValue() + last_margin = inner_sizer.GetChildren()[3].GetWindow().GetValue() + width -= last_margin + return round(width, 2) + + def _update(self, event=None): + width = self.get_total_width() + self.total_width.SetLabel(_("Total width: {width}%").format(width=width)) + if width > 100: + self.total_width.SetForegroundColour("red") + else: + self.total_width.SetForegroundColour(wx.NullColour) + self.panel.update_preview() + + def _update_colors(self): + equidistance = self.equististance.GetValue() + num_colors = len(self.color_sizer.GetChildren()) + if equidistance: + max_width = 100 / max(1, num_colors) + monochrome_value = self.monochrome_width.GetValue() + if monochrome_value > max_width: + self._set_widget_width_value(max_width) + else: + margin = (100 - monochrome_value * num_colors) / max(1, num_colors - 1) + self._set_widget_width_value(monochrome_value, margin) + self.monochrome_width.SetMax(max_width) + self.Refresh() + self._update() diff --git a/lib/gui/satin_multicolor/main_panel.py b/lib/gui/satin_multicolor/main_panel.py index 0ceebb4f..49c819fd 100644 --- a/lib/gui/satin_multicolor/main_panel.py +++ b/lib/gui/satin_multicolor/main_panel.py @@ -15,8 +15,7 @@ from ...i18n import _ from ...stitch_plan import stitch_groups_to_stitch_plan from ...utils.threading import ExitThread, check_stop_flag from .. import PreviewRenderer, WarningPanel -from .colorize import ColorizePanel -from .help_panel import HelpPanel +from . import ColorizePanel, HelpPanel class MultiColorSatinPanel(wx.Panel): -- cgit v1.2.3