فهرست منبع

- changed the color of the marked apertures to the global_selection_color
- Gerber Editor: added Transformation Tool and Rotation key shortcut

Marius Stanciu 6 سال پیش
والد
کامیت
b3aeb497ec
5فایلهای تغییر یافته به همراه1077 افزوده شده و 85 حذف شده
  1. 2 2
      FlatCAMObj.py
  2. 5 0
      README.md
  3. 32 38
      flatcamEditors/FlatCAMGeoEditor.py
  4. 1026 43
      flatcamEditors/FlatCAMGrbEditor.py
  5. 12 2
      flatcamGUI/FlatCAMGUI.py

+ 2 - 2
FlatCAMObj.py

@@ -1172,7 +1172,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
                 aperture = self.ui.apertures_table.item(row, 1).text()
                 aperture = self.ui.apertures_table.item(row, 1).text()
                 # self.plot_apertures(color='#2d4606bf', marked_aperture=aperture, visible=True)
                 # self.plot_apertures(color='#2d4606bf', marked_aperture=aperture, visible=True)
-                self.plot_apertures(color='#FD6A02', marked_aperture=aperture, visible=True)
+                self.plot_apertures(color=self.app.defaults['global_sel_draw_color'], marked_aperture=aperture, visible=True)
             else:
             else:
                 self.marked_rows.append(False)
                 self.marked_rows.append(False)
 
 
@@ -1209,7 +1209,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         if mark_all:
         if mark_all:
             for aperture in self.apertures:
             for aperture in self.apertures:
                 # self.plot_apertures(color='#2d4606bf', marked_aperture=aperture, visible=True)
                 # self.plot_apertures(color='#2d4606bf', marked_aperture=aperture, visible=True)
-                self.plot_apertures(color='#FD6A02', marked_aperture=aperture, visible=True)
+                self.plot_apertures(color=self.app.defaults['global_sel_draw_color'], marked_aperture=aperture, visible=True)
         else:
         else:
             self.clear_plot_apertures()
             self.clear_plot_apertures()
 
 

+ 5 - 0
README.md

@@ -9,6 +9,11 @@ CAD program, and create G-Code for Isolation routing.
 
 
 =================================================
 =================================================
 
 
+11.04.2019
+
+- changed the color of the marked apertures to the global_selection_color
+- Gerber Editor: added Transformation Tool and Rotation key shortcut
+
 10.04.2019
 10.04.2019
 
 
 - Gerber Editor: added Add Track and Add Region functions
 - Gerber Editor: added Add Track and Add Region functions

+ 32 - 38
flatcamEditors/FlatCAMGeoEditor.py

@@ -2653,34 +2653,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app = app
         self.app = app
         self.canvas = app.plotcanvas
         self.canvas = app.plotcanvas
 
 
-        self.app.ui.geo_add_circle_menuitem.triggered.connect(lambda: self.select_tool('circle'))
-        self.app.ui.geo_add_arc_menuitem.triggered.connect(lambda: self.select_tool('arc'))
-        self.app.ui.geo_add_rectangle_menuitem.triggered.connect(lambda: self.select_tool('rectangle'))
-        self.app.ui.geo_add_polygon_menuitem.triggered.connect(lambda: self.select_tool('polygon'))
-        self.app.ui.geo_add_path_menuitem.triggered.connect(lambda: self.select_tool('path'))
-        self.app.ui.geo_add_text_menuitem.triggered.connect(lambda: self.select_tool('text'))
-        self.app.ui.geo_paint_menuitem.triggered.connect(self.on_paint_tool)
-        self.app.ui.geo_buffer_menuitem.triggered.connect(self.on_buffer_tool)
-        self.app.ui.geo_transform_menuitem.triggered.connect(self.on_transform_tool)
-
-        self.app.ui.geo_delete_menuitem.triggered.connect(self.on_delete_btn)
-        self.app.ui.geo_union_menuitem.triggered.connect(self.union)
-        self.app.ui.geo_intersection_menuitem.triggered.connect(self.intersection)
-        self.app.ui.geo_subtract_menuitem.triggered.connect(self.subtract)
-        self.app.ui.geo_cutpath_menuitem.triggered.connect(self.cutpath)
-        self.app.ui.geo_copy_menuitem.triggered.connect(lambda: self.select_tool('copy'))
-
-        self.app.ui.geo_union_btn.triggered.connect(self.union)
-        self.app.ui.geo_intersection_btn.triggered.connect(self.intersection)
-        self.app.ui.geo_subtract_btn.triggered.connect(self.subtract)
-        self.app.ui.geo_cutpath_btn.triggered.connect(self.cutpath)
-        self.app.ui.geo_delete_btn.triggered.connect(self.on_delete_btn)
-
-        self.app.ui.geo_move_menuitem.triggered.connect(self.on_move)
-        self.app.ui.geo_cornersnap_menuitem.triggered.connect(self.on_corner_snap)
-
-        self.transform_complete.connect(self.on_transform_complete)
-
         ## Toolbar events and properties
         ## Toolbar events and properties
         self.tools = {
         self.tools = {
             "select": {"button": self.app.ui.geo_select_btn,
             "select": {"button": self.app.ui.geo_select_btn,
@@ -2814,15 +2786,43 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app.ui.snap_max_dist_entry.textChanged.connect(
         self.app.ui.snap_max_dist_entry.textChanged.connect(
             lambda: entry2option("snap_max", self.app.ui.snap_max_dist_entry))
             lambda: entry2option("snap_max", self.app.ui.snap_max_dist_entry))
 
 
-        # store the status of the editor so the Delete at object level will not work until the edit is finished
-        self.editor_active = False
-
         # if using Paint store here the tool diameter used
         # if using Paint store here the tool diameter used
         self.paint_tooldia = None
         self.paint_tooldia = None
 
 
         self.paint_tool = PaintOptionsTool(self.app, self)
         self.paint_tool = PaintOptionsTool(self.app, self)
         self.transform_tool = TransformEditorTool(self.app, self)
         self.transform_tool = TransformEditorTool(self.app, self)
 
 
+        self.app.ui.geo_add_circle_menuitem.triggered.connect(lambda: self.select_tool('circle'))
+        self.app.ui.geo_add_arc_menuitem.triggered.connect(lambda: self.select_tool('arc'))
+        self.app.ui.geo_add_rectangle_menuitem.triggered.connect(lambda: self.select_tool('rectangle'))
+        self.app.ui.geo_add_polygon_menuitem.triggered.connect(lambda: self.select_tool('polygon'))
+        self.app.ui.geo_add_path_menuitem.triggered.connect(lambda: self.select_tool('path'))
+        self.app.ui.geo_add_text_menuitem.triggered.connect(lambda: self.select_tool('text'))
+        self.app.ui.geo_paint_menuitem.triggered.connect(self.on_paint_tool)
+        self.app.ui.geo_buffer_menuitem.triggered.connect(self.on_buffer_tool)
+        self.app.ui.geo_transform_menuitem.triggered.connect(self.transform_tool.run)
+
+        self.app.ui.geo_delete_menuitem.triggered.connect(self.on_delete_btn)
+        self.app.ui.geo_union_menuitem.triggered.connect(self.union)
+        self.app.ui.geo_intersection_menuitem.triggered.connect(self.intersection)
+        self.app.ui.geo_subtract_menuitem.triggered.connect(self.subtract)
+        self.app.ui.geo_cutpath_menuitem.triggered.connect(self.cutpath)
+        self.app.ui.geo_copy_menuitem.triggered.connect(lambda: self.select_tool('copy'))
+
+        self.app.ui.geo_union_btn.triggered.connect(self.union)
+        self.app.ui.geo_intersection_btn.triggered.connect(self.intersection)
+        self.app.ui.geo_subtract_btn.triggered.connect(self.subtract)
+        self.app.ui.geo_cutpath_btn.triggered.connect(self.cutpath)
+        self.app.ui.geo_delete_btn.triggered.connect(self.on_delete_btn)
+
+        self.app.ui.geo_move_menuitem.triggered.connect(self.on_move)
+        self.app.ui.geo_cornersnap_menuitem.triggered.connect(self.on_corner_snap)
+
+        self.transform_complete.connect(self.on_transform_complete)
+
+        # store the status of the editor so the Delete at object level will not work until the edit is finished
+        self.editor_active = False
+
     def pool_recreated(self, pool):
     def pool_recreated(self, pool):
         self.shapes.pool = pool
         self.shapes.pool = pool
         self.tool_shape.pool = pool
         self.tool_shape.pool = pool
@@ -2856,8 +2856,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         self.app.ui.geo_edit_toolbar.setDisabled(False)
         self.app.ui.geo_edit_toolbar.setDisabled(False)
         self.app.ui.geo_edit_toolbar.setVisible(True)
         self.app.ui.geo_edit_toolbar.setVisible(True)
-        self.app.ui.grb_edit_toolbar.setDisabled(False)
-        self.app.ui.grb_edit_toolbar.setVisible(True)
+
         self.app.ui.snap_toolbar.setDisabled(False)
         self.app.ui.snap_toolbar.setDisabled(False)
 
 
         # prevent the user to change anything in the Selected Tab while the Geo Editor is active
         # prevent the user to change anything in the Selected Tab while the Geo Editor is active
@@ -2937,7 +2936,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.canvas.vis_connect('mouse_move', self.on_canvas_move)
         self.canvas.vis_connect('mouse_move', self.on_canvas_move)
         self.canvas.vis_connect('mouse_release', self.on_canvas_click_release)
         self.canvas.vis_connect('mouse_release', self.on_canvas_click_release)
 
 
-
     def disconnect_canvas_event_handlers(self):
     def disconnect_canvas_event_handlers(self):
 
 
         self.canvas.vis_disconnect('mouse_press', self.on_canvas_click)
         self.canvas.vis_disconnect('mouse_press', self.on_canvas_click)
@@ -3089,10 +3087,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
         paint_tool = PaintOptionsTool(self.app, self)
         paint_tool = PaintOptionsTool(self.app, self)
         paint_tool.run()
         paint_tool.run()
 
 
-    def on_transform_tool(self):
-        transform_tool = TransformEditorTool(self.app, self)
-        transform_tool.run()
-
     def on_tool_select(self, tool):
     def on_tool_select(self, tool):
         """
         """
         Behavior of the toolbar. Tool initialization.
         Behavior of the toolbar. Tool initialization.

+ 1026 - 43
flatcamEditors/FlatCAMGrbEditor.py

@@ -12,7 +12,7 @@ import copy
 
 
 from camlib import *
 from camlib import *
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, \
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, \
-    SpinBoxDelegate, EvalEntry
+    SpinBoxDelegate, EvalEntry, EvalEntry2, FCInputDialog, FCButton, OptionalInputSection, FCCheckBox
 from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
 from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
 from FlatCAMObj import FlatCAMGerber
 from FlatCAMObj import FlatCAMGerber
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
@@ -242,9 +242,9 @@ class FCScale(FCShapeTool):
 
 
         if self.draw_app.app.ui.splitter.sizes()[0] == 0:
         if self.draw_app.app.ui.splitter.sizes()[0] == 0:
             self.draw_app.app.ui.splitter.setSizes([1, 1])
             self.draw_app.app.ui.splitter.setSizes([1, 1])
-        self.activate()
+        self.activate_scale()
 
 
-    def activate(self):
+    def activate_scale(self):
         self.draw_app.hide_tool('all')
         self.draw_app.hide_tool('all')
         self.draw_app.scale_tool_frame.show()
         self.draw_app.scale_tool_frame.show()
 
 
@@ -254,7 +254,7 @@ class FCScale(FCShapeTool):
             pass
             pass
         self.draw_app.scale_button.clicked.connect(self.on_scale_click)
         self.draw_app.scale_button.clicked.connect(self.on_scale_click)
 
 
-    def deactivate(self):
+    def deactivate_scale(self):
         self.draw_app.scale_button.clicked.disconnect()
         self.draw_app.scale_button.clicked.disconnect()
         self.complete = True
         self.complete = True
         self.draw_app.select_tool("select")
         self.draw_app.select_tool("select")
@@ -262,7 +262,7 @@ class FCScale(FCShapeTool):
 
 
     def on_scale_click(self):
     def on_scale_click(self):
         self.draw_app.on_scale()
         self.draw_app.on_scale()
-        self.deactivate()
+        self.deactivate_scale()
 
 
 
 
 class FCBuffer(FCShapeTool):
 class FCBuffer(FCShapeTool):
@@ -279,9 +279,9 @@ class FCBuffer(FCShapeTool):
 
 
         if self.draw_app.app.ui.splitter.sizes()[0] == 0:
         if self.draw_app.app.ui.splitter.sizes()[0] == 0:
             self.draw_app.app.ui.splitter.setSizes([1, 1])
             self.draw_app.app.ui.splitter.setSizes([1, 1])
-        self.activate()
+        self.activate_buffer()
 
 
-    def activate(self):
+    def activate_buffer(self):
         self.draw_app.hide_tool('all')
         self.draw_app.hide_tool('all')
         self.draw_app.buffer_tool_frame.show()
         self.draw_app.buffer_tool_frame.show()
 
 
@@ -291,7 +291,7 @@ class FCBuffer(FCShapeTool):
             pass
             pass
         self.draw_app.buffer_button.clicked.connect(self.on_buffer_click)
         self.draw_app.buffer_button.clicked.connect(self.on_buffer_click)
 
 
-    def deactivate(self):
+    def deactivate_buffer(self):
         self.draw_app.buffer_button.clicked.disconnect()
         self.draw_app.buffer_button.clicked.disconnect()
         self.complete = True
         self.complete = True
         self.draw_app.select_tool("select")
         self.draw_app.select_tool("select")
@@ -299,7 +299,7 @@ class FCBuffer(FCShapeTool):
 
 
     def on_buffer_click(self):
     def on_buffer_click(self):
         self.draw_app.on_buffer()
         self.draw_app.on_buffer()
-        self.deactivate()
+        self.deactivate_buffer()
 
 
 
 
 class FCApertureMove(FCShapeTool):
 class FCApertureMove(FCShapeTool):
@@ -492,6 +492,20 @@ class FCApertureSelect(DrawTool):
         return ""
         return ""
 
 
 
 
+class FCTransform(FCShapeTool):
+    def __init__(self, draw_app):
+        FCShapeTool.__init__(self, draw_app)
+        self.name = 'transformation'
+
+        # self.shape_buffer = self.draw_app.shape_buffer
+        self.draw_app = draw_app
+        self.app = draw_app.app
+
+        self.start_msg = _("Shape transformations ...")
+        self.origin = (0, 0)
+        self.draw_app.transform_tool.run()
+
+
 class FlatCAMGrbEditor(QtCore.QObject):
 class FlatCAMGrbEditor(QtCore.QObject):
 
 
     draw_shape_idx = -1
     draw_shape_idx = -1
@@ -759,6 +773,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
                                 "constructor": FCScale},
                                 "constructor": FCScale},
             "copy": {"button": self.app.ui.aperture_copy_btn,
             "copy": {"button": self.app.ui.aperture_copy_btn,
                      "constructor": FCApertureCopy},
                      "constructor": FCApertureCopy},
+            "transform": {"button": self.app.ui.grb_transform_btn,
+                          "constructor": FCTransform},
             "move": {"button": self.app.ui.aperture_move_btn,
             "move": {"button": self.app.ui.aperture_move_btn,
                      "constructor": FCApertureMove},
                      "constructor": FCApertureMove},
         }
         }
@@ -794,32 +810,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # this var will store the state of the toolbar before starting the editor
         # this var will store the state of the toolbar before starting the editor
         self.toolbar_old_state = False
         self.toolbar_old_state = False
 
 
-        # Signals
-        self.buffer_button.clicked.connect(self.on_buffer)
-        self.scale_button.clicked.connect(self.on_scale)
-
-        self.app.ui.delete_drill_btn.triggered.connect(self.on_delete_btn)
-        self.name_entry.returnPressed.connect(self.on_name_activate)
-
-        self.aptype_cb.currentIndexChanged[str].connect(self.on_aptype_changed)
-
-        self.addaperture_btn.clicked.connect(self.on_aperture_add)
-        self.delaperture_btn.clicked.connect(self.on_aperture_delete)
-        self.apertures_table.cellPressed.connect(self.on_row_selected)
-
-        self.app.ui.grb_add_pad_menuitem.triggered.connect(self.on_pad_add)
-        self.app.ui.grb_add_track_menuitem.triggered.connect(self.on_track_add)
-        self.app.ui.grb_add_region_menuitem.triggered.connect(self.on_region_add)
-
-        self.app.ui.grb_add_buffer_menuitem.triggered.connect(self.on_buffer)
-        self.app.ui.grb_add_scale_menuitem.triggered.connect(self.on_scale)
-
-        self.app.ui.grb_copy_menuitem.triggered.connect(self.on_copy_button)
-        self.app.ui.grb_delete_menuitem.triggered.connect(self.on_delete_btn)
-
-        self.app.ui.grb_move_menuitem.triggered.connect(self.on_move_button)
-
-
         # Init GUI
         # Init GUI
         self.apdim_lbl.hide()
         self.apdim_lbl.hide()
         self.apdim_entry.hide()
         self.apdim_entry.hide()
@@ -889,6 +879,34 @@ class FlatCAMGrbEditor(QtCore.QObject):
         def entry2option(option, entry):
         def entry2option(option, entry):
             self.options[option] = float(entry.text())
             self.options[option] = float(entry.text())
 
 
+        self.transform_tool = TransformEditorTool(self.app, self)
+
+        # Signals
+        self.buffer_button.clicked.connect(self.on_buffer)
+        self.scale_button.clicked.connect(self.on_scale)
+
+        self.app.ui.delete_drill_btn.triggered.connect(self.on_delete_btn)
+        self.name_entry.returnPressed.connect(self.on_name_activate)
+
+        self.aptype_cb.currentIndexChanged[str].connect(self.on_aptype_changed)
+
+        self.addaperture_btn.clicked.connect(self.on_aperture_add)
+        self.delaperture_btn.clicked.connect(self.on_aperture_delete)
+        self.apertures_table.cellPressed.connect(self.on_row_selected)
+
+        self.app.ui.grb_add_pad_menuitem.triggered.connect(self.on_pad_add)
+        self.app.ui.grb_add_track_menuitem.triggered.connect(self.on_track_add)
+        self.app.ui.grb_add_region_menuitem.triggered.connect(self.on_region_add)
+
+        self.app.ui.grb_add_buffer_menuitem.triggered.connect(self.on_buffer)
+        self.app.ui.grb_add_scale_menuitem.triggered.connect(self.on_scale)
+        self.app.ui.grb_transform_menuitem.triggered.connect(self.transform_tool.run)
+
+        self.app.ui.grb_copy_menuitem.triggered.connect(self.on_copy_button)
+        self.app.ui.grb_delete_menuitem.triggered.connect(self.on_delete_btn)
+
+        self.app.ui.grb_move_menuitem.triggered.connect(self.on_move_button)
+
         # store the status of the editor so the Delete at object level will not work until the edit is finished
         # store the status of the editor so the Delete at object level will not work until the edit is finished
         self.editor_active = False
         self.editor_active = False
 
 
@@ -1256,7 +1274,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             self.apdim_entry.hide()
             self.apdim_entry.hide()
             self.apsize_entry.setReadOnly(False)
             self.apsize_entry.setReadOnly(False)
 
 
-    def activate(self):
+    def activate_grb_editor(self):
         self.connect_canvas_event_handlers()
         self.connect_canvas_event_handlers()
 
 
         # init working objects
         # init working objects
@@ -1294,7 +1312,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # Tell the App that the editor is active
         # Tell the App that the editor is active
         self.editor_active = True
         self.editor_active = True
 
 
-    def deactivate(self):
+    def deactivate_grb_editor(self):
         self.disconnect_canvas_event_handlers()
         self.disconnect_canvas_event_handlers()
         self.clear()
         self.clear()
         self.app.ui.grb_edit_toolbar.setDisabled(True)
         self.app.ui.grb_edit_toolbar.setDisabled(True)
@@ -1394,8 +1412,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
         :return: None
         :return: None
         """
         """
 
 
-        self.deactivate()
-        self.activate()
+        self.deactivate_grb_editor()
+        self.activate_grb_editor()
 
 
         # create a reference to the source object
         # create a reference to the source object
         self.gerber_obj = orig_grb_obj
         self.gerber_obj = orig_grb_obj
@@ -1445,9 +1463,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
                     except ValueError:
                     except ValueError:
                         break
                         break
 
 
-        for k, v in self.gerber_obj.apertures.items():
-            print(k, v)
-
         for apid in self.gerber_obj.apertures:
         for apid in self.gerber_obj.apertures:
             self.grb_plot_promises.append(apid)
             self.grb_plot_promises.append(apid)
             self.app.worker_task.emit({'fcn': job_thread, 'params': [self, apid]})
             self.app.worker_task.emit({'fcn': job_thread, 'params': [self, apid]})
@@ -2251,4 +2266,972 @@ class FlatCAMGrbEditor(QtCore.QObject):
         if tool_name == 'scale' or tool_name == 'all':
         if tool_name == 'scale' or tool_name == 'all':
             self.scale_tool_frame.hide()
             self.scale_tool_frame.hide()
 
 
-        self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+
+class TransformEditorTool(FlatCAMTool):
+    """
+    Inputs to specify how to paint the selected polygons.
+    """
+
+    toolName = _("Transform Tool")
+    rotateName = _("Rotate")
+    skewName = _("Skew/Shear")
+    scaleName = _("Scale")
+    flipName = _("Mirror (Flip)")
+    offsetName = _("Offset")
+
+    def __init__(self, app, draw_app):
+        FlatCAMTool.__init__(self, app)
+
+        self.app = app
+        self.draw_app = draw_app
+
+        self.transform_lay = QtWidgets.QVBoxLayout()
+        self.layout.addLayout(self.transform_lay)
+        ## Title
+        title_label = QtWidgets.QLabel("%s" % (_('Editor %s') % self.toolName))
+        title_label.setStyleSheet("""
+                QLabel
+                {
+                    font-size: 16px;
+                    font-weight: bold;
+                }
+                """)
+        self.transform_lay.addWidget(title_label)
+
+        self.empty_label = QtWidgets.QLabel("")
+        self.empty_label.setFixedWidth(50)
+
+        self.empty_label1 = QtWidgets.QLabel("")
+        self.empty_label1.setFixedWidth(70)
+        self.empty_label2 = QtWidgets.QLabel("")
+        self.empty_label2.setFixedWidth(70)
+        self.empty_label3 = QtWidgets.QLabel("")
+        self.empty_label3.setFixedWidth(70)
+        self.empty_label4 = QtWidgets.QLabel("")
+        self.empty_label4.setFixedWidth(70)
+        self.transform_lay.addWidget(self.empty_label)
+
+        ## Rotate Title
+        rotate_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.rotateName)
+        self.transform_lay.addWidget(rotate_title_label)
+
+        ## Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.transform_lay.addLayout(form_layout)
+        form_child = QtWidgets.QHBoxLayout()
+
+        self.rotate_label = QtWidgets.QLabel(_("Angle:"))
+        self.rotate_label.setToolTip(
+            _("Angle for Rotation action, in degrees.\n"
+              "Float number between -360 and 359.\n"
+              "Positive numbers for CW motion.\n"
+              "Negative numbers for CCW motion.")
+        )
+        self.rotate_label.setFixedWidth(50)
+
+        self.rotate_entry = FCEntry()
+        # self.rotate_entry.setFixedWidth(60)
+        self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+
+        self.rotate_button = FCButton()
+        self.rotate_button.set_value(_("Rotate"))
+        self.rotate_button.setToolTip(
+            _("Rotate the selected shape(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected shapes.")
+        )
+        self.rotate_button.setFixedWidth(60)
+
+        form_child.addWidget(self.rotate_entry)
+        form_child.addWidget(self.rotate_button)
+
+        form_layout.addRow(self.rotate_label, form_child)
+
+        self.transform_lay.addWidget(self.empty_label1)
+
+        ## Skew Title
+        skew_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.skewName)
+        self.transform_lay.addWidget(skew_title_label)
+
+        ## Form Layout
+        form1_layout = QtWidgets.QFormLayout()
+        self.transform_lay.addLayout(form1_layout)
+        form1_child_1 = QtWidgets.QHBoxLayout()
+        form1_child_2 = QtWidgets.QHBoxLayout()
+
+        self.skewx_label = QtWidgets.QLabel(_("Angle X:"))
+        self.skewx_label.setToolTip(
+            _("Angle for Skew action, in degrees.\n"
+              "Float number between -360 and 359.")
+        )
+        self.skewx_label.setFixedWidth(50)
+        self.skewx_entry = FCEntry()
+        self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        # self.skewx_entry.setFixedWidth(60)
+
+        self.skewx_button = FCButton()
+        self.skewx_button.set_value(_("Skew X"))
+        self.skewx_button.setToolTip(
+            _("Skew/shear the selected shape(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected shapes."))
+        self.skewx_button.setFixedWidth(60)
+
+        self.skewy_label = QtWidgets.QLabel(_("Angle Y:"))
+        self.skewy_label.setToolTip(
+            _("Angle for Skew action, in degrees.\n"
+              "Float number between -360 and 359.")
+        )
+        self.skewy_label.setFixedWidth(50)
+        self.skewy_entry = FCEntry()
+        self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        # self.skewy_entry.setFixedWidth(60)
+
+        self.skewy_button = FCButton()
+        self.skewy_button.set_value(_("Skew Y"))
+        self.skewy_button.setToolTip(
+            _("Skew/shear the selected shape(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected shapes."))
+        self.skewy_button.setFixedWidth(60)
+
+        form1_child_1.addWidget(self.skewx_entry)
+        form1_child_1.addWidget(self.skewx_button)
+
+        form1_child_2.addWidget(self.skewy_entry)
+        form1_child_2.addWidget(self.skewy_button)
+
+        form1_layout.addRow(self.skewx_label, form1_child_1)
+        form1_layout.addRow(self.skewy_label, form1_child_2)
+
+        self.transform_lay.addWidget(self.empty_label2)
+
+        ## Scale Title
+        scale_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.scaleName)
+        self.transform_lay.addWidget(scale_title_label)
+
+        ## Form Layout
+        form2_layout = QtWidgets.QFormLayout()
+        self.transform_lay.addLayout(form2_layout)
+        form2_child_1 = QtWidgets.QHBoxLayout()
+        form2_child_2 = QtWidgets.QHBoxLayout()
+
+        self.scalex_label = QtWidgets.QLabel(_("Factor X:"))
+        self.scalex_label.setToolTip(
+            _("Factor for Scale action over X axis.")
+        )
+        self.scalex_label.setFixedWidth(50)
+        self.scalex_entry = FCEntry()
+        self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        # self.scalex_entry.setFixedWidth(60)
+
+        self.scalex_button = FCButton()
+        self.scalex_button.set_value(_("Scale X"))
+        self.scalex_button.setToolTip(
+            _("Scale the selected shape(s).\n"
+              "The point of reference depends on \n"
+              "the Scale reference checkbox state."))
+        self.scalex_button.setFixedWidth(60)
+
+        self.scaley_label = QtWidgets.QLabel(_("Factor Y:"))
+        self.scaley_label.setToolTip(
+            _("Factor for Scale action over Y axis.")
+        )
+        self.scaley_label.setFixedWidth(50)
+        self.scaley_entry = FCEntry()
+        self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        # self.scaley_entry.setFixedWidth(60)
+
+        self.scaley_button = FCButton()
+        self.scaley_button.set_value(_("Scale Y"))
+        self.scaley_button.setToolTip(
+            _("Scale the selected shape(s).\n"
+              "The point of reference depends on \n"
+              "the Scale reference checkbox state."))
+        self.scaley_button.setFixedWidth(60)
+
+        self.scale_link_cb = FCCheckBox()
+        self.scale_link_cb.set_value(True)
+        self.scale_link_cb.setText(_("Link"))
+        self.scale_link_cb.setToolTip(
+            _("Scale the selected shape(s)\n"
+              "using the Scale Factor X for both axis."))
+        self.scale_link_cb.setFixedWidth(50)
+
+        self.scale_zero_ref_cb = FCCheckBox()
+        self.scale_zero_ref_cb.set_value(True)
+        self.scale_zero_ref_cb.setText(_("Scale Reference"))
+        self.scale_zero_ref_cb.setToolTip(
+            _("Scale the selected shape(s)\n"
+              "using the origin reference when checked,\n"
+              "and the center of the biggest bounding box\n"
+              "of the selected shapes when unchecked."))
+
+        form2_child_1.addWidget(self.scalex_entry)
+        form2_child_1.addWidget(self.scalex_button)
+
+        form2_child_2.addWidget(self.scaley_entry)
+        form2_child_2.addWidget(self.scaley_button)
+
+        form2_layout.addRow(self.scalex_label, form2_child_1)
+        form2_layout.addRow(self.scaley_label, form2_child_2)
+        form2_layout.addRow(self.scale_link_cb, self.scale_zero_ref_cb)
+        self.ois_scale = OptionalInputSection(self.scale_link_cb, [self.scaley_entry, self.scaley_button],
+                                              logic=False)
+
+        self.transform_lay.addWidget(self.empty_label3)
+
+        ## Offset Title
+        offset_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.offsetName)
+        self.transform_lay.addWidget(offset_title_label)
+
+        ## Form Layout
+        form3_layout = QtWidgets.QFormLayout()
+        self.transform_lay.addLayout(form3_layout)
+        form3_child_1 = QtWidgets.QHBoxLayout()
+        form3_child_2 = QtWidgets.QHBoxLayout()
+
+        self.offx_label = QtWidgets.QLabel(_("Value X:"))
+        self.offx_label.setToolTip(
+            _("Value for Offset action on X axis.")
+        )
+        self.offx_label.setFixedWidth(50)
+        self.offx_entry = FCEntry()
+        self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        # self.offx_entry.setFixedWidth(60)
+
+        self.offx_button = FCButton()
+        self.offx_button.set_value(_("Offset X"))
+        self.offx_button.setToolTip(
+            _("Offset the selected shape(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected shapes.\n")
+        )
+        self.offx_button.setFixedWidth(60)
+
+        self.offy_label = QtWidgets.QLabel(_("Value Y:"))
+        self.offy_label.setToolTip(
+            _("Value for Offset action on Y axis.")
+        )
+        self.offy_label.setFixedWidth(50)
+        self.offy_entry = FCEntry()
+        self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        # self.offy_entry.setFixedWidth(60)
+
+        self.offy_button = FCButton()
+        self.offy_button.set_value(_("Offset Y"))
+        self.offy_button.setToolTip(
+            _("Offset the selected shape(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected shapes.\n")
+        )
+        self.offy_button.setFixedWidth(60)
+
+        form3_child_1.addWidget(self.offx_entry)
+        form3_child_1.addWidget(self.offx_button)
+
+        form3_child_2.addWidget(self.offy_entry)
+        form3_child_2.addWidget(self.offy_button)
+
+        form3_layout.addRow(self.offx_label, form3_child_1)
+        form3_layout.addRow(self.offy_label, form3_child_2)
+
+        self.transform_lay.addWidget(self.empty_label4)
+
+        ## Flip Title
+        flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
+        self.transform_lay.addWidget(flip_title_label)
+
+        ## Form Layout
+        form4_layout = QtWidgets.QFormLayout()
+        form4_child_hlay = QtWidgets.QHBoxLayout()
+        self.transform_lay.addLayout(form4_child_hlay)
+        self.transform_lay.addLayout(form4_layout)
+        form4_child_1 = QtWidgets.QHBoxLayout()
+
+        self.flipx_button = FCButton()
+        self.flipx_button.set_value(_("Flip on X"))
+        self.flipx_button.setToolTip(
+            _("Flip the selected shape(s) over the X axis.\n"
+              "Does not create a new shape.")
+        )
+        self.flipx_button.setFixedWidth(60)
+
+        self.flipy_button = FCButton()
+        self.flipy_button.set_value(_("Flip on Y"))
+        self.flipy_button.setToolTip(
+            _("Flip the selected shape(s) over the X axis.\n"
+              "Does not create a new shape.")
+        )
+        self.flipy_button.setFixedWidth(60)
+
+        self.flip_ref_cb = FCCheckBox()
+        self.flip_ref_cb.set_value(True)
+        self.flip_ref_cb.setText(_("Ref Pt"))
+        self.flip_ref_cb.setToolTip(
+            _("Flip the selected shape(s)\n"
+              "around the point in Point Entry Field.\n"
+              "\n"
+              "The point coordinates can be captured by\n"
+              "left click on canvas together with pressing\n"
+              "SHIFT key. \n"
+              "Then click Add button to insert coordinates.\n"
+              "Or enter the coords in format (x, y) in the\n"
+              "Point Entry field and click Flip on X(Y)")
+        )
+        self.flip_ref_cb.setFixedWidth(50)
+
+        self.flip_ref_label = QtWidgets.QLabel(_("Point:"))
+        self.flip_ref_label.setToolTip(
+            _("Coordinates in format (x, y) used as reference for mirroring.\n"
+              "The 'x' in (x, y) will be used when using Flip on X and\n"
+              "the 'y' in (x, y) will be used when using Flip on Y.")
+        )
+        self.flip_ref_label.setFixedWidth(50)
+        self.flip_ref_entry = EvalEntry2("(0, 0)")
+        self.flip_ref_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        # self.flip_ref_entry.setFixedWidth(60)
+
+        self.flip_ref_button = FCButton()
+        self.flip_ref_button.set_value(_("Add"))
+        self.flip_ref_button.setToolTip(
+            _("The point coordinates can be captured by\n"
+              "left click on canvas together with pressing\n"
+              "SHIFT key. Then click Add button to insert.")
+        )
+        self.flip_ref_button.setFixedWidth(60)
+
+        form4_child_hlay.addStretch()
+        form4_child_hlay.addWidget(self.flipx_button)
+        form4_child_hlay.addWidget(self.flipy_button)
+
+        form4_child_1.addWidget(self.flip_ref_entry)
+        form4_child_1.addWidget(self.flip_ref_button)
+
+        form4_layout.addRow(self.flip_ref_cb)
+        form4_layout.addRow(self.flip_ref_label, form4_child_1)
+        self.ois_flip = OptionalInputSection(self.flip_ref_cb,
+                                             [self.flip_ref_entry, self.flip_ref_button], logic=True)
+
+        self.transform_lay.addStretch()
+
+        ## Signals
+        self.rotate_button.clicked.connect(self.on_rotate)
+        self.skewx_button.clicked.connect(self.on_skewx)
+        self.skewy_button.clicked.connect(self.on_skewy)
+        self.scalex_button.clicked.connect(self.on_scalex)
+        self.scaley_button.clicked.connect(self.on_scaley)
+        self.offx_button.clicked.connect(self.on_offx)
+        self.offy_button.clicked.connect(self.on_offy)
+        self.flipx_button.clicked.connect(self.on_flipx)
+        self.flipy_button.clicked.connect(self.on_flipy)
+        self.flip_ref_button.clicked.connect(self.on_flip_add_coords)
+
+        self.rotate_entry.returnPressed.connect(self.on_rotate)
+        self.skewx_entry.returnPressed.connect(self.on_skewx)
+        self.skewy_entry.returnPressed.connect(self.on_skewy)
+        self.scalex_entry.returnPressed.connect(self.on_scalex)
+        self.scaley_entry.returnPressed.connect(self.on_scaley)
+        self.offx_entry.returnPressed.connect(self.on_offx)
+        self.offy_entry.returnPressed.connect(self.on_offy)
+
+        self.set_tool_ui()
+
+    def run(self):
+        self.app.report_usage("Geo Editor Transform Tool()")
+        FlatCAMTool.run(self)
+        self.set_tool_ui()
+
+        # if the splitter us hidden, display it
+        if self.app.ui.splitter.sizes()[0] == 0:
+            self.app.ui.splitter.setSizes([1, 1])
+
+        self.app.ui.notebook.setTabText(2, _("Transform Tool"))
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+T', **kwargs)
+
+    def set_tool_ui(self):
+        ## Initialize form
+        if self.app.defaults["tools_transform_rotate"]:
+            self.rotate_entry.set_value(self.app.defaults["tools_transform_rotate"])
+        else:
+            self.rotate_entry.set_value(0.0)
+
+        if self.app.defaults["tools_transform_skew_x"]:
+            self.skewx_entry.set_value(self.app.defaults["tools_transform_skew_x"])
+        else:
+            self.skewx_entry.set_value(0.0)
+
+        if self.app.defaults["tools_transform_skew_y"]:
+            self.skewy_entry.set_value(self.app.defaults["tools_transform_skew_y"])
+        else:
+            self.skewy_entry.set_value(0.0)
+
+        if self.app.defaults["tools_transform_scale_x"]:
+            self.scalex_entry.set_value(self.app.defaults["tools_transform_scale_x"])
+        else:
+            self.scalex_entry.set_value(1.0)
+
+        if self.app.defaults["tools_transform_scale_y"]:
+            self.scaley_entry.set_value(self.app.defaults["tools_transform_scale_y"])
+        else:
+            self.scaley_entry.set_value(1.0)
+
+        if self.app.defaults["tools_transform_scale_link"]:
+            self.scale_link_cb.set_value(self.app.defaults["tools_transform_scale_link"])
+        else:
+            self.scale_link_cb.set_value(True)
+
+        if self.app.defaults["tools_transform_scale_reference"]:
+            self.scale_zero_ref_cb.set_value(self.app.defaults["tools_transform_scale_reference"])
+        else:
+            self.scale_zero_ref_cb.set_value(True)
+
+        if self.app.defaults["tools_transform_offset_x"]:
+            self.offx_entry.set_value(self.app.defaults["tools_transform_offset_x"])
+        else:
+            self.offx_entry.set_value(0.0)
+
+        if self.app.defaults["tools_transform_offset_y"]:
+            self.offy_entry.set_value(self.app.defaults["tools_transform_offset_y"])
+        else:
+            self.offy_entry.set_value(0.0)
+
+        if self.app.defaults["tools_transform_mirror_reference"]:
+            self.flip_ref_cb.set_value(self.app.defaults["tools_transform_mirror_reference"])
+        else:
+            self.flip_ref_cb.set_value(False)
+
+        if self.app.defaults["tools_transform_mirror_point"]:
+            self.flip_ref_entry.set_value(self.app.defaults["tools_transform_mirror_point"])
+        else:
+            self.flip_ref_entry.set_value((0, 0))
+
+    def template(self):
+        if not self.fcdraw.selected:
+            self.app.inform.emit(_("[WARNING_NOTCL] Transformation cancelled. No shape selected."))
+            return
+
+        self.draw_app.select_tool("select")
+        self.app.ui.notebook.setTabText(2, "Tools")
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
+
+        self.app.ui.splitter.setSizes([0, 1])
+
+    def on_rotate(self, sig=None, val=None):
+        if val:
+            value = val
+        else:
+            try:
+                value = float(self.rotate_entry.get_value())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    value = float(self.rotate_entry.get_value().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered for Rotate, "
+                                           "use a number."))
+                    return
+        self.app.worker_task.emit({'fcn': self.on_rotate_action,
+                                   'params': [value]})
+        # self.on_rotate_action(value)
+        return
+
+    def on_flipx(self):
+        # self.on_flip("Y")
+        axis = 'Y'
+        self.app.worker_task.emit({'fcn': self.on_flip,
+                                   'params': [axis]})
+        return
+
+    def on_flipy(self):
+        # self.on_flip("X")
+        axis = 'X'
+        self.app.worker_task.emit({'fcn': self.on_flip,
+                                   'params': [axis]})
+        return
+
+    def on_flip_add_coords(self):
+        val = self.app.clipboard.text()
+        self.flip_ref_entry.set_value(val)
+
+    def on_skewx(self, sig=None, val=None):
+        if val:
+            value = val
+        else:
+            try:
+                value = float(self.skewx_entry.get_value())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    value = float(self.skewx_entry.get_value().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered for Skew X, "
+                                           "use a number."))
+                    return
+
+        # self.on_skew("X", value)
+        axis = 'X'
+        self.app.worker_task.emit({'fcn': self.on_skew,
+                                   'params': [axis, value]})
+        return
+
+    def on_skewy(self, sig=None, val=None):
+        if val:
+            value = val
+        else:
+            try:
+                value = float(self.skewy_entry.get_value())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    value = float(self.skewy_entry.get_value().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered for Skew Y, "
+                                           "use a number."))
+                    return
+
+        # self.on_skew("Y", value)
+        axis = 'Y'
+        self.app.worker_task.emit({'fcn': self.on_skew,
+                                   'params': [axis, value]})
+        return
+
+    def on_scalex(self, sig=None, val=None):
+        if val:
+            xvalue = val
+        else:
+            try:
+                xvalue = float(self.scalex_entry.get_value())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    xvalue = float(self.scalex_entry.get_value().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered for Scale X, "
+                                           "use a number."))
+                    return
+
+        # scaling to zero has no sense so we remove it, because scaling with 1 does nothing
+        if xvalue == 0:
+            xvalue = 1
+        if self.scale_link_cb.get_value():
+            yvalue = xvalue
+        else:
+            yvalue = 1
+
+        axis = 'X'
+        point = (0, 0)
+        if self.scale_zero_ref_cb.get_value():
+            self.app.worker_task.emit({'fcn': self.on_scale,
+                                       'params': [axis, xvalue, yvalue, point]})
+            # self.on_scale("X", xvalue, yvalue, point=(0,0))
+        else:
+            # self.on_scale("X", xvalue, yvalue)
+            self.app.worker_task.emit({'fcn': self.on_scale,
+                                       'params': [axis, xvalue, yvalue]})
+
+        return
+
+    def on_scaley(self, sig=None, val=None):
+        xvalue = 1
+        if val:
+            yvalue = val
+        else:
+            try:
+                yvalue = float(self.scaley_entry.get_value())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    yvalue = float(self.scaley_entry.get_value().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered for Scale Y, "
+                                           "use a number."))
+                    return
+
+        # scaling to zero has no sense so we remove it, because scaling with 1 does nothing
+        if yvalue == 0:
+            yvalue = 1
+
+        axis = 'Y'
+        point = (0, 0)
+        if self.scale_zero_ref_cb.get_value():
+            self.app.worker_task.emit({'fcn': self.on_scale,
+                                       'params': [axis, xvalue, yvalue, point]})
+            # self.on_scale("Y", xvalue, yvalue, point=(0,0))
+        else:
+            # self.on_scale("Y", xvalue, yvalue)
+            self.app.worker_task.emit({'fcn': self.on_scale,
+                                       'params': [axis, xvalue, yvalue]})
+
+        return
+
+    def on_offx(self, sig=None, val=None):
+        if val:
+            value = val
+        else:
+            try:
+                value = float(self.offx_entry.get_value())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    value = float(self.offx_entry.get_value().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered for Offset X, "
+                                           "use a number."))
+                    return
+
+        # self.on_offset("X", value)
+        axis = 'X'
+        self.app.worker_task.emit({'fcn': self.on_offset,
+                                   'params': [axis, value]})
+        return
+
+    def on_offy(self, sig=None, val=None):
+        if val:
+            value = val
+        else:
+            try:
+                value = float(self.offy_entry.get_value())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    value = float(self.offy_entry.get_value().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered for Offset Y, "
+                                           "use a number."))
+                    return
+
+        # self.on_offset("Y", value)
+        axis = 'Y'
+        self.app.worker_task.emit({'fcn': self.on_offset,
+                                   'params': [axis, value]})
+        return
+
+    def on_rotate_action(self, num):
+        shape_list = self.draw_app.selected
+        xminlist = []
+        yminlist = []
+        xmaxlist = []
+        ymaxlist = []
+
+        if not shape_list:
+            self.app.inform.emit(_("[WARNING_NOTCL] No shape selected. Please Select a shape to rotate!"))
+            return
+        else:
+            with self.app.proc_container.new(_("Appying Rotate")):
+                try:
+                    # first get a bounding box to fit all
+                    for sha in shape_list:
+                        xmin, ymin, xmax, ymax = sha.bounds()
+                        xminlist.append(xmin)
+                        yminlist.append(ymin)
+                        xmaxlist.append(xmax)
+                        ymaxlist.append(ymax)
+
+                    # get the minimum x,y and maximum x,y for all objects selected
+                    xminimal = min(xminlist)
+                    yminimal = min(yminlist)
+                    xmaximal = max(xmaxlist)
+                    ymaximal = max(ymaxlist)
+
+                    self.app.progress.emit(20)
+
+                    for sel_sha in shape_list:
+                        px = 0.5 * (xminimal + xmaximal)
+                        py = 0.5 * (yminimal + ymaximal)
+
+                        sel_sha.rotate(-num, point=(px, py))
+                        self.draw_app.plot_all()
+                        # self.draw_app.add_shape(DrawToolShape(sel_sha.geo))
+
+                    # self.draw_app.transform_complete.emit()
+
+                    self.app.inform.emit(_("[success] Done. Rotate completed."))
+
+                    self.app.progress.emit(100)
+
+                except Exception as e:
+                    self.app.inform.emit(_("[ERROR_NOTCL] Due of %s, rotation movement was not executed.") % str(e))
+                    return
+
+    def on_flip(self, axis):
+        shape_list = self.draw_app.selected
+        xminlist = []
+        yminlist = []
+        xmaxlist = []
+        ymaxlist = []
+
+        if not shape_list:
+            self.app.inform.emit(_("[WARNING_NOTCL] No shape selected. Please Select a shape to flip!"))
+            return
+        else:
+            with self.app.proc_container.new(_("Applying Flip")):
+                try:
+                    # get mirroring coords from the point entry
+                    if self.flip_ref_cb.isChecked():
+                        px, py = eval('{}'.format(self.flip_ref_entry.text()))
+                    # get mirroing coords from the center of an all-enclosing bounding box
+                    else:
+                        # first get a bounding box to fit all
+                        for sha in shape_list:
+                            xmin, ymin, xmax, ymax = sha.bounds()
+                            xminlist.append(xmin)
+                            yminlist.append(ymin)
+                            xmaxlist.append(xmax)
+                            ymaxlist.append(ymax)
+
+                        # get the minimum x,y and maximum x,y for all objects selected
+                        xminimal = min(xminlist)
+                        yminimal = min(yminlist)
+                        xmaximal = max(xmaxlist)
+                        ymaximal = max(ymaxlist)
+
+                        px = 0.5 * (xminimal + xmaximal)
+                        py = 0.5 * (yminimal + ymaximal)
+
+                    self.app.progress.emit(20)
+
+                    # execute mirroring
+                    for sha in shape_list:
+                        if axis is 'X':
+                            sha.mirror('X', (px, py))
+                            self.app.inform.emit(_('[success] Flip on the Y axis done ...'))
+                        elif axis is 'Y':
+                            sha.mirror('Y', (px, py))
+                            self.app.inform.emit(_('[success] Flip on the X axis done ...'))
+                        self.draw_app.plot_all()
+
+                    #     self.draw_app.add_shape(DrawToolShape(sha.geo))
+                    #
+                    # self.draw_app.transform_complete.emit()
+
+                    self.app.progress.emit(100)
+
+                except Exception as e:
+                    self.app.inform.emit(_("[ERROR_NOTCL] Due of %s, Flip action was not executed.") % str(e))
+                    return
+
+    def on_skew(self, axis, num):
+        shape_list = self.draw_app.selected
+        xminlist = []
+        yminlist = []
+
+        if not shape_list:
+            self.app.inform.emit(_("[WARNING_NOTCL] No shape selected. Please Select a shape to shear/skew!"))
+            return
+        else:
+            with self.app.proc_container.new(_("Applying Skew")):
+                try:
+                    # first get a bounding box to fit all
+                    for sha in shape_list:
+                        xmin, ymin, xmax, ymax = sha.bounds()
+                        xminlist.append(xmin)
+                        yminlist.append(ymin)
+
+                    # get the minimum x,y and maximum x,y for all objects selected
+                    xminimal = min(xminlist)
+                    yminimal = min(yminlist)
+
+                    self.app.progress.emit(20)
+
+                    for sha in shape_list:
+                        if axis is 'X':
+                            sha.skew(num, 0, point=(xminimal, yminimal))
+                        elif axis is 'Y':
+                            sha.skew(0, num, point=(xminimal, yminimal))
+                        self.draw_app.plot_all()
+
+                    #     self.draw_app.add_shape(DrawToolShape(sha.geo))
+                    #
+                    # self.draw_app.transform_complete.emit()
+
+                    self.app.inform.emit(_('[success] Skew on the %s axis done ...') % str(axis))
+                    self.app.progress.emit(100)
+
+                except Exception as e:
+                    self.app.inform.emit(_("[ERROR_NOTCL] Due of %s, Skew action was not executed.") % str(e))
+                    return
+
+    def on_scale(self, axis, xfactor, yfactor, point=None):
+        shape_list = self.draw_app.selected
+        xminlist = []
+        yminlist = []
+        xmaxlist = []
+        ymaxlist = []
+
+        if not shape_list:
+            self.app.inform.emit(_("[WARNING_NOTCL] No shape selected. Please Select a shape to scale!"))
+            return
+        else:
+            with self.app.proc_container.new(_("Applying Scale")):
+                try:
+                    # first get a bounding box to fit all
+                    for sha in shape_list:
+                        xmin, ymin, xmax, ymax = sha.bounds()
+                        xminlist.append(xmin)
+                        yminlist.append(ymin)
+                        xmaxlist.append(xmax)
+                        ymaxlist.append(ymax)
+
+                    # get the minimum x,y and maximum x,y for all objects selected
+                    xminimal = min(xminlist)
+                    yminimal = min(yminlist)
+                    xmaximal = max(xmaxlist)
+                    ymaximal = max(ymaxlist)
+
+                    self.app.progress.emit(20)
+
+                    if point is None:
+                        px = 0.5 * (xminimal + xmaximal)
+                        py = 0.5 * (yminimal + ymaximal)
+                    else:
+                        px = 0
+                        py = 0
+
+                    for sha in shape_list:
+                        sha.scale(xfactor, yfactor, point=(px, py))
+                        self.draw_app.plot_all()
+
+                    #     self.draw_app.add_shape(DrawToolShape(sha.geo))
+                    #
+                    # self.draw_app.transform_complete.emit()
+
+                    self.app.inform.emit(_('[success] Scale on the %s axis done ...') % str(axis))
+                    self.app.progress.emit(100)
+                except Exception as e:
+                    self.app.inform.emit(_("[ERROR_NOTCL] Due of %s, Scale action was not executed.") % str(e))
+                    return
+
+    def on_offset(self, axis, num):
+        shape_list = self.draw_app.selected
+        xminlist = []
+        yminlist = []
+
+        if not shape_list:
+            self.app.inform.emit(_("[WARNING_NOTCL] No shape selected. Please Select a shape to offset!"))
+            return
+        else:
+            with self.app.proc_container.new(_("Applying Offset")):
+                try:
+                    # first get a bounding box to fit all
+                    for sha in shape_list:
+                        xmin, ymin, xmax, ymax = sha.bounds()
+                        xminlist.append(xmin)
+                        yminlist.append(ymin)
+
+                    # get the minimum x,y and maximum x,y for all objects selected
+                    xminimal = min(xminlist)
+                    yminimal = min(yminlist)
+                    self.app.progress.emit(20)
+
+                    for sha in shape_list:
+                        if axis is 'X':
+                            sha.offset((num, 0))
+                        elif axis is 'Y':
+                            sha.offset((0, num))
+                        self.draw_app.plot_all()
+
+                    #     self.draw_app.add_shape(DrawToolShape(sha.geo))
+                    #
+                    # self.draw_app.transform_complete.emit()
+
+                    self.app.inform.emit(_('[success] Offset on the %s axis done ...') % str(axis))
+                    self.app.progress.emit(100)
+
+                except Exception as e:
+                    self.app.inform.emit(_("[ERROR_NOTCL] Due of %s, Offset action was not executed.") % str(e))
+                    return
+
+    def on_rotate_key(self):
+        val_box = FCInputDialog(title=_("Rotate ..."),
+                                text=_('Enter an Angle Value (degrees):'),
+                                min=-359.9999, max=360.0000, decimals=4,
+                                init_val=float(self.app.defaults['tools_transform_rotate']))
+        val_box.setWindowIcon(QtGui.QIcon('share/rotate.png'))
+
+        val, ok = val_box.get_value()
+        if ok:
+            self.on_rotate(val=val)
+            self.app.inform.emit(
+                _("[success] Geometry shape rotate done...")
+            )
+            return
+        else:
+            self.app.inform.emit(
+                _("[WARNING_NOTCL] Geometry shape rotate cancelled...")
+            )
+
+    def on_offx_key(self):
+        units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+
+        val_box = FCInputDialog(title=_("Offset on X axis ..."),
+                                text=(_('Enter a distance Value (%s):') % str(units)),
+                                min=-9999.9999, max=10000.0000, decimals=4,
+                                init_val=float(self.app.defaults['tools_transform_offset_x']))
+        val_box.setWindowIcon(QtGui.QIcon('share/offsetx32.png'))
+
+        val, ok = val_box.get_value()
+        if ok:
+            self.on_offx(val=val)
+            self.app.inform.emit(
+                _("[success] Geometry shape offset on X axis done..."))
+            return
+        else:
+            self.app.inform.emit(
+                _("[WARNING_NOTCL] Geometry shape offset X cancelled..."))
+
+    def on_offy_key(self):
+        units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+
+        val_box = FCInputDialog(title=_("Offset on Y axis ..."),
+                                text=(_('Enter a distance Value (%s):') % str(units)),
+                                min=-9999.9999, max=10000.0000, decimals=4,
+                                init_val=float(self.app.defaults['tools_transform_offset_y']))
+        val_box.setWindowIcon(QtGui.QIcon('share/offsety32.png'))
+
+        val, ok = val_box.get_value()
+        if ok:
+            self.on_offx(val=val)
+            self.app.inform.emit(
+                _("[success] Geometry shape offset on Y axis done..."))
+            return
+        else:
+            self.app.inform.emit(
+                _("[WARNING_NOTCL] Geometry shape offset Y cancelled..."))
+
+    def on_skewx_key(self):
+        val_box = FCInputDialog(title=_("Skew on X axis ..."),
+                                text=_('Enter an Angle Value (degrees):'),
+                                min=-359.9999, max=360.0000, decimals=4,
+                                init_val=float(self.app.defaults['tools_transform_skew_x']))
+        val_box.setWindowIcon(QtGui.QIcon('share/skewX.png'))
+
+        val, ok = val_box.get_value()
+        if ok:
+            self.on_skewx(val=val)
+            self.app.inform.emit(
+                _("[success] Geometry shape skew on X axis done..."))
+            return
+        else:
+            self.app.inform.emit(
+                _("[WARNING_NOTCL] Geometry shape skew X cancelled..."))
+
+    def on_skewy_key(self):
+        val_box = FCInputDialog(title=_("Skew on Y axis ..."),
+                                text=_('Enter an Angle Value (degrees):'),
+                                min=-359.9999, max=360.0000, decimals=4,
+                                init_val=float(self.app.defaults['tools_transform_skew_y']))
+        val_box.setWindowIcon(QtGui.QIcon('share/skewY.png'))
+
+        val, ok = val_box.get_value()
+        if ok:
+            self.on_skewx(val=val)
+            self.app.inform.emit(
+                _("[success] Geometry shape skew on Y axis done..."))
+            return
+        else:
+            self.app.inform.emit(
+                _("[WARNING_NOTCL] Geometry shape skew Y cancelled..."))

