Переглянути джерело

- Tool Punch Gerber - updated the UI
- Tool Panelize - updated the UI
- Tool Extract Drills - updated the UI
- Tool QRcode - updated the UI
- Tool SolderPaste - updated the UI
- Tool DblSided - updated the UI

Marius Stanciu 5 роки тому
батько
коміт
0a64b02397

+ 6 - 0
CHANGELOG.md

@@ -17,6 +17,12 @@ CHANGELOG for FlatCAM beta
 - more typos fixed in Excellon parser, slots processing
 - fixed Extract Drills Tool to work with the new Excellon data format
 - minor fix in App Tools that were updated to have UI in a separate class
+- Tool Punch Gerber - updated the UI
+- Tool Panelize - updated the UI
+- Tool Extract Drills - updated the UI
+- Tool QRcode - updated the UI
+- Tool SolderPaste - updated the UI
+- Tool DblSided - updated the UI
 
 15.06.2020
 

+ 748 - 714
appTools/ToolDblSided.py

@@ -23,874 +23,908 @@ log = logging.getLogger('base')
 
 class DblSidedTool(AppTool):
 
-    toolName = _("2-Sided PCB")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
         self.decimals = self.app.decimals
 
-        # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-
-        self.layout.addWidget(QtWidgets.QLabel(""))
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = DsidedUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
-        # ## Grid Layout
-        grid_lay = QtWidgets.QGridLayout()
-        grid_lay.setColumnStretch(0, 1)
-        grid_lay.setColumnStretch(1, 0)
-        self.layout.addLayout(grid_lay)
+        # ## Signals
+        self.ui.mirror_gerber_button.clicked.connect(self.on_mirror_gerber)
+        self.ui.mirror_exc_button.clicked.connect(self.on_mirror_exc)
+        self.ui.mirror_geo_button.clicked.connect(self.on_mirror_geo)
 
-        # Objects to be mirrored
-        self.m_objects_label = QtWidgets.QLabel("<b>%s:</b>" % _("Mirror Operation"))
-        self.m_objects_label.setToolTip('%s.' % _("Objects to be mirrored"))
+        self.ui.add_point_button.clicked.connect(self.on_point_add)
+        self.ui.add_drill_point_button.clicked.connect(self.on_drill_add)
+        self.ui.delete_drill_point_button.clicked.connect(self.on_drill_delete_last)
+        self.ui.box_type_radio.activated_custom.connect(self.on_combo_box_type)
 
-        grid_lay.addWidget(self.m_objects_label, 0, 0, 1, 2)
+        self.ui.axis_location.group_toggle_fn = self.on_toggle_pointbox
 
-        # ## Gerber Object to mirror
-        self.gerber_object_combo = FCComboBox()
-        self.gerber_object_combo.setModel(self.app.collection)
-        self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.gerber_object_combo.is_last = True
-        self.gerber_object_combo.obj_type = "Gerber"
+        self.ui.point_entry.textChanged.connect(lambda val: self.ui.align_ref_label_val.set_value(val))
 
-        self.botlay_label = QtWidgets.QLabel("%s:" % _("GERBER"))
-        self.botlay_label.setToolTip('%s.' % _("Gerber to be mirrored"))
+        self.ui.xmin_btn.clicked.connect(self.on_xmin_clicked)
+        self.ui.ymin_btn.clicked.connect(self.on_ymin_clicked)
+        self.ui.xmax_btn.clicked.connect(self.on_xmax_clicked)
+        self.ui.ymax_btn.clicked.connect(self.on_ymax_clicked)
 
-        self.mirror_gerber_button = QtWidgets.QPushButton(_("Mirror"))
-        self.mirror_gerber_button.setToolTip(
-            _("Mirrors (flips) the specified object around \n"
-              "the specified axis. Does not create a new \n"
-              "object, but modifies it.")
+        self.ui.center_btn.clicked.connect(
+            lambda: self.ui.point_entry.set_value(self.ui.center_entry.get_value())
         )
-        self.mirror_gerber_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.mirror_gerber_button.setMinimumWidth(60)
 
-        grid_lay.addWidget(self.botlay_label, 1, 0)
-        grid_lay.addWidget(self.gerber_object_combo, 2, 0)
-        grid_lay.addWidget(self.mirror_gerber_button, 2, 1)
+        self.ui.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes)
+        self.ui.calculate_bb_button.clicked.connect(self.on_bbox_coordinates)
 
-        # ## Excellon Object to mirror
-        self.exc_object_combo = FCComboBox()
-        self.exc_object_combo.setModel(self.app.collection)
-        self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
-        self.exc_object_combo.is_last = True
-        self.exc_object_combo.obj_type = "Excellon"
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
 
-        self.excobj_label = QtWidgets.QLabel("%s:" % _("EXCELLON"))
-        self.excobj_label.setToolTip(_("Excellon Object to be mirrored."))
+        self.drill_values = ""
 
-        self.mirror_exc_button = QtWidgets.QPushButton(_("Mirror"))
-        self.mirror_exc_button.setToolTip(
-            _("Mirrors (flips) the specified object around \n"
-              "the specified axis. Does not create a new \n"
-              "object, but modifies it.")
-        )
-        self.mirror_exc_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.mirror_exc_button.setMinimumWidth(60)
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='Alt+D', **kwargs)
 
-        grid_lay.addWidget(self.excobj_label, 3, 0)
-        grid_lay.addWidget(self.exc_object_combo, 4, 0)
-        grid_lay.addWidget(self.mirror_exc_button, 4, 1)
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("Tool2Sided()")
 
-        # ## Geometry Object to mirror
-        self.geo_object_combo = FCComboBox()
-        self.geo_object_combo.setModel(self.app.collection)
-        self.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
-        self.geo_object_combo.is_last = True
-        self.geo_object_combo.obj_type = "Geometry"
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
 
-        self.geoobj_label = QtWidgets.QLabel("%s:" % _("GEOMETRY"))
-        self.geoobj_label.setToolTip(
-            _("Geometry Obj to be mirrored.")
-        )
+        AppTool.run(self)
+        self.set_tool_ui()
 
-        self.mirror_geo_button = QtWidgets.QPushButton(_("Mirror"))
-        self.mirror_geo_button.setToolTip(
-            _("Mirrors (flips) the specified object around \n"
-              "the specified axis. Does not create a new \n"
-              "object, but modifies it.")
-        )
-        self.mirror_geo_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.mirror_geo_button.setMinimumWidth(60)
+        self.app.ui.notebook.setTabText(2, _("2-Sided Tool"))
 
-        # grid_lay.addRow("Bottom Layer:", self.object_combo)
-        grid_lay.addWidget(self.geoobj_label, 5, 0)
-        grid_lay.addWidget(self.geo_object_combo, 6, 0)
-        grid_lay.addWidget(self.mirror_geo_button, 6, 1)
+    def set_tool_ui(self):
+        self.reset_fields()
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line, 7, 0, 1, 2)
+        self.ui.point_entry.set_value("")
+        self.ui.alignment_holes.set_value("")
 
-        self.layout.addWidget(QtWidgets.QLabel(""))
+        self.ui.mirror_axis.set_value(self.app.defaults["tools_2sided_mirror_axis"])
+        self.ui.axis_location.set_value(self.app.defaults["tools_2sided_axis_loc"])
+        self.ui.drill_dia.set_value(self.app.defaults["tools_2sided_drilldia"])
+        self.ui.align_axis_radio.set_value(self.app.defaults["tools_2sided_allign_axis"])
 
-        # ## Grid Layout
-        grid_lay1 = QtWidgets.QGridLayout()
-        grid_lay1.setColumnStretch(0, 0)
-        grid_lay1.setColumnStretch(1, 1)
-        self.layout.addLayout(grid_lay1)
+        self.ui.xmin_entry.set_value(0.0)
+        self.ui.ymin_entry.set_value(0.0)
+        self.ui.xmax_entry.set_value(0.0)
+        self.ui.ymax_entry.set_value(0.0)
+        self.ui.center_entry.set_value('')
 
-        # Objects to be mirrored
-        self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Mirror Parameters"))
-        self.param_label.setToolTip('%s.' % _("Parameters for the mirror operation"))
+        self.ui.align_ref_label_val.set_value('%.*f' % (self.decimals, 0.0))
 
-        grid_lay1.addWidget(self.param_label, 0, 0, 1, 2)
+        # run once to make sure that the obj_type attribute is updated in the FCComboBox
+        self.ui.box_type_radio.set_value('grb')
+        self.on_combo_box_type('grb')
 
-        # ## Axis
-        self.mirax_label = QtWidgets.QLabel('%s:' % _("Mirror Axis"))
-        self.mirax_label.setToolTip(_("Mirror vertically (X) or horizontally (Y)."))
-        self.mirror_axis = RadioSet([{'label': 'X', 'value': 'X'},
-                                     {'label': 'Y', 'value': 'Y'}])
+    def on_combo_box_type(self, val):
+        obj_type = {'grb': 0, 'exc': 1, 'geo': 2}[val]
+        self.ui.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.ui.box_combo.setCurrentIndex(0)
+        self.ui.box_combo.obj_type = {
+            "grb": "Gerber", "exc": "Excellon", "geo": "Geometry"}[val]
 
-        grid_lay1.addWidget(self.mirax_label, 2, 0)
-        grid_lay1.addWidget(self.mirror_axis, 2, 1, 1, 2)
+    def on_create_alignment_holes(self):
+        axis = self.ui.align_axis_radio.get_value()
+        mode = self.ui.axis_location.get_value()
 
-        # ## Axis Location
-        self.axloc_label = QtWidgets.QLabel('%s:' % _("Reference"))
-        self.axloc_label.setToolTip(
-            _("The coordinates used as reference for the mirror operation.\n"
-              "Can be:\n"
-              "- Point -> a set of coordinates (x,y) around which the object is mirrored\n"
-              "- Box -> a set of coordinates (x, y) obtained from the center of the\n"
-              "bounding box of another object selected below")
-        )
-        self.axis_location = RadioSet([{'label': _('Point'), 'value': 'point'},
-                                       {'label': _('Box'), 'value': 'box'}])
+        if mode == "point":
+            try:
+                px, py = self.ui.point_entry.get_value()
+            except TypeError:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("'Point' reference is selected and 'Point' coordinates "
+                                                              "are missing. Add them and retry."))
+                return
+        else:
+            selection_index = self.ui.box_combo.currentIndex()
+            model_index = self.app.collection.index(selection_index, 0, self.ui.gerber_object_combo.rootModelIndex())
+            try:
+                bb_obj = model_index.internalPointer().obj
+            except AttributeError:
+                model_index = self.app.collection.index(selection_index, 0, self.ui.exc_object_combo.rootModelIndex())
+                try:
+                    bb_obj = model_index.internalPointer().obj
+                except AttributeError:
+                    model_index = self.app.collection.index(selection_index, 0,
+                                                            self.ui.geo_object_combo.rootModelIndex())
+                    try:
+                        bb_obj = model_index.internalPointer().obj
+                    except AttributeError:
+                        self.app.inform.emit(
+                            '[WARNING_NOTCL] %s' % _("There is no Box reference object loaded. Load one and retry."))
+                        return
 
-        grid_lay1.addWidget(self.axloc_label, 4, 0)
-        grid_lay1.addWidget(self.axis_location, 4, 1, 1, 2)
+            xmin, ymin, xmax, ymax = bb_obj.bounds()
+            px = 0.5 * (xmin + xmax)
+            py = 0.5 * (ymin + ymax)
 
-        # ## Point/Box
-        self.point_entry = EvalEntry()
-        self.point_entry.setPlaceholderText(_("Point coordinates"))
+        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
 
-        # Add a reference
-        self.add_point_button = QtWidgets.QPushButton(_("Add"))
-        self.add_point_button.setToolTip(
-            _("Add the coordinates in format <b>(x, y)</b> through which the mirroring axis\n "
-              "selected in 'MIRROR AXIS' pass.\n"
-              "The (x, y) coordinates are captured by pressing SHIFT key\n"
-              "and left mouse button click on canvas or you can enter the coordinates manually.")
-        )
-        self.add_point_button.setStyleSheet("""
-                                QPushButton
-                                {
-                                    font-weight: bold;
-                                }
-                                """)
-        self.add_point_button.setMinimumWidth(60)
+        dia = float(self.drill_dia.get_value())
+        if dia == '':
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("No value or wrong format in Drill Dia entry. Add it and retry."))
+            return
 
-        grid_lay1.addWidget(self.point_entry, 7, 0, 1, 2)
-        grid_lay1.addWidget(self.add_point_button, 7, 2)
+        tools = {}
+        tools[1] = {}
+        tools[1]["tooldia"] = dia
+        tools[1]['solid_geometry'] = []
 
-        # ## Grid Layout
-        grid_lay2 = QtWidgets.QGridLayout()
-        grid_lay2.setColumnStretch(0, 0)
-        grid_lay2.setColumnStretch(1, 1)
-        self.layout.addLayout(grid_lay2)
+        # holes = self.alignment_holes.get_value()
+        holes = eval('[{}]'.format(self.ui.alignment_holes.text()))
+        if not holes:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Alignment Drill Coordinates to use. "
+                                                          "Add them and retry."))
+            return
 
-        self.box_type_label = QtWidgets.QLabel('%s:' % _("Reference Object"))
-        self.box_type_label.setToolTip(
-            _("It can be of type: Gerber or Excellon or Geometry.\n"
-              "The coordinates of the center of the bounding box are used\n"
-              "as reference for mirror operation.")
-        )
+        for hole in holes:
+            point = Point(hole)
+            point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
 
-        # Type of object used as BOX reference
-        self.box_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'grb'},
-                                        {'label': _('Excellon'), 'value': 'exc'},
-                                        {'label': _('Geometry'), 'value': 'geo'}])
+            tools[1]['drills'] = [point, point_mirror]
+            tools[1]['solid_geometry'].append(point)
+            tools[1]['solid_geometry'].append(point_mirror)
 
-        self.box_type_label.hide()
-        self.box_type_radio.hide()
+        def obj_init(obj_inst, app_inst):
+            obj_inst.tools = tools
+            obj_inst.create_geometry()
+            obj_inst.source_file = app_inst.export_excellon(obj_name=obj_inst.options['name'], local_use=obj_inst,
+                                                            filename=None, use_thread=False)
 
-        grid_lay2.addWidget(self.box_type_label, 0, 0, 1, 2)
-        grid_lay2.addWidget(self.box_type_radio, 1, 0, 1, 2)
+        self.app.app_obj.new_object("excellon", "Alignment Drills", obj_init)
+        self.drill_values = ''
+        self.app.inform.emit('[success] %s' % _("Excellon object with alignment drills created..."))
 
-        # Object used as BOX reference
-        self.box_combo = FCComboBox()
-        self.box_combo.setModel(self.app.collection)
-        self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.box_combo.is_last = True
+    def on_mirror_gerber(self):
+        selection_index = self.ui.gerber_object_combo.currentIndex()
+        # fcobj = self.app.collection.object_list[selection_index]
+        model_index = self.app.collection.index(selection_index, 0, self.ui.gerber_object_combo.rootModelIndex())
+        try:
+            fcobj = model_index.internalPointer().obj
+        except Exception:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return
 
-        self.box_combo.hide()
+        if fcobj.kind != 'gerber':
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored."))
+            return
 
-        grid_lay2.addWidget(self.box_combo, 3, 0, 1, 2)
+        axis = self.ui.mirror_axis.get_value()
+        mode = self.ui.axis_location.get_value()
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay2.addWidget(separator_line, 4, 0, 1, 2)
+        if mode == "point":
+            try:
+                px, py = self.ui.point_entry.get_value()
+            except TypeError:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Point coordinates in the Point field. "
+                                                              "Add coords and try again ..."))
+                return
 
-        grid_lay2.addWidget(QtWidgets.QLabel(""), 5, 0, 1, 2)
+        else:
+            selection_index_box = self.ui.box_combo.currentIndex()
+            model_index_box = self.app.collection.index(selection_index_box, 0, self.ui.box_combo.rootModelIndex())
+            try:
+                bb_obj = model_index_box.internalPointer().obj
+            except Exception:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ..."))
+                return
 
-        # ## Title Bounds Values
-        self.bv_label = QtWidgets.QLabel("<b>%s:</b>" % _('Bounds Values'))
-        self.bv_label.setToolTip(
-            _("Select on canvas the object(s)\n"
-              "for which to calculate bounds values.")
-        )
-        grid_lay2.addWidget(self.bv_label, 6, 0, 1, 2)
+            xmin, ymin, xmax, ymax = bb_obj.bounds()
+            px = 0.5 * (xmin + xmax)
+            py = 0.5 * (ymin + ymax)
 
-        # Xmin value
-        self.xmin_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.xmin_entry.set_precision(self.decimals)
-        self.xmin_entry.set_range(-9999.9999, 9999.9999)
+        fcobj.mirror(axis, [px, py])
+        self.app.app_obj.object_changed.emit(fcobj)
+        fcobj.plot()
+        self.app.inform.emit('[success] Gerber %s %s...' % (str(fcobj.options['name']), _("was mirrored")))
 
-        self.xmin_btn = FCButton('%s:' % _("X min"))
-        self.xmin_btn.setToolTip(
-            _("Minimum location.")
-        )
-        self.xmin_entry.setReadOnly(True)
+    def on_mirror_exc(self):
+        selection_index = self.ui.exc_object_combo.currentIndex()
+        # fcobj = self.app.collection.object_list[selection_index]
+        model_index = self.app.collection.index(selection_index, 0, self.ui.exc_object_combo.rootModelIndex())
+        try:
+            fcobj = model_index.internalPointer().obj
+        except Exception:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ..."))
+            return
 
-        grid_lay2.addWidget(self.xmin_btn, 7, 0)
-        grid_lay2.addWidget(self.xmin_entry, 7, 1)
+        if fcobj.kind != 'excellon':
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored."))
+            return
 
-        # Ymin value
-        self.ymin_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.ymin_entry.set_precision(self.decimals)
-        self.ymin_entry.set_range(-9999.9999, 9999.9999)
+        axis = self.ui.mirror_axis.get_value()
+        mode = self.ui.axis_location.get_value()
 
-        self.ymin_btn = FCButton('%s:' % _("Y min"))
-        self.ymin_btn.setToolTip(
-            _("Minimum location.")
-        )
-        self.ymin_entry.setReadOnly(True)
+        if mode == "point":
+            try:
+                px, py = self.ui.point_entry.get_value()
+            except Exception as e:
+                log.debug("DblSidedTool.on_mirror_geo() --> %s" % str(e))
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Point coordinates in the Point field. "
+                                                              "Add coords and try again ..."))
+                return
+        else:
+            selection_index_box = self.ui.box_combo.currentIndex()
+            model_index_box = self.app.collection.index(selection_index_box, 0, self.ui.box_combo.rootModelIndex())
+            try:
+                bb_obj = model_index_box.internalPointer().obj
+            except Exception as e:
+                log.debug("DblSidedTool.on_mirror_geo() --> %s" % str(e))
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ..."))
+                return
 
-        grid_lay2.addWidget(self.ymin_btn, 8, 0)
-        grid_lay2.addWidget(self.ymin_entry, 8, 1)
+            xmin, ymin, xmax, ymax = bb_obj.bounds()
+            px = 0.5 * (xmin + xmax)
+            py = 0.5 * (ymin + ymax)
 
-        # Xmax value
-        self.xmax_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.xmax_entry.set_precision(self.decimals)
-        self.xmax_entry.set_range(-9999.9999, 9999.9999)
+        fcobj.mirror(axis, [px, py])
+        self.app.app_obj.object_changed.emit(fcobj)
+        fcobj.plot()
+        self.app.inform.emit('[success] Excellon %s %s...' % (str(fcobj.options['name']), _("was mirrored")))
 
-        self.xmax_btn = FCButton('%s:' % _("X max"))
-        self.xmax_btn.setToolTip(
-            _("Maximum location.")
-        )
-        self.xmax_entry.setReadOnly(True)
+    def on_mirror_geo(self):
+        selection_index = self.ui.geo_object_combo.currentIndex()
+        # fcobj = self.app.collection.object_list[selection_index]
+        model_index = self.app.collection.index(selection_index, 0, self.ui.geo_object_combo.rootModelIndex())
+        try:
+            fcobj = model_index.internalPointer().obj
+        except Exception:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Geometry object loaded ..."))
+            return
 
-        grid_lay2.addWidget(self.xmax_btn, 9, 0)
-        grid_lay2.addWidget(self.xmax_entry, 9, 1)
+        if fcobj.kind != 'geometry':
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored."))
+            return
 
-        # Ymax value
-        self.ymax_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.ymax_entry.set_precision(self.decimals)
-        self.ymax_entry.set_range(-9999.9999, 9999.9999)
+        axis = self.ui.mirror_axis.get_value()
+        mode = self.ui.axis_location.get_value()
 
-        self.ymax_btn = FCButton('%s:' % _("Y max"))
-        self.ymax_btn.setToolTip(
-            _("Maximum location.")
-        )
-        self.ymax_entry.setReadOnly(True)
+        if mode == "point":
+            px, py = self.ui.point_entry.get_value()
+        else:
+            selection_index_box = self.ui.box_combo.currentIndex()
+            model_index_box = self.app.collection.index(selection_index_box, 0, self.ui.box_combo.rootModelIndex())
+            try:
+                bb_obj = model_index_box.internalPointer().obj
+            except Exception:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ..."))
+                return
 
-        grid_lay2.addWidget(self.ymax_btn, 10, 0)
-        grid_lay2.addWidget(self.ymax_entry, 10, 1)
+            xmin, ymin, xmax, ymax = bb_obj.bounds()
+            px = 0.5 * (xmin + xmax)
+            py = 0.5 * (ymin + ymax)
 
-        # Center point value
-        self.center_entry = FCEntry()
-        self.center_entry.setPlaceholderText(_("Center point coordinates"))
+        fcobj.mirror(axis, [px, py])
+        self.app.app_obj.object_changed.emit(fcobj)
+        fcobj.plot()
+        self.app.inform.emit('[success] Geometry %s %s...' % (str(fcobj.options['name']), _("was mirrored")))
 
-        self.center_btn = FCButton('%s:' % _("Centroid"))
-        self.center_btn.setToolTip(
-            _("The center point location for the rectangular\n"
-              "bounding shape. Centroid. Format is (x, y).")
-        )
-        self.center_entry.setReadOnly(True)
+    def on_point_add(self):
+        val = self.app.defaults["global_point_clipboard_format"] % \
+              (self.decimals, self.app.pos[0], self.decimals, self.app.pos[1])
+        self.ui.point_entry.set_value(val)
 
-        grid_lay2.addWidget(self.center_btn, 12, 0)
-        grid_lay2.addWidget(self.center_entry, 12, 1)
+    def on_drill_add(self):
+        self.drill_values += (self.app.defaults["global_point_clipboard_format"] %
+                              (self.decimals, self.app.pos[0], self.decimals, self.app.pos[1])) + ','
+        self.ui.alignment_holes.set_value(self.drill_values)
 
-        # Calculate Bounding box
-        self.calculate_bb_button = QtWidgets.QPushButton(_("Calculate Bounds Values"))
-        self.calculate_bb_button.setToolTip(
-            _("Calculate the enveloping rectangular shape coordinates,\n"
-              "for the selection of objects.\n"
-              "The envelope shape is parallel with the X, Y axis.")
-        )
-        self.calculate_bb_button.setStyleSheet("""
-                                QPushButton
-                                {
-                                    font-weight: bold;
-                                }
-                                """)
-        grid_lay2.addWidget(self.calculate_bb_button, 13, 0, 1, 2)
+    def on_drill_delete_last(self):
+        drill_values_without_last_tupple = self.drill_values.rpartition('(')[0]
+        self.drill_values = drill_values_without_last_tupple
+        self.ui.alignment_holes.set_value(self.drill_values)
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay2.addWidget(separator_line, 14, 0, 1, 2)
+    def on_toggle_pointbox(self):
+        if self.ui.axis_location.get_value() == "point":
+            self.ui.point_entry.show()
+            self.ui.add_point_button.show()
+            self.ui.box_type_label.hide()
+            self.ui.box_type_radio.hide()
+            self.ui.box_combo.hide()
+
+            self.ui.align_ref_label_val.set_value(self.ui.point_entry.get_value())
+        else:
+            self.ui.point_entry.hide()
+            self.ui.add_point_button.hide()
 
-        grid_lay2.addWidget(QtWidgets.QLabel(""), 15, 0, 1, 2)
+            self.ui.box_type_label.show()
+            self.ui.box_type_radio.show()
+            self.ui.box_combo.show()
 
-        # ## Alignment holes
-        self.alignment_label = QtWidgets.QLabel("<b>%s:</b>" % _('PCB Alignment'))
-        self.alignment_label.setToolTip(
-            _("Creates an Excellon Object containing the\n"
-              "specified alignment holes and their mirror\n"
-              "images.")
-        )
-        grid_lay2.addWidget(self.alignment_label, 25, 0, 1, 2)
+            self.ui.align_ref_label_val.set_value("Box centroid")
 
-        # ## Drill diameter for alignment holes
-        self.dt_label = QtWidgets.QLabel("%s:" % _('Drill Diameter'))
-        self.dt_label.setToolTip(
-            _("Diameter of the drill for the alignment holes.")
-        )
+    def on_bbox_coordinates(self):
 
-        self.drill_dia = FCDoubleSpinner(callback=self.confirmation_message)
-        self.drill_dia.setToolTip(
-            _("Diameter of the drill for the alignment holes.")
-        )
-        self.drill_dia.set_precision(self.decimals)
-        self.drill_dia.set_range(0.0000, 9999.9999)
+        xmin = Inf
+        ymin = Inf
+        xmax = -Inf
+        ymax = -Inf
 
-        grid_lay2.addWidget(self.dt_label, 26, 0)
-        grid_lay2.addWidget(self.drill_dia, 26, 1)
+        obj_list = self.app.collection.get_selected()
 
-        # ## Alignment Axis
-        self.align_ax_label = QtWidgets.QLabel('%s:' % _("Align Axis"))
-        self.align_ax_label.setToolTip(
-            _("Mirror vertically (X) or horizontally (Y).")
-        )
-        self.align_axis_radio = RadioSet([{'label': 'X', 'value': 'X'},
-                                          {'label': 'Y', 'value': 'Y'}])
+        if not obj_list:
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No object(s) selected..."))
+            return
 
-        grid_lay2.addWidget(self.align_ax_label, 27, 0)
-        grid_lay2.addWidget(self.align_axis_radio, 27, 1)
+        for obj in obj_list:
+            try:
+                gxmin, gymin, gxmax, gymax = obj.bounds()
+                xmin = min([xmin, gxmin])
+                ymin = min([ymin, gymin])
+                xmax = max([xmax, gxmax])
+                ymax = max([ymax, gymax])
+            except Exception as e:
+                log.warning("DEV WARNING: Tried to get bounds of empty geometry in DblSidedTool. %s" % str(e))
 
