Commit b039ec78 authored by sumpfralle's avatar sumpfralle

added copy and paste to and from the clipboard


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@1018 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent ba7d4da4
...@@ -32,6 +32,7 @@ Version 0.5 - UNRELEASED ...@@ -32,6 +32,7 @@ Version 0.5 - UNRELEASED
* improved stability of remote processing: disconnected nodes should not cause problems anymore * improved stability of remote processing: disconnected nodes should not cause problems anymore
* automatically distributed support bridges can now be placed at corners or edges * automatically distributed support bridges can now be placed at corners or edges
* visualize "inside" polygons (holes) through partial transparency * visualize "inside" polygons (holes) through partial transparency
* added copy/paste of model to and from the clipboard
Version 0.4.0.1 - 2010-10-24 Version 0.4.0.1 - 2010-10-24
* disabled parallel processing for Windows standalone executable * disabled parallel processing for Windows standalone executable
......
...@@ -13,6 +13,9 @@ ...@@ -13,6 +13,9 @@
</menu> </menu>
<menu action="EditMenu"> <menu action="EditMenu">
<menuitem action="UndoButton"/> <menuitem action="UndoButton"/>
<separator />
<menuitem action="CopyModelToClipboard"/>
<menuitem action="PasteModelFromClipboard"/>
</menu> </menu>
<menu action="SettingsMenu"> <menu action="SettingsMenu">
<menuitem action="LoadTaskSettings"/> <menuitem action="LoadTaskSettings"/>
......
...@@ -9501,6 +9501,7 @@ upon interesting bugs and weird results.</property> ...@@ -9501,6 +9501,7 @@ upon interesting bugs and weird results.</property>
<property name="label">_Touch off and tool change</property> <property name="label">_Touch off and tool change</property>
<property name="short_label">_Touch off</property> <property name="short_label">_Touch off</property>
<property name="stock_id">gtk-help</property> <property name="stock_id">gtk-help</property>
<property name="always_show_image">True</property>
</object> </object>
<object class="GtkImage" id="ExportVisibleToolpathsIcon"> <object class="GtkImage" id="ExportVisibleToolpathsIcon">
<property name="visible">True</property> <property name="visible">True</property>
...@@ -9510,4 +9511,18 @@ upon interesting bugs and weird results.</property> ...@@ -9510,4 +9511,18 @@ upon interesting bugs and weird results.</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="stock">gtk-save-as</property> <property name="stock">gtk-save-as</property>
</object> </object>
<object class="GtkAction" id="CopyModelToClipboard">
<property name="label">_Copy</property>
<property name="short_label">_Copy</property>
<property name="tooltip">Copy model to clipboard</property>
<property name="stock_id">gtk-copy</property>
<property name="always_show_image">True</property>
</object>
<object class="GtkAction" id="PasteModelFromClipboard">
<property name="label">_Paste</property>
<property name="short_label">_Paste</property>
<property name="tooltip">Paste a model from clipboard</property>
<property name="stock_id">gtk-paste</property>
<property name="always_show_image">True</property>
</object>
</interface> </interface>
...@@ -40,7 +40,7 @@ from pycam.Gui.OpenGLTools import ModelViewWindowGL ...@@ -40,7 +40,7 @@ from pycam.Gui.OpenGLTools import ModelViewWindowGL
from pycam.Geometry.Letters import TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER, \ from pycam.Geometry.Letters import TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER, \
TEXT_ALIGN_RIGHT TEXT_ALIGN_RIGHT
import pycam.Geometry.Model import pycam.Geometry.Model
from pycam.Utils import ProgressCounter, check_uri_exists from pycam.Utils import check_uri_exists
from pycam.Toolpath import Bounds from pycam.Toolpath import Bounds
import pycam.Utils.FontCache import pycam.Utils.FontCache
from pycam import VERSION from pycam import VERSION
...@@ -102,6 +102,9 @@ FILTER_CONFIG = (("Config files", "*.conf"),) ...@@ -102,6 +102,9 @@ FILTER_CONFIG = (("Config files", "*.conf"),)
FILTER_EMC_TOOL = (("EMC tool files", "*.tbl"),) FILTER_EMC_TOOL = (("EMC tool files", "*.tbl"),)
CLIPBOARD_TARGETS = { CLIPBOARD_TARGETS = {
"dxf": ("image/vnd.dxf", ),
"ps": ("application/postscript", ),
"stl": ("application/sla", ),
"svg": ("image/x-inkscape-svg", "image/svg+xml"), "svg": ("image/x-inkscape-svg", "image/svg+xml"),
"filename_drag": ("text/uri-list", "text-plain"), "filename_drag": ("text/uri-list", "text-plain"),
} }
...@@ -190,8 +193,8 @@ def get_data_file_location(filename, silent=False): ...@@ -190,8 +193,8 @@ def get_data_file_location(filename, silent=False):
def report_exception(): def report_exception():
log.error("An unexpected exception occoured: please send the " \ log.error("An unexpected exception occoured: please send the " \
+ "text below to the developers of PyCAM. Thanks a lot!\n" \ + "text below to the developers of PyCAM. Thanks a lot!" \
+ traceback.format_exc()) + os.linesep + traceback.format_exc())
def get_filters_from_list(filter_list): def get_filters_from_list(filter_list):
result = [] result = []
...@@ -270,7 +273,6 @@ class ProjectGui: ...@@ -270,7 +273,6 @@ class ProjectGui:
self._progress_cancel_requested = False self._progress_cancel_requested = False
self._last_gtk_events_time = None self._last_gtk_events_time = None
self._undo_states = [] self._undo_states = []
self.clipboard = gtk.clipboard_get()
self._fonts_cache = pycam.Utils.FontCache.FontCache(get_font_dir(), self._fonts_cache = pycam.Utils.FontCache.FontCache(get_font_dir(),
callback=self.update_progress_bar) callback=self.update_progress_bar)
self.gui = gtk.Builder() self.gui = gtk.Builder()
...@@ -319,6 +321,8 @@ class ProjectGui: ...@@ -319,6 +321,8 @@ class ProjectGui:
("ToggleProcessPoolWindow", self.toggle_process_pool_window, None, None), ("ToggleProcessPoolWindow", self.toggle_process_pool_window, None, None),
("ShowFontDialog", self.toggle_font_dialog_window, None, "<Control><Shift>t"), ("ShowFontDialog", self.toggle_font_dialog_window, None, "<Control><Shift>t"),
("UndoButton", self._restore_undo_state, None, "<Control>z"), ("UndoButton", self._restore_undo_state, None, "<Control>z"),
("CopyModelToClipboard", self.copy_model_to_clipboard, None, "<Control>c"),
("PasteModelFromClipboard", self.paste_model_from_clipboard, None, "<Control>v"),
("HelpUserManual", self.show_help, "User_Manual", "F1"), ("HelpUserManual", self.show_help, "User_Manual", "F1"),
("HelpIntroduction", self.show_help, "Introduction", None), ("HelpIntroduction", self.show_help, "Introduction", None),
("HelpSupportedFormats", self.show_help, "SupportedFormats", None), ("HelpSupportedFormats", self.show_help, "SupportedFormats", None),
...@@ -345,6 +349,9 @@ class ProjectGui: ...@@ -345,6 +349,9 @@ class ProjectGui:
action = "toggled" action = "toggled"
else: else:
action = "activate" action = "activate"
if data is None:
item.connect(action, callback)
else:
item.connect(action, callback, data) item.connect(action, callback, data)
if accel_key: if accel_key:
key, mod = gtk.accelerator_parse(accel_key) key, mod = gtk.accelerator_parse(accel_key)
...@@ -355,6 +362,8 @@ class ProjectGui: ...@@ -355,6 +362,8 @@ class ProjectGui:
self.gui.get_object("UndoButton").set_sensitive(False) self.gui.get_object("UndoButton").set_sensitive(False)
# configure drag-n-drop for config files and models # configure drag-n-drop for config files and models
self.configure_drag_and_drop(self.window) self.configure_drag_and_drop(self.window)
self.clipboard = gtk.clipboard_get()
self.clipboard.connect("owner-change", self.update_clipboard_state)
# other events # other events
self.window.connect("destroy", self.destroy) self.window.connect("destroy", self.destroy)
# the settings window # the settings window
...@@ -439,6 +448,7 @@ class ProjectGui: ...@@ -439,6 +448,7 @@ class ProjectGui:
self.task_list = [] self.task_list = []
self.grid_adjustments_x = [] self.grid_adjustments_x = []
self.grid_adjustments_y = [] self.grid_adjustments_y = []
self.font_selector = None
self._last_unit = None self._last_unit = None
self._toolpath_for_grid_data = {} self._toolpath_for_grid_data = {}
# add some dummies - to be implemented later ... # add some dummies - to be implemented later ...
...@@ -1134,6 +1144,7 @@ class ProjectGui: ...@@ -1134,6 +1144,7 @@ class ProjectGui:
self.update_parallel_processes_settings() self.update_parallel_processes_settings()
self.update_model_type_related_controls() self.update_model_type_related_controls()
self.update_toolpath_related_controls() self.update_toolpath_related_controls()
self.update_clipboard_state()
def update_gcode_controls(self, widget=None): def update_gcode_controls(self, widget=None):
# path mode # path mode
...@@ -1190,6 +1201,9 @@ class ProjectGui: ...@@ -1190,6 +1201,9 @@ class ProjectGui:
# disable the lower boundary for contour models # disable the lower boundary for contour models
is_contour = isinstance(self.model, pycam.Geometry.Model.ContourModel) is_contour = isinstance(self.model, pycam.Geometry.Model.ContourModel)
self.gui.get_object("boundary_z_low").set_sensitive(not is_contour) self.gui.get_object("boundary_z_low").set_sensitive(not is_contour)
# copy button
self.gui.get_object("CopyModelToClipboard").set_sensitive(
bool(self.model and self.model.is_export_supported()))
def update_ode_settings(self, widget=None): def update_ode_settings(self, widget=None):
if pycam.Utils.threading.is_multiprocessing_enabled() \ if pycam.Utils.threading.is_multiprocessing_enabled() \
...@@ -1540,8 +1554,9 @@ class ProjectGui: ...@@ -1540,8 +1554,9 @@ class ProjectGui:
if self.number_of_processes.get_value() > 0: if self.number_of_processes.get_value() > 0:
log.warn("Mixed local and remote processes are " + \ log.warn("Mixed local and remote processes are " + \
"currently not available on the Windows platform. " + \ "currently not available on the Windows platform. " + \
"Setting the number of local processes to zero.\n" + \ "Setting the number of local processes to zero." + \
"See <a href=\"" + HELP_WIKI_URL % "Parallel_Processing_on_different_Platforms" + \ os.linesep + "See <a href=\"" + \
HELP_WIKI_URL % "Parallel_Processing_on_different_Platforms" + \
"\">platform feature matrix</a> for more details.") "\">platform feature matrix</a> for more details.")
self.number_of_processes.set_value(0) self.number_of_processes.set_value(0)
self.number_of_processes.set_sensitive(False) self.number_of_processes.set_sensitive(False)
...@@ -1580,7 +1595,8 @@ class ProjectGui: ...@@ -1580,7 +1595,8 @@ class ProjectGui:
location = pycam.Utils.get_external_program_location(key) location = pycam.Utils.get_external_program_location(key)
if not location: if not location:
log.error("Failed to locate the external program '%s'. " % key \ log.error("Failed to locate the external program '%s'. " % key \
+ "Please install the program and try again.\n" \ + "Please install the program and try again." \
+ os.linesep \
+ "Or maybe you need to specify the location manually.") + "Or maybe you need to specify the location manually.")
else: else:
# store the new setting # store the new setting
...@@ -1922,7 +1938,7 @@ class ProjectGui: ...@@ -1922,7 +1938,7 @@ class ProjectGui:
if state is None: if state is None:
state = not self._font_dialog_window_visible state = not self._font_dialog_window_visible
if state: if state:
if not self._fonts_cache.is_loading_complete(): if self.font_selector is None:
self.update_progress_bar("Initializing fonts") self.update_progress_bar("Initializing fonts")
# create it manually to ease access # create it manually to ease access
font_selector = gtk.combo_box_new_text() font_selector = gtk.combo_box_new_text()
...@@ -2005,25 +2021,84 @@ class ProjectGui: ...@@ -2005,25 +2021,84 @@ class ProjectGui:
text = text_buffer.read() text = text_buffer.read()
self._copy_text_to_clipboard(text, CLIPBOARD_TARGETS["svg"]) self._copy_text_to_clipboard(text, CLIPBOARD_TARGETS["svg"])
def update_clipboard_state(self, clipboard=None, event=None):
data, importer = self._get_data_and_importer_from_clipboard()
paste_button = self.gui.get_object("PasteModelFromClipboard")
paste_button.set_sensitive(not data is None)
def _copy_text_to_clipboard(self, text, targets): def _copy_text_to_clipboard(self, text, targets):
targets = [(key, gtk.TARGET_OTHER_WIDGET, index) clip_targets = [(key, gtk.TARGET_OTHER_WIDGET, index)
for index, key in enumerate(targets)] for index, key in enumerate(targets)]
def get_func(clipboard, selectiondata, info, text): def get_func(clipboard, selectiondata, info, (text, clip_type)):
selectiondata.set(clip_type, 8, text)
if "svg" in "".join(targets).lower():
# Inkscape for Windows strictly requires the BITMAP type # Inkscape for Windows strictly requires the BITMAP type
selectiondata.set(gtk.gdk.SELECTION_TYPE_BITMAP, 8, text.read()) clip_type = gtk.gdk.SELECTION_TYPE_BITMAP
result = self.clipboard.set_with_data(targets, get_func, else:
lambda *args: None, text) clip_type = gtk.gdk.SELECTION_TYPE_STRING
result = self.clipboard.set_with_data(clip_targets, get_func,
lambda *args: None, (text, clip_type))
self.clipboard.store() self.clipboard.store()
def copy_model_to_clipboard(self, widget=None):
if not self.model.is_export_supported():
return
text_buffer = StringIO.StringIO()
self.model.export(comment=self.get_meta_data(),
unit=self.settings.get("unit")).write(text_buffer)
text_buffer.seek(0)
is_contour = isinstance(self.model, pycam.Geometry.Model.ContourModel)
# TODO: this should not be decided here
if is_contour:
targets = CLIPBOARD_TARGETS["svg"]
else:
targets = CLIPBOARD_TARGETS["stl"]
self._copy_text_to_clipboard(text_buffer.read(), targets)
def _get_data_and_importer_from_clipboard(self):
for targets, filename in ((CLIPBOARD_TARGETS["svg"], "foo.svg"),
(CLIPBOARD_TARGETS["stl"], "foo.stl"),
(CLIPBOARD_TARGETS["ps"], "foo.ps"),
(CLIPBOARD_TARGETS["dxf"], "foo.dxf")):
for target in targets:
data = self.clipboard.wait_for_contents(target)
if not data is None:
importer = pycam.Importers.detect_file_type(filename)[1]
return data, importer
return None, None
@progress_activity_guard
@gui_activity_guard
def paste_model_from_clipboard(self, widget=None):
data, importer = self._get_data_and_importer_from_clipboard()
if data:
self.update_progress_bar(text="Loading model from clipboard")
text_buffer = StringIO.StringIO(data.data)
model = importer(text_buffer,
program_locations=self._get_program_locations(),
unit=self.settings.get("unit"),
fonts_cache=self._fonts_cache,
callback=self.update_progress_bar)
if model:
log.info("Loaded a model from clipboard")
self.load_model(model)
else:
log.warn("Failed to load a model from clipboard")
else:
log.warn("The clipboard does not contain suitable data")
@gui_activity_guard @gui_activity_guard
def update_font_dialog_preview(self, widget=None, event=None): def update_font_dialog_preview(self, widget=None, event=None):
if not self.font_selector:
# not initialized
return
if len(self._fonts_cache) == 0: if len(self._fonts_cache) == 0:
# empty # empty
return return
font_name = self.font_selector.get_active_text() font_name = self.font_selector.get_active_text()
font = self._fonts_cache.get_font(font_name) font = self._fonts_cache.get_font(font_name)
self.gui.get_object("FontAuthorText").set_label( self.gui.get_object("FontAuthorText").set_label(
"\n".join(font.get_authors())) os.linesep.join(font.get_authors()))
preview_widget = self.gui.get_object("FontDialogPreview") preview_widget = self.gui.get_object("FontDialogPreview")
final_drawing_area = preview_widget.window final_drawing_area = preview_widget.window
text_model = self.get_font_dialog_text_rendered() text_model = self.get_font_dialog_text_rendered()
...@@ -2955,6 +3030,15 @@ class ProjectGui: ...@@ -2955,6 +3030,15 @@ class ProjectGui:
uri = widget.get_current_uri() uri = widget.get_current_uri()
self.load_model_file(filename=uri) self.load_model_file(filename=uri)
def _get_program_locations(self):
# import all external program locations into a dict
program_locations = {}
prefix = "external_program_"
for key in self.settings.get_keys():
if key.startswith(prefix) and self.settings.get(key):
program_locations[key[len(prefix):]] = self.settings.get(key)
return program_locations
@gui_activity_guard @gui_activity_guard
@progress_activity_guard @progress_activity_guard
def load_model_file(self, widget=None, filename=None): def load_model_file(self, widget=None, filename=None):
...@@ -2964,19 +3048,13 @@ class ProjectGui: ...@@ -2964,19 +3048,13 @@ class ProjectGui:
filename = self.get_filename_via_dialog("Loading model ...", filename = self.get_filename_via_dialog("Loading model ...",
mode_load=True, type_filter=FILTER_MODEL) mode_load=True, type_filter=FILTER_MODEL)
if filename: if filename:
# import all external program locations into a dict
program_locations = {}
prefix = "external_program_"
for key in self.settings.get_keys():
if key.startswith(prefix) and self.settings.get(key):
program_locations[key[len(prefix):]] = self.settings.get(key)
file_type, importer = pycam.Importers.detect_file_type(filename) file_type, importer = pycam.Importers.detect_file_type(filename)
if file_type and callable(importer): if file_type and callable(importer):
self.update_progress_bar(text="Loading model file ...") self.update_progress_bar(text="Loading model ...")
# "cancel" is not allowed # "cancel" is not allowed
self.disable_progress_cancel_button() self.disable_progress_cancel_button()
if self.load_model(importer(filename, if self.load_model(importer(filename,
program_locations=program_locations, program_locations=self._get_program_locations(),
unit=self.settings.get("unit"), unit=self.settings.get("unit"),
fonts_cache=self._fonts_cache, fonts_cache=self._fonts_cache,
callback=self.update_progress_bar)): callback=self.update_progress_bar)):
......
...@@ -832,6 +832,9 @@ class DXFParser(object): ...@@ -832,6 +832,9 @@ class DXFParser(object):
def import_model(filename, color_as_height=False, fonts_cache=None, def import_model(filename, color_as_height=False, fonts_cache=None,
callback=None, **kwargs): callback=None, **kwargs):
if hasattr(filename, "read"):
infile = filename
else:
try: try:
infile = open_url(filename) infile = open_url(filename)
except IOError, err_msg: except IOError, err_msg:
......
...@@ -31,12 +31,25 @@ log = pycam.Utils.log.get_logger() ...@@ -31,12 +31,25 @@ log = pycam.Utils.log.get_logger()
def import_model(filename, program_locations=None, unit="mm", callback=None, def import_model(filename, program_locations=None, unit="mm", callback=None,
**kwargs): **kwargs):
local_file = False
if hasattr(filename, "read"):
infile = filename
ps_file_handle, ps_file_name = tempfile.mkstemp(suffix=".ps")
try:
temp_file = os.fdopen(ps_file_handle, "w")
temp_file.write(infile.read())
temp_file.close()
except IOError, err_msg:
log.error("PSImporter: Failed to create temporary local file " + \
"(%s): %s" % (ps_file_name, err_msg))
return
filename = ps_file_name
else:
if not check_uri_exists(filename): if not check_uri_exists(filename):
log.error("PSImporter: file (%s) does not exist" % filename) log.error("PSImporter: file (%s) does not exist" % filename)
return None return None
if not os.path.isfile(filename): if not os.path.isfile(filename):
# non-local file - write it to a temporary file first # non-local file - write it to a temporary file first
local_file = False
uri = filename uri = filename
ps_file_handle, ps_file_name = tempfile.mkstemp(suffix=".ps") ps_file_handle, ps_file_name = tempfile.mkstemp(suffix=".ps")
os.close(ps_file_handle) os.close(ps_file_handle)
...@@ -45,6 +58,7 @@ def import_model(filename, program_locations=None, unit="mm", callback=None, ...@@ -45,6 +58,7 @@ def import_model(filename, program_locations=None, unit="mm", callback=None,
if not retrieve_uri(uri, ps_file_name, callback=callback): if not retrieve_uri(uri, ps_file_name, callback=callback):
log.error("PSImporter: Failed to retrieve the PS model file: " + \ log.error("PSImporter: Failed to retrieve the PS model file: " + \
"%s -> %s" % (uri, ps_file_name)) "%s -> %s" % (uri, ps_file_name))
return
filename = ps_file_name filename = ps_file_name
else: else:
local_file = True local_file = True
......
...@@ -60,10 +60,15 @@ def ImportModel(filename, use_kdtree=True, callback=None, **kwargs): ...@@ -60,10 +60,15 @@ def ImportModel(filename, use_kdtree=True, callback=None, **kwargs):
normal_conflict_warning_seen = False normal_conflict_warning_seen = False
if hasattr(filename, "read"):
f = filename
# useful for later error messages
filename = "input data"
else:
try: try:
url_file = open_url(filename) url_file = open_url(filename)
# urllib.urlopen objects do not support "seek" - so we need to read the # urllib.urlopen objects do not support "seek" - so we need to read
# whole file at once. This is ugly - anyone with a better idea? # the whole file at once. This is ugly - anyone with a better idea?
f = StringIO.StringIO(url_file.read()) f = StringIO.StringIO(url_file.read())
url_file.close() url_file.close()
except IOError, err_msg: except IOError, err_msg:
......
...@@ -83,20 +83,33 @@ def convert_eps2dxf(eps_filename, dxf_filename, location=None, unit="mm"): ...@@ -83,20 +83,33 @@ def convert_eps2dxf(eps_filename, dxf_filename, location=None, unit="mm"):
def import_model(filename, program_locations=None, unit="mm", callback=None, def import_model(filename, program_locations=None, unit="mm", callback=None,
**kwargs): **kwargs):
local_file = False
if hasattr(filename, "read"):
infile = filename
svg_file_handle, svg_file_name = tempfile.mkstemp(suffix=".svg")
try:
temp_file = os.fdopen(svg_file_handle, "w")
temp_file.write(infile.read())
temp_file.close()
except IOError, err_msg:
log.error("SVGImporter: Failed to create temporary local file " + \
"(%s): %s" % (svg_file_name, err_msg))
return
filename = svg_file_name
else:
if not check_uri_exists(filename): if not check_uri_exists(filename):
log.error("SVGImporter: file (%s) does not exist" % filename) log.error("SVGImporter: file (%s) does not exist" % filename)
return None return None
if not os.path.isfile(filename): if not os.path.isfile(filename):
# non-local file - write it to a temporary file first # non-local file - write it to a temporary file first
local_file = False
uri = filename uri = filename
svg_file_handle, svg_file_name = tempfile.mkstemp(suffix=".svg") svg_file_handle, svg_file_name = tempfile.mkstemp(suffix=".svg")
os.close(svg_file_handle) os.close(svg_file_handle)
log.debug("Retrieving SVG file for local access: %s -> %s" % \ log.debug("Retrieving SVG file for local access: %s -> %s" % \
(uri, svg_file_name)) (uri, svg_file_name))
if not retrieve_uri(uri, svg_file_name, callback=callback): if not retrieve_uri(uri, svg_file_name, callback=callback):
log.error("SVGImporter: Failed to retrieve the SVG model file: " + \ log.error("SVGImporter: Failed to retrieve the SVG model " + \
"%s -> %s" % (uri, svg_file_name)) "file: %s -> %s" % (uri, svg_file_name))
filename = svg_file_name filename = svg_file_name
else: else:
local_file = True local_file = True
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment