Explorar o código

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

Marius Stanciu %!s(int64=6) %!d(string=hai) anos
pai
achega
b3aeb497ec
Modificáronse 5 ficheiros con 1077 adicións e 85 borrados
  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()
                 # 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:
                 self.marked_rows.append(False)
 
@@ -1209,7 +1209,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         if mark_all:
             for aperture in self.apertures:
                 # 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:
             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
 
 - 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.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
         self.tools = {
             "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(
             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
         self.paint_tooldia = None
 
         self.paint_tool = PaintOptionsTool(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):
         self.shapes.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.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)
 
         # 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_release', self.on_canvas_click_release)
 
-
     def disconnect_canvas_event_handlers(self):
 
         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.run()
 
-    def on_transform_tool(self):
-        transform_tool = TransformEditorTool(self.app, self)
-        transform_tool.run()
-
     def on_tool_select(self, tool):
         """
         Behavior of the toolbar. Tool initialization.

+ 1026 - 43
flatcamEditors/FlatCAMGrbEditor.py

@@ -12,7 +12,7 @@ import copy
 
 from camlib import *
 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 FlatCAMObj import FlatCAMGerber
 from FlatCAMTool import FlatCAMTool
@@ -242,9 +242,9 @@ class FCScale(FCShapeTool):
 
         if self.draw_app.app.ui.splitter.sizes()[0] == 0:
             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.scale_tool_frame.show()
 
@@ -254,7 +254,7 @@ class FCScale(FCShapeTool):
             pass
         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.complete = True
         self.draw_app.select_tool("select")
@@ -262,7 +262,7 @@ class FCScale(FCShapeTool):
 
     def on_scale_click(self):
         self.draw_app.on_scale()
-        self.deactivate()
+        self.deactivate_scale()
 
 
 class FCBuffer(FCShapeTool):
@@ -279,9 +279,9 @@ class FCBuffer(FCShapeTool):
 
         if self.draw_app.app.ui.splitter.sizes()[0] == 0:
             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.buffer_tool_frame.show()
 
@@ -291,7 +291,7 @@ class FCBuffer(FCShapeTool):
             pass
         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.complete = True
         self.draw_app.select_tool("select")
@@ -299,7 +299,7 @@ class FCBuffer(FCShapeTool):
 
     def on_buffer_click(self):
         self.draw_app.on_buffer()
-        self.deactivate()
+        self.deactivate_buffer()
 
 
 class FCApertureMove(FCShapeTool):
@@ -492,6 +492,20 @@ class FCApertureSelect(DrawTool):
         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):
 
     draw_shape_idx = -1
@@ -759,6 +773,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
                                 "constructor": FCScale},
             "copy": {"button": self.app.ui.aperture_copy_btn,
                      "constructor": FCApertureCopy},
+            "transform": {"button": self.app.ui.grb_transform_btn,
+                          "constructor": FCTransform},
             "move": {"button": self.app.ui.aperture_move_btn,
                      "constructor": FCApertureMove},
         }
@@ -794,32 +810,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # this var will store the state of the toolbar before starting the editor
         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
         self.apdim_lbl.hide()
         self.apdim_entry.hide()
@@ -889,6 +879,34 @@ class FlatCAMGrbEditor(QtCore.QObject):
         def entry2option(option, entry):
             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
         self.editor_active = False
 
@@ -1256,7 +1274,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             self.apdim_entry.hide()
             self.apsize_entry.setReadOnly(False)
 
-    def activate(self):
+    def activate_grb_editor(self):
         self.connect_canvas_event_handlers()
 
         # init working objects
@@ -1294,7 +1312,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # Tell the App that the editor is active
         self.editor_active = True
 
-    def deactivate(self):
+    def deactivate_grb_editor(self):
         self.disconnect_canvas_event_handlers()
         self.clear()
         self.app.ui.grb_edit_toolbar.setDisabled(True)
@@ -1394,8 +1412,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
         :return: None
         """
 
-        self.deactivate()
-        self.activate()
+        self.deactivate_grb_editor()
+        self.activate_grb_editor()
 
         # create a reference to the source object
         self.gerber_obj = orig_grb_obj
@@ -1445,9 +1463,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
                     except ValueError:
                         break
 
-        for k, v in self.gerber_obj.apertures.items():
-            print(k, v)
-
         for apid in self.gerber_obj.apertures:
             self.grb_plot_promises.append(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':
             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'))
         self.grb_add_scale_menuitem = self.grb_editor_menu.addAction(QtGui.QIcon('share/scale32.png'),
                                                                     _('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_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_delete_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/trash32.png'),
                                                                    _("Delete"))
+        self.grb_transform_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/transform.png'),
+                                                                 _("Transformations"))
         self.grb_edit_toolbar.addSeparator()
         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_delete_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/trash32.png'),
                                                                    _("Delete"))
+        self.grb_transform_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/transform.png'),
+                                                                 _("Transformations"))
         self.grb_edit_toolbar.addSeparator()
         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.replot()
 
-                # Move
+                # Rotate
                 if key == QtCore.Qt.Key_Space or key == 'Space':
                     self.app.geo_editor.transform_tool.on_rotate_key()
 
@@ -2484,6 +2491,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.on_toggle_notebook()
                     return
 
+                # Rotate
+                if key == QtCore.Qt.Key_Space or key == 'Space':
+                    self.app.grb_editor.transform_tool.on_rotate_key()
+
                 # Switch to Project Tab
                 if key == QtCore.Qt.Key_1 or key == '1':
                     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."))
                     return
 
-
                 # Scale Tool
                 if key == QtCore.Qt.Key_B or key == 'B':
                     self.app.grb_editor.launched_from_shortcuts = True