-        # ## Alignment Reference Point
-        self.align_ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
-        self.align_ref_label.setToolTip(
-            _("The reference point used to create the second alignment drill\n"
-              "from the first alignment drill, by doing mirror.\n"
-              "It can be modified in the Mirror Parameters -> Reference section")
-        )
+        self.ui.xmin_entry.set_value(xmin)
+        self.ui.ymin_entry.set_value(ymin)
+        self.ui.xmax_entry.set_value(xmax)
+        self.ui.ymax_entry.set_value(ymax)
+        cx = '%.*f' % (self.decimals, (((xmax - xmin) / 2.0) + xmin))
+        cy = '%.*f' % (self.decimals, (((ymax - ymin) / 2.0) + ymin))
+        val_txt = '(%s, %s)' % (cx, cy)
 
-        self.align_ref_label_val = EvalEntry()
-        self.align_ref_label_val.setToolTip(
-            _("The reference point used to create the second alignment drill\n"
-              "from the first alignment drill, by doing mirror.\n"
-              "It can be modified in the Mirror Parameters -> Reference section")
-        )
-        self.align_ref_label_val.setDisabled(True)
+        self.ui.center_entry.set_value(val_txt)
+        self.ui.axis_location.set_value('point')
+        self.ui.point_entry.set_value(val_txt)
+        self.app.delete_selection_shape()
 
-        grid_lay2.addWidget(self.align_ref_label, 28, 0)
-        grid_lay2.addWidget(self.align_ref_label_val, 28, 1)
+    def on_xmin_clicked(self):
+        xmin = self.ui.xmin_entry.get_value()
+        self.ui.axis_location.set_value('point')
 
-        grid_lay4 = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid_lay4)
+        try:
+            px, py = self.ui.point_entry.get_value()
+            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmin, self.decimals, py)
+        except TypeError:
+            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmin, self.decimals, 0.0)
+        self.ui.point_entry.set_value(val)
 
-        # ## Alignment holes
-        self.ah_label = QtWidgets.QLabel("%s:" % _('Alignment Drill Coordinates'))
-        self.ah_label.setToolTip(
-           _("Alignment holes (x1, y1), (x2, y2), ... "
-             "on one side of the mirror axis. For each set of (x, y) coordinates\n"
-             "entered here, a pair of drills will be created:\n\n"
-             "- one drill at the coordinates from the field\n"
-             "- one drill in mirror position over the axis selected above in the 'Align Axis'.")
-        )
+    def on_ymin_clicked(self):
+        ymin = self.ui.ymin_entry.get_value()
+        self.ui.axis_location.set_value('point')
 
-        self.alignment_holes = EvalEntry()
-        self.alignment_holes.setPlaceholderText(_("Drill coordinates"))
+        try:
+            px, py = self.ui.point_entry.get_value()
+            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, px, self.decimals, ymin)
+        except TypeError:
+            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, 0.0, self.decimals, ymin)
+        self.ui.point_entry.set_value(val)
 
-        grid_lay4.addWidget(self.ah_label, 0, 0, 1, 2)
-        grid_lay4.addWidget(self.alignment_holes, 1, 0, 1, 2)
+    def on_xmax_clicked(self):
+        xmax = self.ui.xmax_entry.get_value()
+        self.ui.axis_location.set_value('point')
 
-        self.add_drill_point_button = FCButton(_("Add"))
-        self.add_drill_point_button.setToolTip(
-            _("Add alignment drill holes coordinates in the format: (x1, y1), (x2, y2), ... \n"
-              "on one side of the alignment axis.\n\n"
-              "The coordinates set can be obtained:\n"
-              "- press SHIFT key and left mouse clicking on canvas. Then click Add.\n"
-              "- press SHIFT key and left mouse clicking on canvas. Then Ctrl+V in the field.\n"
-              "- press SHIFT key and left mouse clicking on canvas. Then RMB click in the field and click Paste.\n"
-              "- by entering the coords manually in the format: (x1, y1), (x2, y2), ...")
-        )
-        # self.add_drill_point_button.setStyleSheet("""
-        #                 QPushButton
-        #                 {
-        #                     font-weight: bold;
-        #                 }
-        #                 """)
+        try:
+            px, py = self.ui.point_entry.get_value()
+            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmax, self.decimals, py)
+        except TypeError:
+            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmax, self.decimals, 0.0)
+        self.ui.point_entry.set_value(val)
 
-        self.delete_drill_point_button = FCButton(_("Delete Last"))
-        self.delete_drill_point_button.setToolTip(
-            _("Delete the last coordinates tuple in the list.")
-        )
-        drill_hlay = QtWidgets.QHBoxLayout()
+    def on_ymax_clicked(self):
+        ymax = self.ui.ymax_entry.get_value()
+        self.ui.axis_location.set_value('point')
 
-        drill_hlay.addWidget(self.add_drill_point_button)
-        drill_hlay.addWidget(self.delete_drill_point_button)
+        try:
+            px, py = self.ui.point_entry.get_value()
+            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, px, self.decimals, ymax)
+        except TypeError:
+            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, 0.0, self.decimals, ymax)
+        self.ui.point_entry.set_value(val)
 
-        grid_lay4.addLayout(drill_hlay, 2, 0, 1, 2)
+    def reset_fields(self):
+        self.ui.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ui.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.ui.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
+        self.ui.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
 
-        # ## Buttons
-        self.create_alignment_hole_button = QtWidgets.QPushButton(_("Create Excellon Object"))
-        self.create_alignment_hole_button.setToolTip(
-            _("Creates an Excellon Object containing the\n"
-              "specified alignment holes and their mirror\n"
-              "images.")
-        )
-        self.create_alignment_hole_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.create_alignment_hole_button)
+        self.ui.gerber_object_combo.setCurrentIndex(0)
+        self.ui.exc_object_combo.setCurrentIndex(0)
+        self.ui.geo_object_combo.setCurrentIndex(0)
+        self.ui.box_combo.setCurrentIndex(0)
+        self.ui.box_type_radio.set_value('grb')
 
-        self.layout.addStretch()
+        self.drill_values = ""
+        self.ui.align_ref_label_val.set_value('')
 
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
 
-        # ## Signals
-        self.mirror_gerber_button.clicked.connect(self.on_mirror_gerber)
-        self.mirror_exc_button.clicked.connect(self.on_mirror_exc)
-        self.mirror_geo_button.clicked.connect(self.on_mirror_geo)
+class DsidedUI:
 
-        self.add_point_button.clicked.connect(self.on_point_add)
-        self.add_drill_point_button.clicked.connect(self.on_drill_add)
-        self.delete_drill_point_button.clicked.connect(self.on_drill_delete_last)
-        self.box_type_radio.activated_custom.connect(self.on_combo_box_type)
+    toolName = _("2-Sided PCB")
 
-        self.axis_location.group_toggle_fn = self.on_toggle_pointbox
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
 
-        self.point_entry.textChanged.connect(lambda val: self.align_ref_label_val.set_value(val))
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+
+        self.layout.addWidget(QtWidgets.QLabel(""))
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        grid_lay.setColumnStretch(0, 1)
+        grid_lay.setColumnStretch(1, 0)
+        self.layout.addLayout(grid_lay)
 
-        self.xmin_btn.clicked.connect(self.on_xmin_clicked)
-        self.ymin_btn.clicked.connect(self.on_ymin_clicked)
-        self.xmax_btn.clicked.connect(self.on_xmax_clicked)
-        self.ymax_btn.clicked.connect(self.on_ymax_clicked)
+        # Objects to be mirrored
+        self.m_objects_label = QtWidgets.QLabel("<b>%s:</b>" % _("Mirror Operation"))
+        self.m_objects_label.setToolTip('%s.' % _("Objects to be mirrored"))
 
-        self.center_btn.clicked.connect(
-            lambda: self.point_entry.set_value(self.center_entry.get_value())
+        grid_lay.addWidget(self.m_objects_label, 0, 0, 1, 2)
+
+        # ## Gerber Object to mirror
+        self.gerber_object_combo = FCComboBox()
+        self.gerber_object_combo.setModel(self.app.collection)
+        self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.gerber_object_combo.is_last = True
+        self.gerber_object_combo.obj_type = "Gerber"
+
+        self.botlay_label = QtWidgets.QLabel("%s:" % _("GERBER"))
+        self.botlay_label.setToolTip('%s.' % _("Gerber to be mirrored"))
+
+        self.mirror_gerber_button = QtWidgets.QPushButton(_("Mirror"))
+        self.mirror_gerber_button.setToolTip(
+            _("Mirrors (flips) the specified object around \n"
+              "the specified axis. Does not create a new \n"
+              "object, but modifies it.")
         )
+        self.mirror_gerber_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.mirror_gerber_button.setMinimumWidth(60)
 
-        self.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes)
-        self.calculate_bb_button.clicked.connect(self.on_bbox_coordinates)
+        grid_lay.addWidget(self.botlay_label, 1, 0)
+        grid_lay.addWidget(self.gerber_object_combo, 2, 0)
+        grid_lay.addWidget(self.mirror_gerber_button, 2, 1)
 
-        self.reset_button.clicked.connect(self.set_tool_ui)
+        # ## Excellon Object to mirror
+        self.exc_object_combo = FCComboBox()
+        self.exc_object_combo.setModel(self.app.collection)
+        self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.exc_object_combo.is_last = True
+        self.exc_object_combo.obj_type = "Excellon"
 
-        self.drill_values = ""
+        self.excobj_label = QtWidgets.QLabel("%s:" % _("EXCELLON"))
+        self.excobj_label.setToolTip(_("Excellon Object to be mirrored."))
 
-    def install(self, icon=None, separator=None, **kwargs):
-        AppTool.install(self, icon, separator, shortcut='Alt+D', **kwargs)
+        self.mirror_exc_button = QtWidgets.QPushButton(_("Mirror"))
+        self.mirror_exc_button.setToolTip(
+            _("Mirrors (flips) the specified object around \n"
+              "the specified axis. Does not create a new \n"
+              "object, but modifies it.")
+        )
+        self.mirror_exc_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.mirror_exc_button.setMinimumWidth(60)
 
-    def run(self, toggle=True):
-        self.app.defaults.report_usage("Tool2Sided()")
+        grid_lay.addWidget(self.excobj_label, 3, 0)
+        grid_lay.addWidget(self.exc_object_combo, 4, 0)
+        grid_lay.addWidget(self.mirror_exc_button, 4, 1)
 
-        if toggle:
-            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-            else:
-                try:
-                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        # if tab is populated with the tool but it does not have the focus, focus on it
-                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
-                            # focus on Tool Tab
-                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-                        else:
-                            self.app.ui.splitter.setSizes([0, 1])
-                except AttributeError:
-                    pass
-        else:
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
+        # ## Geometry Object to mirror
+        self.geo_object_combo = FCComboBox()
+        self.geo_object_combo.setModel(self.app.collection)
+        self.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
+        self.geo_object_combo.is_last = True
+        self.geo_object_combo.obj_type = "Geometry"
 
-        AppTool.run(self)
-        self.set_tool_ui()
+        self.geoobj_label = QtWidgets.QLabel("%s:" % _("GEOMETRY"))
+        self.geoobj_label.setToolTip(
+            _("Geometry Obj to be mirrored.")
+        )
 
-        self.app.ui.notebook.setTabText(2, _("2-Sided Tool"))
+        self.mirror_geo_button = QtWidgets.QPushButton(_("Mirror"))
+        self.mirror_geo_button.setToolTip(
+            _("Mirrors (flips) the specified object around \n"
+              "the specified axis. Does not create a new \n"
+              "object, but modifies it.")
+        )
+        self.mirror_geo_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.mirror_geo_button.setMinimumWidth(60)
 
-    def set_tool_ui(self):
-        self.reset_fields()
+        # grid_lay.addRow("Bottom Layer:", self.object_combo)
+        grid_lay.addWidget(self.geoobj_label, 5, 0)
+        grid_lay.addWidget(self.geo_object_combo, 6, 0)
+        grid_lay.addWidget(self.mirror_geo_button, 6, 1)
 
-        self.point_entry.set_value("")
-        self.alignment_holes.set_value("")
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 7, 0, 1, 2)
 
-        self.mirror_axis.set_value(self.app.defaults["tools_2sided_mirror_axis"])
-        self.axis_location.set_value(self.app.defaults["tools_2sided_axis_loc"])
-        self.drill_dia.set_value(self.app.defaults["tools_2sided_drilldia"])
-        self.align_axis_radio.set_value(self.app.defaults["tools_2sided_allign_axis"])
+        self.layout.addWidget(QtWidgets.QLabel(""))
 
-        self.xmin_entry.set_value(0.0)
-        self.ymin_entry.set_value(0.0)
-        self.xmax_entry.set_value(0.0)
-        self.ymax_entry.set_value(0.0)
-        self.center_entry.set_value('')
+        # ## Grid Layout
+        grid_lay1 = QtWidgets.QGridLayout()
+        grid_lay1.setColumnStretch(0, 0)
+        grid_lay1.setColumnStretch(1, 1)
+        self.layout.addLayout(grid_lay1)
 
-        self.align_ref_label_val.set_value('%.*f' % (self.decimals, 0.0))
+        # Objects to be mirrored
+        self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Mirror Parameters"))
+        self.param_label.setToolTip('%s.' % _("Parameters for the mirror operation"))
 
-        # run once to make sure that the obj_type attribute is updated in the FCComboBox
-        self.box_type_radio.set_value('grb')
-        self.on_combo_box_type('grb')
+        grid_lay1.addWidget(self.param_label, 0, 0, 1, 2)
 
-    def on_combo_box_type(self, val):
-        obj_type = {'grb': 0, 'exc': 1, 'geo': 2}[val]
-        self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        self.box_combo.setCurrentIndex(0)
-        self.box_combo.obj_type = {
-            "grb": "Gerber", "exc": "Excellon", "geo": "Geometry"}[val]
+        # ## Axis
+        self.mirax_label = QtWidgets.QLabel('%s:' % _("Mirror Axis"))
+        self.mirax_label.setToolTip(_("Mirror vertically (X) or horizontally (Y)."))
+        self.mirror_axis = RadioSet([{'label': 'X', 'value': 'X'},
+                                     {'label': 'Y', 'value': 'Y'}])
 
-    def on_create_alignment_holes(self):
-        axis = self.align_axis_radio.get_value()
-        mode = self.axis_location.get_value()
+        grid_lay1.addWidget(self.mirax_label, 2, 0)
+        grid_lay1.addWidget(self.mirror_axis, 2, 1, 1, 2)
 
-        if mode == "point":
-            try:
-                px, py = self.point_entry.get_value()
-            except TypeError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("'Point' reference is selected and 'Point' coordinates "
-                                                              "are missing. Add them and retry."))
-                return
-        else:
-            selection_index = self.box_combo.currentIndex()
-            model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
-            try:
-                bb_obj = model_index.internalPointer().obj
-            except AttributeError:
-                model_index = self.app.collection.index(selection_index, 0, self.exc_object_combo.rootModelIndex())
-                try:
-                    bb_obj = model_index.internalPointer().obj
-                except AttributeError:
-                    model_index = self.app.collection.index(selection_index, 0,
-                                                            self.geo_object_combo.rootModelIndex())
-                    try:
-                        bb_obj = model_index.internalPointer().obj
-                    except AttributeError:
-                        self.app.inform.emit(
-                            '[WARNING_NOTCL] %s' % _("There is no Box reference object loaded. Load one and retry."))
-                        return
+        # ## Axis Location
+        self.axloc_label = QtWidgets.QLabel('%s:' % _("Reference"))
+        self.axloc_label.setToolTip(
+            _("The coordinates used as reference for the mirror operation.\n"
+              "Can be:\n"
+              "- Point -> a set of coordinates (x,y) around which the object is mirrored\n"
+              "- Box -> a set of coordinates (x, y) obtained from the center of the\n"
+              "bounding box of another object selected below")
+        )
+        self.axis_location = RadioSet([{'label': _('Point'), 'value': 'point'},
+                                       {'label': _('Box'), 'value': 'box'}])
 
-            xmin, ymin, xmax, ymax = bb_obj.bounds()
-            px = 0.5 * (xmin + xmax)
-            py = 0.5 * (ymin + ymax)
+        grid_lay1.addWidget(self.axloc_label, 4, 0)
+        grid_lay1.addWidget(self.axis_location, 4, 1, 1, 2)
 
-        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+        # ## Point/Box
+        self.point_entry = EvalEntry()
+        self.point_entry.setPlaceholderText(_("Point coordinates"))
 
-        dia = float(self.drill_dia.get_value())
-        if dia == '':
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("No value or wrong format in Drill Dia entry. Add it and retry."))
-            return
+        # Add a reference
+        self.add_point_button = QtWidgets.QPushButton(_("Add"))
+        self.add_point_button.setToolTip(
+            _("Add the coordinates in format <b>(x, y)</b> through which the mirroring axis\n "
+              "selected in 'MIRROR AXIS' pass.\n"
+              "The (x, y) coordinates are captured by pressing SHIFT key\n"
+              "and left mouse button click on canvas or you can enter the coordinates manually.")
+        )
+        self.add_point_button.setStyleSheet("""
+                                        QPushButton
+                                        {
+                                            font-weight: bold;
+                                        }
+                                        """)
+        self.add_point_button.setMinimumWidth(60)
 
-        tools = {}
-        tools[1] = {}
-        tools[1]["tooldia"] = dia
-        tools[1]['solid_geometry'] = []
+        grid_lay1.addWidget(self.point_entry, 7, 0, 1, 2)
+        grid_lay1.addWidget(self.add_point_button, 7, 2)
 
-        # holes = self.alignment_holes.get_value()
-        holes = eval('[{}]'.format(self.alignment_holes.text()))
-        if not holes:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Alignment Drill Coordinates to use. "
-                                                          "Add them and retry."))
-            return
+        # ## Grid Layout
+        grid_lay2 = QtWidgets.QGridLayout()
+        grid_lay2.setColumnStretch(0, 0)
+        grid_lay2.setColumnStretch(1, 1)
+        self.layout.addLayout(grid_lay2)
 
-        for hole in holes:
-            point = Point(hole)
-            point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
+        self.box_type_label = QtWidgets.QLabel('%s:' % _("Reference Object"))
+        self.box_type_label.setToolTip(
+            _("It can be of type: Gerber or Excellon or Geometry.\n"
+              "The coordinates of the center of the bounding box are used\n"
+              "as reference for mirror operation.")
+        )
 
-            tools[1]['drills'] = [point, point_mirror]
-            tools[1]['solid_geometry'].append(point)
-            tools[1]['solid_geometry'].append(point_mirror)
+        # Type of object used as BOX reference
+        self.box_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'grb'},
+                                        {'label': _('Excellon'), 'value': 'exc'},
+                                        {'label': _('Geometry'), 'value': 'geo'}])
 
-        def obj_init(obj_inst, app_inst):
-            obj_inst.tools = tools
-            obj_inst.create_geometry()
-            obj_inst.source_file = app_inst.export_excellon(obj_name=obj_inst.options['name'], local_use=obj_inst,
-                                                            filename=None, use_thread=False)
+        self.box_type_label.hide()
+        self.box_type_radio.hide()
 
-        self.app.app_obj.new_object("excellon", "Alignment Drills", obj_init)
-        self.drill_values = ''
-        self.app.inform.emit('[success] %s' % _("Excellon object with alignment drills created..."))
+        grid_lay2.addWidget(self.box_type_label, 0, 0, 1, 2)
+        grid_lay2.addWidget(self.box_type_radio, 1, 0, 1, 2)
 
-    def on_mirror_gerber(self):
-        selection_index = self.gerber_object_combo.currentIndex()
-        # fcobj = self.app.collection.object_list[selection_index]
-        model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
-        try:
-            fcobj = model_index.internalPointer().obj
-        except Exception:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
-            return
+        # Object used as BOX reference
+        self.box_combo = FCComboBox()
+        self.box_combo.setModel(self.app.collection)
+        self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.box_combo.is_last = True
 
-        if fcobj.kind != 'gerber':
-            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored."))
-            return
+        self.box_combo.hide()
 
-        axis = self.mirror_axis.get_value()
-        mode = self.axis_location.get_value()
+        grid_lay2.addWidget(self.box_combo, 3, 0, 1, 2)
 
-        if mode == "point":
-            try:
-                px, py = self.point_entry.get_value()
-            except TypeError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Point coordinates in the Point field. "
-                                                              "Add coords and try again ..."))
-                return
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay2.addWidget(separator_line, 4, 0, 1, 2)
 
-        else:
-            selection_index_box = self.box_combo.currentIndex()
-            model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
-            try:
-                bb_obj = model_index_box.internalPointer().obj
-            except Exception:
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ..."))
-                return
+        grid_lay2.addWidget(QtWidgets.QLabel(""), 5, 0, 1, 2)
 
-            xmin, ymin, xmax, ymax = bb_obj.bounds()
-            px = 0.5 * (xmin + xmax)
-            py = 0.5 * (ymin + ymax)
+        # ## Title Bounds Values
+        self.bv_label = QtWidgets.QLabel("<b>%s:</b>" % _('Bounds Values'))
+        self.bv_label.setToolTip(
+            _("Select on canvas the object(s)\n"
+              "for which to calculate bounds values.")
+        )
+        grid_lay2.addWidget(self.bv_label, 6, 0, 1, 2)
 
-        fcobj.mirror(axis, [px, py])
-        self.app.app_obj.object_changed.emit(fcobj)
-        fcobj.plot()
-        self.app.inform.emit('[success] Gerber %s %s...' % (str(fcobj.options['name']), _("was mirrored")))
+        # Xmin value
+        self.xmin_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.xmin_entry.set_precision(self.decimals)
+        self.xmin_entry.set_range(-9999.9999, 9999.9999)
 
-    def on_mirror_exc(self):
-        selection_index = self.exc_object_combo.currentIndex()
-        # fcobj = self.app.collection.object_list[selection_index]
-        model_index = self.app.collection.index(selection_index, 0, self.exc_object_combo.rootModelIndex())
-        try:
-            fcobj = model_index.internalPointer().obj
-        except Exception:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ..."))
-            return
+        self.xmin_btn = FCButton('%s:' % _("X min"))
+        self.xmin_btn.setToolTip(
+            _("Minimum location.")
+        )
+        self.xmin_entry.setReadOnly(True)
 
-        if fcobj.kind != 'excellon':
-            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored."))
-            return
+        grid_lay2.addWidget(self.xmin_btn, 7, 0)
+        grid_lay2.addWidget(self.xmin_entry, 7, 1)
+
+        # Ymin value
+        self.ymin_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.ymin_entry.set_precision(self.decimals)
+        self.ymin_entry.set_range(-9999.9999, 9999.9999)
+
+        self.ymin_btn = FCButton('%s:' % _("Y min"))
+        self.ymin_btn.setToolTip(
+            _("Minimum location.")
+        )
+        self.ymin_entry.setReadOnly(True)
+
+        grid_lay2.addWidget(self.ymin_btn, 8, 0)
+        grid_lay2.addWidget(self.ymin_entry, 8, 1)
+
+        # Xmax value
+        self.xmax_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.xmax_entry.set_precision(self.decimals)
+        self.xmax_entry.set_range(-9999.9999, 9999.9999)
 
-        axis = self.mirror_axis.get_value()
-        mode = self.axis_location.get_value()
+        self.xmax_btn = FCButton('%s:' % _("X max"))
+        self.xmax_btn.setToolTip(
+            _("Maximum location.")
+        )
+        self.xmax_entry.setReadOnly(True)
 
-        if mode == "point":
-            try:
-                px, py = self.point_entry.get_value()
-            except Exception as e:
-                log.debug("DblSidedTool.on_mirror_geo() --> %s" % str(e))
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Point coordinates in the Point field. "
-                                                              "Add coords and try again ..."))
-                return
-        else:
-            selection_index_box = self.box_combo.currentIndex()
-            model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
-            try:
-                bb_obj = model_index_box.internalPointer().obj
-            except Exception as e:
-                log.debug("DblSidedTool.on_mirror_geo() --> %s" % str(e))
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ..."))
-                return
+        grid_lay2.addWidget(self.xmax_btn, 9, 0)
+        grid_lay2.addWidget(self.xmax_entry, 9, 1)
 
-            xmin, ymin, xmax, ymax = bb_obj.bounds()
-            px = 0.5 * (xmin + xmax)
-            py = 0.5 * (ymin + ymax)
+        # Ymax value
+        self.ymax_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.ymax_entry.set_precision(self.decimals)
+        self.ymax_entry.set_range(-9999.9999, 9999.9999)
 
-        fcobj.mirror(axis, [px, py])
-        self.app.app_obj.object_changed.emit(fcobj)
-        fcobj.plot()
-        self.app.inform.emit('[success] Excellon %s %s...' % (str(fcobj.options['name']), _("was mirrored")))
+        self.ymax_btn = FCButton('%s:' % _("Y max"))
+        self.ymax_btn.setToolTip(
+            _("Maximum location.")
+        )
+        self.ymax_entry.setReadOnly(True)
 
-    def on_mirror_geo(self):
-        selection_index = self.geo_object_combo.currentIndex()
-        # fcobj = self.app.collection.object_list[selection_index]
-        model_index = self.app.collection.index(selection_index, 0, self.geo_object_combo.rootModelIndex())
-        try:
-            fcobj = model_index.internalPointer().obj
-        except Exception:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Geometry object loaded ..."))
-            return
+        grid_lay2.addWidget(self.ymax_btn, 10, 0)
+        grid_lay2.addWidget(self.ymax_entry, 10, 1)
 
-        if fcobj.kind != 'geometry':
-            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored."))
-            return
+        # Center point value
+        self.center_entry = FCEntry()
+        self.center_entry.setPlaceholderText(_("Center point coordinates"))
 
-        axis = self.mirror_axis.get_value()
-        mode = self.axis_location.get_value()
+        self.center_btn = FCButton('%s:' % _("Centroid"))
+        self.center_btn.setToolTip(
+            _("The center point location for the rectangular\n"
+              "bounding shape. Centroid. Format is (x, y).")
+        )
+        self.center_entry.setReadOnly(True)
 
-        if mode == "point":
-            px, py = self.point_entry.get_value()
-        else:
-            selection_index_box = self.box_combo.currentIndex()
-            model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
-            try:
-                bb_obj = model_index_box.internalPointer().obj
-            except Exception:
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ..."))
-                return
+        grid_lay2.addWidget(self.center_btn, 12, 0)
+        grid_lay2.addWidget(self.center_entry, 12, 1)
 
-            xmin, ymin, xmax, ymax = bb_obj.bounds()
-            px = 0.5 * (xmin + xmax)
-            py = 0.5 * (ymin + ymax)
+        # Calculate Bounding box
+        self.calculate_bb_button = QtWidgets.QPushButton(_("Calculate Bounds Values"))
+        self.calculate_bb_button.setToolTip(
+            _("Calculate the enveloping rectangular shape coordinates,\n"
+              "for the selection of objects.\n"
+              "The envelope shape is parallel with the X, Y axis.")
+        )
+        self.calculate_bb_button.setStyleSheet("""
+                                        QPushButton
+                                        {
+                                            font-weight: bold;
+                                        }
+                                        """)
+        grid_lay2.addWidget(self.calculate_bb_button, 13, 0, 1, 2)
 
