diff options
Diffstat (limited to 'lib/extensions')
| -rw-r--r-- | lib/extensions/base.py | 11 | ||||
| -rw-r--r-- | lib/extensions/lettering.py | 236 | ||||
| -rw-r--r-- | lib/extensions/params.py | 280 | ||||
| -rw-r--r-- | lib/extensions/simulate.py | 4 |
4 files changed, 252 insertions, 279 deletions
diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 279ca396..98673541 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -211,16 +211,15 @@ class InkstitchExtension(inkex.Effect): # care that it's unique. That defines a "namespace" of element and # attribute names to disambiguate conflicts with element and # attribute names other XML namespaces. - # - # Updating inkex.NSS here allows us to pass 'inkstitch' into - # inkex.addNS(). - inkex.NSS['inkstitch'] = 'http://inkstitch.org/namespace' # call the superclass's method first inkex.Effect.parse(self) - # This is the only way I could find to add a namespace to an existing - # element tree at the top without getting ugly prefixes like "ns0". + # Add the inkstitch namespace to the SVG. The inkstitch namespace is + # added to inkex.NSS in ../svg/tags.py at import time. + + # The below is the only way I could find to add a namespace to an + # existing element tree at the top without getting ugly prefixes like "ns0". inkex.etree.cleanup_namespaces(self.document, top_nsmap=inkex.NSS, keep_ns_prefixes=inkex.NSS.keys()) diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 0d6629f8..b6d67c0b 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -1,39 +1,233 @@ +# -*- coding: UTF-8 -*- + +from base64 import b64encode, b64decode +import json import os +import sys + +import inkex +import wx +from ..elements import nodes_to_elements +from ..gui import PresetsPanel, SimulatorPreview from ..i18n import _ from ..lettering import Font -from ..svg.tags import SVG_PATH_TAG, SVG_GROUP_TAG, INKSCAPE_LABEL -from ..utils import get_bundled_dir +from ..svg.tags import SVG_PATH_TAG, SVG_GROUP_TAG, INKSCAPE_LABEL, INKSTITCH_LETTERING +from ..utils import get_bundled_dir, DotDict from .commands import CommandsExtension +class LetteringFrame(wx.Frame): + def __init__(self, *args, **kwargs): + # begin wxGlade: MyFrame.__init__ + self.group = kwargs.pop('group') + self.cancel_hook = kwargs.pop('on_cancel', None) + wx.Frame.__init__(self, None, wx.ID_ANY, + _("Ink/Stitch Lettering") + ) + + self.preview = SimulatorPreview(self, target_duration=1) + self.presets_panel = PresetsPanel(self) + + self.load_settings() + + # options + self.options_box = wx.StaticBox(self, wx.ID_ANY, label=_("Options")) + + self.back_and_forth_checkbox = wx.CheckBox(self, label=_("Stitch lines of text back and forth")) + self.back_and_forth_checkbox.SetValue(self.settings.back_and_forth) + self.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("back_and_forth", event)) + + # text editor + self.text_editor_box = wx.StaticBox(self, wx.ID_ANY, label=_("Text")) + + self.font_chooser = wx.ComboBox(self, wx.ID_ANY) + self.update_font_list() + + self.text_editor = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_DONTWRAP, value=self.settings.text) + self.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) + + self.__do_layout() + # end wxGlade + + def load_settings(self): + try: + if INKSTITCH_LETTERING in self.group.attrib: + self.settings = DotDict(json.loads(b64decode(self.group.get(INKSTITCH_LETTERING)))) + return + except (TypeError, ValueError): + pass + + self.settings = DotDict({ + "text": u"", + "back_and_forth": True, + "font": "small_font" + }) + + def save_settings(self): + # We base64 encode the string before storing it in an XML attribute. + # In theory, lxml should properly html-encode the string, using HTML + # entities like as necessary. However, we've found that Inkscape + # incorrectly interpolates the HTML entities upon reading the + # extension's output, rather than leaving them as is. + # + # Details: + # https://bugs.launchpad.net/inkscape/+bug/1804346 + self.group.set(INKSTITCH_LETTERING, b64encode(json.dumps(self.settings))) + + def on_change(self, attribute, event): + self.settings[attribute] = event.GetEventObject().GetValue() + self.preview.update() + + def generate_patches(self, abort_early=None): + patches = [] + + font_path = os.path.join(get_bundled_dir("fonts"), self.settings.font) + font = Font(font_path) + + try: + lines = font.render_text(self.settings.text, back_and_forth=self.settings.back_and_forth) + self.group[:] = lines + elements = nodes_to_elements(self.group.iterdescendants(SVG_PATH_TAG)) + + for element in elements: + if abort_early and abort_early.is_set(): + # cancel; settings were updated and we need to start over + return [] + + patches.extend(element.embroider(None)) + except SystemExit: + 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 + + return patches + + def update_font_list(self): + pass + + def get_preset_data(self): + # called by self.presets_panel + preset = {} + return preset + + def apply_preset_data(self): + # called by self.presets_panel + return + + def get_preset_suite_name(self): + # called by self.presets_panel + return "lettering" + + def apply(self, event): + self.preview.disable() + self.generate_patches() + self.save_settings() + self.close() + + def close(self): + self.preview.close() + self.Destroy() + + def cancel(self, event): + if self.cancel_hook: + self.cancel_hook() + + self.close() + + def __do_layout(self): + outer_sizer = wx.BoxSizer(wx.VERTICAL) + + options_sizer = wx.StaticBoxSizer(self.options_box, wx.VERTICAL) + options_sizer.Add(self.back_and_forth_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 10) + outer_sizer.Add(options_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + + text_editor_sizer = wx.StaticBoxSizer(self.text_editor_box, wx.VERTICAL) + text_editor_sizer.Add(self.font_chooser, 0, wx.ALL, 10) + text_editor_sizer.Add(self.text_editor, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10) + outer_sizer.Add(text_editor_sizer, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + + 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.ALIGN_RIGHT | wx.RIGHT, 10) + buttons_sizer.Add(self.apply_button, 0, wx.ALIGN_RIGHT | 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) + + class Lettering(CommandsExtension): COMMANDS = ["trim"] def __init__(self, *args, **kwargs): + self.cancelled = False CommandsExtension.__init__(self, *args, **kwargs) - self.OptionParser.add_option("-t", "--text") + def cancel(self): + self.cancelled = True - def effect(self): - font_path = os.path.join(get_bundled_dir("fonts"), "small_font") - font = Font(font_path) - self.ensure_current_layer() + def get_or_create_group(self): + if self.selected: + groups = set() + + for node in self.selected.itervalues(): + if node.tag == SVG_GROUP_TAG and INKSTITCH_LETTERING in node.attrib: + groups.add(node) + + for group in node.iterancestors(SVG_GROUP_TAG): + if INKSTITCH_LETTERING in group.attrib: + groups.add(group) - lines = font.render_text(self.options.text.decode('utf-8')) - self.set_labels(lines) - self.current_layer.append(lines) + if len(groups) > 1: + inkex.errormsg(_("Please select only one block of text.")) + sys.exit(1) + elif len(groups) == 0: + inkex.errormsg(_("You've selected objects that were not created by the Lettering extension. " + "Please clear your selection or select different objects before running Lettering again.")) + sys.exit(1) + else: + return list(groups)[0] + else: + self.ensure_current_layer() + return inkex.etree.SubElement(self.current_layer, SVG_GROUP_TAG, { + INKSCAPE_LABEL: _("Ink/Stitch Lettering") + }) + + def effect(self): + app = wx.App() + frame = LetteringFrame(group=self.get_or_create_group(), on_cancel=self.cancel) - def set_labels(self, lines): - path = 1 - for node in lines.iterdescendants(): - if node.tag == SVG_PATH_TAG: - node.set("id", self.uniqueId("lettering")) + # position left, center + current_screen = wx.Display.GetFromPoint(wx.GetMousePosition()) + display = wx.Display(current_screen) + display_size = display.GetClientArea() + frame_size = frame.GetSize() + frame.SetPosition((display_size[0], display_size[3] / 2 - frame_size[1] / 2)) - # L10N Label for an object created by the Lettering extension - node.set(INKSCAPE_LABEL, _("Lettering %d") % path) - path += 1 - elif node.tag == SVG_GROUP_TAG: - node.set("id", self.uniqueId("letteringline")) + frame.Show() + app.MainLoop() - # lettering extension already set the label + if self.cancelled: + # This prevents the superclass from outputting the SVG, because we + # may have modified the DOM. + sys.exit(0) diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 1f3032ca..d3cb154a 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -1,78 +1,20 @@ # -*- coding: UTF-8 -*- +from collections import defaultdict +from copy import copy +from itertools import groupby import os import sys -import json -import traceback -from threading import Thread, Event -from copy import copy + import wx from wx.lib.scrolledpanel import ScrolledPanel -from collections import defaultdict -from itertools import groupby -from .base import InkstitchExtension -from ..i18n import _ -from ..stitch_plan import patches_to_stitch_plan +from ..commands import is_command from ..elements import EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn +from ..gui import PresetsPanel, SimulatorPreview +from ..i18n import _ from ..utils import get_resource_dir -from ..simulator import EmbroiderySimulator -from ..commands import is_command - - -def presets_path(): - try: - import appdirs - config_path = appdirs.user_config_dir('inkstitch') - except ImportError: - config_path = os.path.expanduser('~/.inkstitch') - - if not os.path.exists(config_path): - os.makedirs(config_path) - return os.path.join(config_path, 'presets.json') - - -def load_presets(): - try: - with open(presets_path(), 'r') as presets: - presets = json.load(presets) - return presets - except IOError: - return {} - - -def save_presets(presets): - with open(presets_path(), 'w') as presets_file: - json.dump(presets, presets_file) - - -def load_preset(name): - return load_presets().get(name) - - -def save_preset(name, data): - presets = load_presets() - presets[name] = data - save_presets(presets) - - -def delete_preset(name): - presets = load_presets() - presets.pop(name, None) - save_presets(presets) - - -def confirm_dialog(parent, question, caption='ink/stitch'): - dlg = wx.MessageDialog(parent, question, caption, wx.YES_NO | wx.ICON_QUESTION) - result = dlg.ShowModal() == wx.ID_YES - dlg.Destroy() - return result - - -def info_dialog(parent, message, caption='ink/stitch'): - dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_INFORMATION) - dlg.ShowModal() - dlg.Destroy() +from .base import InkstitchExtension class ParamsTab(ScrolledPanel): @@ -358,7 +300,7 @@ class ParamsTab(ScrolledPanel): self.changed_inputs.add(self.param_inputs[param]) - if self.on_change_hook(): + if self.on_change_hook: self.on_change_hook(self) # end of class SatinPane @@ -376,33 +318,10 @@ class SettingsFrame(wx.Frame): self.tabs = self.tabs_factory(self.notebook) for tab in self.tabs: - tab.on_change(self.update_simulator) - - self.simulate_window = None - self.simulate_thread = None - self.simulate_refresh_needed = Event() - - # used when closing to avoid having the window reopen at the last second - self.disable_simulate_window = False + tab.on_change(self.update_preview) - wx.CallLater(1000, self.update_simulator) - - self.presets_box = wx.StaticBox(self, wx.ID_ANY, label=_("Presets")) - - self.preset_chooser = wx.ComboBox(self, wx.ID_ANY) - self.update_preset_list() - - self.load_preset_button = wx.Button(self, wx.ID_ANY, _("Load")) - self.load_preset_button.Bind(wx.EVT_BUTTON, self.load_preset) - - self.add_preset_button = wx.Button(self, wx.ID_ANY, _("Add")) - self.add_preset_button.Bind(wx.EVT_BUTTON, self.add_preset) - - self.overwrite_preset_button = wx.Button(self, wx.ID_ANY, _("Overwrite")) - self.overwrite_preset_button.Bind(wx.EVT_BUTTON, self.overwrite_preset) - - self.delete_preset_button = wx.Button(self, wx.ID_ANY, _("Delete")) - self.delete_preset_button.Bind(wx.EVT_BUTTON, self.delete_preset) + self.preview = SimulatorPreview(self) + self.presets_panel = PresetsPanel(self) self.cancel_button = wx.Button(self, wx.ID_ANY, _("Cancel")) self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel) @@ -414,83 +333,17 @@ class SettingsFrame(wx.Frame): self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit")) self.apply_button.Bind(wx.EVT_BUTTON, self.apply) - self.__set_properties() + self.notebook.SetMinSize((800, 600)) + self.__do_layout() # end wxGlade - def update_simulator(self, tab=None): - if self.simulate_window: - self.simulate_window.stop() - self.simulate_window.clear() - - if self.disable_simulate_window: - return + def update_preview(self, tab): + self.preview.update() - if not self.simulate_thread or not self.simulate_thread.is_alive(): - self.simulate_thread = Thread(target=self.simulate_worker) - self.simulate_thread.daemon = True - self.simulate_thread.start() + def generate_patches(self, abort_early): + # called by self.preview - self.simulate_refresh_needed.set() - - def simulate_worker(self): - while True: - self.simulate_refresh_needed.wait() - self.simulate_refresh_needed.clear() - self.update_patches() - - def update_patches(self): - patches = self.generate_patches() - - if patches and not self.simulate_refresh_needed.is_set(): - wx.CallAfter(self.refresh_simulator, patches) - - def refresh_simulator(self, patches): - stitch_plan = patches_to_stitch_plan(patches) - if self.simulate_window: - self.simulate_window.stop() - self.simulate_window.load(stitch_plan) - else: - params_rect = self.GetScreenRect() - simulator_pos = params_rect.GetTopRight() - simulator_pos.x += 5 - - current_screen = wx.Display.GetFromPoint(wx.GetMousePosition()) - display = wx.Display(current_screen) - screen_rect = display.GetClientArea() - simulator_pos.y = screen_rect.GetTop() - - width = screen_rect.GetWidth() - params_rect.GetWidth() - height = screen_rect.GetHeight() - - try: - self.simulate_window = EmbroiderySimulator(None, -1, _("Preview"), - simulator_pos, - size=(width, height), - stitch_plan=stitch_plan, - on_close=self.simulate_window_closed, - target_duration=5) - except Exception: - error = traceback.format_exc() - - try: - # a window may have been created, so we need to destroy it - # or the app will never exit - wx.Window.FindWindowByName(_("Preview")).Destroy() - except Exception: - pass - - info_dialog(self, error, _("Internal Error")) - - self.simulate_window.Show() - wx.CallLater(10, self.Raise) - - wx.CallAfter(self.simulate_window.go) - - def simulate_window_closed(self): - self.simulate_window = None - - def generate_patches(self): patches = [] nodes = [] @@ -505,7 +358,7 @@ class SettingsFrame(wx.Frame): try: for node in nodes: - if self.simulate_refresh_needed.is_set(): + if abort_early.is_set(): # cancel; params were updated and we need to start over return [] @@ -523,27 +376,9 @@ class SettingsFrame(wx.Frame): return patches - def update_preset_list(self): - preset_names = load_presets().keys() - preset_names = [preset for preset in preset_names if preset != "__LAST__"] - self.preset_chooser.SetItems(sorted(preset_names)) - - def get_preset_name(self): - preset_name = self.preset_chooser.GetValue().strip() - if preset_name: - return preset_name - else: - info_dialog(self, _("Please enter or select a preset name first."), caption=_('Preset')) - return - - def check_and_load_preset(self, preset_name): - preset = load_preset(preset_name) - if not preset: - info_dialog(self, _('Preset "%s" not found.') % preset_name, caption=_('Preset')) - - return preset - def get_preset_data(self): + # called by self.presets_panel + preset = {} current_tab = self.tabs[self.notebook.GetSelection()] @@ -561,53 +396,13 @@ class SettingsFrame(wx.Frame): return preset - def add_preset(self, event, overwrite=False): - preset_name = self.get_preset_name() - if not preset_name: - return - - if not overwrite and load_preset(preset_name): - info_dialog(self, _('Preset "%s" already exists. Please use another name or press "Overwrite"') % preset_name, caption=_('Preset')) - - save_preset(preset_name, self.get_preset_data()) - self.update_preset_list() - - event.Skip() - - def overwrite_preset(self, event): - self.add_preset(event, overwrite=True) - - def _load_preset(self, preset_name): - preset = self.check_and_load_preset(preset_name) - if not preset: - return + def apply_preset_data(self, preset_data): + # called by self.presets_panel for tab in self.tabs: - tab.load_preset(preset) - - def load_preset(self, event): - preset_name = self.get_preset_name() - if not preset_name: - return + tab.load_preset(preset_data) - self._load_preset(preset_name) - - event.Skip() - - def delete_preset(self, event): - preset_name = self.get_preset_name() - if not preset_name: - return - - preset = self.check_and_load_preset(preset_name) - if not preset: - return - - delete_preset(preset_name) - self.update_preset_list() - self.preset_chooser.SetValue("") - - event.Skip() + self.preview.update() def _apply(self): for tab in self.tabs: @@ -615,19 +410,16 @@ class SettingsFrame(wx.Frame): def apply(self, event): self._apply() - save_preset("__LAST__", self.get_preset_data()) + self.presets_panel.store_preset("__LAST__", self.get_preset_data()) self.close() def use_last(self, event): - self.disable_simulate_window = True - self._load_preset("__LAST__") + self.preview.disable() + self.presets_panel.load_preset("__LAST__") self.apply(event) def close(self): - if self.simulate_window: - self.simulate_window.stop() - self.simulate_window.Close() - + self.preview.close() self.Destroy() def cancel(self, event): @@ -636,27 +428,15 @@ class SettingsFrame(wx.Frame): self.close() - def __set_properties(self): - # begin wxGlade: MyFrame.__set_properties - self.notebook.SetMinSize((800, 600)) - self.preset_chooser.SetSelection(-1) - # end wxGlade - def __do_layout(self): # begin wxGlade: MyFrame.__do_layout sizer_1 = wx.BoxSizer(wx.VERTICAL) # self.sizer_3_staticbox.Lower() - sizer_2 = wx.StaticBoxSizer(self.presets_box, wx.HORIZONTAL) sizer_3 = wx.BoxSizer(wx.HORIZONTAL) for tab in self.tabs: self.notebook.AddPage(tab, tab.name) sizer_1.Add(self.notebook, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) - sizer_2.Add(self.preset_chooser, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) - sizer_2.Add(self.load_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) - sizer_2.Add(self.add_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) - sizer_2.Add(self.overwrite_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) - sizer_2.Add(self.delete_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) - sizer_1.Add(sizer_2, 0, flag=wx.EXPAND | wx.ALL, border=10) + sizer_1.Add(self.presets_panel, 0, flag=wx.EXPAND | wx.ALL, border=10) sizer_3.Add(self.cancel_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 5) sizer_3.Add(self.use_last_button, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.BOTTOM, 5) sizer_3.Add(self.apply_button, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.BOTTOM, 5) diff --git a/lib/extensions/simulate.py b/lib/extensions/simulate.py index f962b206..a4da26d5 100644 --- a/lib/extensions/simulate.py +++ b/lib/extensions/simulate.py @@ -1,6 +1,6 @@ -from .base import InkstitchExtension -from ..simulator import show_simulator +from ..gui import show_simulator from ..stitch_plan import patches_to_stitch_plan +from .base import InkstitchExtension class Simulate(InkstitchExtension): |
