kopia lustrzana https://github.com/inkstitch/inkstitch
basic lettering GUI (#351)
rodzic
8389d792ad
commit
1e0280db10
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
from dialogs import info_dialog, confirm_dialog
|
||||
from presets import PresetsPanel
|
||||
from simulator import EmbroiderySimulator, SimulatorPreview, show_simulator
|
|
@ -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()
|
|
@ -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()
|
|
@ -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())
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
|
|
Ładowanie…
Reference in New Issue