-        fcobj.mirror(axis, [px, py])
-        self.app.app_obj.object_changed.emit(fcobj)
-        fcobj.plot()
-        self.app.inform.emit('[success] Geometry %s %s...' % (str(fcobj.options['name']), _("was mirrored")))
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay2.addWidget(separator_line, 14, 0, 1, 2)
 
-    def on_point_add(self):
-        val = self.app.defaults["global_point_clipboard_format"] % \
-              (self.decimals, self.app.pos[0], self.decimals, self.app.pos[1])
-        self.point_entry.set_value(val)
+        grid_lay2.addWidget(QtWidgets.QLabel(""), 15, 0, 1, 2)
 
-    def on_drill_add(self):
-        self.drill_values += (self.app.defaults["global_point_clipboard_format"] %
-                              (self.decimals, self.app.pos[0], self.decimals, self.app.pos[1])) + ','
-        self.alignment_holes.set_value(self.drill_values)
+        # ## Alignment holes
+        self.alignment_label = QtWidgets.QLabel("<b>%s:</b>" % _('PCB Alignment'))
+        self.alignment_label.setToolTip(
+            _("Creates an Excellon Object containing the\n"
+              "specified alignment holes and their mirror\n"
+              "images.")
+        )
+        grid_lay2.addWidget(self.alignment_label, 25, 0, 1, 2)
 
-    def on_drill_delete_last(self):
-        drill_values_without_last_tupple = self.drill_values.rpartition('(')[0]
-        self.drill_values = drill_values_without_last_tupple
-        self.alignment_holes.set_value(self.drill_values)
+        # ## Drill diameter for alignment holes
+        self.dt_label = QtWidgets.QLabel("%s:" % _('Drill Diameter'))
+        self.dt_label.setToolTip(
+            _("Diameter of the drill for the alignment holes.")
+        )
 
-    def on_toggle_pointbox(self):
-        if self.axis_location.get_value() == "point":
-            self.point_entry.show()
-            self.add_point_button.show()
-            self.box_type_label.hide()
-            self.box_type_radio.hide()
-            self.box_combo.hide()
-
-            self.align_ref_label_val.set_value(self.point_entry.get_value())
-        else:
-            self.point_entry.hide()
-            self.add_point_button.hide()
+        self.drill_dia = FCDoubleSpinner(callback=self.confirmation_message)
+        self.drill_dia.setToolTip(
+            _("Diameter of the drill for the alignment holes.")
+        )
+        self.drill_dia.set_precision(self.decimals)
+        self.drill_dia.set_range(0.0000, 9999.9999)
 
-            self.box_type_label.show()
-            self.box_type_radio.show()
-            self.box_combo.show()
+        grid_lay2.addWidget(self.dt_label, 26, 0)
+        grid_lay2.addWidget(self.drill_dia, 26, 1)
 
-            self.align_ref_label_val.set_value("Box centroid")
+        # ## Alignment Axis
+        self.align_ax_label = QtWidgets.QLabel('%s:' % _("Align Axis"))
+        self.align_ax_label.setToolTip(
+            _("Mirror vertically (X) or horizontally (Y).")
+        )
+        self.align_axis_radio = RadioSet([{'label': 'X', 'value': 'X'},
+                                          {'label': 'Y', 'value': 'Y'}])
 
-    def on_bbox_coordinates(self):
+        grid_lay2.addWidget(self.align_ax_label, 27, 0)
+        grid_lay2.addWidget(self.align_axis_radio, 27, 1)
 
-        xmin = Inf
-        ymin = Inf
-        xmax = -Inf
-        ymax = -Inf
+        # ## Alignment Reference Point
+        self.align_ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
+        self.align_ref_label.setToolTip(
+            _("The reference point used to create the second alignment drill\n"
+              "from the first alignment drill, by doing mirror.\n"
+              "It can be modified in the Mirror Parameters -> Reference section")
+        )
 
-        obj_list = self.app.collection.get_selected()
+        self.align_ref_label_val = EvalEntry()
+        self.align_ref_label_val.setToolTip(
+            _("The reference point used to create the second alignment drill\n"
+              "from the first alignment drill, by doing mirror.\n"
+              "It can be modified in the Mirror Parameters -> Reference section")
+        )
+        self.align_ref_label_val.setDisabled(True)
 
-        if not obj_list:
-            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No object(s) selected..."))
-            return
+        grid_lay2.addWidget(self.align_ref_label, 28, 0)
+        grid_lay2.addWidget(self.align_ref_label_val, 28, 1)
 
-        for obj in obj_list:
-            try:
-                gxmin, gymin, gxmax, gymax = obj.bounds()
-                xmin = min([xmin, gxmin])
-                ymin = min([ymin, gymin])
-                xmax = max([xmax, gxmax])
-                ymax = max([ymax, gymax])
-            except Exception as e:
-                log.warning("DEV WARNING: Tried to get bounds of empty geometry in DblSidedTool. %s" % str(e))
+        grid_lay4 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay4)
 
-        self.xmin_entry.set_value(xmin)
-        self.ymin_entry.set_value(ymin)
-        self.xmax_entry.set_value(xmax)
-        self.ymax_entry.set_value(ymax)
-        cx = '%.*f' % (self.decimals, (((xmax - xmin) / 2.0) + xmin))
-        cy = '%.*f' % (self.decimals, (((ymax - ymin) / 2.0) + ymin))
-        val_txt = '(%s, %s)' % (cx, cy)
+        # ## Alignment holes
+        self.ah_label = QtWidgets.QLabel("%s:" % _('Alignment Drill Coordinates'))
+        self.ah_label.setToolTip(
+            _("Alignment holes (x1, y1), (x2, y2), ... "
+              "on one side of the mirror axis. For each set of (x, y) coordinates\n"
+              "entered here, a pair of drills will be created:\n\n"
+              "- one drill at the coordinates from the field\n"
+              "- one drill in mirror position over the axis selected above in the 'Align Axis'.")
+        )
 
-        self.center_entry.set_value(val_txt)
-        self.axis_location.set_value('point')
-        self.point_entry.set_value(val_txt)
-        self.app.delete_selection_shape()
+        self.alignment_holes = EvalEntry()
+        self.alignment_holes.setPlaceholderText(_("Drill coordinates"))
 
-    def on_xmin_clicked(self):
-        xmin = self.xmin_entry.get_value()
-        self.axis_location.set_value('point')
+        grid_lay4.addWidget(self.ah_label, 0, 0, 1, 2)
+        grid_lay4.addWidget(self.alignment_holes, 1, 0, 1, 2)
 
-        try:
-            px, py = self.point_entry.get_value()
-            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmin, self.decimals, py)
-        except TypeError:
-            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmin, self.decimals, 0.0)
-        self.point_entry.set_value(val)
+        self.add_drill_point_button = FCButton(_("Add"))
+        self.add_drill_point_button.setToolTip(
+            _("Add alignment drill holes coordinates in the format: (x1, y1), (x2, y2), ... \n"
+              "on one side of the alignment axis.\n\n"
+              "The coordinates set can be obtained:\n"
+              "- press SHIFT key and left mouse clicking on canvas. Then click Add.\n"
+              "- press SHIFT key and left mouse clicking on canvas. Then Ctrl+V in the field.\n"
+              "- press SHIFT key and left mouse clicking on canvas. Then RMB click in the field and click Paste.\n"
+              "- by entering the coords manually in the format: (x1, y1), (x2, y2), ...")
+        )
+        # self.add_drill_point_button.setStyleSheet("""
+        #                 QPushButton
+        #                 {
+        #                     font-weight: bold;
+        #                 }
+        #                 """)
 
-    def on_ymin_clicked(self):
-        ymin = self.ymin_entry.get_value()
-        self.axis_location.set_value('point')
+        self.delete_drill_point_button = FCButton(_("Delete Last"))
+        self.delete_drill_point_button.setToolTip(
+            _("Delete the last coordinates tuple in the list.")
+        )
+        drill_hlay = QtWidgets.QHBoxLayout()
 
-        try:
-            px, py = self.point_entry.get_value()
-            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, px, self.decimals, ymin)
-        except TypeError:
-            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, 0.0, self.decimals, ymin)
-        self.point_entry.set_value(val)
+        drill_hlay.addWidget(self.add_drill_point_button)
+        drill_hlay.addWidget(self.delete_drill_point_button)
 
-    def on_xmax_clicked(self):
-        xmax = self.xmax_entry.get_value()
-        self.axis_location.set_value('point')
+        grid_lay4.addLayout(drill_hlay, 2, 0, 1, 2)
 
-        try:
-            px, py = self.point_entry.get_value()
-            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmax, self.decimals, py)
-        except TypeError:
-            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmax, self.decimals, 0.0)
-        self.point_entry.set_value(val)
+        # ## Buttons
+        self.create_alignment_hole_button = QtWidgets.QPushButton(_("Create Excellon Object"))
+        self.create_alignment_hole_button.setToolTip(
+            _("Creates an Excellon Object containing the\n"
+              "specified alignment holes and their mirror\n"
+              "images.")
+        )
+        self.create_alignment_hole_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.create_alignment_hole_button)
 
-    def on_ymax_clicked(self):
-        ymax = self.ymax_entry.get_value()
-        self.axis_location.set_value('point')
+        self.layout.addStretch()
 
-        try:
-            px, py = self.point_entry.get_value()
-            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, px, self.decimals, ymax)
-        except TypeError:
-            val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, 0.0, self.decimals, ymax)
-        self.point_entry.set_value(val)
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.reset_button)
 
-    def reset_fields(self):
-        self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
-        self.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
-        self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
 
-        self.gerber_object_combo.setCurrentIndex(0)
-        self.exc_object_combo.setCurrentIndex(0)
-        self.geo_object_combo.setCurrentIndex(0)
-        self.box_combo.setCurrentIndex(0)
-        self.box_type_radio.set_value('grb')
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
 
-        self.drill_values = ""
-        self.align_ref_label_val.set_value('')
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 251 - 216
appTools/ToolEtchCompensation.py

@@ -29,14 +29,232 @@ log = logging.getLogger('base')
 
 class ToolEtchCompensation(AppTool):
 
-    toolName = _("Etch Compensation Tool")
-
     def __init__(self, app):
         self.app = app
         self.decimals = self.app.decimals
 
         AppTool.__init__(self, app)
 
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = EtchUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
+
+        self.ui.compensate_btn.clicked.connect(self.on_compensate)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
+        self.ui.ratio_radio.activated_custom.connect(self.on_ratio_change)
+
+        self.ui.oz_entry.textChanged.connect(self.on_oz_conversion)
+        self.ui.mils_entry.textChanged.connect(self.on_mils_conversion)
+
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='', **kwargs)
+
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("ToolEtchCompensation()")
+        log.debug("ToolEtchCompensation() is running ...")
+
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+
+        AppTool.run(self)
+        self.set_tool_ui()
+
+        self.app.ui.notebook.setTabText(2, _("Etch Compensation Tool"))
+
+    def set_tool_ui(self):
+        self.ui.thick_entry.set_value(18.0)
+        self.ui.ratio_radio.set_value('factor')
+
+    def on_ratio_change(self, val):
+        """
+        Called on activated_custom signal of the RadioSet GUI element self.radio_ratio
+
+        :param val:     'c' or 'p': 'c' means custom factor and 'p' means preselected etchants
+        :type val:      str
+        :return:        None
+        :rtype:
+        """
+        if val == 'factor':
+            self.ui.etchants_label.hide()
+            self.ui.etchants_combo.hide()
+            self.ui.factor_label.show()
+            self.ui.factor_entry.show()
+            self.ui.offset_label.hide()
+            self.ui.offset_entry.hide()
+        elif val == 'etch_list':
+            self.ui.etchants_label.show()
+            self.ui.etchants_combo.show()
+            self.ui.factor_label.hide()
+            self.ui.factor_entry.hide()
+            self.ui.offset_label.hide()
+            self.ui.offset_entry.hide()
+        else:
+            self.ui.etchants_label.hide()
+            self.ui.etchants_combo.hide()
+            self.ui.factor_label.hide()
+            self.ui.factor_entry.hide()
+            self.ui.offset_label.show()
+            self.ui.offset_entry.show()
+
+    def on_oz_conversion(self, txt):
+        try:
+            val = eval(txt)
+            # oz thickness to mils by multiplying with 1.37
+            # mils to microns by multiplying with 25.4
+            val *= 34.798
+        except Exception:
+            self.ui.oz_to_um_entry.set_value('')
+            return
+        self.ui.oz_to_um_entry.set_value(val, self.decimals)
+
+    def on_mils_conversion(self, txt):
+        try:
+            val = eval(txt)
+            val *= 25.4
+        except Exception:
+            self.ui.mils_to_um_entry.set_value('')
+            return
+        self.ui.mils_to_um_entry.set_value(val, self.decimals)
+
+    def on_compensate(self):
+        log.debug("ToolEtchCompensation.on_compensate()")
+
+        ratio_type = self.ui.ratio_radio.get_value()
+        thickness = self.ui.thick_entry.get_value() / 1000     # in microns
+
+        grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
+        obj_name = self.ui.gerber_combo.currentText()
+
+        outname = obj_name + "_comp"
+
+        # Get source object.
+        try:
+            grb_obj = self.app.collection.get_by_name(obj_name)
+        except Exception as e:
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(obj_name)))
+            return "Could not retrieve object: %s with error: %s" % (obj_name, str(e))
+
+        if grb_obj is None:
+            if obj_name == '':
+                obj_name = 'None'
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name)))
+            return
+
+        if ratio_type == 'factor':
+            etch_factor = 1 / self.ui.factor_entry.get_value()
+            offset = thickness / etch_factor
+        elif ratio_type == 'etch_list':
+            etchant = self.ui.etchants_combo.get_value()
+            if etchant == "CuCl2":
+                etch_factor = 0.33
+            else:
+                etch_factor = 0.25
+            offset = thickness / etch_factor
+        else:
+            offset = self.ui.offset_entry.get_value() / 1000   # in microns
+
+        try:
+            __ = iter(grb_obj.solid_geometry)
+        except TypeError:
+            grb_obj.solid_geometry = list(grb_obj.solid_geometry)
+
+        new_solid_geometry = []
+
+        for poly in grb_obj.solid_geometry:
+            new_solid_geometry.append(poly.buffer(offset, int(grb_circle_steps)))
+        new_solid_geometry = unary_union(new_solid_geometry)
+
+        new_options = {}
+        for opt in grb_obj.options:
+            new_options[opt] = deepcopy(grb_obj.options[opt])
+
+        new_apertures = deepcopy(grb_obj.apertures)
+
+        # update the apertures attributes (keys in the apertures dict)
+        for ap in new_apertures:
+            type = new_apertures[ap]['type']
+            for k in new_apertures[ap]:
+                if type == 'R' or type == 'O':
+                    if k == 'width' or k == 'height':
+                        new_apertures[ap][k] += offset
+                else:
+                    if k == 'size' or k == 'width' or k == 'height':
+                        new_apertures[ap][k] += offset
+
+                if k == 'geometry':
+                    for geo_el in new_apertures[ap][k]:
+                        if 'solid' in geo_el:
+                            geo_el['solid'] = geo_el['solid'].buffer(offset, int(grb_circle_steps))
+
+        # in case of 'R' or 'O' aperture type we need to update the aperture 'size' after
+        # the 'width' and 'height' keys were updated
+        for ap in new_apertures:
+            type = new_apertures[ap]['type']
+            for k in new_apertures[ap]:
+                if type == 'R' or type == 'O':
+                    if k == 'size':
+                        new_apertures[ap][k] = math.sqrt(
+                            new_apertures[ap]['width'] ** 2 + new_apertures[ap]['height'] ** 2)
+
+        def init_func(new_obj, app_obj):
+            """
+            Init a new object in FlatCAM Object collection
+
+            :param new_obj:     New object
+            :type new_obj:      ObjectCollection
+            :param app_obj:     App
+            :type app_obj:      app_Main.App
+            :return:            None
+            :rtype:
+            """
+            new_obj.options.update(new_options)
+            new_obj.options['name'] = outname
+            new_obj.fill_color = deepcopy(grb_obj.fill_color)
+            new_obj.outline_color = deepcopy(grb_obj.outline_color)
+
+            new_obj.apertures = deepcopy(new_apertures)
+
+            new_obj.solid_geometry = deepcopy(new_solid_geometry)
+            new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
+                                                         local_use=new_obj, use_thread=False)
+
+        self.app.app_obj.new_object('gerber', outname, init_func)
+
+    def reset_fields(self):
+        self.ui.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+
+    @staticmethod
+    def poly2rings(poly):
+        return [poly.exterior] + [interior for interior in poly.interiors]
+
+
+class EtchUI:
+
+    toolName = _("Etch Compensation Tool")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
         self.tools_frame = QtWidgets.QFrame()
         self.tools_frame.setContentsMargins(0, 0, 0, 0)
         self.layout.addWidget(self.tools_frame)
@@ -47,12 +265,12 @@ class ToolEtchCompensation(AppTool):
         # Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
         self.tools_box.addWidget(title_label)
 
         # Grid Layout
@@ -227,11 +445,11 @@ class ToolEtchCompensation(AppTool):
             _("Will increase the copper features thickness to compensate the lateral etch.")
         )
         self.compensate_btn.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
         grid0.addWidget(self.compensate_btn, 24, 0, 1, 2)
 
         self.tools_box.addStretch()
@@ -242,214 +460,31 @@ class ToolEtchCompensation(AppTool):
             _("Will reset the tool parameters.")
         )
         self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
         self.tools_box.addWidget(self.reset_button)
 
-        self.compensate_btn.clicked.connect(self.on_compensate)
-        self.reset_button.clicked.connect(self.set_tool_ui)
-        self.ratio_radio.activated_custom.connect(self.on_ratio_change)
-
-        self.oz_entry.textChanged.connect(self.on_oz_conversion)
-        self.mils_entry.textChanged.connect(self.on_mils_conversion)
-
-    def install(self, icon=None, separator=None, **kwargs):
-        AppTool.install(self, icon, separator, shortcut='', **kwargs)
-
-    def run(self, toggle=True):
-        self.app.defaults.report_usage("ToolEtchCompensation()")
-        log.debug("ToolEtchCompensation() is running ...")
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
 
-        if toggle:
-            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-            else:
-                try:
-                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        # if tab is populated with the tool but it does not have the focus, focus on it
-                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
-                            # focus on Tool Tab
-                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-                        else:
-                            self.app.ui.splitter.setSizes([0, 1])
-                except AttributeError:
-                    pass
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
         else:
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-
-        AppTool.run(self)
-        self.set_tool_ui()
-
-        self.app.ui.notebook.setTabText(2, _("Etch Compensation Tool"))
-
-    def set_tool_ui(self):
-        self.thick_entry.set_value(18.0)
-        self.ratio_radio.set_value('factor')
-
-    def on_ratio_change(self, val):
-        """
-        Called on activated_custom signal of the RadioSet GUI element self.radio_ratio
-
-        :param val:     'c' or 'p': 'c' means custom factor and 'p' means preselected etchants
-        :type val:      str
-        :return:        None
-        :rtype:
-        """
-        if val == 'factor':
-            self.etchants_label.hide()
-            self.etchants_combo.hide()
-            self.factor_label.show()
-            self.factor_entry.show()
-            self.offset_label.hide()
-            self.offset_entry.hide()
-        elif val == 'etch_list':
-            self.etchants_label.show()
-            self.etchants_combo.show()
-            self.factor_label.hide()
-            self.factor_entry.hide()
-            self.offset_label.hide()
-            self.offset_entry.hide()
-        else:
-            self.etchants_label.hide()
-            self.etchants_combo.hide()
-            self.factor_label.hide()
-            self.factor_entry.hide()
-            self.offset_label.show()
-            self.offset_entry.show()
-
-    def on_oz_conversion(self, txt):
-        try:
-            val = eval(txt)
-            # oz thickness to mils by multiplying with 1.37
-            # mils to microns by multiplying with 25.4
-            val *= 34.798
-        except Exception:
-            self.oz_to_um_entry.set_value('')
-            return
-        self.oz_to_um_entry.set_value(val, self.decimals)
-
-    def on_mils_conversion(self, txt):
-        try:
-            val = eval(txt)
-            val *= 25.4
-        except Exception:
-            self.mils_to_um_entry.set_value('')
-            return
-        self.mils_to_um_entry.set_value(val, self.decimals)
-
-    def on_compensate(self):
-        log.debug("ToolEtchCompensation.on_compensate()")
-
-        ratio_type = self.ratio_radio.get_value()
-        thickness = self.thick_entry.get_value() / 1000     # in microns
-
-        grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
-        obj_name = self.gerber_combo.currentText()
-
-        outname = obj_name + "_comp"
-
-        # Get source object.
-        try:
-            grb_obj = self.app.collection.get_by_name(obj_name)
-        except Exception as e:
-            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(obj_name)))
-            return "Could not retrieve object: %s with error: %s" % (obj_name, str(e))
-
-        if grb_obj is None:
-            if obj_name == '':
-                obj_name = 'None'
-            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name)))
-            return
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
 
-        if ratio_type == 'factor':
-            etch_factor = 1 / self.factor_entry.get_value()
-            offset = thickness / etch_factor
-        elif ratio_type == 'etch_list':
-            etchant = self.etchants_combo.get_value()
-            if etchant == "CuCl2":
-                etch_factor = 0.33
-            else:
-                etch_factor = 0.25
-            offset = thickness / etch_factor
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
         else:
-            offset = self.offset_entry.get_value() / 1000   # in microns
-
-        try:
-            __ = iter(grb_obj.solid_geometry)
-        except TypeError:
-            grb_obj.solid_geometry = list(grb_obj.solid_geometry)
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
 
-        new_solid_geometry = []
-
-        for poly in grb_obj.solid_geometry:
-            new_solid_geometry.append(poly.buffer(offset, int(grb_circle_steps)))
-        new_solid_geometry = unary_union(new_solid_geometry)
-
-        new_options = {}
-        for opt in grb_obj.options:
-            new_options[opt] = deepcopy(grb_obj.options[opt])
-
-        new_apertures = deepcopy(grb_obj.apertures)
-
-        # update the apertures attributes (keys in the apertures dict)
-        for ap in new_apertures:
-            type = new_apertures[ap]['type']
-            for k in new_apertures[ap]:
-                if type == 'R' or type == 'O':
-                    if k == 'width' or k == 'height':
-                        new_apertures[ap][k] += offset
-                else:
-                    if k == 'size' or k == 'width' or k == 'height':
-                        new_apertures[ap][k] += offset
-
-                if k == 'geometry':
-                    for geo_el in new_apertures[ap][k]:
-                        if 'solid' in geo_el:
-                            geo_el['solid'] = geo_el['solid'].buffer(offset, int(grb_circle_steps))
-
-        # in case of 'R' or 'O' aperture type we need to update the aperture 'size' after
-        # the 'width' and 'height' keys were updated
-        for ap in new_apertures:
-            type = new_apertures[ap]['type']
-            for k in new_apertures[ap]:
-                if type == 'R' or type == 'O':
-                    if k == 'size':
-                        new_apertures[ap][k] = math.sqrt(
-                            new_apertures[ap]['width'] ** 2 + new_apertures[ap]['height'] ** 2)
-
-        def init_func(new_obj, app_obj):
-            """
-            Init a new object in FlatCAM Object collection
-
-            :param new_obj:     New object
-            :type new_obj:      ObjectCollection
-            :param app_obj:     App
-            :type app_obj:      app_Main.App
-            :return:            None
-            :rtype:
-            """
-            new_obj.options.update(new_options)
-            new_obj.options['name'] = outname
-            new_obj.fill_color = deepcopy(grb_obj.fill_color)
-            new_obj.outline_color = deepcopy(grb_obj.outline_color)
-
-            new_obj.apertures = deepcopy(new_apertures)
-
-            new_obj.solid_geometry = deepcopy(new_solid_geometry)
-            new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
-                                                         local_use=new_obj, use_thread=False)
-
-        self.app.app_obj.new_object('gerber', outname, init_func)
-
-    def reset_fields(self):
-        self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-
-    @staticmethod
-    def poly2rings(poly):
-        return [poly.exterior] + [interior for interior in poly.interiors]
 # end of file

+ 501 - 467
appTools/ToolExtractDrills.py

@@ -26,497 +26,202 @@ log = logging.getLogger('base')
 
 class ToolExtractDrills(AppTool):
 
-    toolName = _("Extract Drills")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
         self.decimals = self.app.decimals
 
-        # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-
-        self.layout.addWidget(QtWidgets.QLabel(""))
-
-        # ## Grid Layout
-        grid_lay = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid_lay)
-        grid_lay.setColumnStretch(0, 1)
-        grid_lay.setColumnStretch(1, 0)
-
-        # ## Gerber Object
-        self.gerber_object_combo = FCComboBox()
-        self.gerber_object_combo.setModel(self.app.collection)
-        self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.gerber_object_combo.is_last = True
-        self.gerber_object_combo.obj_type = "Gerber"
-
-        self.grb_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
-        self.grb_label.setToolTip('%s.' % _("Gerber from which to extract drill holes"))
-
-        # grid_lay.addRow("Bottom Layer:", self.object_combo)
-        grid_lay.addWidget(self.grb_label, 0, 0, 1, 2)
-        grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
-
-        self.padt_label = QtWidgets.QLabel("<b>%s</b>" % _("Processed Pads Type"))
-        self.padt_label.setToolTip(
-            _("The type of pads shape to be processed.\n"
-              "If the PCB has many SMD pads with rectangular pads,\n"
-              "disable the Rectangular aperture.")
-        )
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = ExtractDrillsUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
-        grid_lay.addWidget(self.padt_label, 2, 0, 1, 2)
+        # ## Signals
+        self.ui.hole_size_radio.activated_custom.connect(self.on_hole_size_toggle)
+        self.ui.e_drills_button.clicked.connect(self.on_extract_drills_click)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
 
