Commit 2b1bf811 authored by sumpfralle's avatar sumpfralle

moved "Task" tab to a separate plugin

moved some more toolpath related actions to separate plugins


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@1120 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent f148c002
This diff is collapsed.
This diff is collapsed.
...@@ -177,14 +177,10 @@ ...@@ -177,14 +177,10 @@
<property name="upper">1000</property> <property name="upper">1000</property>
<property name="step_increment">1</property> <property name="step_increment">1</property>
</object> </object>
<object class="GtkWindow" id="window1"> <object class="GtkButton" id="ToolpathCropButton">
<child> <property name="label" translatable="yes">Crop</property>
<object class="GtkButton" id="ToolpathCropButton"> <property name="visible">True</property>
<property name="label" translatable="yes">Crop</property> <property name="can_focus">True</property>
<property name="visible">True</property> <property name="receives_default">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
</child>
</object> </object>
</interface> </interface>
<?xml version="1.0"?>
<interface>
<!-- interface-requires gtk+ 2.12 -->
<!-- interface-naming-policy project-wide -->
<object class="GtkButton" id="ExportAllToolpathsButton">
<property name="label" translatable="yes">Export all</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="image">ExportAllToolpathsIcon</property>
</object>
<object class="GtkImage" id="ExportAllToolpathsIcon">
<property name="visible">True</property>
<property name="stock">gtk-save-as</property>
</object>
<object class="GtkButton" id="ExportVisibleToolpathsButton">
<property name="label" translatable="yes">Export visible</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
</interface>
<?xml version="1.0"?>
<interface>
<!-- interface-requires gtk+ 2.12 -->
<!-- interface-naming-policy project-wide -->
<object class="GtkButton" id="ToolpathGrid">
<property name="label">Clone grid</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
</interface>
<?xml version="1.0"?>
<interface>
<!-- interface-requires gtk+ 2.12 -->
<!-- interface-naming-policy project-wide -->
<object class="GtkButton" id="toolpath_simulate">
<property name="label" translatable="yes">Simulate</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
</interface>
This diff is collapsed.
This diff is collapsed.
...@@ -76,6 +76,7 @@ class Bounds(pycam.Plugins.ListPluginBase): ...@@ -76,6 +76,7 @@ class Bounds(pycam.Plugins.ListPluginBase):
if not id(item) in cache: if not id(item) in cache:
cache[id(item)] = [id(item), "Bounds #%d" % index] cache[id(item)] = [id(item), "Bounds #%d" % index]
self._treemodel.append(cache[id(item)]) self._treemodel.append(cache[id(item)])
self.core.emit_event("bounds-list-changed")
self.register_model_update(update_model) self.register_model_update(update_model)
for action, obj_name in ((self.ACTION_UP, "BoundsMoveUp"), for action, obj_name in ((self.ACTION_UP, "BoundsMoveUp"),
(self.ACTION_DOWN, "BoundsMoveDown"), (self.ACTION_DOWN, "BoundsMoveDown"),
...@@ -84,14 +85,7 @@ class Bounds(pycam.Plugins.ListPluginBase): ...@@ -84,14 +85,7 @@ class Bounds(pycam.Plugins.ListPluginBase):
self.gui.get_object(obj_name)) self.gui.get_object(obj_name))
self.gui.get_object("BoundsNew").connect("clicked", self.gui.get_object("BoundsNew").connect("clicked",
self._bounds_new) self._bounds_new)
# Trigger a re-calculation of the bounds values after changing its type. # quickly adjust the bounds via buttons
# TODO: recalculate %/mm
"""
for obj_name in ("TypeRelativeMargin", "TypeCustom"):
self.gui.get_object(obj_name).connect("toggled",
self._store_bounds_settings)
"""
# the boundary manager
for obj_name in ("MarginIncreaseX", "MarginIncreaseY", for obj_name in ("MarginIncreaseX", "MarginIncreaseY",
"MarginIncreaseZ", "MarginDecreaseX", "MarginDecreaseY", "MarginIncreaseZ", "MarginDecreaseX", "MarginDecreaseY",
"MarginDecreaseZ", "MarginResetX", "MarginResetY", "MarginDecreaseZ", "MarginResetX", "MarginResetY",
...@@ -164,7 +158,7 @@ class Bounds(pycam.Plugins.ListPluginBase): ...@@ -164,7 +158,7 @@ class Bounds(pycam.Plugins.ListPluginBase):
def select(self, bounds): def select(self, bounds):
if bounds in self: if bounds in self:
selection = self._boundsview.get_selection() selection = self._boundsview.get_selection()
index = [id(b) for b in self].index(id(b)) index = [id(b) for b in self].index(id(bounds))
selection.unselect_all() selection.unselect_all()
selection.select_path((index,)) selection.select_path((index,))
......
...@@ -77,6 +77,7 @@ class Processes(pycam.Plugins.ListPluginBase): ...@@ -77,6 +77,7 @@ class Processes(pycam.Plugins.ListPluginBase):
if not id(item) in cache: if not id(item) in cache:
cache[id(item)] = [id(item), "Process #%d" % index] cache[id(item)] = [id(item), "Process #%d" % index]
self._treemodel.append(cache[id(item)]) self._treemodel.append(cache[id(item)])
self.core.emit_event("process-list-changed")
self.register_model_update(update_model) self.register_model_update(update_model)
# process settings # process settings
self._detail_handlers = [] self._detail_handlers = []
......
This diff is collapsed.
...@@ -42,13 +42,11 @@ class ToolpathCrop(pycam.Plugins.PluginBase): ...@@ -42,13 +42,11 @@ class ToolpathCrop(pycam.Plugins.PluginBase):
action_button = self.gui.get_object("ToolpathCropButton") action_button = self.gui.get_object("ToolpathCropButton")
action_button.unparent() action_button.unparent()
self.core.register_ui("toolpath_crop", "Crop", action_button, -3) self.core.register_ui("toolpath_crop", "Crop", action_button, -3)
self.core.register_event("model-change-after",
self._update_model_type_controls)
return True return True
def teardown(self): def teardown(self):
if self.gui: if self.gui:
self.core.unregister_ui("toolpath__crop", self.core.unregister_ui("toolpath_crop",
self.gui.get_object("ToolpathCropButton")) self.gui.get_object("ToolpathCropButton"))
def _update_model_type_controls(self): def _update_model_type_controls(self):
......
# -*- coding: utf-8 -*-
"""
$Id$
Copyright 2011 Lars Kruse <devel@sumpfralle.de>
This file is part of PyCAM.
PyCAM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
PyCAM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
"""
import pycam.Plugins
class ToolpathGrid(pycam.Plugins.PluginBase):
UI_FILE = "toolpath_grid.ui"
def update_toolpath_grid_window(self, widget=None):
return False
data = self._toolpath_for_grid_data
x_dim = data["maxx"] - data["minx"]
y_dim = data["maxy"] - data["miny"]
x_count = self.gui.get_object("GridXCount").get_value()
x_space = self.gui.get_object("GridXDistance").get_value()
y_count = self.gui.get_object("GridYCount").get_value()
y_space = self.gui.get_object("GridYDistance").get_value()
x_width = x_dim * x_count + x_space * (x_count - 1)
y_width = y_dim * y_count + y_space * (y_count - 1)
self.gui.get_object("LabelGridXWidth").set_label("%g%s" % \
(x_width, self.settings.get("unit")))
self.gui.get_object("LabelGridYWidth").set_label("%g%s" % \
(y_width, self.settings.get("unit")))
for objname in ("GridYCount", "GridXCount", "GridYDistance",
"GridXDistance"):
self.gui.get_object(objname).connect("value-changed",
self.update_toolpath_grid_window)
def create_toolpath_grid(self, toolpath):
dialog = self.gui.get_object("ToolpathGridDialog")
data = self._toolpath_for_grid_data
data["minx"] = toolpath.minx()
data["maxx"] = toolpath.maxx()
data["miny"] = toolpath.miny()
data["maxy"] = toolpath.maxy()
self.gui.get_object("GridXCount").set_value(1)
self.gui.get_object("GridYCount").set_value(1)
self.update_toolpath_grid_window()
result = dialog.run()
if result == 1:
# "OK" was pressed
new_tp = []
x_count = int(self.gui.get_object("GridXCount").get_value())
y_count = int(self.gui.get_object("GridYCount").get_value())
x_space = self.gui.get_object("GridXDistance").get_value()
y_space = self.gui.get_object("GridYDistance").get_value()
x_dim = data["maxx"] - data["minx"]
y_dim = data["maxy"] - data["miny"]
for x in range(x_count):
for y in range(y_count):
shift = Point(x * (x_space + x_dim),
y * (y_space + y_dim), 0)
for path in toolpath.get_paths():
new_path = pycam.Geometry.Path.Path()
new_path.points = [shift.add(p) for p in path.points]
new_tp.append(new_path)
new_toolpath = pycam.Toolpath.Toolpath(new_tp, toolpath.name,
toolpath.toolpath_settings)
toolpath.visible = False
new_toolpath.visible = True
self.toolpath.append(new_toolpath)
self.update_toolpath_table()
dialog.hide()
# -*- coding: utf-8 -*-
"""
$Id$
Copyright 2011 Lars Kruse <devel@sumpfralle.de>
This file is part of PyCAM.
PyCAM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
PyCAM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
"""
import pycam.Plugins
class ToolpathSimulation(pycam.Plugins.PluginBase):
UI_FILE = "toolpath_simulation.ui"
def setup(self):
return False
speed_factor_widget = self.gui.get_object("SimulationSpeedFactor")
self.settings.add_item("simulation_speed_factor",
lambda: pow(10, speed_factor_widget.get_value()),
lambda value: speed_factor_widget.set_value(math.log10(max(0.001, value))))
simulation_progress = self.gui.get_object("SimulationProgressTimelineValue")
def update_simulation_progress(widget):
if widget.get_value() == 100:
# a negative value indicates, that the simulation is finished
self.settings.set("simulation_current_distance", -1)
else:
complete = self.settings.get("simulation_complete_distance")
partial = widget.get_value() / 100.0 * complete
self.settings.set("simulation_current_distance", partial)
simulation_progress.connect("value-changed", update_simulation_progress)
# update the speed factor label
speed_factor_widget.connect("value-changed",
lambda widget: self.gui.get_object("SimulationSpeedFactorValueLabel").set_label(
"%.2f" % self.settings.get("simulation_speed_factor")))
self.simulation_window = self.gui.get_object("SimulationDialog")
self.simulation_window.connect("delete-event", self.finish_toolpath_simulation)
def finish_toolpath_simulation(self, widget=None, data=None):
# hide the simulation tab
self.simulation_window.hide()
# enable all other tabs again
self.toggle_tabs_for_simulation(True)
self.settings.set("simulation_object", None)
self.settings.set("simulation_toolpath_moves", None)
self.settings.set("show_simulation", False)
self.settings.set("simulation_toolpath", None)
self.update_view()
# don't destroy the simulation window (for "destroy" event)
return True
def update_toolpath_simulation(self, widget=None, toolpath=None):
s = self.settings
# update the GUI
while gtk.events_pending():
gtk.main_iteration()
if not s.get("show_simulation"):
# cancel
return False
safety_height = s.get("gcode_safety_height")
if not s.get("simulation_toolpath"):
# get the currently selected toolpath, if none is give
if toolpath is None:
toolpath_index = self._treeview_get_active_index(self.toolpath_table, self.toolpath)
if toolpath_index is None:
return
else:
toolpath = self.toolpath[toolpath_index]
s.set("simulation_toolpath", toolpath)
# set the current cutter
self.cutter = toolpath.toolpath_settings.get_tool()
# calculate steps
s.set("simulation_machine_time",
toolpath.get_machine_time(safety_height=safety_height))
s.set("simulation_complete_distance",
toolpath.get_machine_movement_distance(
safety_height=safety_height))
s.set("simulation_current_distance", 0)
else:
toolpath = s.get("simulation_toolpath")
if (s.get("simulation_current_distance") \
< s.get("simulation_complete_distance")):
if s.get("simulation_current_distance") < 0:
# "-1" -> simulation is finished
updated_distance = s.get("simulation_complete_distance")
else:
time_step = 1.0 / s.get("drill_progress_max_fps")
feedrate = toolpath.toolpath_settings.get_tool_settings(
)["feedrate"]
distance_step = s.get("simulation_speed_factor") * \
time_step * feedrate / 60
updated_distance = min(distance_step + \
s.get("simulation_current_distance"),
s.get("simulation_complete_distance"))
if updated_distance != s.get("simulation_current_distance"):
s.set("simulation_current_distance", updated_distance)
moves = toolpath.get_moves(safety_height=safety_height,
max_movement=updated_distance)
s.set("simulation_toolpath_moves", moves)
if moves:
self.cutter.moveto(moves[-1][0])
self.update_view()
progress_value_percent = 100.0 * s.get("simulation_current_distance") \
/ s.get("simulation_complete_distance")
self.gui.get_object("SimulationProgressTimelineValue").set_value(
progress_value_percent)
return True
def show_toolpath_simulation(self, toolpath=None):
# disable the main controls
self.toggle_tabs_for_simulation(False)
# show the simulation controls
self.simulation_window.show()
# start the simulation
self.settings.set("show_simulation", True)
time_step = int(1000 / self.settings.get("drill_progress_max_fps"))
# update the toolpath simulation repeatedly
gobject.timeout_add(time_step, self.update_toolpath_simulation)
def update_toolpath_simulation_ode(self, widget=None, toolpath=None):
import pycam.Simulation.ODEBlocks as ODEBlocks
# get the currently selected toolpath, if none is give
if toolpath is None:
toolpath_index = self._treeview_get_active_index(self.toolpath_table, self.toolpath)
if toolpath_index is None:
return
else:
toolpath = self.toolpath[toolpath_index]
paths = toolpath.get_paths()
# set the current cutter
self.cutter = pycam.Cutters.get_tool_from_settings(
toolpath.get_tool_settings())
# calculate steps
detail_level = self.gui.get_object("SimulationDetailsValue").get_value()
grid_size = 100 * pow(2, detail_level - 1)
bounding_box = toolpath.get_toolpath_settings().get_bounds()
(minx, miny, minz), (maxx, maxy, maxz) = bounding_box.get_bounds()
# proportion = dimension_x / dimension_y
proportion = (maxx - minx) / (maxy - miny)
x_steps = int(sqrt(grid_size) * proportion)
y_steps = int(sqrt(grid_size) / proportion)
simulation_backend = ODEBlocks.ODEBlocks(toolpath.get_tool_settings(),
toolpath.get_bounding_box(), x_steps=x_steps, y_steps=y_steps)
self.settings.set("simulation_object", simulation_backend)
# disable the simulation widget (avoids confusion regarding "cancel")
if not widget is None:
self.gui.get_object("SimulationTab").set_sensitive(False)
# update the view
self.update_view()
# calculate the simulation and show it simulteneously
progress = self.settings.get("progress")
for path_index, path in enumerate(paths):
progress_text = "Simulating path %d/%d" % (path_index, len(paths))
progress_value_percent = 100.0 * path_index / len(paths)
if progress.update(text=progress_text, percent=progress_value_percent):
# break if the user pressed the "cancel" button
break
for index in range(len(path.points)):
self.cutter.moveto(path.points[index])
if index != 0:
start = path.points[index - 1]
end = path.points[index]
if start != end:
simulation_backend.process_cutter_movement(start, end)
self.update_view()
# break the loop if someone clicked the "cancel" button
if progress.update():
break
progress.finish()
# enable the simulation widget again (if we were started from the GUI)
if not widget is None:
self.gui.get_object("SimulationTab").set_sensitive(True)
...@@ -25,7 +25,15 @@ import pycam.Plugins ...@@ -25,7 +25,15 @@ import pycam.Plugins
class Toolpaths(pycam.Plugins.ListPluginBase): class Toolpaths(pycam.Plugins.ListPluginBase):
UI_FILE = "toolpaths.ui"
def setup(self): def setup(self):
"""
("ExportGCodeAll", self.save_toolpath, False, "<Control><Shift>e"),
("ExportGCodeVisible", self.save_toolpath, True, None),
# store the original content (for adding the number of current toolpaths in "update_toolpath_table")
self._original_toolpath_tab_label = self.gui.get_object("ToolpathsTabLabel").get_text()
"""
self.core.add_item("toolpaths", lambda: self) self.core.add_item("toolpaths", lambda: self)
return True return True
...@@ -33,3 +41,55 @@ class Toolpaths(pycam.Plugins.ListPluginBase): ...@@ -33,3 +41,55 @@ class Toolpaths(pycam.Plugins.ListPluginBase):
self.core.set("toolpaths", None) self.core.set("toolpaths", None)
return True return True
def _update_toolpath_related_controls(self):
# show or hide the "toolpath" tab
toolpath_tab = self.gui.get_object("ToolpathsTab")
if not self.toolpath:
toolpath_tab.hide()
else:
self.gui.get_object("ToolpathsTabLabel").set_text(
"%s (%d)" % (self._original_toolpath_tab_label, len(self.toolpath)))
toolpath_tab.show()
# enable/disable the export menu item
self.gui.get_object("ExportGCodeAll").set_sensitive(len(self.toolpath) > 0)
toolpaths_are_visible = any([tp.visible for tp in self.toolpath])
self.gui.get_object("ExportGCodeVisible").set_sensitive(
toolpaths_are_visible)
self.gui.get_object("ExportVisibleToolpathsButton").set_sensitive(
toolpaths_are_visible)
def _update_toolpath_table(self, new_index=None, skip_model_update=False):
def get_time_string(minutes):
if minutes > 180:
return "%d hours" % int(round(minutes / 60))
elif minutes > 3:
return "%d minutes" % int(round(minutes))
else:
return "%d seconds" % int(round(minutes * 60))
self.update_toolpath_related_controls()
# reset the model data and the selection
if new_index is None:
# keep the old selection - this may return "None" if nothing is selected
new_index = self._treeview_get_active_index(self.toolpath_table, self.toolpath)
if not skip_model_update:
# update the TreeModel data
model = self.gui.get_object("ToolPathListModel")
model.clear()
# columns: name, visible, drill_size, drill_id, allowance, speed, feedrate
for index in range(len(self.toolpath)):
tp = self.toolpath[index]
toolpath_settings = tp.get_toolpath_settings()
tool = toolpath_settings.get_tool_settings()
process = toolpath_settings.get_process_settings()
items = (index, tp.name, tp.visible, tool["tool_radius"],
tool["id"], process["material_allowance"],
tool["speed"], tool["feedrate"],
get_time_string(tp.get_machine_time(
self.settings.get("gcode_safety_height"))))
model.append(items)
if not new_index is None:
self._treeview_set_active_index(self.toolpath_table, new_index)
# enable/disable the modification buttons
self.gui.get_object("toolpath_simulate").set_sensitive(not new_index is None)
self.gui.get_object("ToolpathGrid").set_sensitive(not new_index is None)
...@@ -72,6 +72,7 @@ class Tools(pycam.Plugins.ListPluginBase): ...@@ -72,6 +72,7 @@ class Tools(pycam.Plugins.ListPluginBase):
cache[id(item)] = [id(item), index + 1, cache[id(item)] = [id(item), index + 1,
"Tool #%d" % index] "Tool #%d" % index]
self._treemodel.append(cache[id(item)]) self._treemodel.append(cache[id(item)])
self.core.emit_event("tool-list-changed")
self.register_model_update(update_model) self.register_model_update(update_model)
# drill settings # drill settings
self._detail_handlers = [] self._detail_handlers = []
......
...@@ -141,10 +141,14 @@ class PluginManager(object): ...@@ -141,10 +141,14 @@ class PluginManager(object):
self.modules[plugin_name].teardown() self.modules[plugin_name].teardown()
_log.debug("Initializing module %s (%s)" % (plugin_name, filename)) _log.debug("Initializing module %s (%s)" % (plugin_name, filename))
new_plugin = obj(self.core, plugin_name) new_plugin = obj(self.core, plugin_name)
if not new_plugin.setup(): try:
raise RuntimeError("Failed to load plugin '%s'" % str(plugin_name)) if not new_plugin.setup():
else: _log.info("Failed to setup plugin '%s'" % str(plugin_name))
self.modules[plugin_name] = new_plugin else:
self.modules[plugin_name] = new_plugin
except NotImplementedError, err_msg:
_log.info("Skipping incomplete plugin '%s': %s" % \
(plugin_name, err_msg))
class ListPluginBase(PluginBase, list): class ListPluginBase(PluginBase, list):
...@@ -173,6 +177,14 @@ class ListPluginBase(PluginBase, list): ...@@ -173,6 +177,14 @@ class ListPluginBase(PluginBase, list):
selection = modelview.get_selection() selection = modelview.get_selection()
selection_mode = selection.get_mode() selection_mode = selection.get_mode()
paths = selection.get_selected_rows()[1] paths = selection.get_selected_rows()[1]
elif hasattr(modelview, "get_active"):
# combobox
selection_mode = gtk.SELECTION_SINGLE
active = modelview.get_active()
if active < 0:
paths = []
else:
paths = [[active]]
else: else:
# an iconview # an iconview
selection_mode = modelview.get_selection_mode() selection_mode = modelview.get_selection_mode()
...@@ -282,23 +294,28 @@ class ListPluginBase(PluginBase, list): ...@@ -282,23 +294,28 @@ class ListPluginBase(PluginBase, list):
modelview, action, button) modelview, action, button)
button.connect("clicked", self._list_action, modelview, action) button.connect("clicked", self._list_action, modelview, action)
def get_attr(self, model, attr): def get_attr(self, item, attr, model=None, id_col=None):
return self.__get_set_attr(model, attr, write=False) return self.__get_set_attr(item, attr, write=False, model=model, id_col=id_col)
def set_attr(self, model, attr, value): def set_attr(self, item, attr, value, model=None, id_col=None):
return self.__get_set_attr(model, attr, value=value, write=True) return self.__get_set_attr(item, attr, value=value, write=True, model=model, id_col=id_col)
def __get_set_attr(self, model, attr, value=None, write=True): def __get_set_attr(self, item, attr, value=None, write=True, model=None, id_col=None):
if model is None:
# TODO: "self.treemodel" should not be used here
model = self._treemodel
if id_col is None:
id_col = self.COLUMN_ID
if attr in self.LIST_ATTRIBUTE_MAP: if attr in self.LIST_ATTRIBUTE_MAP:
col = self.LIST_ATTRIBUTE_MAP[attr] col = self.LIST_ATTRIBUTE_MAP[attr]
for index in range(len(self)): for index in range(len(model)):
if self._treemodel[index][self.COLUMN_ID] == id(model): if model[index][id_col] == id(item):
if write: if write:
self._treemodel[index][col] = value model[index][col] = value
return return
else: else:
return self._treemodel[index][col] return model[index][col]
raise IndexError("Model not found: %s" % str(model)) raise IndexError("Item '%s' not found in %s" % (item, list(model)))
else: else:
raise KeyError("Attribute '%s' is not part of this list: %s" % \ raise KeyError("Attribute '%s' is not part of this list: %s" % \
(attr, ", ".join(self.LIST_ATTRIBUTE_MAP.keys()))) (attr, ", ".join(self.LIST_ATTRIBUTE_MAP.keys())))
......
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