basic lettering GUI (#351)

pull/372/head
Lex Neva 2018-12-15 20:21:41 -05:00 zatwierdzone przez GitHub
rodzic 8389d792ad
commit 1e0280db10
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
14 zmienionych plików z 626 dodań i 295 usunięć

Wyświetl plik

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

Wyświetl plik

@ -1,39 +1,233 @@
import os
# -*- 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 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)
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):
font_path = os.path.join(get_bundled_dir("fonts"), "small_font")
font = Font(font_path)
self.ensure_current_layer()
app = wx.App()
frame = LetteringFrame(group=self.get_or_create_group(), on_cancel=self.cancel)
lines = font.render_text(self.options.text.decode('utf-8'))
self.set_labels(lines)
self.current_layer.append(lines)
# 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))
def set_labels(self, lines):
path = 1
for node in lines.iterdescendants():
if node.tag == SVG_PATH_TAG:
node.set("id", self.uniqueId("lettering"))
frame.Show()
app.MainLoop()
# 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"))
# 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)

Wyświetl plik

@ -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 ..elements import EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn
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 ..elements import EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn
from ..gui import PresetsPanel, SimulatorPreview
from ..i18n import _
from ..utils import get_resource_dir
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)
tab.on_change(self.update_preview)
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
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()
def update_preview(self, tab):
self.preview.update()
if self.disable_simulate_window:
return
def generate_patches(self, abort_early):
# called by self.preview
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()
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)
tab.load_preset(preset_data)
def load_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
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)

Wyświetl plik

@ -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):

Wyświetl plik

@ -0,0 +1,3 @@
from dialogs import info_dialog, confirm_dialog
from presets import PresetsPanel
from simulator import EmbroiderySimulator, SimulatorPreview, show_simulator

14
lib/gui/dialogs.py 100644
Wyświetl plik

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

183
lib/gui/presets.py 100644
Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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

Wyświetl plik

@ -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:

Wyświetl plik

@ -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)

Wyświetl plik

@ -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 *

Wyświetl plik

@ -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

Wyświetl plik

@ -4,7 +4,6 @@
<id>org.inkstitch.lettering.{{ locale }}</id>
<dependency type="executable" location="extensions">inkstitch.py</dependency>
<dependency type="executable" location="extensions">inkex.py</dependency>
<param name="text" type="string" _gui-text="{% trans %}Text{% endtrans %}"></param>
<param name="extension" type="string" gui-hidden="true">lettering</param>
<effect>
<object-type>all</object-type>