-        # Circular Aperture Selection
-        self.circular_cb = FCCheckBox('%s' % _("Circular"))
-        self.circular_cb.setToolTip(
-            _("Process Circular Pads.")
+        self.ui.circular_cb.stateChanged.connect(
+            lambda state:
+            self.ui.circular_ring_entry.setDisabled(False) if state else self.ui.circular_ring_entry.setDisabled(True)
         )
 
-        grid_lay.addWidget(self.circular_cb, 3, 0, 1, 2)
-
-        # Oblong Aperture Selection
-        self.oblong_cb = FCCheckBox('%s' % _("Oblong"))
-        self.oblong_cb.setToolTip(
-            _("Process Oblong Pads.")
+        self.ui.oblong_cb.stateChanged.connect(
+            lambda state:
+            self.ui.oblong_ring_entry.setDisabled(False) if state else self.ui.oblong_ring_entry.setDisabled(True)
         )
 
-        grid_lay.addWidget(self.oblong_cb, 4, 0, 1, 2)
-
-        # Square Aperture Selection
-        self.square_cb = FCCheckBox('%s' % _("Square"))
-        self.square_cb.setToolTip(
-            _("Process Square Pads.")
+        self.ui.square_cb.stateChanged.connect(
+            lambda state:
+            self.ui.square_ring_entry.setDisabled(False) if state else self.ui.square_ring_entry.setDisabled(True)
         )
 
-        grid_lay.addWidget(self.square_cb, 5, 0, 1, 2)
-
-        # Rectangular Aperture Selection
-        self.rectangular_cb = FCCheckBox('%s' % _("Rectangular"))
-        self.rectangular_cb.setToolTip(
-            _("Process Rectangular Pads.")
+        self.ui.rectangular_cb.stateChanged.connect(
+            lambda state:
+            self.ui.rectangular_ring_entry.setDisabled(False) if state else
+            self.ui.rectangular_ring_entry.setDisabled(True)
         )
 
-        grid_lay.addWidget(self.rectangular_cb, 6, 0, 1, 2)
-
-        # Others type of Apertures Selection
-        self.other_cb = FCCheckBox('%s' % _("Others"))
-        self.other_cb.setToolTip(
-            _("Process pads not in the categories above.")
+        self.ui.other_cb.stateChanged.connect(
+            lambda state:
+            self.ui.other_ring_entry.setDisabled(False) if state else self.ui.other_ring_entry.setDisabled(True)
         )
 
-        grid_lay.addWidget(self.other_cb, 7, 0, 1, 2)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line, 8, 0, 1, 2)
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='Alt+I', **kwargs)
 
-        # ## Grid Layout
-        grid1 = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid1)
-        grid1.setColumnStretch(0, 0)
-        grid1.setColumnStretch(1, 1)
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("Extract Drills()")
 
-        self.method_label = QtWidgets.QLabel('<b>%s</b>' % _("Method"))
-        self.method_label.setToolTip(
-            _("The method for processing pads. Can be:\n"
-              "- Fixed Diameter -> all holes will have a set size\n"
-              "- Fixed Annular Ring -> all holes will have a set annular ring\n"
-              "- Proportional -> each hole size will be a fraction of the pad size"))
-        grid1.addWidget(self.method_label, 2, 0, 1, 2)
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
 
-        # ## Holes Size
-        self.hole_size_radio = RadioSet(
-            [
-                {'label': _("Fixed Diameter"), 'value': 'fixed'},
-                {'label': _("Fixed Annular Ring"), 'value': 'ring'},
-                {'label': _("Proportional"), 'value': 'prop'}
-            ],
-            orientation='vertical',
-            stretch=False)
+        AppTool.run(self)
+        self.set_tool_ui()
 
-        grid1.addWidget(self.hole_size_radio, 3, 0, 1, 2)
+        self.app.ui.notebook.setTabText(2, _("Extract Drills Tool"))
 
-        # grid_lay1.addWidget(QtWidgets.QLabel(''))
+    def set_tool_ui(self):
+        self.reset_fields()
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid1.addWidget(separator_line, 5, 0, 1, 2)
+        self.ui.hole_size_radio.set_value(self.app.defaults["tools_edrills_hole_type"])
 
-        # Annular Ring
-        self.fixed_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Diameter"))
-        grid1.addWidget(self.fixed_label, 6, 0, 1, 2)
+        self.ui.dia_entry.set_value(float(self.app.defaults["tools_edrills_hole_fixed_dia"]))
 
-        # Diameter value
-        self.dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.dia_entry.set_precision(self.decimals)
-        self.dia_entry.set_range(0.0000, 9999.9999)
+        self.ui.circular_ring_entry.set_value(float(self.app.defaults["tools_edrills_circular_ring"]))
+        self.ui.oblong_ring_entry.set_value(float(self.app.defaults["tools_edrills_oblong_ring"]))
+        self.ui.square_ring_entry.set_value(float(self.app.defaults["tools_edrills_square_ring"]))
+        self.ui.rectangular_ring_entry.set_value(float(self.app.defaults["tools_edrills_rectangular_ring"]))
+        self.ui.other_ring_entry.set_value(float(self.app.defaults["tools_edrills_others_ring"]))
 
-        self.dia_label = QtWidgets.QLabel('%s:' % _("Value"))
-        self.dia_label.setToolTip(
-            _("Fixed hole diameter.")
-        )
+        self.ui.circular_cb.set_value(self.app.defaults["tools_edrills_circular"])
+        self.ui.oblong_cb.set_value(self.app.defaults["tools_edrills_oblong"])
+        self.ui.square_cb.set_value(self.app.defaults["tools_edrills_square"])
+        self.ui.rectangular_cb.set_value(self.app.defaults["tools_edrills_rectangular"])
+        self.ui.other_cb.set_value(self.app.defaults["tools_edrills_others"])
 
-        grid1.addWidget(self.dia_label, 8, 0)
-        grid1.addWidget(self.dia_entry, 8, 1)
+        self.ui.factor_entry.set_value(float(self.app.defaults["tools_edrills_hole_prop_factor"]))
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid1.addWidget(separator_line, 9, 0, 1, 2)
+    def on_extract_drills_click(self):
 
-        self.ring_frame = QtWidgets.QFrame()
-        self.ring_frame.setContentsMargins(0, 0, 0, 0)
-        self.layout.addWidget(self.ring_frame)
+        drill_dia = self.ui.dia_entry.get_value()
+        circ_r_val = self.ui.circular_ring_entry.get_value()
+        oblong_r_val = self.ui.oblong_ring_entry.get_value()
+        square_r_val = self.ui.square_ring_entry.get_value()
+        rect_r_val = self.ui.rectangular_ring_entry.get_value()
+        other_r_val = self.ui.other_ring_entry.get_value()
 
-        self.ring_box = QtWidgets.QVBoxLayout()
-        self.ring_box.setContentsMargins(0, 0, 0, 0)
-        self.ring_frame.setLayout(self.ring_box)
+        prop_factor = self.ui.factor_entry.get_value() / 100.0
 
-        # ## Grid Layout
-        grid2 = QtWidgets.QGridLayout()
-        grid2.setColumnStretch(0, 0)
-        grid2.setColumnStretch(1, 1)
-        self.ring_box.addLayout(grid2)
+        drills = []
+        tools = {}
 
-        # Annular Ring value
-        self.ring_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Annular Ring"))
-        self.ring_label.setToolTip(
-            _("The size of annular ring.\n"
-              "The copper sliver between the hole exterior\n"
-              "and the margin of the copper pad.")
-        )
-        grid2.addWidget(self.ring_label, 0, 0, 1, 2)
+        selection_index = self.ui.gerber_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.ui.gerber_object_combo.rootModelIndex())
 
-        # Circular Annular Ring Value
-        self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular"))
-        self.circular_ring_label.setToolTip(
-            _("The size of annular ring for circular pads.")
-        )
+        try:
+            fcobj = model_index.internalPointer().obj
+        except Exception:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return
 
-        self.circular_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.circular_ring_entry.set_precision(self.decimals)
-        self.circular_ring_entry.set_range(0.0000, 9999.9999)
+        outname = fcobj.options['name'].rpartition('.')[0]
 
-        grid2.addWidget(self.circular_ring_label, 1, 0)
-        grid2.addWidget(self.circular_ring_entry, 1, 1)
+        mode = self.ui.hole_size_radio.get_value()
 
-        # Oblong Annular Ring Value
-        self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong"))
-        self.oblong_ring_label.setToolTip(
-            _("The size of annular ring for oblong pads.")
-        )
+        if mode == 'fixed':
+            tools = {
+                1: {
+                    "tooldia": drill_dia,
+                    "drills": [],
+                    "slots": []
+                }
+            }
+            for apid, apid_value in fcobj.apertures.items():
+                ap_type = apid_value['type']
 
-        self.oblong_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.oblong_ring_entry.set_precision(self.decimals)
-        self.oblong_ring_entry.set_range(0.0000, 9999.9999)
+                if ap_type == 'C':
+                    if self.ui.circular_cb.get_value() is False:
+                        continue
+                elif ap_type == 'O':
+                    if self.ui.oblong_cb.get_value() is False:
+                        continue
+                elif ap_type == 'R':
+                    width = float(apid_value['width'])
+                    height = float(apid_value['height'])
 
-        grid2.addWidget(self.oblong_ring_label, 2, 0)
-        grid2.addWidget(self.oblong_ring_entry, 2, 1)
+                    # if the height == width (float numbers so the reason for the following)
+                    if round(width, self.decimals) == round(height, self.decimals):
+                        if self.ui.square_cb.get_value() is False:
+                            continue
+                    else:
+                        if self.ui.rectangular_cb.get_value() is False:
+                            continue
+                else:
+                    if self.ui.other_cb.get_value() is False:
+                        continue
 
-        # Square Annular Ring Value
-        self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square"))
-        self.square_ring_label.setToolTip(
-            _("The size of annular ring for square pads.")
-        )
+                for geo_el in apid_value['geometry']:
+                    if 'follow' in geo_el and isinstance(geo_el['follow'], Point):
+                        tools[1]["drills"].append(geo_el['follow'])
+                        if 'solid_geometry' not in tools[1]:
+                            tools[1]['solid_geometry'] = []
+                        else:
+                            tools[1]['solid_geometry'].append(geo_el['follow'])
 
-        self.square_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.square_ring_entry.set_precision(self.decimals)
-        self.square_ring_entry.set_range(0.0000, 9999.9999)
+            if 'solid_geometry' not in tools[1] or not tools[1]['solid_geometry']:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("No drills extracted. Try different parameters."))
+                return
+        elif mode == 'ring':
+            drills_found = set()
+            for apid, apid_value in fcobj.apertures.items():
+                ap_type = apid_value['type']
 
-        grid2.addWidget(self.square_ring_label, 3, 0)
-        grid2.addWidget(self.square_ring_entry, 3, 1)
-
-        # Rectangular Annular Ring Value
-        self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular"))
-        self.rectangular_ring_label.setToolTip(
-            _("The size of annular ring for rectangular pads.")
-        )
-
-        self.rectangular_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.rectangular_ring_entry.set_precision(self.decimals)
-        self.rectangular_ring_entry.set_range(0.0000, 9999.9999)
-
-        grid2.addWidget(self.rectangular_ring_label, 4, 0)
-        grid2.addWidget(self.rectangular_ring_entry, 4, 1)
-
-        # Others Annular Ring Value
-        self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others"))
-        self.other_ring_label.setToolTip(
-            _("The size of annular ring for other pads.")
-        )
-
-        self.other_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.other_ring_entry.set_precision(self.decimals)
-        self.other_ring_entry.set_range(0.0000, 9999.9999)
-
-        grid2.addWidget(self.other_ring_label, 5, 0)
-        grid2.addWidget(self.other_ring_entry, 5, 1)
-
-        grid3 = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid3)
-        grid3.setColumnStretch(0, 0)
-        grid3.setColumnStretch(1, 1)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid3.addWidget(separator_line, 1, 0, 1, 2)
-
-        # Annular Ring value
-        self.prop_label = QtWidgets.QLabel('<b>%s</b>' % _("Proportional Diameter"))
-        grid3.addWidget(self.prop_label, 2, 0, 1, 2)
-
-        # Diameter value
-        self.factor_entry = FCDoubleSpinner(callback=self.confirmation_message, suffix='%')
-        self.factor_entry.set_precision(self.decimals)
-        self.factor_entry.set_range(0.0000, 100.0000)
-        self.factor_entry.setSingleStep(0.1)
-
-        self.factor_label = QtWidgets.QLabel('%s:' % _("Value"))
-        self.factor_label.setToolTip(
-            _("Proportional Diameter.\n"
-              "The hole diameter will be a fraction of the pad size.")
-        )
-
-        grid3.addWidget(self.factor_label, 3, 0)
-        grid3.addWidget(self.factor_entry, 3, 1)
-
-        # Extract drills from Gerber apertures flashes (pads)
-        self.e_drills_button = QtWidgets.QPushButton(_("Extract Drills"))
-        self.e_drills_button.setToolTip(
-            _("Extract drills from a given Gerber file.")
-        )
-        self.e_drills_button.setStyleSheet("""
-                                QPushButton
-                                {
-                                    font-weight: bold;
-                                }
-                                """)
-        self.layout.addWidget(self.e_drills_button)
-
-        self.layout.addStretch()
-
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
-
-        self.circular_ring_entry.setEnabled(False)
-        self.oblong_ring_entry.setEnabled(False)
-        self.square_ring_entry.setEnabled(False)
-        self.rectangular_ring_entry.setEnabled(False)
-        self.other_ring_entry.setEnabled(False)
-
-        self.dia_entry.setDisabled(True)
-        self.dia_label.setDisabled(True)
-        self.factor_label.setDisabled(True)
-        self.factor_entry.setDisabled(True)
-
-        self.ring_frame.setDisabled(True)
-
-        # ## Signals
-        self.hole_size_radio.activated_custom.connect(self.on_hole_size_toggle)
-        self.e_drills_button.clicked.connect(self.on_extract_drills_click)
-        self.reset_button.clicked.connect(self.set_tool_ui)
-
-        self.circular_cb.stateChanged.connect(
-            lambda state:
-                self.circular_ring_entry.setDisabled(False) if state else self.circular_ring_entry.setDisabled(True)
-        )
-
-        self.oblong_cb.stateChanged.connect(
-            lambda state:
-            self.oblong_ring_entry.setDisabled(False) if state else self.oblong_ring_entry.setDisabled(True)
-        )
-
-        self.square_cb.stateChanged.connect(
-            lambda state:
-            self.square_ring_entry.setDisabled(False) if state else self.square_ring_entry.setDisabled(True)
-        )
-
-        self.rectangular_cb.stateChanged.connect(
-            lambda state:
-            self.rectangular_ring_entry.setDisabled(False) if state else self.rectangular_ring_entry.setDisabled(True)
-        )
-
-        self.other_cb.stateChanged.connect(
-            lambda state:
-            self.other_ring_entry.setDisabled(False) if state else self.other_ring_entry.setDisabled(True)
-        )
-
-    def install(self, icon=None, separator=None, **kwargs):
-        AppTool.install(self, icon, separator, shortcut='Alt+I', **kwargs)
-
-    def run(self, toggle=True):
-        self.app.defaults.report_usage("Extract Drills()")
-
-        if toggle:
-            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-            else:
-                try:
-                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        # if tab is populated with the tool but it does not have the focus, focus on it
-                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
-                            # focus on Tool Tab
-                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-                        else:
-                            self.app.ui.splitter.setSizes([0, 1])
-                except AttributeError:
-                    pass
-        else:
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-
-        AppTool.run(self)
-        self.set_tool_ui()
-
-        self.app.ui.notebook.setTabText(2, _("Extract Drills Tool"))
-
-    def set_tool_ui(self):
-        self.reset_fields()
-
-        self.hole_size_radio.set_value(self.app.defaults["tools_edrills_hole_type"])
-
-        self.dia_entry.set_value(float(self.app.defaults["tools_edrills_hole_fixed_dia"]))
-
-        self.circular_ring_entry.set_value(float(self.app.defaults["tools_edrills_circular_ring"]))
-        self.oblong_ring_entry.set_value(float(self.app.defaults["tools_edrills_oblong_ring"]))
-        self.square_ring_entry.set_value(float(self.app.defaults["tools_edrills_square_ring"]))
-        self.rectangular_ring_entry.set_value(float(self.app.defaults["tools_edrills_rectangular_ring"]))
-        self.other_ring_entry.set_value(float(self.app.defaults["tools_edrills_others_ring"]))
-
-        self.circular_cb.set_value(self.app.defaults["tools_edrills_circular"])
-        self.oblong_cb.set_value(self.app.defaults["tools_edrills_oblong"])
-        self.square_cb.set_value(self.app.defaults["tools_edrills_square"])
-        self.rectangular_cb.set_value(self.app.defaults["tools_edrills_rectangular"])
-        self.other_cb.set_value(self.app.defaults["tools_edrills_others"])
-
-        self.factor_entry.set_value(float(self.app.defaults["tools_edrills_hole_prop_factor"]))
-
-    def on_extract_drills_click(self):
-
-        drill_dia = self.dia_entry.get_value()
-        circ_r_val = self.circular_ring_entry.get_value()
-        oblong_r_val = self.oblong_ring_entry.get_value()
-        square_r_val = self.square_ring_entry.get_value()
-        rect_r_val = self.rectangular_ring_entry.get_value()
-        other_r_val = self.other_ring_entry.get_value()
-
-        prop_factor = self.factor_entry.get_value() / 100.0
-
-        drills = []
-        tools = {}
-
-        selection_index = self.gerber_object_combo.currentIndex()
-        model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
-
-        try:
-            fcobj = model_index.internalPointer().obj
-        except Exception:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
-            return
-
-        outname = fcobj.options['name'].rpartition('.')[0]
-
-        mode = self.hole_size_radio.get_value()
-
-        if mode == 'fixed':
-            tools = {
-                1: {
-                    "tooldia": drill_dia,
-                    "drills": [],
-                    "slots": []
-                }
-            }
-            for apid, apid_value in fcobj.apertures.items():
-                ap_type = apid_value['type']
-
-                if ap_type == 'C':
-                    if self.circular_cb.get_value() is False:
-                        continue
-                elif ap_type == 'O':
-                    if self.oblong_cb.get_value() is False:
-                        continue
-                elif ap_type == 'R':
-                    width = float(apid_value['width'])
-                    height = float(apid_value['height'])
-
-                    # if the height == width (float numbers so the reason for the following)
-                    if round(width, self.decimals) == round(height, self.decimals):
-                        if self.square_cb.get_value() is False:
-                            continue
-                    else:
-                        if self.rectangular_cb.get_value() is False:
-                            continue
-                else:
-                    if self.other_cb.get_value() is False:
-                        continue
-
-                for geo_el in apid_value['geometry']:
-                    if 'follow' in geo_el and isinstance(geo_el['follow'], Point):
-                        tools[1]["drills"].append(geo_el['follow'])
-                        if 'solid_geometry' not in tools[1]:
-                            tools[1]['solid_geometry'] = []
-                        else:
-                            tools[1]['solid_geometry'].append(geo_el['follow'])
-
-            if 'solid_geometry' not in tools[1] or not tools[1]['solid_geometry']:
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("No drills extracted. Try different parameters."))
-                return
-        elif mode == 'ring':
-            drills_found = set()
-            for apid, apid_value in fcobj.apertures.items():
-                ap_type = apid_value['type']
-
-                dia = None
-                if ap_type == 'C':
-                    if self.circular_cb.get_value():
-                        dia = float(apid_value['size']) - (2 * circ_r_val)
-                elif ap_type == 'O':
-                    width = float(apid_value['width'])
-                    height = float(apid_value['height'])
-                    if self.oblong_cb.get_value():
-                        if width > height:
-                            dia = float(apid_value['height']) - (2 * oblong_r_val)
-                        else:
-                            dia = float(apid_value['width']) - (2 * oblong_r_val)
-                elif ap_type == 'R':
-                    width = float(apid_value['width'])
-                    height = float(apid_value['height'])
+                dia = None
+                if ap_type == 'C':
+                    if self.ui.circular_cb.get_value():
+                        dia = float(apid_value['size']) - (2 * circ_r_val)
+                elif ap_type == 'O':
+                    width = float(apid_value['width'])
+                    height = float(apid_value['height'])
+                    if self.ui.oblong_cb.get_value():
+                        if width > height:
+                            dia = float(apid_value['height']) - (2 * oblong_r_val)
+                        else:
+                            dia = float(apid_value['width']) - (2 * oblong_r_val)
+                elif ap_type == 'R':
+                    width = float(apid_value['width'])
+                    height = float(apid_value['height'])
 
                     # if the height == width (float numbers so the reason for the following)
                     if abs(float('%.*f' % (self.decimals, width)) - float('%.*f' % (self.decimals, height))) < \
                             (10 ** -self.decimals):
-                        if self.square_cb.get_value():
+                        if self.ui.square_cb.get_value():
                             dia = float(apid_value['height']) - (2 * square_r_val)
                     else:
-                        if self.rectangular_cb.get_value():
+                        if self.ui.rectangular_cb.get_value():
                             if width > height:
                                 dia = float(apid_value['height']) - (2 * rect_r_val)
                             else:
                                 dia = float(apid_value['width']) - (2 * rect_r_val)
                 else:
-                    if self.other_cb.get_value():
+                    if self.ui.other_cb.get_value():
                         try:
                             dia = float(apid_value['size']) - (2 * other_r_val)
                         except KeyError:
@@ -580,12 +285,12 @@ class ToolExtractDrills(AppTool):
 
                 dia = None
                 if ap_type == 'C':
-                    if self.circular_cb.get_value():
+                    if self.ui.circular_cb.get_value():
                         dia = float(apid_value['size']) * prop_factor
                 elif ap_type == 'O':
                     width = float(apid_value['width'])
                     height = float(apid_value['height'])
-                    if self.oblong_cb.get_value():
+                    if self.ui.oblong_cb.get_value():
                         if width > height:
                             dia = float(apid_value['height']) * prop_factor
                         else:
@@ -597,16 +302,16 @@ class ToolExtractDrills(AppTool):
                     # if the height == width (float numbers so the reason for the following)
                     if abs(float('%.*f' % (self.decimals, width)) - float('%.*f' % (self.decimals, height))) < \
                             (10 ** -self.decimals):
-                        if self.square_cb.get_value():
+                        if self.ui.square_cb.get_value():
                             dia = float(apid_value['height']) * prop_factor
                     else:
-                        if self.rectangular_cb.get_value():
+                        if self.ui.rectangular_cb.get_value():
                             if width > height:
                                 dia = float(apid_value['height']) * prop_factor
                             else:
                                 dia = float(apid_value['width']) * prop_factor
                 else:
-                    if self.other_cb.get_value():
+                    if self.ui.other_cb.get_value():
                         try:
                             dia = float(apid_value['size']) * prop_factor
                         except KeyError:
@@ -675,36 +380,365 @@ class ToolExtractDrills(AppTool):
 
     def on_hole_size_toggle(self, val):
         if val == "fixed":
-            self.fixed_label.setDisabled(False)
-            self.dia_entry.setDisabled(False)
-            self.dia_label.setDisabled(False)
+            self.ui.fixed_label.setDisabled(False)
+            self.ui.dia_entry.setDisabled(False)
+            self.ui.dia_label.setDisabled(False)
 
-            self.ring_frame.setDisabled(True)
+            self.ui.ring_frame.setDisabled(True)
 
-            self.prop_label.setDisabled(True)
-            self.factor_label.setDisabled(True)
-            self.factor_entry.setDisabled(True)
+            self.ui.prop_label.setDisabled(True)
+            self.ui.factor_label.setDisabled(True)
+            self.ui.factor_entry.setDisabled(True)
         elif val == "ring":
-            self.fixed_label.setDisabled(True)
-            self.dia_entry.setDisabled(True)
-            self.dia_label.setDisabled(True)
+            self.ui.fixed_label.setDisabled(True)
+            self.ui.dia_entry.setDisabled(True)
+            self.ui.dia_label.setDisabled(True)
 
-            self.ring_frame.setDisabled(False)
+            self.ui.ring_frame.setDisabled(False)
 
-            self.prop_label.setDisabled(True)
-            self.factor_label.setDisabled(True)
-            self.factor_entry.setDisabled(True)
+            self.ui.prop_label.setDisabled(True)
+            self.ui.factor_label.setDisabled(True)
+            self.ui.factor_entry.setDisabled(True)
         elif val == "prop":
-            self.fixed_label.setDisabled(True)
-            self.dia_entry.setDisabled(True)
-            self.dia_label.setDisabled(True)
+            self.ui.fixed_label.setDisabled(True)
+            self.ui.dia_entry.setDisabled(True)
+            self.ui.dia_label.setDisabled(True)
 
-            self.ring_frame.setDisabled(True)
+            self.ui.ring_frame.setDisabled(True)
 
-            self.prop_label.setDisabled(False)
-            self.factor_label.setDisabled(False)
-            self.factor_entry.setDisabled(False)
+            self.ui.prop_label.setDisabled(False)
+            self.ui.factor_label.setDisabled(False)
+            self.ui.factor_entry.setDisabled(False)
 
     def reset_fields(self):
+        self.ui.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ui.gerber_object_combo.setCurrentIndex(0)
+
+
+class ExtractDrillsUI:
+
+    toolName = _("Extract Drills")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+
+        self.layout.addWidget(QtWidgets.QLabel(""))
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 1)
+        grid_lay.setColumnStretch(1, 0)
+
+        # ## Gerber Object
+        self.gerber_object_combo = FCComboBox()
+        self.gerber_object_combo.setModel(self.app.collection)
         self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.gerber_object_combo.setCurrentIndex(0)