+ 12 - 2
flatcamGUI/FlatCAMGUI.py

@@ -472,6 +472,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                                                                     _('Buffer\tB'))
                                                                     _('Buffer\tB'))
         self.grb_add_scale_menuitem = self.grb_editor_menu.addAction(QtGui.QIcon('share/scale32.png'),
         self.grb_add_scale_menuitem = self.grb_editor_menu.addAction(QtGui.QIcon('share/scale32.png'),
                                                                     _('Scale\tS'))
                                                                     _('Scale\tS'))
+        self.grb_transform_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon('share/transform.png'),_( "Transform\tALT+R")
+        )
         self.grb_editor_menu.addSeparator()
         self.grb_editor_menu.addSeparator()
 
 
         self.grb_copy_menuitem = self.grb_editor_menu.addAction(QtGui.QIcon('share/copy32.png'), _('Copy\tC'))
         self.grb_copy_menuitem = self.grb_editor_menu.addAction(QtGui.QIcon('share/copy32.png'), _('Copy\tC'))
@@ -692,6 +695,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.aperture_copy_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), _("Copy"))
         self.aperture_copy_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), _("Copy"))
         self.aperture_delete_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/trash32.png'),
         self.aperture_delete_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/trash32.png'),
                                                                    _("Delete"))
                                                                    _("Delete"))
