diff options
Diffstat (limited to 'lib')
| -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 | ||||
| -rw-r--r-- | lib/gui/__init__.py | 3 | ||||
| -rw-r--r-- | lib/gui/dialogs.py | 14 | ||||
| -rw-r--r-- | lib/gui/presets.py | 183 | ||||
| -rw-r--r-- | lib/gui/simulator.py (renamed from lib/simulator.py) | 133 | ||||
| -rw-r--r-- | lib/lettering/font.py | 2 | ||||
| -rw-r--r-- | lib/lettering/font_variant.py | 3 | ||||
| -rw-r--r-- | lib/svg/tags.py | 4 | ||||
| -rw-r--r-- | lib/utils/__init__.py | 13 | ||||
| -rw-r--r-- | lib/utils/dotdict.py | 30 |
13 files changed, 624 insertions, 292 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): diff --git a/lib/gui/__init__.py b/lib/gui/__init__.py new file mode 100644 index 00000000..060c3d93 --- /dev/null +++ b/lib/gui/__init__.py @@ -0,0 +1,3 @@ +from dialogs import info_dialog, confirm_dialog +from presets import PresetsPanel +from simulator import EmbroiderySimulator, SimulatorPreview, show_simulator diff --git a/lib/gui/dialogs.py b/lib/gui/dialogs.py new file mode 100644 index 00000000..c09503b3 --- /dev/null +++ b/lib/gui/dialogs.py @@ -0,0 +1,14 @@ +import wx + + +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() diff --git a/lib/gui/presets.py b/lib/gui/presets.py new file mode 100644 index 00000000..e6000718 --- /dev/null +++ b/lib/gui/presets.py @@ -0,0 +1,183 @@ +import json +import os +import re + +import wx + +from ..i18n import _ +from ..utils import cache +from .dialogs import info_dialog + + +class PresetsPanel(wx.Panel): + """A wx.Panel for loading, saving, and applying presets. + + A preset is a named collection of settings. From the perspective of this + class, a preset is an opaque JSON-serializable object. + + The PresetsPanel will handle interaction with the user and inform the + instantiator of events such as a preset being loaded. Presets starting + and ending with "__" will not be shown to the user. This allows for the + instantiator to manage hidden presets such as "__LAST__". + """ + + HIDDEN_PRESET_RE = re.compile('^__.*__$') + + def __init__(self, parent, *args, **kwargs): + """Construct a PresetsPanel. + + The parent is the parent window for this wx.Panel. The parent is + expected to implement the following methods: + + def get_preset_data(self) + returns a JSON object representing the current state as a preset + + def apply_preset_data(self, preset_data): + apply the preset data to the GUI, updating GUI elements as necessary + + def get_preset_suite_name(self): + Return a string used in the presets filename, e.g. "lettering" -> "lettering_presets.json". + If not defined, "presets.json" will be used. + """ + + kwargs.setdefault('style', wx.BORDER_NONE) + wx.Panel.__init__(self, parent, wx.ID_ANY, *args, **kwargs) + self.parent = parent + + 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.preset_chooser.SetSelection(-1) + + self.load_preset_button = wx.Button(self, wx.ID_ANY, _("Load")) + self.load_preset_button.Bind(wx.EVT_BUTTON, self.load_selected_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) + + presets_sizer = wx.StaticBoxSizer(self.presets_box, wx.HORIZONTAL) + presets_sizer.Add(self.preset_chooser, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) + presets_sizer.Add(self.load_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) + presets_sizer.Add(self.add_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) + presets_sizer.Add(self.overwrite_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) + presets_sizer.Add(self.delete_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) + + self.SetSizerAndFit(presets_sizer) + self.Layout() + + @property + @cache + def suite_name(self): + try: + return self.parent.get_preset_suite_name() + "_presets" + except AttributeError: + return "presets" + + @cache + def presets_path(self): + 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, '%s.json' % self.suite_name) + + def _load_presets(self): + try: + with open(self.presets_path(), 'r') as presets: + presets = json.load(presets) + return presets + except IOError: + return {} + + def _save_presets(self, presets): + with open(self.presets_path(), 'w') as presets_file: + json.dump(presets, presets_file) + + def update_preset_list(self): + preset_names = self._load_presets().keys() + preset_names = [preset for preset in preset_names if not self.is_hidden(preset)] + self.preset_chooser.SetItems(sorted(preset_names)) + + def is_hidden(self, preset_name): + return self.HIDDEN_PRESET_RE.match(preset_name) + + 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 = self._load_presets().get(preset_name) + if not preset: + info_dialog(self, _('Preset "%s" not found.') % preset_name, caption=_('Preset')) + + return preset + + def store_preset(self, preset_name, data): + presets = self._load_presets() + presets[preset_name] = data + self._save_presets(presets) + self.update_preset_list() + + def add_preset(self, event, overwrite=False): + preset_name = self.get_preset_name() + if not preset_name: + return + + if not overwrite and preset_name in self._load_presets(): + info_dialog(self, _('Preset "%s" already exists. Please use another name or press "Overwrite"') % preset_name, caption=_('Preset')) + + self.store_preset(self, preset_name, self.parent.get_preset_data()) + + 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 + + self.parent.apply_preset_data(preset) + + def load_selected_preset(self, event): + preset_name = self.get_preset_name() + if not preset_name: + return + + 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 + + presets = self._load_presets() + presets.pop(preset_name, None) + self._save_presets(presets) + + self.update_preset_list() + self.preset_chooser.SetValue("") + + event.Skip() diff --git a/lib/simulator.py b/lib/gui/simulator.py index 34d12887..0eed18c9 100644 --- a/lib/simulator.py +++ b/lib/gui/simulator.py @@ -1,12 +1,20 @@ +from itertools import izip import sys +from threading import Thread, Event +import time +import traceback + import wx from wx.lib.intctrl import IntCtrl -import time -from itertools import izip -from .svg import PIXELS_PER_MM -from .i18n import _ -from .stitch_plan import stitch_plan_from_file +from ..i18n import _ +from ..stitch_plan import stitch_plan_from_file, patches_to_stitch_plan + +from ..svg import PIXELS_PER_MM + + +from .dialogs import info_dialog + # L10N command label at bottom of simulator window COMMAND_NAMES = [_("STITCH"), _("JUMP"), _("TRIM"), _("STOP"), _("COLOR CHANGE")] @@ -636,6 +644,121 @@ class EmbroiderySimulator(wx.Frame): self.simulator_panel.clear() +class SimulatorPreview(Thread): + """Manages a preview simulation and a background thread for generating patches.""" + + def __init__(self, parent, *args, **kwargs): + """Construct a SimulatorPreview. + + The parent is expected to be a wx.Window and also implement the following methods: + + def generate_patches(self, abort_event): + Produce an list of Patch instances. This method will be + invoked in a background thread and it is expected that it may + take awhile. + + If possible, this method should periodically check + abort_event.is_set(), and if True, stop early. The return + value will be ignored in this case. + """ + self.parent = parent + self.target_duration = kwargs.pop('target_duration', 5) + super(SimulatorPreview, self).__init__(*args, **kwargs) + self.daemon = True + + self.simulate_window = None + self.refresh_needed = Event() + + # used when closing to avoid having the window reopen at the last second + self._disabled = False + + wx.CallLater(1000, self.update) + + def disable(self): + self._disabled = True + + def update(self): + """Request an update of the simulator preview with freshly-generated patches.""" + + if self.simulate_window: + self.simulate_window.stop() + self.simulate_window.clear() + + if self._disabled: + return + + if not self.is_alive(): + self.start() + + self.refresh_needed.set() + + def run(self): + while True: + self.refresh_needed.wait() + self.refresh_needed.clear() + self.update_patches() + + def update_patches(self): + patches = self.parent.generate_patches(self.refresh_needed) + + if patches and not self.refresh_needed.is_set(): + stitch_plan = patches_to_stitch_plan(patches) + + # GUI stuff needs to happen in the main thread, so we ask the main + # thread to call refresh_simulator(). + wx.CallAfter(self.refresh_simulator, patches, stitch_plan) + + def refresh_simulator(self, patches, stitch_plan): + if self.simulate_window: + self.simulate_window.stop() + self.simulate_window.load(stitch_plan) + else: + params_rect = self.parent.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=self.target_duration) + 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.parent.Raise) + + wx.CallAfter(self.simulate_window.go) + + def simulate_window_closed(self): + self.simulate_window = None + + def close(self): + self.disable() + if self.simulate_window: + self.simulate_window.stop() + self.simulate_window.Close() + + def show_simulator(stitch_plan): app = wx.App() current_screen = wx.Display.GetFromPoint(wx.GetMousePosition()) diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 4a89df47..9d0389a0 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -106,7 +106,7 @@ class Font(object): position.x = 0 position.y += self.leading - if self.auto_satin: + if self.auto_satin and len(line_group) > 0: self._apply_auto_satin(line_group) return line_group diff --git a/lib/lettering/font_variant.py b/lib/lettering/font_variant.py index 445946e2..d826dca6 100644 --- a/lib/lettering/font_variant.py +++ b/lib/lettering/font_variant.py @@ -53,7 +53,8 @@ class FontVariant(object): def _load_glyphs(self): svg_path = os.path.join(self.path, u"%s.svg" % self.variant) - svg = inkex.etree.parse(svg_path) + with open(svg_path) as svg_file: + svg = inkex.etree.parse(svg_file) glyph_layers = svg.xpath(".//svg:g[starts-with(@inkscape:label, 'GlyphLayer-')]", namespaces=inkex.NSS) for layer in glyph_layers: diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 55352be2..84509f90 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -1,5 +1,8 @@ import inkex +# This is used below and added to the document in ../extensions/base.py. +inkex.NSS['inkstitch'] = 'http://inkstitch.org/namespace' + SVG_PATH_TAG = inkex.addNS('path', 'svg') SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg') @@ -16,5 +19,6 @@ CONNECTOR_TYPE = inkex.addNS('connector-type', 'inkscape') XLINK_HREF = inkex.addNS('href', 'xlink') SODIPODI_NAMEDVIEW = inkex.addNS('namedview', 'sodipodi') SODIPODI_GUIDE = inkex.addNS('guide', 'sodipodi') +INKSTITCH_LETTERING = inkex.addNS('lettering', 'inkstitch') EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG) diff --git a/lib/utils/__init__.py b/lib/utils/__init__.py index a6ae4374..2b39da71 100644 --- a/lib/utils/__init__.py +++ b/lib/utils/__init__.py @@ -1,6 +1,7 @@ -from geometry import * -from cache import cache -from io import * -from inkscape import * -from paths import * -from string import * +from .cache import cache +from .dotdict import DotDict +from .geometry import * +from .inkscape import * +from .io import * +from .paths import * +from .string import * diff --git a/lib/utils/dotdict.py b/lib/utils/dotdict.py new file mode 100644 index 00000000..1ab3a4fe --- /dev/null +++ b/lib/utils/dotdict.py @@ -0,0 +1,30 @@ +class DotDict(dict): + """A dict subclass that allows accessing methods using dot notation. + + adapted from: https://stackoverflow.com/questions/13520421/recursive-dotdict + """ + + def __init__(self, *args, **kwargs): + super(DotDict, self).__init__(*args, **kwargs) + + for k, v in self.iteritems(): + if isinstance(v, dict): + self[k] = DotDict(v) + + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + + def __getattr__(self, name): + if name.startswith('_'): + raise AttributeError("'DotDict' object has no attribute '%s'" % name) + + if name in self: + return self.__getitem__(name) + else: + new_dict = DotDict() + self.__setitem__(name, new_dict) + return new_dict + + def __repr__(self): + super_repr = super(DotDict, self).__repr__() + return "DotDict(%s)" % super_repr |