+        self.gerber_object_combo.is_last = True
+        self.gerber_object_combo.obj_type = "Gerber"
+
+        self.grb_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
+        self.grb_label.setToolTip('%s.' % _("Gerber from which to extract drill holes"))
+
+        # grid_lay.addRow("Bottom Layer:", self.object_combo)
+        grid_lay.addWidget(self.grb_label, 0, 0, 1, 2)
+        grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
+
+        self.padt_label = QtWidgets.QLabel("<b>%s</b>" % _("Processed Pads Type"))
+        self.padt_label.setToolTip(
+            _("The type of pads shape to be processed.\n"
+              "If the PCB has many SMD pads with rectangular pads,\n"
+              "disable the Rectangular aperture.")
+        )
+
+        grid_lay.addWidget(self.padt_label, 2, 0, 1, 2)
+
+        # Circular Aperture Selection
+        self.circular_cb = FCCheckBox('%s' % _("Circular"))
+        self.circular_cb.setToolTip(
+            _("Process Circular Pads.")
+        )
+
+        grid_lay.addWidget(self.circular_cb, 3, 0, 1, 2)
+
+        # Oblong Aperture Selection
+        self.oblong_cb = FCCheckBox('%s' % _("Oblong"))
+        self.oblong_cb.setToolTip(
+            _("Process Oblong Pads.")
+        )
+
+        grid_lay.addWidget(self.oblong_cb, 4, 0, 1, 2)
+
+        # Square Aperture Selection
+        self.square_cb = FCCheckBox('%s' % _("Square"))
+        self.square_cb.setToolTip(
+            _("Process Square Pads.")
+        )
+
+        grid_lay.addWidget(self.square_cb, 5, 0, 1, 2)
+
+        # Rectangular Aperture Selection
+        self.rectangular_cb = FCCheckBox('%s' % _("Rectangular"))
+        self.rectangular_cb.setToolTip(
+            _("Process Rectangular Pads.")
+        )
+
+        grid_lay.addWidget(self.rectangular_cb, 6, 0, 1, 2)
+
+        # Others type of Apertures Selection
+        self.other_cb = FCCheckBox('%s' % _("Others"))
+        self.other_cb.setToolTip(
+            _("Process pads not in the categories above.")
+        )
+
+        grid_lay.addWidget(self.other_cb, 7, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 8, 0, 1, 2)
+
+        # ## Grid Layout
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
+
+        self.method_label = QtWidgets.QLabel('<b>%s</b>' % _("Method"))
+        self.method_label.setToolTip(
+            _("The method for processing pads. Can be:\n"
+              "- Fixed Diameter -> all holes will have a set size\n"
+              "- Fixed Annular Ring -> all holes will have a set annular ring\n"
+              "- Proportional -> each hole size will be a fraction of the pad size"))
+        grid1.addWidget(self.method_label, 2, 0, 1, 2)
+
+        # ## Holes Size
+        self.hole_size_radio = RadioSet(
+            [
+                {'label': _("Fixed Diameter"), 'value': 'fixed'},
+                {'label': _("Fixed Annular Ring"), 'value': 'ring'},
+                {'label': _("Proportional"), 'value': 'prop'}
+            ],
+            orientation='vertical',
+            stretch=False)
+
+        grid1.addWidget(self.hole_size_radio, 3, 0, 1, 2)
+
+        # grid_lay1.addWidget(QtWidgets.QLabel(''))
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 5, 0, 1, 2)
+
+        # Annular Ring
+        self.fixed_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Diameter"))
+        grid1.addWidget(self.fixed_label, 6, 0, 1, 2)
+
+        # Diameter value
+        self.dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.dia_entry.set_precision(self.decimals)
+        self.dia_entry.set_range(0.0000, 9999.9999)
+
+        self.dia_label = QtWidgets.QLabel('%s:' % _("Value"))
+        self.dia_label.setToolTip(
+            _("Fixed hole diameter.")
+        )
+
+        grid1.addWidget(self.dia_label, 8, 0)
+        grid1.addWidget(self.dia_entry, 8, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 9, 0, 1, 2)
+
+        self.ring_frame = QtWidgets.QFrame()
+        self.ring_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.ring_frame)
+
+        self.ring_box = QtWidgets.QVBoxLayout()
+        self.ring_box.setContentsMargins(0, 0, 0, 0)
+        self.ring_frame.setLayout(self.ring_box)
+
+        # ## Grid Layout
+        grid2 = QtWidgets.QGridLayout()
+        grid2.setColumnStretch(0, 0)
+        grid2.setColumnStretch(1, 1)
+        self.ring_box.addLayout(grid2)
+
+        # Annular Ring value
+        self.ring_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Annular Ring"))
+        self.ring_label.setToolTip(
+            _("The size of annular ring.\n"
+              "The copper sliver between the hole exterior\n"
+              "and the margin of the copper pad.")
+        )
+        grid2.addWidget(self.ring_label, 0, 0, 1, 2)
+
+        # Circular Annular Ring Value
+        self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular"))
+        self.circular_ring_label.setToolTip(
+            _("The size of annular ring for circular pads.")
+        )
+
+        self.circular_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.circular_ring_entry.set_precision(self.decimals)
+        self.circular_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid2.addWidget(self.circular_ring_label, 1, 0)
+        grid2.addWidget(self.circular_ring_entry, 1, 1)
+
+        # Oblong Annular Ring Value
+        self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong"))
+        self.oblong_ring_label.setToolTip(
+            _("The size of annular ring for oblong pads.")
+        )
+
+        self.oblong_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.oblong_ring_entry.set_precision(self.decimals)
+        self.oblong_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid2.addWidget(self.oblong_ring_label, 2, 0)
+        grid2.addWidget(self.oblong_ring_entry, 2, 1)
+
+        # Square Annular Ring Value
+        self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square"))
+        self.square_ring_label.setToolTip(
+            _("The size of annular ring for square pads.")
+        )
+
+        self.square_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.square_ring_entry.set_precision(self.decimals)
+        self.square_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid2.addWidget(self.square_ring_label, 3, 0)
+        grid2.addWidget(self.square_ring_entry, 3, 1)
+
+        # Rectangular Annular Ring Value
+        self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular"))
+        self.rectangular_ring_label.setToolTip(
+            _("The size of annular ring for rectangular pads.")
+        )
+
+        self.rectangular_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.rectangular_ring_entry.set_precision(self.decimals)
+        self.rectangular_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid2.addWidget(self.rectangular_ring_label, 4, 0)
+        grid2.addWidget(self.rectangular_ring_entry, 4, 1)
+
+        # Others Annular Ring Value
+        self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others"))
+        self.other_ring_label.setToolTip(
+            _("The size of annular ring for other pads.")
+        )
+
+        self.other_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.other_ring_entry.set_precision(self.decimals)
+        self.other_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid2.addWidget(self.other_ring_label, 5, 0)
+        grid2.addWidget(self.other_ring_entry, 5, 1)
+
+        grid3 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid3)
+        grid3.setColumnStretch(0, 0)
+        grid3.setColumnStretch(1, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid3.addWidget(separator_line, 1, 0, 1, 2)
+
+        # Annular Ring value
+        self.prop_label = QtWidgets.QLabel('<b>%s</b>' % _("Proportional Diameter"))
+        grid3.addWidget(self.prop_label, 2, 0, 1, 2)
+
+        # Diameter value
+        self.factor_entry = FCDoubleSpinner(callback=self.confirmation_message, suffix='%')
+        self.factor_entry.set_precision(self.decimals)
+        self.factor_entry.set_range(0.0000, 100.0000)
+        self.factor_entry.setSingleStep(0.1)
+
+        self.factor_label = QtWidgets.QLabel('%s:' % _("Value"))
+        self.factor_label.setToolTip(
+            _("Proportional Diameter.\n"
+              "The hole diameter will be a fraction of the pad size.")
+        )
+
+        grid3.addWidget(self.factor_label, 3, 0)
+        grid3.addWidget(self.factor_entry, 3, 1)
+
+        # Extract drills from Gerber apertures flashes (pads)
+        self.e_drills_button = QtWidgets.QPushButton(_("Extract Drills"))
+        self.e_drills_button.setToolTip(
+            _("Extract drills from a given Gerber file.")
+        )
+        self.e_drills_button.setStyleSheet("""
+                                        QPushButton
+                                        {
+                                            font-weight: bold;
+                                        }
+                                        """)
+        self.layout.addWidget(self.e_drills_button)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.reset_button)
+
+        self.circular_ring_entry.setEnabled(False)
+        self.oblong_ring_entry.setEnabled(False)
+        self.square_ring_entry.setEnabled(False)
+        self.rectangular_ring_entry.setEnabled(False)
+        self.other_ring_entry.setEnabled(False)
+
+        self.dia_entry.setDisabled(True)
+        self.dia_label.setDisabled(True)
+        self.factor_label.setDisabled(True)
+        self.factor_entry.setDisabled(True)
+
+        self.ring_frame.setDisabled(True)
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 334 - 298
appTools/ToolPanelize.py

@@ -35,266 +35,22 @@ class Panelize(AppTool):
     toolName = _("Panelize PCB")
 
     def __init__(self, app):
-        self.decimals = app.decimals
-
         AppTool.__init__(self, app)
+        self.decimals = app.decimals
+        self.app = app
 
-        # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-
-        self.object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Source Object"))
-        self.object_label.setToolTip(
-            _("Specify the type of object to be panelized\n"
-              "It can be of type: Gerber, Excellon or Geometry.\n"
-              "The selection here decide the type of objects that will be\n"
-              "in the Object combobox.")
-        )
-
-        self.layout.addWidget(self.object_label)
-
-        # Form Layout
-        form_layout_0 = QtWidgets.QFormLayout()
-        self.layout.addLayout(form_layout_0)
-
-        # Type of object to be panelized
-        self.type_obj_combo = FCComboBox()
-        self.type_obj_combo.addItem("Gerber")
-        self.type_obj_combo.addItem("Excellon")
-        self.type_obj_combo.addItem("Geometry")
-
-        self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
-        self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
-        self.type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
-
-        self.type_object_label = QtWidgets.QLabel('%s:' % _("Object Type"))
-
-        form_layout_0.addRow(self.type_object_label, self.type_obj_combo)
-
-        # Object to be panelized
-        self.object_combo = FCComboBox()
-        self.object_combo.setModel(self.app.collection)
-        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.object_combo.is_last = True
-
-        self.object_combo.setToolTip(
-            _("Object to be panelized. This means that it will\n"
-              "be duplicated in an array of rows and columns.")
-        )
-        form_layout_0.addRow(self.object_combo)
-
-        # Form Layout
-        form_layout = QtWidgets.QFormLayout()
-        self.layout.addLayout(form_layout)
-
-        # Type of box Panel object
-        self.reference_radio = RadioSet([{'label': _('Object'), 'value': 'object'},
-                                         {'label': _('Bounding Box'), 'value': 'bbox'}])
-        self.box_label = QtWidgets.QLabel("<b>%s:</b>" % _("Penelization Reference"))
-        self.box_label.setToolTip(
-            _("Choose the reference for panelization:\n"
-              "- Object = the bounding box of a different object\n"
-              "- Bounding Box = the bounding box of the object to be panelized\n"
-              "\n"
-              "The reference is useful when doing panelization for more than one\n"
-              "object. The spacings (really offsets) will be applied in reference\n"
-              "to this reference object therefore maintaining the panelized\n"
-              "objects in sync.")
-        )
-        form_layout.addRow(self.box_label)
-        form_layout.addRow(self.reference_radio)
-
-        # Type of Box Object to be used as an envelope for panelization
-        self.type_box_combo = FCComboBox()
-        self.type_box_combo.addItems([_("Gerber"), _("Geometry")])
-
-        # we get rid of item1 ("Excellon") as it is not suitable for use as a "box" for panelizing
-        # self.type_box_combo.view().setRowHidden(1, True)
-        self.type_box_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
-        self.type_box_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
-
-        self.type_box_combo_label = QtWidgets.QLabel('%s:' % _("Box Type"))
-        self.type_box_combo_label.setToolTip(
-            _("Specify the type of object to be used as an container for\n"
-              "panelization. It can be: Gerber or Geometry type.\n"
-              "The selection here decide the type of objects that will be\n"
-              "in the Box Object combobox.")
-        )
-        form_layout.addRow(self.type_box_combo_label, self.type_box_combo)
-
-        # Box
-        self.box_combo = FCComboBox()
-        self.box_combo.setModel(self.app.collection)
-        self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.box_combo.is_last = True
-
-        self.box_combo.setToolTip(
-            _("The actual object that is used as container for the\n "
-              "selected object that is to be panelized.")
-        )
-        form_layout.addRow(self.box_combo)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        form_layout.addRow(separator_line)
-
-        panel_data_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Data"))
-        panel_data_label.setToolTip(
-            _("This informations will shape the resulting panel.\n"
-              "The number of rows and columns will set how many\n"
-              "duplicates of the original geometry will be generated.\n"
-              "\n"
-              "The spacings will set the distance between any two\n"
-              "elements of the panel array.")
-        )
-        form_layout.addRow(panel_data_label)
-
-        # Spacing Columns
-        self.spacing_columns = FCDoubleSpinner(callback=self.confirmation_message)
-        self.spacing_columns.set_range(0, 9999)
-        self.spacing_columns.set_precision(4)
-
-        self.spacing_columns_label = QtWidgets.QLabel('%s:' % _("Spacing cols"))
-        self.spacing_columns_label.setToolTip(
-            _("Spacing between columns of the desired panel.\n"
-              "In current units.")
-        )
-        form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
-
-        # Spacing Rows
-        self.spacing_rows = FCDoubleSpinner(callback=self.confirmation_message)
-        self.spacing_rows.set_range(0, 9999)
-        self.spacing_rows.set_precision(4)
-
-        self.spacing_rows_label = QtWidgets.QLabel('%s:' % _("Spacing rows"))
-        self.spacing_rows_label.setToolTip(
-            _("Spacing between rows of the desired panel.\n"
-              "In current units.")
-        )
-        form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
-
-        # Columns
-        self.columns = FCSpinner(callback=self.confirmation_message_int)
-        self.columns.set_range(0, 9999)
-
-        self.columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
-        self.columns_label.setToolTip(
-            _("Number of columns of the desired panel")
-        )
-        form_layout.addRow(self.columns_label, self.columns)
-
-        # Rows
-        self.rows = FCSpinner(callback=self.confirmation_message_int)
-        self.rows.set_range(0, 9999)
-
-        self.rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
-        self.rows_label.setToolTip(
-            _("Number of rows of the desired panel")
-        )
-        form_layout.addRow(self.rows_label, self.rows)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        form_layout.addRow(separator_line)
-
-        # Type of resulting Panel object
-        self.panel_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'gerber'},
-                                          {'label': _('Geo'), 'value': 'geometry'}])
-        self.panel_type_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Type"))
-        self.panel_type_label.setToolTip(
-            _("Choose the type of object for the panel object:\n"
-              "- Geometry\n"
-              "- Gerber")
-        )
-        form_layout.addRow(self.panel_type_label)
-        form_layout.addRow(self.panel_type_radio)
-
-        # Constrains
-        self.constrain_cb = FCCheckBox('%s:' % _("Constrain panel within"))
-        self.constrain_cb.setToolTip(
-            _("Area define by DX and DY within to constrain the panel.\n"
-              "DX and DY values are in current units.\n"
-              "Regardless of how many columns and rows are desired,\n"
-              "the final panel will have as many columns and rows as\n"
-              "they fit completely within selected area.")
-        )
-        form_layout.addRow(self.constrain_cb)
-
-        self.x_width_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.x_width_entry.set_precision(4)
-        self.x_width_entry.set_range(0, 9999)
-
-        self.x_width_lbl = QtWidgets.QLabel('%s:' % _("Width (DX)"))
-        self.x_width_lbl.setToolTip(
-            _("The width (DX) within which the panel must fit.\n"
-              "In current units.")
-        )
-        form_layout.addRow(self.x_width_lbl, self.x_width_entry)
-
-        self.y_height_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.y_height_entry.set_range(0, 9999)
-        self.y_height_entry.set_precision(4)
-
-        self.y_height_lbl = QtWidgets.QLabel('%s:' % _("Height (DY)"))
-        self.y_height_lbl.setToolTip(
-            _("The height (DY)within which the panel must fit.\n"
-              "In current units.")
-        )
-        form_layout.addRow(self.y_height_lbl, self.y_height_entry)
-
-        self.constrain_sel = OptionalInputSection(
-            self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        form_layout.addRow(separator_line)
-
-        # Buttons
-        self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object"))
-        self.panelize_object_button.setToolTip(
-            _("Panelize the specified object around the specified box.\n"
-              "In other words it creates multiple copies of the source object,\n"
-              "arranged in a 2D array of rows and columns.")
-        )
-        self.panelize_object_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.panelize_object_button)
-
-        self.layout.addStretch()
-
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = PanelizeUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
         # Signals
-        self.reference_radio.activated_custom.connect(self.on_reference_radio_changed)
-        self.panelize_object_button.clicked.connect(self.on_panelize)
-        self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
-        self.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
-        self.reset_button.clicked.connect(self.set_tool_ui)
+        self.ui.reference_radio.activated_custom.connect(self.on_reference_radio_changed)
+        self.ui.panelize_object_button.clicked.connect(self.on_panelize)
+        self.ui.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
+        self.ui.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
 
         # list to hold the temporary objects
         self.objs = []
@@ -342,35 +98,35 @@ class Panelize(AppTool):
 
         sp_c = self.app.defaults["tools_panelize_spacing_columns"] if \
             self.app.defaults["tools_panelize_spacing_columns"] else 0.0
-        self.spacing_columns.set_value(float(sp_c))
+        self.ui.spacing_columns.set_value(float(sp_c))
 
         sp_r = self.app.defaults["tools_panelize_spacing_rows"] if \
             self.app.defaults["tools_panelize_spacing_rows"] else 0.0
-        self.spacing_rows.set_value(float(sp_r))
+        self.ui.spacing_rows.set_value(float(sp_r))
 
         rr = self.app.defaults["tools_panelize_rows"] if \
             self.app.defaults["tools_panelize_rows"] else 0.0
-        self.rows.set_value(int(rr))
+        self.ui.rows.set_value(int(rr))
 
         cc = self.app.defaults["tools_panelize_columns"] if \
             self.app.defaults["tools_panelize_columns"] else 0.0
-        self.columns.set_value(int(cc))
+        self.ui.columns.set_value(int(cc))
 
         c_cb = self.app.defaults["tools_panelize_constrain"] if \
             self.app.defaults["tools_panelize_constrain"] else False
-        self.constrain_cb.set_value(c_cb)
+        self.ui.constrain_cb.set_value(c_cb)
 
         x_w = self.app.defaults["tools_panelize_constrainx"] if \
             self.app.defaults["tools_panelize_constrainx"] else 0.0
-        self.x_width_entry.set_value(float(x_w))
+        self.ui.x_width_entry.set_value(float(x_w))
 
         y_w = self.app.defaults["tools_panelize_constrainy"] if \
             self.app.defaults["tools_panelize_constrainy"] else 0.0
-        self.y_height_entry.set_value(float(y_w))
+        self.ui.y_height_entry.set_value(float(y_w))
 
         panel_type = self.app.defaults["tools_panelize_panel_type"] if \
             self.app.defaults["tools_panelize_panel_type"] else 'gerber'
-        self.panel_type_radio.set_value(panel_type)
+        self.ui.panel_type_radio.set_value(panel_type)
 
         # run once the following so the obj_type attribute is updated in the FCComboBoxes
         # such that the last loaded object is populated in the combo boxes
@@ -378,43 +134,43 @@ class Panelize(AppTool):
         self.on_type_box_index_changed()
 
     def on_type_obj_index_changed(self):
-        obj_type = self.type_obj_combo.currentIndex()
-        self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        self.object_combo.setCurrentIndex(0)
-        self.object_combo.obj_type = {
+        obj_type = self.ui.type_obj_combo.currentIndex()
+        self.ui.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.ui.object_combo.setCurrentIndex(0)
+        self.ui.object_combo.obj_type = {
             _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
-        }[self.type_obj_combo.get_value()]
+        }[self.ui.type_obj_combo.get_value()]
 
         # hide the panel type for Excellons, the panel can be only of type Geometry
-        if self.type_obj_combo.currentText() != 'Excellon':
-            self.panel_type_label.setDisabled(False)
-            self.panel_type_radio.setDisabled(False)
+        if self.ui.type_obj_combo.currentText() != 'Excellon':
+            self.ui.panel_type_label.setDisabled(False)
+            self.ui.panel_type_radio.setDisabled(False)
         else:
-            self.panel_type_label.setDisabled(True)
-            self.panel_type_radio.setDisabled(True)
-            self.panel_type_radio.set_value('geometry')
+            self.ui.panel_type_label.setDisabled(True)
+            self.ui.panel_type_radio.setDisabled(True)
+            self.ui.panel_type_radio.set_value('geometry')
 
     def on_type_box_index_changed(self):
-        obj_type = self.type_box_combo.currentIndex()
+        obj_type = self.ui.type_box_combo.currentIndex()
         obj_type = 2 if obj_type == 1 else obj_type
-        self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        self.box_combo.setCurrentIndex(0)
-        self.box_combo.obj_type = {
+        self.ui.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.ui.box_combo.setCurrentIndex(0)
+        self.ui.box_combo.obj_type = {
             _("Gerber"): "Gerber", _("Geometry"): "Geometry"
-        }[self.type_box_combo.get_value()]
+        }[self.ui.type_box_combo.get_value()]
 
     def on_reference_radio_changed(self, current_val):
         if current_val == 'object':
-            self.type_box_combo.setDisabled(False)
-            self.type_box_combo_label.setDisabled(False)
-            self.box_combo.setDisabled(False)
+            self.ui.type_box_combo.setDisabled(False)
+            self.ui.type_box_combo_label.setDisabled(False)
+            self.ui.box_combo.setDisabled(False)
         else:
-            self.type_box_combo.setDisabled(True)
-            self.type_box_combo_label.setDisabled(True)
-            self.box_combo.setDisabled(True)
+            self.ui.type_box_combo.setDisabled(True)
+            self.ui.type_box_combo_label.setDisabled(True)
+            self.ui.box_combo.setDisabled(True)
 
     def on_panelize(self):
-        name = self.object_combo.currentText()
+        name = self.ui.object_combo.currentText()
 
         # Get source object to be panelized.
         try:
@@ -429,7 +185,7 @@ class Panelize(AppTool):
                                  (_("Object not found"), panel_source_obj))
             return
 
-        boxname = self.box_combo.currentText()
+        boxname = self.ui.box_combo.currentText()
 
         try:
             box = self.app.collection.get_by_name(boxname)
@@ -440,29 +196,29 @@ class Panelize(AppTool):
 
         if box is None:
             self.app.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), panel_source_obj))
-            self.reference_radio.set_value('bbox')
+            self.ui.reference_radio.set_value('bbox')
 
-        if self.reference_radio.get_value() == 'bbox':
+        if self.ui.reference_radio.get_value() == 'bbox':
             box = panel_source_obj
 
         self.outname = name + '_panelized'
 
-        spacing_columns = float(self.spacing_columns.get_value())
+        spacing_columns = float(self.ui.spacing_columns.get_value())
         spacing_columns = spacing_columns if spacing_columns is not None else 0
 
-        spacing_rows = float(self.spacing_rows.get_value())
+        spacing_rows = float(self.ui.spacing_rows.get_value())
         spacing_rows = spacing_rows if spacing_rows is not None else 0
 
-        rows = int(self.rows.get_value())
+        rows = int(self.ui.rows.get_value())
         rows = rows if rows is not None else 1
 
-        columns = int(self.columns.get_value())
+        columns = int(self.ui.columns.get_value())
         columns = columns if columns is not None else 1
 
-        constrain_dx = float(self.x_width_entry.get_value())
-        constrain_dy = float(self.y_height_entry.get_value())
+        constrain_dx = float(self.ui.x_width_entry.get_value())
+        constrain_dy = float(self.ui.y_height_entry.get_value())
 
-        panel_type = str(self.panel_type_radio.get_value())
+        panel_type = str(self.ui.panel_type_radio.get_value())
 
         if 0 in {columns, rows}:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
@@ -474,7 +230,7 @@ class Panelize(AppTool):
         lenghty = ymax - ymin + spacing_rows
 
         # check if constrain within an area is desired
-        if self.constrain_cb.isChecked():
+        if self.ui.constrain_cb.isChecked():
             panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
             panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
 
@@ -820,3 +576,283 @@ class Panelize(AppTool):
     def reset_fields(self):
         self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+
+
+class PanelizeUI:
+
+    toolName = _("Panelize PCB")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+
+        self.object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Source Object"))
+        self.object_label.setToolTip(
+            _("Specify the type of object to be panelized\n"
+              "It can be of type: Gerber, Excellon or Geometry.\n"
+              "The selection here decide the type of objects that will be\n"
+              "in the Object combobox.")
+        )
+
+        self.layout.addWidget(self.object_label)
+
+        # Form Layout
+        form_layout_0 = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout_0)
+
+        # Type of object to be panelized
+        self.type_obj_combo = FCComboBox()
+        self.type_obj_combo.addItem("Gerber")
+        self.type_obj_combo.addItem("Excellon")
+        self.type_obj_combo.addItem("Geometry")
+
+        self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
+        self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
+        self.type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
+
+        self.type_object_label = QtWidgets.QLabel('%s:' % _("Object Type"))
+
+        form_layout_0.addRow(self.type_object_label, self.type_obj_combo)
+
+        # Object to be panelized
+        self.object_combo = FCComboBox()
+        self.object_combo.setModel(self.app.collection)
+        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.object_combo.is_last = True
+
+        self.object_combo.setToolTip(
+            _("Object to be panelized. This means that it will\n"
+              "be duplicated in an array of rows and columns.")
+        )
+        form_layout_0.addRow(self.object_combo)
+
+        # Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout)
+
+        # Type of box Panel object
+        self.reference_radio = RadioSet([{'label': _('Object'), 'value': 'object'},
+                                         {'label': _('Bounding Box'), 'value': 'bbox'}])
+        self.box_label = QtWidgets.QLabel("<b>%s:</b>" % _("Penelization Reference"))
+        self.box_label.setToolTip(
+            _("Choose the reference for panelization:\n"
+              "- Object = the bounding box of a different object\n"
+              "- Bounding Box = the bounding box of the object to be panelized\n"
+              "\n"
+              "The reference is useful when doing panelization for more than one\n"
+              "object. The spacings (really offsets) will be applied in reference\n"
+              "to this reference object therefore maintaining the panelized\n"
+              "objects in sync.")
+        )
+        form_layout.addRow(self.box_label)
+        form_layout.addRow(self.reference_radio)
+
+        # Type of Box Object to be used as an envelope for panelization
+        self.type_box_combo = FCComboBox()
+        self.type_box_combo.addItems([_("Gerber"), _("Geometry")])
+
+        # we get rid of item1 ("Excellon") as it is not suitable for use as a "box" for panelizing
+        # self.type_box_combo.view().setRowHidden(1, True)
+        self.type_box_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
+        self.type_box_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
+
+        self.type_box_combo_label = QtWidgets.QLabel('%s:' % _("Box Type"))
+        self.type_box_combo_label.setToolTip(
+            _("Specify the type of object to be used as an container for\n"
+              "panelization. It can be: Gerber or Geometry type.\n"
+              "The selection here decide the type of objects that will be\n"
+              "in the Box Object combobox.")
+        )
+        form_layout.addRow(self.type_box_combo_label, self.type_box_combo)
+
+        # Box
+        self.box_combo = FCComboBox()
+        self.box_combo.setModel(self.app.collection)
+        self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.box_combo.is_last = True
+
+        self.box_combo.setToolTip(
+            _("The actual object that is used as container for the\n "
+              "selected object that is to be panelized.")
+        )
+        form_layout.addRow(self.box_combo)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        form_layout.addRow(separator_line)
+
+        panel_data_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Data"))
+        panel_data_label.setToolTip(
+            _("This informations will shape the resulting panel.\n"
+              "The number of rows and columns will set how many\n"
+              "duplicates of the original geometry will be generated.\n"
+              "\n"
+              "The spacings will set the distance between any two\n"
+              "elements of the panel array.")
+        )
+        form_layout.addRow(panel_data_label)
+
+        # Spacing Columns
+        self.spacing_columns = FCDoubleSpinner(callback=self.confirmation_message)
+        self.spacing_columns.set_range(0, 9999)
+        self.spacing_columns.set_precision(4)
+
+        self.spacing_columns_label = QtWidgets.QLabel('%s:' % _("Spacing cols"))
+        self.spacing_columns_label.setToolTip(
+            _("Spacing between columns of the desired panel.\n"
+              "In current units.")
+        )
+        form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
+
+        # Spacing Rows
+        self.spacing_rows = FCDoubleSpinner(callback=self.confirmation_message)
+        self.spacing_rows.set_range(0, 9999)
+        self.spacing_rows.set_precision(4)
+
+        self.spacing_rows_label = QtWidgets.QLabel('%s:' % _("Spacing rows"))
+        self.spacing_rows_label.setToolTip(
+            _("Spacing between rows of the desired panel.\n"
+              "In current units.")
+        )
+        form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
+
+        # Columns
+        self.columns = FCSpinner(callback=self.confirmation_message_int)
+        self.columns.set_range(0, 9999)
+
+        self.columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
+        self.columns_label.setToolTip(
+            _("Number of columns of the desired panel")
+        )
+        form_layout.addRow(self.columns_label, self.columns)
+
+        # Rows
+        self.rows = FCSpinner(callback=self.confirmation_message_int)
+        self.rows.set_range(0, 9999)
+
+        self.rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
+        self.rows_label.setToolTip(
+            _("Number of rows of the desired panel")
+        )
+        form_layout.addRow(self.rows_label, self.rows)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        form_layout.addRow(separator_line)
+
+        # Type of resulting Panel object
+        self.panel_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'gerber'},
+                                          {'label': _('Geo'), 'value': 'geometry'}])
+        self.panel_type_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Type"))
+        self.panel_type_label.setToolTip(
+            _("Choose the type of object for the panel object:\n"
+              "- Geometry\n"
+              "- Gerber")
+        )
+        form_layout.addRow(self.panel_type_label)
+        form_layout.addRow(self.panel_type_radio)
+
+        # Constrains
+        self.constrain_cb = FCCheckBox('%s:' % _("Constrain panel within"))
+        self.constrain_cb.setToolTip(
+            _("Area define by DX and DY within to constrain the panel.\n"
+              "DX and DY values are in current units.\n"
+              "Regardless of how many columns and rows are desired,\n"
+              "the final panel will have as many columns and rows as\n"
+              "they fit completely within selected area.")
+        )
+        form_layout.addRow(self.constrain_cb)
+
+        self.x_width_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.x_width_entry.set_precision(4)
+        self.x_width_entry.set_range(0, 9999)
+
+        self.x_width_lbl = QtWidgets.QLabel('%s:' % _("Width (DX)"))
+        self.x_width_lbl.setToolTip(
+            _("The width (DX) within which the panel must fit.\n"
+              "In current units.")
+        )
+        form_layout.addRow(self.x_width_lbl, self.x_width_entry)
+
+        self.y_height_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.y_height_entry.set_range(0, 9999)
+        self.y_height_entry.set_precision(4)
+
+        self.y_height_lbl = QtWidgets.QLabel('%s:' % _("Height (DY)"))
+        self.y_height_lbl.setToolTip(
+            _("The height (DY)within which the panel must fit.\n"
+              "In current units.")
+        )
+        form_layout.addRow(self.y_height_lbl, self.y_height_entry)
+
+        self.constrain_sel = OptionalInputSection(
+            self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        form_layout.addRow(separator_line)
+
+        # Buttons
+        self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object"))
+        self.panelize_object_button.setToolTip(
+            _("Panelize the specified object around the specified box.\n"
+              "In other words it creates multiple copies of the source object,\n"
+              "arranged in a 2D array of rows and columns.")
+        )
+        self.panelize_object_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.panelize_object_button)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.reset_button)
+
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 531 - 500
appTools/ToolPunchGerber.py

@@ -27,528 +27,204 @@ log = logging.getLogger('base')
 
 class ToolPunchGerber(AppTool):
 
-    toolName = _("Punch Gerber")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
 
+        self.app = app
         self.decimals = self.app.decimals
+        self.units = self.app.defaults['units']
 
-        # Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-
-        # Punch Drill holes
-        self.layout.addWidget(QtWidgets.QLabel(""))
-
-        # ## Grid Layout
-        grid_lay = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid_lay)
-        grid_lay.setColumnStretch(0, 1)
-        grid_lay.setColumnStretch(1, 0)
-
-        # ## Gerber Object
-        self.gerber_object_combo = FCComboBox()
-        self.gerber_object_combo.setModel(self.app.collection)
-        self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.gerber_object_combo.is_last = True
-        self.gerber_object_combo.obj_type = "Gerber"
-
-        self.grb_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
-        self.grb_label.setToolTip('%s.' % _("Gerber into which to punch holes"))
-
-        grid_lay.addWidget(self.grb_label, 0, 0, 1, 2)
-        grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = PunchUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line, 2, 0, 1, 2)
+        # ## Signals
+        self.ui.method_punch.activated_custom.connect(self.on_method)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
+        self.ui.punch_object_button.clicked.connect(self.on_generate_object)
 
-        self.padt_label = QtWidgets.QLabel("<b>%s</b>" % _("Processed Pads Type"))
-        self.padt_label.setToolTip(
-            _("The type of pads shape to be processed.\n"
-              "If the PCB has many SMD pads with rectangular pads,\n"
-              "disable the Rectangular aperture.")
+        self.ui.circular_cb.stateChanged.connect(
+            lambda state:
+                self.ui.circular_ring_entry.setDisabled(False) if state else
+                self.ui.circular_ring_entry.setDisabled(True)
         )
 
-        grid_lay.addWidget(self.padt_label, 3, 0, 1, 2)
-
-        # Select all
-        self.select_all_cb = FCCheckBox('%s' % _("ALL"))
-        grid_lay.addWidget(self.select_all_cb)
+        self.ui.oblong_cb.stateChanged.connect(
+            lambda state:
+            self.ui.oblong_ring_entry.setDisabled(False) if state else self.ui.oblong_ring_entry.setDisabled(True)
+        )
 
-        # Circular Aperture Selection
-        self.circular_cb = FCCheckBox('%s' % _("Circular"))
-        self.circular_cb.setToolTip(
-            _("Process Circular Pads.")
+        self.ui.square_cb.stateChanged.connect(
+            lambda state:
+            self.ui.square_ring_entry.setDisabled(False) if state else self.ui.square_ring_entry.setDisabled(True)
         )
 
-        grid_lay.addWidget(self.circular_cb, 5, 0, 1, 2)
+        self.ui.rectangular_cb.stateChanged.connect(
+            lambda state:
+            self.ui.rectangular_ring_entry.setDisabled(False) if state else
+            self.ui.rectangular_ring_entry.setDisabled(True)
+        )
 
-        # Oblong Aperture Selection
-        self.oblong_cb = FCCheckBox('%s' % _("Oblong"))
-        self.oblong_cb.setToolTip(
-            _("Process Oblong Pads.")
+        self.ui.other_cb.stateChanged.connect(
+            lambda state:
+            self.ui.other_ring_entry.setDisabled(False) if state else self.ui.other_ring_entry.setDisabled(True)
         )
 
-        grid_lay.addWidget(self.oblong_cb, 6, 0, 1, 2)
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("ToolPunchGerber()")
 
-        # Square Aperture Selection
-        self.square_cb = FCCheckBox('%s' % _("Square"))
-        self.square_cb.setToolTip(
-            _("Process Square Pads.")
-        )
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
 
-        grid_lay.addWidget(self.square_cb, 7, 0, 1, 2)
+        AppTool.run(self)
 
-        # Rectangular Aperture Selection
-        self.rectangular_cb = FCCheckBox('%s' % _("Rectangular"))
-        self.rectangular_cb.setToolTip(
-            _("Process Rectangular Pads.")
-        )
+        self.set_tool_ui()
 
-        grid_lay.addWidget(self.rectangular_cb, 8, 0, 1, 2)
+        self.app.ui.notebook.setTabText(2, _("Punch Tool"))
 
-        # Others type of Apertures Selection
-        self.other_cb = FCCheckBox('%s' % _("Others"))
-        self.other_cb.setToolTip(
-            _("Process pads not in the categories above.")
-        )
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='Alt+H', **kwargs)
 
-        grid_lay.addWidget(self.other_cb, 9, 0, 1, 2)
+    def set_tool_ui(self):
+        self.reset_fields()
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line, 10, 0, 1, 2)
+        self.ui_connect()
+        self.ui.method_punch.set_value(self.app.defaults["tools_punch_hole_type"])
+        self.ui.select_all_cb.set_value(False)
 
-        # Grid Layout
-        grid0 = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid0)
-        grid0.setColumnStretch(0, 0)
-        grid0.setColumnStretch(1, 1)
+        self.ui.dia_entry.set_value(float(self.app.defaults["tools_punch_hole_fixed_dia"]))
 
-        self.method_label = QtWidgets.QLabel('<b>%s:</b>' % _("Method"))
-        self.method_label.setToolTip(
-            _("The punch hole source can be:\n"
-              "- Excellon Object-> the Excellon object drills center will serve as reference.\n"
-              "- Fixed Diameter -> will try to use the pads center as reference adding fixed diameter holes.\n"
-              "- Fixed Annular Ring -> will try to keep a set annular ring.\n"
-              "- Proportional -> will make a Gerber punch hole having the diameter a percentage of the pad diameter.")
-        )
-        self.method_punch = RadioSet(
-            [
-                {'label': _('Excellon'), 'value': 'exc'},
-                {'label': _("Fixed Diameter"), 'value': 'fixed'},
-                {'label': _("Fixed Annular Ring"), 'value': 'ring'},
-                {'label': _("Proportional"), 'value': 'prop'}
-            ],
-            orientation='vertical',
-            stretch=False)
-        grid0.addWidget(self.method_label, 0, 0, 1, 2)
-        grid0.addWidget(self.method_punch, 1, 0, 1, 2)
+        self.ui.circular_ring_entry.set_value(float(self.app.defaults["tools_punch_circular_ring"]))
+        self.ui.oblong_ring_entry.set_value(float(self.app.defaults["tools_punch_oblong_ring"]))
+        self.ui.square_ring_entry.set_value(float(self.app.defaults["tools_punch_square_ring"]))
+        self.ui.rectangular_ring_entry.set_value(float(self.app.defaults["tools_punch_rectangular_ring"]))
+        self.ui.other_ring_entry.set_value(float(self.app.defaults["tools_punch_others_ring"]))
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 2, 0, 1, 2)
+        self.ui.circular_cb.set_value(self.app.defaults["tools_punch_circular"])
+        self.ui.oblong_cb.set_value(self.app.defaults["tools_punch_oblong"])
+        self.ui.square_cb.set_value(self.app.defaults["tools_punch_square"])
+        self.ui.rectangular_cb.set_value(self.app.defaults["tools_punch_rectangular"])
+        self.ui.other_cb.set_value(self.app.defaults["tools_punch_others"])
 
-        self.exc_label = QtWidgets.QLabel('<b>%s</b>' % _("Excellon"))
-        self.exc_label.setToolTip(
-            _("Remove the geometry of Excellon from the Gerber to create the holes in pads.")
-        )
+        self.ui.factor_entry.set_value(float(self.app.defaults["tools_punch_hole_prop_factor"]))
 
-        self.exc_combo = FCComboBox()
-        self.exc_combo.setModel(self.app.collection)
-        self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
-        self.exc_combo.is_last = True
-        self.exc_combo.obj_type = "Excellon"
+    def on_select_all(self, state):
+        self.ui_disconnect()
+        if state:
+            self.ui.circular_cb.setChecked(True)
+            self.ui.oblong_cb.setChecked(True)
+            self.ui.square_cb.setChecked(True)
+            self.ui.rectangular_cb.setChecked(True)
+            self.ui.other_cb.setChecked(True)
+        else:
+            self.ui.circular_cb.setChecked(False)
+            self.ui.oblong_cb.setChecked(False)
+            self.ui.square_cb.setChecked(False)
+            self.ui.rectangular_cb.setChecked(False)
+            self.ui.other_cb.setChecked(False)
+        self.ui_connect()
 
-        grid0.addWidget(self.exc_label, 3, 0, 1, 2)
-        grid0.addWidget(self.exc_combo, 4, 0, 1, 2)
+    def on_method(self, val):
+        self.ui.exc_label.setEnabled(False)
+        self.ui.exc_combo.setEnabled(False)
+        self.ui.fixed_label.setEnabled(False)
+        self.ui.dia_label.setEnabled(False)
+        self.ui.dia_entry.setEnabled(False)
+        self.ui.ring_frame.setEnabled(False)
+        self.ui.prop_label.setEnabled(False)
+        self.ui.factor_label.setEnabled(False)
+        self.ui.factor_entry.setEnabled(False)
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 5, 0, 1, 2)
+        if val == 'exc':
+            self.ui.exc_label.setEnabled(True)
+            self.ui.exc_combo.setEnabled(True)
+        elif val == 'fixed':
+            self.ui.fixed_label.setEnabled(True)
+            self.ui.dia_label.setEnabled(True)
+            self.ui.dia_entry.setEnabled(True)
+        elif val == 'ring':
+            self.ui.ring_frame.setEnabled(True)
+        elif val == 'prop':
+            self.ui.prop_label.setEnabled(True)
+            self.ui.factor_label.setEnabled(True)
+            self.ui.factor_entry.setEnabled(True)
 
-        # Fixed Dia
-        self.fixed_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Diameter"))
-        grid0.addWidget(self.fixed_label, 6, 0, 1, 2)
+    def ui_connect(self):
+        self.ui.select_all_cb.stateChanged.connect(self.on_select_all)
 
-        # Diameter value
-        self.dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.dia_entry.set_precision(self.decimals)
-        self.dia_entry.set_range(0.0000, 9999.9999)
+    def ui_disconnect(self):
+        try:
+            self.ui.select_all_cb.stateChanged.disconnect()
+        except (AttributeError, TypeError):
+            pass
 
-        self.dia_label = QtWidgets.QLabel('%s:' % _("Value"))
-        self.dia_label.setToolTip(
-            _("Fixed hole diameter.")
-        )
+    def on_generate_object(self):
 
-        grid0.addWidget(self.dia_label, 8, 0)
-        grid0.addWidget(self.dia_entry, 8, 1)
+        # get the Gerber file who is the source of the punched Gerber
+        selection_index = self.ui.gerber_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.ui.gerber_object_combo.rootModelIndex())
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 9, 0, 1, 2)
+        try:
+            grb_obj = model_index.internalPointer().obj
+        except Exception:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return
 
-        self.ring_frame = QtWidgets.QFrame()
-        self.ring_frame.setContentsMargins(0, 0, 0, 0)
-        grid0.addWidget(self.ring_frame, 10, 0, 1, 2)
+        name = grb_obj.options['name'].rpartition('.')[0]
+        outname = name + "_punched"
 
-        self.ring_box = QtWidgets.QVBoxLayout()
-        self.ring_box.setContentsMargins(0, 0, 0, 0)
-        self.ring_frame.setLayout(self.ring_box)
+        punch_method = self.ui.method_punch.get_value()
 
-        # Annular Ring value
-        self.ring_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Annular Ring"))
-        self.ring_label.setToolTip(
-            _("The size of annular ring.\n"
-              "The copper sliver between the hole exterior\n"
-              "and the margin of the copper pad.")
-        )
-        self.ring_box.addWidget(self.ring_label)
+        new_options = {}
+        for opt in grb_obj.options:
+            new_options[opt] = deepcopy(grb_obj.options[opt])
 
-        # ## Grid Layout
-        self.grid1 = QtWidgets.QGridLayout()
-        self.grid1.setColumnStretch(0, 0)
-        self.grid1.setColumnStretch(1, 1)
-        self.ring_box.addLayout(self.grid1)
+        if punch_method == 'exc':
 
-        # Circular Annular Ring Value
-        self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular"))
-        self.circular_ring_label.setToolTip(
-            _("The size of annular ring for circular pads.")
-        )
+            # get the Excellon file whose geometry will create the punch holes
+            selection_index = self.ui.exc_combo.currentIndex()
+            model_index = self.app.collection.index(selection_index, 0, self.ui.exc_combo.rootModelIndex())
 
-        self.circular_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.circular_ring_entry.set_precision(self.decimals)
-        self.circular_ring_entry.set_range(0.0000, 9999.9999)
+            try:
+                exc_obj = model_index.internalPointer().obj
+            except Exception:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ..."))
+                return
 
-        self.grid1.addWidget(self.circular_ring_label, 3, 0)
-        self.grid1.addWidget(self.circular_ring_entry, 3, 1)
+            # this is the punching geometry
+            exc_solid_geometry = MultiPolygon(exc_obj.solid_geometry)
+            if isinstance(grb_obj.solid_geometry, list):
+                grb_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
+            else:
+                grb_solid_geometry = grb_obj.solid_geometry
 
-        # Oblong Annular Ring Value
-        self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong"))
-        self.oblong_ring_label.setToolTip(
-            _("The size of annular ring for oblong pads.")
-        )
+                # create the punched Gerber solid_geometry
+            punched_solid_geometry = grb_solid_geometry.difference(exc_solid_geometry)
 
-        self.oblong_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.oblong_ring_entry.set_precision(self.decimals)
-        self.oblong_ring_entry.set_range(0.0000, 9999.9999)
+            # update the gerber apertures to include the clear geometry so it can be exported successfully
+            new_apertures = deepcopy(grb_obj.apertures)
+            new_apertures_items = new_apertures.items()
 
-        self.grid1.addWidget(self.oblong_ring_label, 4, 0)
-        self.grid1.addWidget(self.oblong_ring_entry, 4, 1)
-
-        # Square Annular Ring Value
-        self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square"))
-        self.square_ring_label.setToolTip(
-            _("The size of annular ring for square pads.")
-        )
-
-        self.square_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.square_ring_entry.set_precision(self.decimals)
-        self.square_ring_entry.set_range(0.0000, 9999.9999)
-
-        self.grid1.addWidget(self.square_ring_label, 5, 0)
-        self.grid1.addWidget(self.square_ring_entry, 5, 1)
-
-        # Rectangular Annular Ring Value
-        self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular"))
-        self.rectangular_ring_label.setToolTip(
-            _("The size of annular ring for rectangular pads.")
-        )
-
-        self.rectangular_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.rectangular_ring_entry.set_precision(self.decimals)
-        self.rectangular_ring_entry.set_range(0.0000, 9999.9999)
-
-        self.grid1.addWidget(self.rectangular_ring_label, 6, 0)
-        self.grid1.addWidget(self.rectangular_ring_entry, 6, 1)
-
-        # Others Annular Ring Value
-        self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others"))
-        self.other_ring_label.setToolTip(
-            _("The size of annular ring for other pads.")
-        )
-
-        self.other_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.other_ring_entry.set_precision(self.decimals)
-        self.other_ring_entry.set_range(0.0000, 9999.9999)
-
-        self.grid1.addWidget(self.other_ring_label, 7, 0)
-        self.grid1.addWidget(self.other_ring_entry, 7, 1)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 11, 0, 1, 2)
-
-        # Proportional value
-        self.prop_label = QtWidgets.QLabel('<b>%s</b>' % _("Proportional Diameter"))
-        grid0.addWidget(self.prop_label, 12, 0, 1, 2)
-
-        # Diameter value
-        self.factor_entry = FCDoubleSpinner(callback=self.confirmation_message, suffix='%')
-        self.factor_entry.set_precision(self.decimals)
-        self.factor_entry.set_range(0.0000, 100.0000)
-        self.factor_entry.setSingleStep(0.1)
-
-        self.factor_label = QtWidgets.QLabel('%s:' % _("Value"))
-        self.factor_label.setToolTip(
-            _("Proportional Diameter.\n"
-              "The hole diameter will be a fraction of the pad size.")
-        )
-
-        grid0.addWidget(self.factor_label, 13, 0)
-        grid0.addWidget(self.factor_entry, 13, 1)
-
-        separator_line3 = QtWidgets.QFrame()
-        separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line3, 14, 0, 1, 2)
-
-        # Buttons
-        self.punch_object_button = QtWidgets.QPushButton(_("Punch Gerber"))
-        self.punch_object_button.setToolTip(
-            _("Create a Gerber object from the selected object, within\n"
-              "the specified box.")
-        )
-        self.punch_object_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.punch_object_button)
-
-        self.layout.addStretch()
-
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
-
-        self.units = self.app.defaults['units']
-
-        # self.cb_items = [
-        #     self.grid1.itemAt(w).widget() for w in range(self.grid1.count())
-        #     if isinstance(self.grid1.itemAt(w).widget(), FCCheckBox)
-        # ]
-
-        self.circular_ring_entry.setEnabled(False)
-        self.oblong_ring_entry.setEnabled(False)
-        self.square_ring_entry.setEnabled(False)
-        self.rectangular_ring_entry.setEnabled(False)
-        self.other_ring_entry.setEnabled(False)
-
-        self.dia_entry.setDisabled(True)
-        self.dia_label.setDisabled(True)
-        self.factor_label.setDisabled(True)
-        self.factor_entry.setDisabled(True)
-
-        # ## Signals
-        self.method_punch.activated_custom.connect(self.on_method)
-        self.reset_button.clicked.connect(self.set_tool_ui)
-        self.punch_object_button.clicked.connect(self.on_generate_object)
-
-        self.circular_cb.stateChanged.connect(
-            lambda state:
-                self.circular_ring_entry.setDisabled(False) if state else self.circular_ring_entry.setDisabled(True)
-        )
-
-        self.oblong_cb.stateChanged.connect(
-            lambda state:
-            self.oblong_ring_entry.setDisabled(False) if state else self.oblong_ring_entry.setDisabled(True)
-        )
-
-        self.square_cb.stateChanged.connect(
-            lambda state:
-            self.square_ring_entry.setDisabled(False) if state else self.square_ring_entry.setDisabled(True)
-        )
-
-        self.rectangular_cb.stateChanged.connect(
-            lambda state:
-            self.rectangular_ring_entry.setDisabled(False) if state else self.rectangular_ring_entry.setDisabled(True)
-        )
-
-        self.other_cb.stateChanged.connect(
-            lambda state:
-            self.other_ring_entry.setDisabled(False) if state else self.other_ring_entry.setDisabled(True)
-        )
-
-    def run(self, toggle=True):
-        self.app.defaults.report_usage("ToolPunchGerber()")
-
-        if toggle:
-            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-            else:
-                try:
-                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        # if tab is populated with the tool but it does not have the focus, focus on it
-                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
-                            # focus on Tool Tab
-                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-                        else:
-                            self.app.ui.splitter.setSizes([0, 1])
-                except AttributeError:
-                    pass
-        else:
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-
-        AppTool.run(self)
-
-        self.set_tool_ui()
-
-        self.app.ui.notebook.setTabText(2, _("Punch Tool"))
-
-    def install(self, icon=None, separator=None, **kwargs):
-        AppTool.install(self, icon, separator, shortcut='Alt+H', **kwargs)
-
-    def set_tool_ui(self):
-        self.reset_fields()
-
-        self.ui_connect()
-        self.method_punch.set_value(self.app.defaults["tools_punch_hole_type"])
-        self.select_all_cb.set_value(False)
-
-        self.dia_entry.set_value(float(self.app.defaults["tools_punch_hole_fixed_dia"]))
-
-        self.circular_ring_entry.set_value(float(self.app.defaults["tools_punch_circular_ring"]))
-        self.oblong_ring_entry.set_value(float(self.app.defaults["tools_punch_oblong_ring"]))
-        self.square_ring_entry.set_value(float(self.app.defaults["tools_punch_square_ring"]))
-        self.rectangular_ring_entry.set_value(float(self.app.defaults["tools_punch_rectangular_ring"]))
-        self.other_ring_entry.set_value(float(self.app.defaults["tools_punch_others_ring"]))
-
-        self.circular_cb.set_value(self.app.defaults["tools_punch_circular"])
-        self.oblong_cb.set_value(self.app.defaults["tools_punch_oblong"])
-        self.square_cb.set_value(self.app.defaults["tools_punch_square"])
-        self.rectangular_cb.set_value(self.app.defaults["tools_punch_rectangular"])
-        self.other_cb.set_value(self.app.defaults["tools_punch_others"])
-
-        self.factor_entry.set_value(float(self.app.defaults["tools_punch_hole_prop_factor"]))
-
-    def on_select_all(self, state):
-        self.ui_disconnect()
-        if state:
-            self.circular_cb.setChecked(True)
-            self.oblong_cb.setChecked(True)
-            self.square_cb.setChecked(True)
-            self.rectangular_cb.setChecked(True)
-            self.other_cb.setChecked(True)
-        else:
-            self.circular_cb.setChecked(False)
-            self.oblong_cb.setChecked(False)
-            self.square_cb.setChecked(False)
-            self.rectangular_cb.setChecked(False)
-            self.other_cb.setChecked(False)
-        self.ui_connect()
-
-    def on_method(self, val):
-        self.exc_label.setEnabled(False)
-        self.exc_combo.setEnabled(False)
-        self.fixed_label.setEnabled(False)
-        self.dia_label.setEnabled(False)
-        self.dia_entry.setEnabled(False)
-        self.ring_frame.setEnabled(False)
-        self.prop_label.setEnabled(False)
-        self.factor_label.setEnabled(False)
-        self.factor_entry.setEnabled(False)
-
-        if val == 'exc':
-            self.exc_label.setEnabled(True)
-            self.exc_combo.setEnabled(True)
-        elif val == 'fixed':
-            self.fixed_label.setEnabled(True)
-            self.dia_label.setEnabled(True)
-            self.dia_entry.setEnabled(True)
-        elif val == 'ring':
-            self.ring_frame.setEnabled(True)
-        elif val == 'prop':
-            self.prop_label.setEnabled(True)
-            self.factor_label.setEnabled(True)
-            self.factor_entry.setEnabled(True)
-
-    def ui_connect(self):
-        self.select_all_cb.stateChanged.connect(self.on_select_all)
-
-    def ui_disconnect(self):
-        try:
-            self.select_all_cb.stateChanged.disconnect()
-        except (AttributeError, TypeError):
-            pass
-
-    def on_generate_object(self):
-
-        # get the Gerber file who is the source of the punched Gerber
-        selection_index = self.gerber_object_combo.currentIndex()
-        model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
-
-        try:
-            grb_obj = model_index.internalPointer().obj
-        except Exception:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
-            return
-
-        name = grb_obj.options['name'].rpartition('.')[0]
-        outname = name + "_punched"
-
-        punch_method = self.method_punch.get_value()
-
-        new_options = {}
-        for opt in grb_obj.options:
-            new_options[opt] = deepcopy(grb_obj.options[opt])
-
-        if punch_method == 'exc':
-
-            # get the Excellon file whose geometry will create the punch holes
-            selection_index = self.exc_combo.currentIndex()
-            model_index = self.app.collection.index(selection_index, 0, self.exc_combo.rootModelIndex())
-
-            try:
-                exc_obj = model_index.internalPointer().obj
-            except Exception:
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ..."))
-                return
-
-            # this is the punching geometry
-            exc_solid_geometry = MultiPolygon(exc_obj.solid_geometry)
-            if isinstance(grb_obj.solid_geometry, list):
-                grb_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
-            else:
-                grb_solid_geometry = grb_obj.solid_geometry
-
-                # create the punched Gerber solid_geometry
-            punched_solid_geometry = grb_solid_geometry.difference(exc_solid_geometry)
-
-            # update the gerber apertures to include the clear geometry so it can be exported successfully
-            new_apertures = deepcopy(grb_obj.apertures)
-            new_apertures_items = new_apertures.items()
-
-            # find maximum aperture id
-            new_apid = max([int(x) for x, __ in new_apertures_items])
+            # find maximum aperture id
+            new_apid = max([int(x) for x, __ in new_apertures_items])
 
             # store here the clear geometry, the key is the drill size
             holes_apertures = {}
@@ -593,7 +269,7 @@ class ToolPunchGerber(AppTool):
 
             self.app.app_obj.new_object('gerber', outname, init_func)
         elif punch_method == 'fixed':
-            punch_size = float(self.dia_entry.get_value())
+            punch_size = float(self.ui.dia_entry.get_value())
 
             if punch_size == 0.0:
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("The value of the fixed diameter is 0.0. Aborting."))
@@ -604,7 +280,7 @@ class ToolPunchGerber(AppTool):
 
             punching_geo = []
             for apid in grb_obj.apertures:
-                if grb_obj.apertures[apid]['type'] == 'C' and self.circular_cb.get_value():
+                if grb_obj.apertures[apid]['type'] == 'C' and self.ui.circular_cb.get_value():
                     for elem in grb_obj.apertures[apid]['geometry']:
                         if 'follow' in elem:
                             if isinstance(elem['follow'], Point):
@@ -616,7 +292,7 @@ class ToolPunchGerber(AppTool):
 
                     if round(float(grb_obj.apertures[apid]['width']), self.decimals) == \
                             round(float(grb_obj.apertures[apid]['height']), self.decimals) and \
-                            self.square_cb.get_value():
+                            self.ui.square_cb.get_value():
                         for elem in grb_obj.apertures[apid]['geometry']:
                             if 'follow' in elem:
                                 if isinstance(elem['follow'], Point):
@@ -627,7 +303,7 @@ class ToolPunchGerber(AppTool):
                                     punching_geo.append(elem['follow'].buffer(punch_size / 2))
                     elif round(float(grb_obj.apertures[apid]['width']), self.decimals) != \
                             round(float(grb_obj.apertures[apid]['height']), self.decimals) and \
-                            self.rectangular_cb.get_value():
+                            self.ui.rectangular_cb.get_value():
                         for elem in grb_obj.apertures[apid]['geometry']:
                             if 'follow' in elem:
                                 if isinstance(elem['follow'], Point):
@@ -636,7 +312,7 @@ class ToolPunchGerber(AppTool):
                                         self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg)
                                         return 'fail'
                                     punching_geo.append(elem['follow'].buffer(punch_size / 2))
-                elif grb_obj.apertures[apid]['type'] == 'O' and self.oblong_cb.get_value():
+                elif grb_obj.apertures[apid]['type'] == 'O' and self.ui.oblong_cb.get_value():
                     for elem in grb_obj.apertures[apid]['geometry']:
                         if 'follow' in elem:
                             if isinstance(elem['follow'], Point):
@@ -644,7 +320,7 @@ class ToolPunchGerber(AppTool):
                                     self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg)
                                     return 'fail'
                                 punching_geo.append(elem['follow'].buffer(punch_size / 2))
-                elif grb_obj.apertures[apid]['type'] not in ['C', 'R', 'O'] and self.other_cb.get_value():
+                elif grb_obj.apertures[apid]['type'] not in ['C', 'R', 'O'] and self.ui.other_cb.get_value():
                     for elem in grb_obj.apertures[apid]['geometry']:
                         if 'follow' in elem:
                             if isinstance(elem['follow'], Point):
@@ -716,11 +392,11 @@ class ToolPunchGerber(AppTool):
 
             self.app.app_obj.new_object('gerber', outname, init_func)
         elif punch_method == 'ring':
-            circ_r_val = self.circular_ring_entry.get_value()
-            oblong_r_val = self.oblong_ring_entry.get_value()
-            square_r_val = self.square_ring_entry.get_value()
-            rect_r_val = self.rectangular_ring_entry.get_value()
-            other_r_val = self.other_ring_entry.get_value()
+            circ_r_val = self.ui.circular_ring_entry.get_value()
+            oblong_r_val = self.ui.oblong_ring_entry.get_value()
+            square_r_val = self.ui.square_ring_entry.get_value()
+            rect_r_val = self.ui.rectangular_ring_entry.get_value()
+            other_r_val = self.ui.other_ring_entry.get_value()
 
             dia = None
 
@@ -744,13 +420,13 @@ class ToolPunchGerber(AppTool):
                 ap_type = apid_value['type']
                 punching_geo = []
 
-                if ap_type == 'C' and self.circular_cb.get_value():
+                if ap_type == 'C' and self.ui.circular_cb.get_value():
                     dia = float(apid_value['size']) - (2 * circ_r_val)
                     for elem in apid_value['geometry']:
                         if 'follow' in elem and isinstance(elem['follow'], Point):
                             punching_geo.append(elem['follow'].buffer(dia / 2))
 
-                elif ap_type == 'O' and self.oblong_cb.get_value():
+                elif ap_type == 'O' and self.ui.oblong_cb.get_value():
                     width = float(apid_value['width'])
                     height = float(apid_value['height'])
 
@@ -770,14 +446,14 @@ class ToolPunchGerber(AppTool):
 
                     # if the height == width (float numbers so the reason for the following)
                     if round(width, self.decimals) == round(height, self.decimals):
-                        if self.square_cb.get_value():
+                        if self.ui.square_cb.get_value():
                             dia = float(apid_value['height']) - (2 * square_r_val)
 
                             for elem in grb_obj.apertures[apid]['geometry']:
                                 if 'follow' in elem:
                                     if isinstance(elem['follow'], Point):
                                         punching_geo.append(elem['follow'].buffer(dia / 2))
-                    elif self.rectangular_cb.get_value():
+                    elif self.ui.rectangular_cb.get_value():
                         if width > height:
                             dia = float(apid_value['height']) - (2 * rect_r_val)
                         else:
@@ -788,7 +464,7 @@ class ToolPunchGerber(AppTool):
                                 if isinstance(elem['follow'], Point):
                                     punching_geo.append(elem['follow'].buffer(dia / 2))
 
-                elif self.other_cb.get_value():
+                elif self.ui.other_cb.get_value():
                     try:
                         dia = float(apid_value['size']) - (2 * other_r_val)
                     except KeyError:
@@ -859,7 +535,7 @@ class ToolPunchGerber(AppTool):
             self.app.app_obj.new_object('gerber', outname, init_func)
 
         elif punch_method == 'prop':
-            prop_factor = self.factor_entry.get_value() / 100.0
+            prop_factor = self.ui.factor_entry.get_value() / 100.0
 
             dia = None
 
@@ -883,13 +559,13 @@ class ToolPunchGerber(AppTool):
                 ap_type = apid_value['type']
                 punching_geo = []
 
-                if ap_type == 'C' and self.circular_cb.get_value():
+                if ap_type == 'C' and self.ui.circular_cb.get_value():
                     dia = float(apid_value['size']) * prop_factor
                     for elem in apid_value['geometry']:
                         if 'follow' in elem and isinstance(elem['follow'], Point):
                             punching_geo.append(elem['follow'].buffer(dia / 2))
 
-                elif ap_type == 'O' and self.oblong_cb.get_value():
+                elif ap_type == 'O' and self.ui.oblong_cb.get_value():
                     width = float(apid_value['width'])
                     height = float(apid_value['height'])
 
@@ -909,14 +585,14 @@ class ToolPunchGerber(AppTool):
 
                     # if the height == width (float numbers so the reason for the following)
                     if round(width, self.decimals) == round(height, self.decimals):
-                        if self.square_cb.get_value():
+                        if self.ui.square_cb.get_value():
                             dia = float(apid_value['height']) * prop_factor
 
                             for elem in grb_obj.apertures[apid]['geometry']:
                                 if 'follow' in elem:
                                     if isinstance(elem['follow'], Point):
                                         punching_geo.append(elem['follow'].buffer(dia / 2))
-                    elif self.rectangular_cb.get_value():
+                    elif self.ui.rectangular_cb.get_value():
                         if width > height:
                             dia = float(apid_value['height']) * prop_factor
                         else:
@@ -927,7 +603,7 @@ class ToolPunchGerber(AppTool):
                                 if isinstance(elem['follow'], Point):
                                     punching_geo.append(elem['follow'].buffer(dia / 2))
 
-                elif self.other_cb.get_value():
+                elif self.ui.other_cb.get_value():
                     try:
                         dia = float(apid_value['size']) * prop_factor
                     except KeyError:
@@ -1001,3 +677,358 @@ class ToolPunchGerber(AppTool):
         self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
         self.ui_disconnect()
+
+
+class PunchUI:
+
+    toolName = _("Punch Gerber")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+
+        # Punch Drill holes
+        self.layout.addWidget(QtWidgets.QLabel(""))
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 1)
+        grid_lay.setColumnStretch(1, 0)
+
+        # ## Gerber Object
+        self.gerber_object_combo = FCComboBox()
+        self.gerber_object_combo.setModel(self.app.collection)
+        self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.gerber_object_combo.is_last = True
+        self.gerber_object_combo.obj_type = "Gerber"
+
+        self.grb_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
+        self.grb_label.setToolTip('%s.' % _("Gerber into which to punch holes"))
+
+        grid_lay.addWidget(self.grb_label, 0, 0, 1, 2)
+        grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 2, 0, 1, 2)
+
+        self.padt_label = QtWidgets.QLabel("<b>%s</b>" % _("Processed Pads Type"))
+        self.padt_label.setToolTip(
+            _("The type of pads shape to be processed.\n"
+              "If the PCB has many SMD pads with rectangular pads,\n"
+              "disable the Rectangular aperture.")
+        )
+
+        grid_lay.addWidget(self.padt_label, 3, 0, 1, 2)
+
+        # Select all
+        self.select_all_cb = FCCheckBox('%s' % _("ALL"))
+        grid_lay.addWidget(self.select_all_cb)
+
+        # Circular Aperture Selection
+        self.circular_cb = FCCheckBox('%s' % _("Circular"))
+        self.circular_cb.setToolTip(
+            _("Process Circular Pads.")
+        )
+
+        grid_lay.addWidget(self.circular_cb, 5, 0, 1, 2)
+
+        # Oblong Aperture Selection
+        self.oblong_cb = FCCheckBox('%s' % _("Oblong"))
+        self.oblong_cb.setToolTip(
+            _("Process Oblong Pads.")
+        )
+
+        grid_lay.addWidget(self.oblong_cb, 6, 0, 1, 2)
+
+        # Square Aperture Selection
+        self.square_cb = FCCheckBox('%s' % _("Square"))
+        self.square_cb.setToolTip(
+            _("Process Square Pads.")
+        )
+
+        grid_lay.addWidget(self.square_cb, 7, 0, 1, 2)
+
+        # Rectangular Aperture Selection
+        self.rectangular_cb = FCCheckBox('%s' % _("Rectangular"))
+        self.rectangular_cb.setToolTip(
+            _("Process Rectangular Pads.")
+        )
+
+        grid_lay.addWidget(self.rectangular_cb, 8, 0, 1, 2)
+
+        # Others type of Apertures Selection
+        self.other_cb = FCCheckBox('%s' % _("Others"))
+        self.other_cb.setToolTip(
+            _("Process pads not in the categories above.")
+        )
+
+        grid_lay.addWidget(self.other_cb, 9, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 10, 0, 1, 2)
+
+        # Grid Layout
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+
+        self.method_label = QtWidgets.QLabel('<b>%s:</b>' % _("Method"))
+        self.method_label.setToolTip(
+            _("The punch hole source can be:\n"
+              "- Excellon Object-> the Excellon object drills center will serve as reference.\n"
+              "- Fixed Diameter -> will try to use the pads center as reference adding fixed diameter holes.\n"
+              "- Fixed Annular Ring -> will try to keep a set annular ring.\n"
+              "- Proportional -> will make a Gerber punch hole having the diameter a percentage of the pad diameter.")
+        )
+        self.method_punch = RadioSet(
+            [
+                {'label': _('Excellon'), 'value': 'exc'},
+                {'label': _("Fixed Diameter"), 'value': 'fixed'},
+                {'label': _("Fixed Annular Ring"), 'value': 'ring'},
+                {'label': _("Proportional"), 'value': 'prop'}
+            ],
+            orientation='vertical',
+            stretch=False)
+        grid0.addWidget(self.method_label, 0, 0, 1, 2)
+        grid0.addWidget(self.method_punch, 1, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 2, 0, 1, 2)
+
+        self.exc_label = QtWidgets.QLabel('<b>%s</b>' % _("Excellon"))
+        self.exc_label.setToolTip(
+            _("Remove the geometry of Excellon from the Gerber to create the holes in pads.")
+        )
+
+        self.exc_combo = FCComboBox()
+        self.exc_combo.setModel(self.app.collection)
+        self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.exc_combo.is_last = True
+        self.exc_combo.obj_type = "Excellon"
+
+        grid0.addWidget(self.exc_label, 3, 0, 1, 2)
+        grid0.addWidget(self.exc_combo, 4, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 5, 0, 1, 2)
+
+        # Fixed Dia
+        self.fixed_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Diameter"))
+        grid0.addWidget(self.fixed_label, 6, 0, 1, 2)
+
+        # Diameter value
+        self.dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.dia_entry.set_precision(self.decimals)
+        self.dia_entry.set_range(0.0000, 9999.9999)
+
+        self.dia_label = QtWidgets.QLabel('%s:' % _("Value"))
+        self.dia_label.setToolTip(
+            _("Fixed hole diameter.")
+        )
+
+        grid0.addWidget(self.dia_label, 8, 0)
+        grid0.addWidget(self.dia_entry, 8, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 9, 0, 1, 2)
+
+        self.ring_frame = QtWidgets.QFrame()
+        self.ring_frame.setContentsMargins(0, 0, 0, 0)
+        grid0.addWidget(self.ring_frame, 10, 0, 1, 2)
+
+        self.ring_box = QtWidgets.QVBoxLayout()
+        self.ring_box.setContentsMargins(0, 0, 0, 0)
+        self.ring_frame.setLayout(self.ring_box)
+
+        # Annular Ring value
+        self.ring_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Annular Ring"))
+        self.ring_label.setToolTip(
+            _("The size of annular ring.\n"
+              "The copper sliver between the hole exterior\n"
+              "and the margin of the copper pad.")
+        )
+        self.ring_box.addWidget(self.ring_label)
+
+        # ## Grid Layout
+        self.grid1 = QtWidgets.QGridLayout()
+        self.grid1.setColumnStretch(0, 0)
+        self.grid1.setColumnStretch(1, 1)
+        self.ring_box.addLayout(self.grid1)
+
+        # Circular Annular Ring Value
+        self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular"))
+        self.circular_ring_label.setToolTip(
+            _("The size of annular ring for circular pads.")
+        )
+
+        self.circular_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.circular_ring_entry.set_precision(self.decimals)
+        self.circular_ring_entry.set_range(0.0000, 9999.9999)
+
+        self.grid1.addWidget(self.circular_ring_label, 3, 0)
+        self.grid1.addWidget(self.circular_ring_entry, 3, 1)
+
+        # Oblong Annular Ring Value
+        self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong"))
+        self.oblong_ring_label.setToolTip(
+            _("The size of annular ring for oblong pads.")
+        )
+
+        self.oblong_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.oblong_ring_entry.set_precision(self.decimals)
+        self.oblong_ring_entry.set_range(0.0000, 9999.9999)
+
+        self.grid1.addWidget(self.oblong_ring_label, 4, 0)
+        self.grid1.addWidget(self.oblong_ring_entry, 4, 1)
+
+        # Square Annular Ring Value
+        self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square"))
+        self.square_ring_label.setToolTip(
+            _("The size of annular ring for square pads.")
+        )
+
+        self.square_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.square_ring_entry.set_precision(self.decimals)
+        self.square_ring_entry.set_range(0.0000, 9999.9999)
+
+        self.grid1.addWidget(self.square_ring_label, 5, 0)
+        self.grid1.addWidget(self.square_ring_entry, 5, 1)
+
+        # Rectangular Annular Ring Value
+        self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular"))
+        self.rectangular_ring_label.setToolTip(
+            _("The size of annular ring for rectangular pads.")
+        )
+
+        self.rectangular_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.rectangular_ring_entry.set_precision(self.decimals)
+        self.rectangular_ring_entry.set_range(0.0000, 9999.9999)
+
+        self.grid1.addWidget(self.rectangular_ring_label, 6, 0)
+        self.grid1.addWidget(self.rectangular_ring_entry, 6, 1)
+
+        # Others Annular Ring Value
+        self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others"))
+        self.other_ring_label.setToolTip(
+            _("The size of annular ring for other pads.")
+        )
+
+        self.other_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.other_ring_entry.set_precision(self.decimals)
+        self.other_ring_entry.set_range(0.0000, 9999.9999)
+
+        self.grid1.addWidget(self.other_ring_label, 7, 0)
+        self.grid1.addWidget(self.other_ring_entry, 7, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 11, 0, 1, 2)
+
+        # Proportional value
+        self.prop_label = QtWidgets.QLabel('<b>%s</b>' % _("Proportional Diameter"))
+        grid0.addWidget(self.prop_label, 12, 0, 1, 2)
+
+        # Diameter value
+        self.factor_entry = FCDoubleSpinner(callback=self.confirmation_message, suffix='%')
+        self.factor_entry.set_precision(self.decimals)
+        self.factor_entry.set_range(0.0000, 100.0000)
+        self.factor_entry.setSingleStep(0.1)
+
+        self.factor_label = QtWidgets.QLabel('%s:' % _("Value"))
+        self.factor_label.setToolTip(
+            _("Proportional Diameter.\n"
+              "The hole diameter will be a fraction of the pad size.")
+        )
+
+        grid0.addWidget(self.factor_label, 13, 0)
+        grid0.addWidget(self.factor_entry, 13, 1)
+
+        separator_line3 = QtWidgets.QFrame()
+        separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line3, 14, 0, 1, 2)
+
+        # Buttons
+        self.punch_object_button = QtWidgets.QPushButton(_("Punch Gerber"))
+        self.punch_object_button.setToolTip(
+            _("Create a Gerber object from the selected object, within\n"
+              "the specified box.")
+        )
+        self.punch_object_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.punch_object_button)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.reset_button)
+
+        self.circular_ring_entry.setEnabled(False)
+        self.oblong_ring_entry.setEnabled(False)
+        self.square_ring_entry.setEnabled(False)
+        self.rectangular_ring_entry.setEnabled(False)
+        self.other_ring_entry.setEnabled(False)
+
+        self.dia_entry.setDisabled(True)
+        self.dia_label.setDisabled(True)
+        self.factor_label.setDisabled(True)
+        self.factor_entry.setDisabled(True)
+
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 388 - 354
appTools/ToolQRCode.py

@@ -40,8 +40,6 @@ log = logging.getLogger('base')
 
 class QRCode(AppTool):
 
-    toolName = _("QRCode Tool")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
 
@@ -51,286 +49,11 @@ class QRCode(AppTool):
         self.decimals = self.app.decimals
         self.units = ''
 
-        # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-        self.layout.addWidget(QtWidgets.QLabel(''))
-
-        # ## Grid Layout
-        i_grid_lay = QtWidgets.QGridLayout()
-        self.layout.addLayout(i_grid_lay)
-        i_grid_lay.setColumnStretch(0, 0)
-        i_grid_lay.setColumnStretch(1, 1)
-
-        self.grb_object_combo = FCComboBox()
-        self.grb_object_combo.setModel(self.app.collection)
-        self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.grb_object_combo.is_last = True
-        self.grb_object_combo.obj_type = "Gerber"
-
-        self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
-        self.grbobj_label.setToolTip(
-            _("Gerber Object to which the QRCode will be added.")
-        )
-
-        i_grid_lay.addWidget(self.grbobj_label, 0, 0)
-        i_grid_lay.addWidget(self.grb_object_combo, 1, 0, 1, 2)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        i_grid_lay.addWidget(separator_line, 2, 0, 1, 2)
-
-        # Text box
-        self.text_label = QtWidgets.QLabel('<b>%s</b>:' % _("QRCode Data"))
-        self.text_label.setToolTip(
-            _("QRCode Data. Alphanumeric text to be encoded in the QRCode.")
-        )
-        self.text_data = FCTextArea()
-        self.text_data.setPlaceholderText(
-            _("Add here the text to be included in the QRCode...")
-        )
-        i_grid_lay.addWidget(self.text_label, 5, 0)
-        i_grid_lay.addWidget(self.text_data, 6, 0, 1, 2)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        i_grid_lay.addWidget(separator_line, 7, 0, 1, 2)
-
-        # ## Grid Layout
-        grid_lay = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid_lay)
-        grid_lay.setColumnStretch(0, 0)
-        grid_lay.setColumnStretch(1, 1)
-
-        self.qrcode_label = QtWidgets.QLabel('<b>%s</b>' % _('Parameters'))
-        self.qrcode_label.setToolTip(
-            _("The parameters used to shape the QRCode.")
-        )
-        grid_lay.addWidget(self.qrcode_label, 0, 0, 1, 2)
-
-        # VERSION #
-        self.version_label = QtWidgets.QLabel('%s:' % _("Version"))
-        self.version_label.setToolTip(
-            _("QRCode version can have values from 1 (21x21 boxes)\n"
-              "to 40 (177x177 boxes).")
-        )
-        self.version_entry = FCSpinner(callback=self.confirmation_message_int)
-        self.version_entry.set_range(1, 40)
-        self.version_entry.setWrapping(True)
-
-        grid_lay.addWidget(self.version_label, 1, 0)
-        grid_lay.addWidget(self.version_entry, 1, 1)
-
-        # ERROR CORRECTION #
-        self.error_label = QtWidgets.QLabel('%s:' % _("Error correction"))
-        self.error_label.setToolTip(
-            _("Parameter that controls the error correction used for the QR Code.\n"
-              "L = maximum 7%% errors can be corrected\n"
-              "M = maximum 15%% errors can be corrected\n"
-              "Q = maximum 25%% errors can be corrected\n"
-              "H = maximum 30%% errors can be corrected.")
-        )
-        self.error_radio = RadioSet([{'label': 'L', 'value': 'L'},
-                                     {'label': 'M', 'value': 'M'},
-                                     {'label': 'Q', 'value': 'Q'},
-                                     {'label': 'H', 'value': 'H'}])
-        self.error_radio.setToolTip(
-            _("Parameter that controls the error correction used for the QR Code.\n"
-              "L = maximum 7%% errors can be corrected\n"
-              "M = maximum 15%% errors can be corrected\n"
-              "Q = maximum 25%% errors can be corrected\n"
-              "H = maximum 30%% errors can be corrected.")
-        )
-        grid_lay.addWidget(self.error_label, 2, 0)
-        grid_lay.addWidget(self.error_radio, 2, 1)
-
-        # BOX SIZE #
-        self.bsize_label = QtWidgets.QLabel('%s:' % _("Box Size"))
-        self.bsize_label.setToolTip(
-            _("Box size control the overall size of the QRcode\n"
-              "by adjusting the size of each box in the code.")
-        )
-        self.bsize_entry = FCSpinner(callback=self.confirmation_message_int)
-        self.bsize_entry.set_range(1, 9999)
-        self.bsize_entry.setWrapping(True)
-
-        grid_lay.addWidget(self.bsize_label, 3, 0)
-        grid_lay.addWidget(self.bsize_entry, 3, 1)
-
-        # BORDER SIZE #
-        self.border_size_label = QtWidgets.QLabel('%s:' % _("Border Size"))
-        self.border_size_label.setToolTip(
-            _("Size of the QRCode border. How many boxes thick is the border.\n"
-              "Default value is 4. The width of the clearance around the QRCode.")
-        )
-        self.border_size_entry = FCSpinner(callback=self.confirmation_message_int)
-        self.border_size_entry.set_range(1, 9999)
-        self.border_size_entry.setWrapping(True)
-
-        grid_lay.addWidget(self.border_size_label, 4, 0)
-        grid_lay.addWidget(self.border_size_entry, 4, 1)
-
-        # POLARITY CHOICE #
-        self.pol_label = QtWidgets.QLabel('%s:' % _("Polarity"))
-        self.pol_label.setToolTip(
-            _("Choose the polarity of the QRCode.\n"
-              "It can be drawn in a negative way (squares are clear)\n"
-              "or in a positive way (squares are opaque).")
-        )
-        self.pol_radio = RadioSet([{'label': _('Negative'), 'value': 'neg'},
-                                   {'label': _('Positive'), 'value': 'pos'}])
-        self.pol_radio.setToolTip(
-            _("Choose the type of QRCode to be created.\n"
-              "If added on a Silkscreen Gerber file the QRCode may\n"
-              "be added as positive. If it is added to a Copper Gerber\n"
-              "file then perhaps the QRCode can be added as negative.")
-        )
-        grid_lay.addWidget(self.pol_label, 7, 0)
-        grid_lay.addWidget(self.pol_radio, 7, 1)
-
-        # BOUNDING BOX TYPE #
-        self.bb_label = QtWidgets.QLabel('%s:' % _("Bounding Box"))
-        self.bb_label.setToolTip(
-            _("The bounding box, meaning the empty space that surrounds\n"
-              "the QRCode geometry, can have a rounded or a square shape.")
-        )
-        self.bb_radio = RadioSet([{'label': _('Rounded'), 'value': 'r'},
-                                  {'label': _('Square'), 'value': 's'}])
-        self.bb_radio.setToolTip(
-            _("The bounding box, meaning the empty space that surrounds\n"
-              "the QRCode geometry, can have a rounded or a square shape.")
-        )
-        grid_lay.addWidget(self.bb_label, 8, 0)
-        grid_lay.addWidget(self.bb_radio, 8, 1)
-
-        # Export QRCode
-        self.export_cb = FCCheckBox(_("Export QRCode"))
-        self.export_cb.setToolTip(
-            _("Show a set of controls allowing to export the QRCode\n"
-              "to a SVG file or an PNG file.")
-        )
-        grid_lay.addWidget(self.export_cb, 9, 0, 1, 2)
-
-        # this way I can hide/show the frame
-        self.export_frame = QtWidgets.QFrame()
-        self.export_frame.setContentsMargins(0, 0, 0, 0)
-        self.layout.addWidget(self.export_frame)
-        self.export_lay = QtWidgets.QGridLayout()
-        self.export_lay.setContentsMargins(0, 0, 0, 0)
-        self.export_frame.setLayout(self.export_lay)
-        self.export_lay.setColumnStretch(0, 0)
-        self.export_lay.setColumnStretch(1, 1)
-
-        # default is hidden
-        self.export_frame.hide()
-
-        # FILL COLOR #
-        self.fill_color_label = QtWidgets.QLabel('%s:' % _('Fill Color'))
-        self.fill_color_label.setToolTip(
-            _("Set the QRCode fill color (squares color).")
-        )
-        self.fill_color_entry = FCEntry()
-        self.fill_color_button = QtWidgets.QPushButton()
-        self.fill_color_button.setFixedSize(15, 15)
-
-        fill_lay_child = QtWidgets.QHBoxLayout()
-        fill_lay_child.setContentsMargins(0, 0, 0, 0)
-        fill_lay_child.addWidget(self.fill_color_entry)
-        fill_lay_child.addWidget(self.fill_color_button, alignment=Qt.AlignRight)
-        fill_lay_child.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
-
-        fill_color_widget = QtWidgets.QWidget()
-        fill_color_widget.setLayout(fill_lay_child)
-
-        self.export_lay.addWidget(self.fill_color_label, 0, 0)
-        self.export_lay.addWidget(fill_color_widget, 0, 1)
-
-        self.transparent_cb = FCCheckBox(_("Transparent back color"))
-        self.export_lay.addWidget(self.transparent_cb, 1, 0, 1, 2)
-
-        # BACK COLOR #
-        self.back_color_label = QtWidgets.QLabel('%s:' % _('Back Color'))
-        self.back_color_label.setToolTip(
-            _("Set the QRCode background color.")
-        )
-        self.back_color_entry = FCEntry()
-        self.back_color_button = QtWidgets.QPushButton()
-        self.back_color_button.setFixedSize(15, 15)
-
-        back_lay_child = QtWidgets.QHBoxLayout()
-        back_lay_child.setContentsMargins(0, 0, 0, 0)
-        back_lay_child.addWidget(self.back_color_entry)
-        back_lay_child.addWidget(self.back_color_button, alignment=Qt.AlignRight)
-        back_lay_child.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
-
-        back_color_widget = QtWidgets.QWidget()
-        back_color_widget.setLayout(back_lay_child)
-
-        self.export_lay.addWidget(self.back_color_label, 2, 0)
-        self.export_lay.addWidget(back_color_widget, 2, 1)
-
-        # ## Export QRCode as SVG image
-        self.export_svg_button = QtWidgets.QPushButton(_("Export QRCode SVG"))
-        self.export_svg_button.setToolTip(
-            _("Export a SVG file with the QRCode content.")
-        )
-        self.export_svg_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.export_lay.addWidget(self.export_svg_button, 3, 0, 1, 2)
-
-        # ## Export QRCode as PNG image
-        self.export_png_button = QtWidgets.QPushButton(_("Export QRCode PNG"))
-        self.export_png_button.setToolTip(
-            _("Export a PNG image file with the QRCode content.")
-        )
-        self.export_png_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.export_lay.addWidget(self.export_png_button, 4, 0, 1, 2)
-
-        # ## Insert QRCode
-        self.qrcode_button = QtWidgets.QPushButton(_("Insert QRCode"))
-        self.qrcode_button.setToolTip(
-            _("Create the QRCode object.")
-        )
-        self.qrcode_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.qrcode_button)
-
-        self.layout.addStretch()
-
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = QRcodeUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
         self.grb_object = None
         self.box_poly = None