+        self.grb_transform_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/transform.png'),
+                                                                 _("Transformations"))
         self.grb_edit_toolbar.addSeparator()
         self.grb_edit_toolbar.addSeparator()
         self.aperture_move_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), _("Move"))
         self.aperture_move_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), _("Move"))
 
 
@@ -1784,6 +1789,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.aperture_copy_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), _("Copy"))
         self.aperture_copy_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), _("Copy"))
         self.aperture_delete_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/trash32.png'),
         self.aperture_delete_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/trash32.png'),
                                                                    _("Delete"))
                                                                    _("Delete"))
+        self.grb_transform_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/transform.png'),
+                                                                 _("Transformations"))
         self.grb_edit_toolbar.addSeparator()
         self.grb_edit_toolbar.addSeparator()
         self.aperture_move_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), _("Move"))
         self.aperture_move_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), _("Move"))
 
 
@@ -2268,7 +2275,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.geo_editor.delete_selected()
                     self.app.geo_editor.delete_selected()
                     self.app.geo_editor.replot()
                     self.app.geo_editor.replot()
 
 
-                # Move
+                # Rotate
                 if key == QtCore.Qt.Key_Space or key == 'Space':
                 if key == QtCore.Qt.Key_Space or key == 'Space':
                     self.app.geo_editor.transform_tool.on_rotate_key()
                     self.app.geo_editor.transform_tool.on_rotate_key()
 
 
@@ -2484,6 +2491,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.on_toggle_notebook()
                     self.app.on_toggle_notebook()
                     return
                     return
 
 
+                # Rotate
+                if key == QtCore.Qt.Key_Space or key == 'Space':
+                    self.app.grb_editor.transform_tool.on_rotate_key()
+
                 # Switch to Project Tab
                 # Switch to Project Tab
                 if key == QtCore.Qt.Key_1 or key == '1':
                 if key == QtCore.Qt.Key_1 or key == '1':
                     self.app.grb_editor.launched_from_shortcuts = True
                     self.app.grb_editor.launched_from_shortcuts = True
@@ -2515,7 +2526,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         self.app.inform.emit(_("[WARNING_NOTCL] Cancelled. Nothing selected to copy."))
                         self.app.inform.emit(_("[WARNING_NOTCL] Cancelled. Nothing selected to copy."))
                     return
                     return
 
 
-
                 # Scale Tool
                 # Scale Tool
                 if key == QtCore.Qt.Key_B or key == 'B':
                 if key == QtCore.Qt.Key_B or key == 'B':
                     self.app.grb_editor.launched_from_shortcuts = True
                     self.app.grb_editor.launched_from_shortcuts = True