@@ -349,18 +72,18 @@ class QRCode(AppTool):
         self.old_back_color = ''
 
         # Signals #
-        self.qrcode_button.clicked.connect(self.execute)
-        self.export_cb.stateChanged.connect(self.on_export_frame)
-        self.export_png_button.clicked.connect(self.export_png_file)
-        self.export_svg_button.clicked.connect(self.export_svg_file)
+        self.ui.qrcode_button.clicked.connect(self.execute)
+        self.ui.export_cb.stateChanged.connect(self.on_export_frame)
+        self.ui.export_png_button.clicked.connect(self.export_png_file)
+        self.ui.export_svg_button.clicked.connect(self.export_svg_file)
 
-        self.fill_color_entry.editingFinished.connect(self.on_qrcode_fill_color_entry)
-        self.fill_color_button.clicked.connect(self.on_qrcode_fill_color_button)
-        self.back_color_entry.editingFinished.connect(self.on_qrcode_back_color_entry)
-        self.back_color_button.clicked.connect(self.on_qrcode_back_color_button)
+        self.ui.fill_color_entry.editingFinished.connect(self.on_qrcode_fill_color_entry)
+        self.ui.fill_color_button.clicked.connect(self.on_qrcode_fill_color_button)
+        self.ui.back_color_entry.editingFinished.connect(self.on_qrcode_back_color_entry)
+        self.ui.back_color_button.clicked.connect(self.on_qrcode_back_color_button)
 
-        self.transparent_cb.stateChanged.connect(self.on_transparent_back_color)
-        self.reset_button.clicked.connect(self.set_tool_ui)
+        self.ui.transparent_cb.stateChanged.connect(self.on_transparent_back_color)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
 
     def run(self, toggle=True):
         self.app.defaults.report_usage("QRCode()")
@@ -395,45 +118,45 @@ class QRCode(AppTool):
 
     def set_tool_ui(self):
         self.units = self.app.defaults['units']
-        self.border_size_entry.set_value(4)
+        self.ui.border_size_entry.set_value(4)
 
-        self.version_entry.set_value(int(self.app.defaults["tools_qrcode_version"]))
-        self.error_radio.set_value(self.app.defaults["tools_qrcode_error"])
-        self.bsize_entry.set_value(int(self.app.defaults["tools_qrcode_box_size"]))
-        self.border_size_entry.set_value(int(self.app.defaults["tools_qrcode_border_size"]))
-        self.pol_radio.set_value(self.app.defaults["tools_qrcode_polarity"])
-        self.bb_radio.set_value(self.app.defaults["tools_qrcode_rounded"])
+        self.ui.version_entry.set_value(int(self.app.defaults["tools_qrcode_version"]))
+        self.ui.error_radio.set_value(self.app.defaults["tools_qrcode_error"])
+        self.ui.bsize_entry.set_value(int(self.app.defaults["tools_qrcode_box_size"]))
+        self.ui.border_size_entry.set_value(int(self.app.defaults["tools_qrcode_border_size"]))
+        self.ui.pol_radio.set_value(self.app.defaults["tools_qrcode_polarity"])
+        self.ui.bb_radio.set_value(self.app.defaults["tools_qrcode_rounded"])
 
-        self.text_data.set_value(self.app.defaults["tools_qrcode_qrdata"])
+        self.ui.text_data.set_value(self.app.defaults["tools_qrcode_qrdata"])
 
-        self.fill_color_entry.set_value(self.app.defaults['tools_qrcode_fill_color'])
-        self.fill_color_button.setStyleSheet("background-color:%s" %
-                                             str(self.app.defaults['tools_qrcode_fill_color'])[:7])
+        self.ui.fill_color_entry.set_value(self.app.defaults['tools_qrcode_fill_color'])
+        self.ui.fill_color_button.setStyleSheet("background-color:%s" %
+                                                str(self.app.defaults['tools_qrcode_fill_color'])[:7])
 
-        self.back_color_entry.set_value(self.app.defaults['tools_qrcode_back_color'])
-        self.back_color_button.setStyleSheet("background-color:%s" %
-                                             str(self.app.defaults['tools_qrcode_back_color'])[:7])
+        self.ui.back_color_entry.set_value(self.app.defaults['tools_qrcode_back_color'])
+        self.ui.back_color_button.setStyleSheet("background-color:%s" %
+                                                str(self.app.defaults['tools_qrcode_back_color'])[:7])
 
     def on_export_frame(self, state):
-        self.export_frame.setVisible(state)
-        self.qrcode_button.setVisible(not state)
+        self.ui.export_frame.setVisible(state)
+        self.ui.qrcode_button.setVisible(not state)
 
     def execute(self):
-        text_data = self.text_data.get_value()
+        text_data = self.ui.text_data.get_value()
         if text_data == '':
             self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
-            return 'fail'
+            return
 
         # get the Gerber object on which the QRCode will be inserted
-        selection_index = self.grb_object_combo.currentIndex()
-        model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
+        selection_index = self.ui.grb_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.ui.grb_object_combo.rootModelIndex())
 
         try:
             self.grb_object = model_index.internalPointer().obj
         except Exception as e:
             log.debug("QRCode.execute() --> %s" % str(e))
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
-            return 'fail'
+            return
 
         # we can safely activate the mouse events
         self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
@@ -448,13 +171,13 @@ class QRCode(AppTool):
                 'M': qrcode.constants.ERROR_CORRECT_M,
                 'Q': qrcode.constants.ERROR_CORRECT_Q,
                 'H': qrcode.constants.ERROR_CORRECT_H
-            }[self.error_radio.get_value()]
+            }[self.ui.error_radio.get_value()]
 
             qr = qrcode.QRCode(
-                version=self.version_entry.get_value(),
+                version=self.ui.version_entry.get_value(),
                 error_correction=error_code,
-                box_size=self.bsize_entry.get_value(),
-                border=self.border_size_entry.get_value(),
+                box_size=self.ui.bsize_entry.get_value(),
+                border=self.ui.border_size_entry.get_value(),
                 image_factory=qrcode.image.svg.SvgFragmentImage
             )
             qr.add_data(text_data)
@@ -498,9 +221,9 @@ class QRCode(AppTool):
 
         # this is the bounding box of the QRCode geometry
         a, b, c, d = self.qrcode_utility_geometry.bounds
-        buff_val = self.border_size_entry.get_value() * (self.bsize_entry.get_value() / 10)
+        buff_val = self.ui.border_size_entry.get_value() * (self.ui.bsize_entry.get_value() / 10)
 
-        if self.bb_radio.get_value() == 'r':
+        if self.ui.bb_radio.get_value() == 'r':
             mask_geo = box(a, b, c, d).buffer(buff_val)
         else:
             mask_geo = box(a, b, c, d).buffer(buff_val, join_style=2)
@@ -518,7 +241,7 @@ class QRCode(AppTool):
         geo_list = deepcopy(list(new_solid_geometry))
 
         # Polarity
-        if self.pol_radio.get_value() == 'pos':
+        if self.ui.pol_radio.get_value() == 'pos':
             working_geo = self.qrcode_utility_geometry
         else:
             working_geo = mask_geo.difference(self.qrcode_utility_geometry)
@@ -531,7 +254,7 @@ class QRCode(AppTool):
 
         self.grb_object.solid_geometry = deepcopy(geo_list)
 
-        box_size = float(self.bsize_entry.get_value()) / 10.0
+        box_size = float(self.ui.bsize_entry.get_value()) / 10.0
 
         sort_apid = []
         new_apid = '10'
@@ -644,7 +367,7 @@ class QRCode(AppTool):
         pos_canvas = self.app.plotcanvas.translate_coords((x, y))
 
         # if GRID is active we need to get the snapped positions
-        if self.app.grid_status() == True:
+        if self.app.grid_status():
             pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
         else:
             pos = pos_canvas
@@ -671,7 +394,7 @@ class QRCode(AppTool):
             self.delete_utility_geo()
 
             # if GRID is active we need to get the snapped positions
-            if self.app.grid_status() == True:
+            if self.app.grid_status():
                 pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
             else:
                 pos = pos_canvas
@@ -754,7 +477,7 @@ class QRCode(AppTool):
         self.app.call_source = 'app'
 
     def export_png_file(self):
-        text_data = self.text_data.get_value()
+        text_data = self.ui.text_data.get_value()
         if text_data == '':
             self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
             return 'fail'
@@ -765,20 +488,20 @@ class QRCode(AppTool):
                 'M': qrcode.constants.ERROR_CORRECT_M,
                 'Q': qrcode.constants.ERROR_CORRECT_Q,
                 'H': qrcode.constants.ERROR_CORRECT_H
-            }[self.error_radio.get_value()]
+            }[self.ui.error_radio.get_value()]
 
             qr = qrcode.QRCode(
-                version=self.version_entry.get_value(),
+                version=self.ui.version_entry.get_value(),
                 error_correction=error_code,
-                box_size=self.bsize_entry.get_value(),
-                border=self.border_size_entry.get_value(),
+                box_size=self.ui.bsize_entry.get_value(),
+                border=self.ui.border_size_entry.get_value(),
                 image_factory=qrcode.image.pil.PilImage
             )
             qr.add_data(text_data)
             qr.make(fit=True)
 
-            img = qr.make_image(fill_color=self.fill_color_entry.get_value(),
-                                back_color=self.back_color_entry.get_value())
+            img = qr.make_image(fill_color=self.ui.fill_color_entry.get_value(),
+                                back_color=self.ui.back_color_entry.get_value())
             img.save(fname)
 
             app_obj.call_source = 'qrcode_tool'
@@ -803,7 +526,7 @@ class QRCode(AppTool):
             self.app.worker_task.emit({'fcn': job_thread_qr_png, 'params': [self.app, filename]})
 
     def export_svg_file(self):
-        text_data = self.text_data.get_value()
+        text_data = self.ui.text_data.get_value()
         if text_data == '':
             self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
             return 'fail'
@@ -814,18 +537,18 @@ class QRCode(AppTool):
                 'M': qrcode.constants.ERROR_CORRECT_M,
                 'Q': qrcode.constants.ERROR_CORRECT_Q,
                 'H': qrcode.constants.ERROR_CORRECT_H
-            }[self.error_radio.get_value()]
+            }[self.ui.error_radio.get_value()]
 
             qr = qrcode.QRCode(
-                version=self.version_entry.get_value(),
+                version=self.ui.version_entry.get_value(),
                 error_correction=error_code,
-                box_size=self.bsize_entry.get_value(),
-                border=self.border_size_entry.get_value(),
+                box_size=self.ui.bsize_entry.get_value(),
+                border=self.ui.border_size_entry.get_value(),
                 image_factory=qrcode.image.svg.SvgPathImage
             )
             qr.add_data(text_data)
-            img = qr.make_image(fill_color=self.fill_color_entry.get_value(),
-                                back_color=self.back_color_entry.get_value())
+            img = qr.make_image(fill_color=self.ui.fill_color_entry.get_value(),
+                                back_color=self.ui.back_color_entry.get_value())
             img.save(fname)
 
             app_obj.call_source = 'qrcode_tool'
@@ -850,11 +573,11 @@ class QRCode(AppTool):
             self.app.worker_task.emit({'fcn': job_thread_qr_svg, 'params': [self.app, filename]})
 
     def on_qrcode_fill_color_entry(self):
-        color = self.fill_color_entry.get_value()
-        self.fill_color_button.setStyleSheet("background-color:%s" % str(color))
+        color = self.ui.fill_color_entry.get_value()
+        self.ui.fill_color_button.setStyleSheet("background-color:%s" % str(color))
 
     def on_qrcode_fill_color_button(self):
-        current_color = QtGui.QColor(self.fill_color_entry.get_value())
+        current_color = QtGui.QColor(self.ui.fill_color_entry.get_value())
 
         c_dialog = QtWidgets.QColorDialog()
         fill_color = c_dialog.getColor(initial=current_color)
@@ -862,17 +585,17 @@ class QRCode(AppTool):
         if fill_color.isValid() is False:
             return
 
-        self.fill_color_button.setStyleSheet("background-color:%s" % str(fill_color.name()))
+        self.ui.fill_color_button.setStyleSheet("background-color:%s" % str(fill_color.name()))
 
         new_val_sel = str(fill_color.name())
-        self.fill_color_entry.set_value(new_val_sel)
+        self.ui.fill_color_entry.set_value(new_val_sel)
 
     def on_qrcode_back_color_entry(self):
-        color = self.back_color_entry.get_value()
-        self.back_color_button.setStyleSheet("background-color:%s" % str(color))
+        color = self.ui.back_color_entry.get_value()
+        self.ui.back_color_button.setStyleSheet("background-color:%s" % str(color))
 
     def on_qrcode_back_color_button(self):
-        current_color = QtGui.QColor(self.back_color_entry.get_value())
+        current_color = QtGui.QColor(self.ui.back_color_entry.get_value())
 
         c_dialog = QtWidgets.QColorDialog()
         back_color = c_dialog.getColor(initial=current_color)
@@ -880,18 +603,329 @@ class QRCode(AppTool):
         if back_color.isValid() is False:
             return
 
-        self.back_color_button.setStyleSheet("background-color:%s" % str(back_color.name()))
+        self.ui.back_color_button.setStyleSheet("background-color:%s" % str(back_color.name()))
 
         new_val_sel = str(back_color.name())
-        self.back_color_entry.set_value(new_val_sel)
+        self.ui.back_color_entry.set_value(new_val_sel)
 
     def on_transparent_back_color(self, state):
         if state:
-            self.back_color_entry.setDisabled(True)
-            self.back_color_button.setDisabled(True)
-            self.old_back_color = self.back_color_entry.get_value()
-            self.back_color_entry.set_value('transparent')
+            self.ui.back_color_entry.setDisabled(True)
+            self.ui.back_color_button.setDisabled(True)
+            self.old_back_color = self.ui.back_color_entry.get_value()
+            self.ui.back_color_entry.set_value('transparent')
+        else:
+            self.ui.back_color_entry.setDisabled(False)
+            self.ui.back_color_button.setDisabled(False)
+            self.ui.back_color_entry.set_value(self.old_back_color)
+
+
+class QRcodeUI:
+
+    toolName = _("QRCode Tool")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(QtWidgets.QLabel(''))
+
+        # ## Grid Layout
+        i_grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(i_grid_lay)
+        i_grid_lay.setColumnStretch(0, 0)
+        i_grid_lay.setColumnStretch(1, 1)
+
+        self.grb_object_combo = FCComboBox()
+        self.grb_object_combo.setModel(self.app.collection)
+        self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.grb_object_combo.is_last = True
+        self.grb_object_combo.obj_type = "Gerber"
+
+        self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
+        self.grbobj_label.setToolTip(
+            _("Gerber Object to which the QRCode will be added.")
+        )
+
+        i_grid_lay.addWidget(self.grbobj_label, 0, 0)
+        i_grid_lay.addWidget(self.grb_object_combo, 1, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        i_grid_lay.addWidget(separator_line, 2, 0, 1, 2)
+
+        # Text box
+        self.text_label = QtWidgets.QLabel('<b>%s</b>:' % _("QRCode Data"))
+        self.text_label.setToolTip(
+            _("QRCode Data. Alphanumeric text to be encoded in the QRCode.")
+        )
+        self.text_data = FCTextArea()
+        self.text_data.setPlaceholderText(
+            _("Add here the text to be included in the QRCode...")
+        )
+        i_grid_lay.addWidget(self.text_label, 5, 0)
+        i_grid_lay.addWidget(self.text_data, 6, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        i_grid_lay.addWidget(separator_line, 7, 0, 1, 2)
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+
+        self.qrcode_label = QtWidgets.QLabel('<b>%s</b>' % _('Parameters'))
+        self.qrcode_label.setToolTip(
+            _("The parameters used to shape the QRCode.")
+        )
+        grid_lay.addWidget(self.qrcode_label, 0, 0, 1, 2)
+
+        # VERSION #
+        self.version_label = QtWidgets.QLabel('%s:' % _("Version"))
+        self.version_label.setToolTip(
+            _("QRCode version can have values from 1 (21x21 boxes)\n"
+              "to 40 (177x177 boxes).")
+        )
+        self.version_entry = FCSpinner(callback=self.confirmation_message_int)
+        self.version_entry.set_range(1, 40)
+        self.version_entry.setWrapping(True)
+
+        grid_lay.addWidget(self.version_label, 1, 0)
+        grid_lay.addWidget(self.version_entry, 1, 1)
+
+        # ERROR CORRECTION #
+        self.error_label = QtWidgets.QLabel('%s:' % _("Error correction"))
+        self.error_label.setToolTip(
+            _("Parameter that controls the error correction used for the QR Code.\n"
+              "L = maximum 7%% errors can be corrected\n"
+              "M = maximum 15%% errors can be corrected\n"
+              "Q = maximum 25%% errors can be corrected\n"
+              "H = maximum 30%% errors can be corrected.")
+        )
+        self.error_radio = RadioSet([{'label': 'L', 'value': 'L'},
+                                     {'label': 'M', 'value': 'M'},
+                                     {'label': 'Q', 'value': 'Q'},
+                                     {'label': 'H', 'value': 'H'}])
+        self.error_radio.setToolTip(
+            _("Parameter that controls the error correction used for the QR Code.\n"
+              "L = maximum 7%% errors can be corrected\n"
+              "M = maximum 15%% errors can be corrected\n"
+              "Q = maximum 25%% errors can be corrected\n"
+              "H = maximum 30%% errors can be corrected.")
+        )
+        grid_lay.addWidget(self.error_label, 2, 0)
+        grid_lay.addWidget(self.error_radio, 2, 1)
+
+        # BOX SIZE #
+        self.bsize_label = QtWidgets.QLabel('%s:' % _("Box Size"))
+        self.bsize_label.setToolTip(
+            _("Box size control the overall size of the QRcode\n"
+              "by adjusting the size of each box in the code.")
+        )
+        self.bsize_entry = FCSpinner(callback=self.confirmation_message_int)
+        self.bsize_entry.set_range(1, 9999)
+        self.bsize_entry.setWrapping(True)
+
+        grid_lay.addWidget(self.bsize_label, 3, 0)
+        grid_lay.addWidget(self.bsize_entry, 3, 1)
+
+        # BORDER SIZE #
+        self.border_size_label = QtWidgets.QLabel('%s:' % _("Border Size"))
+        self.border_size_label.setToolTip(
+            _("Size of the QRCode border. How many boxes thick is the border.\n"
+              "Default value is 4. The width of the clearance around the QRCode.")
+        )
+        self.border_size_entry = FCSpinner(callback=self.confirmation_message_int)
+        self.border_size_entry.set_range(1, 9999)
+        self.border_size_entry.setWrapping(True)
+
+        grid_lay.addWidget(self.border_size_label, 4, 0)
+        grid_lay.addWidget(self.border_size_entry, 4, 1)
+
+        # POLARITY CHOICE #
+        self.pol_label = QtWidgets.QLabel('%s:' % _("Polarity"))
+        self.pol_label.setToolTip(
+            _("Choose the polarity of the QRCode.\n"
+              "It can be drawn in a negative way (squares are clear)\n"
+              "or in a positive way (squares are opaque).")
+        )
+        self.pol_radio = RadioSet([{'label': _('Negative'), 'value': 'neg'},
+                                   {'label': _('Positive'), 'value': 'pos'}])
+        self.pol_radio.setToolTip(
+            _("Choose the type of QRCode to be created.\n"
+              "If added on a Silkscreen Gerber file the QRCode may\n"
+              "be added as positive. If it is added to a Copper Gerber\n"
+              "file then perhaps the QRCode can be added as negative.")
+        )
+        grid_lay.addWidget(self.pol_label, 7, 0)
+        grid_lay.addWidget(self.pol_radio, 7, 1)
+
+        # BOUNDING BOX TYPE #
+        self.bb_label = QtWidgets.QLabel('%s:' % _("Bounding Box"))
+        self.bb_label.setToolTip(
+            _("The bounding box, meaning the empty space that surrounds\n"
+              "the QRCode geometry, can have a rounded or a square shape.")
+        )
+        self.bb_radio = RadioSet([{'label': _('Rounded'), 'value': 'r'},
+                                  {'label': _('Square'), 'value': 's'}])
+        self.bb_radio.setToolTip(
+            _("The bounding box, meaning the empty space that surrounds\n"
+              "the QRCode geometry, can have a rounded or a square shape.")
+        )
+        grid_lay.addWidget(self.bb_label, 8, 0)
+        grid_lay.addWidget(self.bb_radio, 8, 1)
+
+        # Export QRCode
+        self.export_cb = FCCheckBox(_("Export QRCode"))
+        self.export_cb.setToolTip(
+            _("Show a set of controls allowing to export the QRCode\n"
+              "to a SVG file or an PNG file.")
+        )
+        grid_lay.addWidget(self.export_cb, 9, 0, 1, 2)
+
+        # this way I can hide/show the frame
+        self.export_frame = QtWidgets.QFrame()
+        self.export_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.export_frame)
+        self.export_lay = QtWidgets.QGridLayout()
+        self.export_lay.setContentsMargins(0, 0, 0, 0)
+        self.export_frame.setLayout(self.export_lay)
+        self.export_lay.setColumnStretch(0, 0)
+        self.export_lay.setColumnStretch(1, 1)
+
+        # default is hidden
+        self.export_frame.hide()
+
+        # FILL COLOR #
+        self.fill_color_label = QtWidgets.QLabel('%s:' % _('Fill Color'))
+        self.fill_color_label.setToolTip(
+            _("Set the QRCode fill color (squares color).")
+        )
+        self.fill_color_entry = FCEntry()
+        self.fill_color_button = QtWidgets.QPushButton()
+        self.fill_color_button.setFixedSize(15, 15)
+
+        fill_lay_child = QtWidgets.QHBoxLayout()
+        fill_lay_child.setContentsMargins(0, 0, 0, 0)
+        fill_lay_child.addWidget(self.fill_color_entry)
+        fill_lay_child.addWidget(self.fill_color_button, alignment=Qt.AlignRight)
+        fill_lay_child.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        fill_color_widget = QtWidgets.QWidget()
+        fill_color_widget.setLayout(fill_lay_child)
+
+        self.export_lay.addWidget(self.fill_color_label, 0, 0)
+        self.export_lay.addWidget(fill_color_widget, 0, 1)
+
+        self.transparent_cb = FCCheckBox(_("Transparent back color"))
+        self.export_lay.addWidget(self.transparent_cb, 1, 0, 1, 2)
+
+        # BACK COLOR #
+        self.back_color_label = QtWidgets.QLabel('%s:' % _('Back Color'))
+        self.back_color_label.setToolTip(
+            _("Set the QRCode background color.")
+        )
+        self.back_color_entry = FCEntry()
+        self.back_color_button = QtWidgets.QPushButton()
+        self.back_color_button.setFixedSize(15, 15)
+
+        back_lay_child = QtWidgets.QHBoxLayout()
+        back_lay_child.setContentsMargins(0, 0, 0, 0)
+        back_lay_child.addWidget(self.back_color_entry)
+        back_lay_child.addWidget(self.back_color_button, alignment=Qt.AlignRight)
+        back_lay_child.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        back_color_widget = QtWidgets.QWidget()
+        back_color_widget.setLayout(back_lay_child)
+
+        self.export_lay.addWidget(self.back_color_label, 2, 0)
+        self.export_lay.addWidget(back_color_widget, 2, 1)
+
+        # ## Export QRCode as SVG image
+        self.export_svg_button = QtWidgets.QPushButton(_("Export QRCode SVG"))
+        self.export_svg_button.setToolTip(
+            _("Export a SVG file with the QRCode content.")
+        )
+        self.export_svg_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.export_lay.addWidget(self.export_svg_button, 3, 0, 1, 2)
+
+        # ## Export QRCode as PNG image
+        self.export_png_button = QtWidgets.QPushButton(_("Export QRCode PNG"))
+        self.export_png_button.setToolTip(
+            _("Export a PNG image file with the QRCode content.")
+        )
+        self.export_png_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.export_lay.addWidget(self.export_png_button, 4, 0, 1, 2)
+
+        # ## Insert QRCode
+        self.qrcode_button = QtWidgets.QPushButton(_("Insert QRCode"))
+        self.qrcode_button.setToolTip(
+            _("Create the QRCode object.")
+        )
+        self.qrcode_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.qrcode_button)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.reset_button)
+
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
         else:
-            self.back_color_entry.setDisabled(False)
-            self.back_color_button.setDisabled(False)
-            self.back_color_entry.set_value(self.old_back_color)
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

Різницю між файлами не показано, бо вона завелика
+ 509 - 962
appTools/ToolSolderPaste.py


Деякі файли не було показано, через те що забагато файлів було змінено