Explorar el Código

Merged in marius_stanciu/flatcam_beta/Beta (pull request #179)

Beta
Marius Stanciu hace 6 años
padre
commit
f7818d50d6

+ 76 - 27
FlatCAMApp.py

@@ -97,7 +97,7 @@ class App(QtCore.QObject):
     # Version and VERSION DATE ###########
     # ####################################
     version = 8.93
-    version_date = "2019/08/31"
+    version_date = "2019/08/10"
     beta = True
 
     # current date now
@@ -513,6 +513,7 @@ class App(QtCore.QObject):
             "tools_nccconnect": self.ui.tools_defaults_form.tools_ncc_group.ncc_connect_cb,
             "tools_ncccontour": self.ui.tools_defaults_form.tools_ncc_group.ncc_contour_cb,
             "tools_nccrest": self.ui.tools_defaults_form.tools_ncc_group.ncc_rest_cb,
+            "tools_nccref": self.ui.tools_defaults_form.tools_ncc_group.reference_radio,
 
             # CutOut Tool
             "tools_cutouttooldia": self.ui.tools_defaults_form.tools_cutout_group.cutout_tooldia_entry,
@@ -852,6 +853,7 @@ class App(QtCore.QObject):
             "tools_nccconnect": True,
             "tools_ncccontour": True,
             "tools_nccrest": False,
+            "tools_nccref": 'itself',
 
             "tools_cutouttooldia": 0.00393701,
             "tools_cutoutkind": "single",
@@ -2841,6 +2843,7 @@ class App(QtCore.QObject):
                 self.inform.emit(_("[ERROR_NOTCL] Failed to parse defaults file."))
                 return
             self.defaults.update(defaults_from_file)
+            self.on_preferences_edited()
             self.inform.emit(_("[success] Imported Defaults from %s") % filename)
 
     def on_export_preferences(self):
@@ -2875,6 +2878,10 @@ class App(QtCore.QObject):
                 f = open(filename, 'w')
                 defaults_file_content = f.read()
                 f.close()
+            except PermissionError:
+                self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
+                                   "Most likely another app is holding the file open and not accessible."))
+                return
             except IOError:
                 App.log.debug('Creating a new preferences file ...')
                 f = open(filename, 'w')
@@ -4669,10 +4676,13 @@ class App(QtCore.QObject):
                 with open(filename, 'w') as f:
                     for line in my_gcode:
                         f.write(line)
-
             except FileNotFoundError:
                 self.inform.emit(_("[WARNING] No such file or directory"))
                 return
+            except PermissionError:
+                self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
+                                   "Most likely another app is holding the file open and not accessible."))
+                return
 
         # Just for adding it to the recent files list.
         if self.defaults["global_open_style"] is False:
@@ -5691,13 +5701,12 @@ class App(QtCore.QObject):
         self.plotcanvas.vispy_canvas.view.camera.pan_button_setting = self.defaults['global_pan_button']
 
         self.pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
+        self.pos = (self.pos_canvas[0], self.pos_canvas[1])
+        self.app_cursor.enabled = False
 
-        if self.grid_status() == True:
+        if self.grid_status():
             self.pos = self.geo_editor.snap(self.pos_canvas[0], self.pos_canvas[1])
             self.app_cursor.enabled = True
-        else:
-            self.pos = (self.pos_canvas[0], self.pos_canvas[1])
-            self.app_cursor.enabled = False
 
         try:
             modifiers = QtWidgets.QApplication.keyboardModifiers()
@@ -5750,7 +5759,7 @@ class App(QtCore.QObject):
         if self.rel_point1 is not None:
             try:  # May fail in case mouse not within axes
                 pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
-                if self.grid_status():
+                if self.grid_status() == True:
                     pos = self.geo_editor.snap(pos_canvas[0], pos_canvas[1])
                     self.app_cursor.enabled = True
                     # Update cursor
@@ -6154,6 +6163,12 @@ class App(QtCore.QObject):
             face_color = kwargs['face_color']
         else:
             face_color = self.defaults['global_sel_fill']
+
+        if 'face_alpha' in kwargs:
+            face_alpha = kwargs['face_alpha']
+        else:
+            face_alpha = 0.3
+
         x0, y0 = old_coords
         x1, y1 = coords
         pt1 = (x0, y0)
@@ -6163,7 +6178,7 @@ class App(QtCore.QObject):
         sel_rect = Polygon([pt1, pt2, pt3, pt4])
 
         color_t = Color(face_color)
-        color_t.alpha = 0.3
+        color_t.alpha = face_alpha
         self.move_tool.sel_shapes.add(sel_rect, color=color, face_color=color_t, update=True,
                                       layer=0, tolerance=None)
 
@@ -7109,8 +7124,14 @@ class App(QtCore.QObject):
             # Parse the xml through a xml parser just to add line feeds
             # and to make it look more pretty for the output
             svgcode = parse_xml_string(svg_elem)
-            with open(filename, 'w') as fp:
-                fp.write(svgcode.toprettyxml())
+            try:
+                with open(filename, 'w') as fp:
+                    fp.write(svgcode.toprettyxml())
+            except PermissionError:
+                self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
+                                   "Most likely another app is holding the file open and not accessible."))
+                return 'fail'
+
             if self.defaults["global_open_style"] is False:
                 self.file_opened.emit("SVG", filename)
             self.file_saved.emit("SVG", filename)
@@ -7213,8 +7234,13 @@ class App(QtCore.QObject):
             # Parse the xml through a xml parser just to add line feeds
             # and to make it look more pretty for the output
             doc = parse_xml_string(svg_elem)
-            with open(filename, 'w') as fp:
-                fp.write(doc.toprettyxml())
+            try:
+                with open(filename, 'w') as fp:
+                    fp.write(doc.toprettyxml())
+            except PermissionError:
+                self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
+                                   "Most likely another app is holding the file open and not accessible."))
+                return 'fail'
 
             self.progress.emit(100)
             if self.defaults["global_open_style"] is False:
@@ -7329,8 +7355,14 @@ class App(QtCore.QObject):
             # Parse the xml through a xml parser just to add line feeds
             # and to make it look more pretty for the output
             doc = parse_xml_string(svg_elem)
-            with open(filename, 'w') as fp:
-                fp.write(doc.toprettyxml())
+            try:
+                with open(filename, 'w') as fp:
+                    fp.write(doc.toprettyxml())
+            except PermissionError:
+                self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
+                                   "Most likely another app is holding the file open and not accessible."))
+                return 'fail'
+
             self.progress.emit(100)
             if self.defaults["global_open_style"] is False:
                 self.file_opened.emit("SVG", filename)
@@ -7371,15 +7403,20 @@ class App(QtCore.QObject):
         file_string = StringIO(obj.source_file)
         time_string = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
 
-        with open(filename, 'w') as file:
-            file.writelines('G04*\n')
-            file.writelines('G04 %s (RE)GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' %
-                            (obj.kind.upper(), str(self.version), str(self.version_date)))
-            file.writelines('G04 Filename: %s*\n' % str(obj_name))
-            file.writelines('G04 Created on : %s*\n' % time_string)
-
-            for line in file_string:
-                file.writelines(line)
+        try:
+            with open(filename, 'w') as file:
+                file.writelines('G04*\n')
+                file.writelines('G04 %s (RE)GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' %
+                                (obj.kind.upper(), str(self.version), str(self.version_date)))
+                file.writelines('G04 Filename: %s*\n' % str(obj_name))
+                file.writelines('G04 Created on : %s*\n' % time_string)
+
+                for line in file_string:
+                    file.writelines(line)
+        except PermissionError:
+            self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
+                               "Most likely another app is holding the file open and not accessible."))
+            return 'fail'
 
     def export_excellon(self, obj_name, filename, use_thread=True):
         """
@@ -7481,8 +7518,14 @@ class App(QtCore.QObject):
                 exported_excellon += excellon_code
                 exported_excellon += footer
 
-                with open(filename, 'w') as fp:
-                    fp.write(exported_excellon)
+                try:
+                    with open(filename, 'w') as fp:
+                        fp.write(exported_excellon)
+                except PermissionError:
+                    self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
+                                       "Most likely another app is holding the file open and not accessible."))
+                    return 'fail'
+
                 if self.defaults["global_open_style"] is False:
                     self.file_opened.emit("Excellon", filename)
                 self.file_saved.emit("Excellon", filename)
@@ -7598,8 +7641,14 @@ class App(QtCore.QObject):
                 exported_gerber += gerber_code
                 exported_gerber += footer
 
-                with open(filename, 'w') as fp:
-                    fp.write(exported_gerber)
+                try:
+                    with open(filename, 'w') as fp:
+                        fp.write(exported_gerber)
+                except PermissionError:
+                    self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
+                                       "Most likely another app is holding the file open and not accessible."))
+                    return 'fail'
+
                 if self.defaults["global_open_style"] is False:
                     self.file_opened.emit("Gerber", filename)
                 self.file_saved.emit("Gerber", filename)

+ 75 - 156
FlatCAMObj.py

@@ -24,8 +24,8 @@ if '_' not in builtins.__dict__:
     _ = gettext.gettext
 
 
+# Interrupts plotting process if FlatCAMObj has been deleted
 class ObjectDeleted(Exception):
-    # Interrupts plotting process if FlatCAMObj has been deleted
     pass
 
 
@@ -364,10 +364,13 @@ class FlatCAMObj(QtCore.QObject):
 
     @property
     def drawing_tolerance(self):
-        return self._drawing_tolerance if self.units == 'MM' or not self.units else self._drawing_tolerance / 25.4
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        tol = self._drawing_tolerance if self.units == 'MM' or not self.units else self._drawing_tolerance / 25.4
+        return tol
 
     @drawing_tolerance.setter
     def drawing_tolerance(self, value):
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
         self._drawing_tolerance = value if self.units == 'MM' or not self.units else value / 25.4
 
     def clear(self, update=False):
@@ -2666,9 +2669,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                     job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.'))
                 except ValueError:
                     self.app.inform.emit(
-                        _(
-                            '[ERROR_NOTCL] Wrong value format for self.defaults["z_pdepth"] or self.options["z_pdepth"]'
-                        ))
+                        _('[ERROR_NOTCL] Wrong value format for self.defaults["z_pdepth"] or self.options["z_pdepth"]'))
 
             try:
                 job_obj.feedrate_probe = float(self.options["feedrate_probe"])
@@ -2678,11 +2679,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                     job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.'))
                 except ValueError:
                     self.app.inform.emit(
-                        _(
-                            '[ERROR_NOTCL] Wrong value format for self.defaults["feedrate_probe"] '
-                            'or self.options["feedrate_probe"]'
-                        )
-                    )
+                        _('[ERROR_NOTCL] Wrong value format for self.defaults["feedrate_probe"] '
+                          'or self.options["feedrate_probe"]'))
 
             # There could be more than one drill size...
             # job_obj.tooldia =   # TODO: duplicate variable!
@@ -2736,10 +2734,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         else:
             coords_xy = [float(eval(coord)) for coord in self.app.defaults["excellon_toolchangexy"].split(",")]
             if len(coords_xy) < 2:
-                self.app.inform.emit(_(
-                    "[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be "
-                    "in the format (x, y) \nbut now there is only one value, not two. "
-                ))
+                self.app.inform.emit(_("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be "
+                                       "in the format (x, y) \nbut now there is only one value, not two. "))
                 return 'fail'
             coords_xy[0] *= factor
             coords_xy[1] *= factor
@@ -2913,7 +2909,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     try:
                         geo_final.options[option] = geo.options[option]
                     except Exception as e:
-                        log.warning("Failed to copy option.", option)
+                        log.warning("Failed to copy option %s. Error: %s" % (str(option), str(e)))
 
             # Expand lists
             if type(geo) is list:
@@ -3022,14 +3018,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
         if "cnctooldia" not in self.options:
             self.options["cnctooldia"] = self.app.defaults["geometry_cnctooldia"]
-            # try:
-            #     self.options["cnctooldia"] = [
-            #         float(eval(dia)) for dia in str(self.app.defaults["geometry_cnctooldia"]).split(",")
-            #     ]
-            # except Exception as e:
-            #     log.error("At least one tool diameter needed. Verify in Edit -> Preferences -> Geometry General -> "
-            #               "Tool dia. %s" % str(e))
-            #     return
 
         self.options["startz"] = self.app.defaults["geometry_startz"]
 
@@ -3143,14 +3131,14 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             self.ui.geo_tools_table.setCellWidget(row_no, 3, type_item)
             self.ui.geo_tools_table.setCellWidget(row_no, 4, tool_type_item)
 
-            # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
+            # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
             self.ui.geo_tools_table.setItem(row_no, 5, tool_uid_item)  # Tool unique ID
             self.ui.geo_tools_table.setCellWidget(row_no, 6, plot_item)
 
             try:
                 self.ui.tool_offset_entry.set_value(tooluid_value['offset_value'])
             except Exception as e:
-                log.debug("build_ui() --> Could not set the 'offset_value' key in self.tools")
+                log.debug("build_ui() --> Could not set the 'offset_value' key in self.tools. Error: %s" % str(e))
 
         # make the diameter column editable
         for row in range(tool_idx):
@@ -3436,31 +3424,14 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                                 self.ui.tool_offset_entry.get_value().replace(',', '.')
                             )
                         except ValueError:
-                            self.app.inform.emit(_(
-                                "[ERROR_NOTCL] Wrong value format entered, "
-                                "use a number."
-                            )
-                            )
+                            self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
+                                                   "use a number."))
                             return
 
     def ui_connect(self):
         # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the
         # changes in geometry UI
         for i in range(self.ui.grid3.count()):
-            # try:
-            #     # works for CheckBoxes
-            #     self.ui.grid3.itemAt(i).widget().stateChanged.connect(self.gui_form_to_storage)
-            # except Exception as e:
-            #     # works for ComboBoxes
-            #     try:
-            #         self.ui.grid3.itemAt(i).widget().currentIndexChanged.connect(self.gui_form_to_storage)
-            #     except Exception as e2:
-            #         # works for Entry
-            #         try:
-            #             self.ui.grid3.itemAt(i).widget().editingFinished.connect(self.gui_form_to_storage)
-            #         except Exception as e3:
-            #             pass
-
             current_widget = self.ui.grid3.itemAt(i).widget()
             if isinstance(current_widget, FCCheckBox):
                 current_widget.stateChanged.connect(self.gui_form_to_storage)
@@ -3494,20 +3465,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the
         # changes in geometry UI
         for i in range(self.ui.grid3.count()):
-            # try:
-            #     # works for CheckBoxes
-            #     self.ui.grid3.itemAt(i).widget().stateChanged.disconnect(self.gui_form_to_storage)
-            # except Exception as e:
-            #     # works for ComboBoxes
-            #     try:
-            #         self.ui.grid3.itemAt(i).widget().currentIndexChanged.disconnect(self.gui_form_to_storage)
-            #     except Exception as e2:
-            #         # works for Entry
-            #         try:
-            #             self.ui.grid3.itemAt(i).widget().editingFinished.disconnect(self.gui_form_to_storage)
-            #         except Exception as e3:
-            #             pass
-
             current_widget = self.ui.grid3.itemAt(i).widget()
             if isinstance(current_widget, FCCheckBox):
                 try:
@@ -3673,14 +3630,10 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         self.ser_attrs.append('tools')
 
         if change_message is False:
-            self.app.inform.emit(_(
-                "[success] Tool added in Tool Table."
-            ))
+            self.app.inform.emit(_("[success] Tool added in Tool Table."))
         else:
             change_message = False
-            self.app.inform.emit(_(
-                "[WARNING_NOTCL] Default Tool added. Wrong value format entered."
-            ))
+            self.app.inform.emit(_("[WARNING_NOTCL] Default Tool added. Wrong value format entered."))
         self.build_ui()
 
         # if there is no tool left in the Tools Table, enable the parameters GUI
@@ -3712,9 +3665,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                         max_uid += 1
                         self.tools[int(max_uid)] = deepcopy(self.tools[tooluid_copy])
                     except AttributeError:
-                        self.app.inform.emit(_(
-                            "[WARNING_NOTCL] Failed. Select a tool to copy."
-                        ))
+                        self.app.inform.emit(_("[WARNING_NOTCL] Failed. Select a tool to copy."))
                         self.build_ui()
                         return
                     except Exception as e:
@@ -3722,9 +3673,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 # deselect the table
                 # self.ui.geo_tools_table.clearSelection()
             else:
-                self.app.inform.emit(_(
-                    "[WARNING_NOTCL] Failed. Select a tool to copy."
-                ))
+                self.app.inform.emit(_("[WARNING_NOTCL] Failed. Select a tool to copy."))
                 self.build_ui()
                 return
         else:
@@ -3746,14 +3695,12 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
         try:
             self.ser_attrs.remove('tools')
-        except Exception as e:
+        except ValueError:
             pass
         self.ser_attrs.append('tools')
 
         self.build_ui()
-        self.app.inform.emit(_(
-            "[success] Tool was copied in Tool Table."
-        ))
+        self.app.inform.emit(_("[success] Tool was copied in Tool Table."))
 
     def on_tool_edit(self, current_item):
 
@@ -3767,10 +3714,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             try:
                 d = float(self.ui.geo_tools_table.item(current_row, 1).text().replace(',', '.'))
             except ValueError:
-                self.app.inform.emit(_(
-                    "[ERROR_NOTCL] Wrong value format entered, "
-                    "use a number."
-                ))
+                self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
+                                       "use a number."))
                 return
 
         tool_dia = float('%.4f' % d)
@@ -3781,12 +3726,10 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         try:
             self.ser_attrs.remove('tools')
             self.ser_attrs.append('tools')
-        except TypeError:
+        except (TypeError, ValueError):
             pass
 
-        self.app.inform.emit(_(
-            "[success] Tool was edited in Tool Table."
-        ))
+        self.app.inform.emit(_("[success] Tool was edited in Tool Table."))
         self.build_ui()
 
     def on_tool_delete(self, all=None):
@@ -3895,7 +3838,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # populate the form with the data from the tool associated with the row parameter
         try:
             item = self.ui.geo_tools_table.item(current_row, 5)
-            if item is not None:
+            if type(item) is not None:
                 tooluid = int(item.text())
             else:
                 return
@@ -3913,7 +3856,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             else:
                 return
         except Exception as e:
-            log.debug("Tool missing. Add a tool in Geo Tool Table. %s" % str(e))
+            log.debug("Tool missing in ui_update_v_shape(). Add a tool in Geo Tool Table. %s" % str(e))
             return
 
         try:
@@ -3958,10 +3901,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             try:
                 vdia = float(self.ui.tipdia_entry.get_value().replace(',', '.'))
             except ValueError:
-                self.app.inform.emit(_(
-                    "[ERROR_NOTCL] Wrong value format entered, "
-                    "use a number."
-                ))
+                self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
+                                       "use a number."))
                 return
 
         try:
@@ -3971,10 +3912,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             try:
                 half_vangle = float(self.ui.tipangle_entry.get_value().replace(',', '.')) / 2
             except ValueError:
-                self.app.inform.emit(_(
-                    "[ERROR_NOTCL] Wrong value format entered, "
-                    "use a number."
-                ))
+                self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
+                                       "use a number."))
                 return
 
         row = self.ui.geo_tools_table.currentRow()
@@ -4091,10 +4030,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             try:
                 offset_value_item = float(self.ui.tool_offset_entry.get_value().replace(',', '.'))
             except ValueError:
-                self.app.inform.emit(_(
-                    "[ERROR_NOTCL] Wrong value format entered, "
-                    "use a number."
-                ))
+                self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
+                                       "use a number."))
                 return
 
         # this new dict will hold the actual useful data, another dict that is the value of key 'data'
@@ -4212,6 +4149,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         else:
             for x in self.ui.geo_tools_table.selectedItems():
                 r = []
+                txt = ''
+
                 # the last 2 columns for single-geo geometry are irrelevant and create problems reading
                 # so we don't read them
                 for column in range(0, self.ui.geo_tools_table.columnCount() - 2):
@@ -4277,8 +4216,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
         try:
             if self.special_group:
-                self.app.inform.emit(_(
-                    "[WARNING_NOTCL] This Geometry can't be processed because it is %s geometry."
+                self.app.inform.emit(_("[WARNING_NOTCL] This Geometry can't be processed because it is %s geometry."
                 ) % str(self.special_group))
                 return
         except AttributeError:
@@ -4294,10 +4232,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     try:
                         tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text().replace(',', '.'))
                     except ValueError:
-                        self.app.inform.emit(_(
-                            "[ERROR_NOTCL] Wrong Tool Dia value format entered, "
-                            "use a number."
-                        ))
+                        self.app.inform.emit(_("[ERROR_NOTCL] Wrong Tool Dia value format entered, "
+                                               "use a number."))
                         return
                 tooluid = int(self.ui.geo_tools_table.item(x.row(), 5).text())
 
@@ -4321,9 +4257,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             self.ui.geo_tools_table.clearSelection()
 
         else:
-            self.app.inform.emit(_(
-                "[ERROR_NOTCL] Failed. No tool selected in the tool table ..."
-            ))
+            self.app.inform.emit(_("[ERROR_NOTCL] Failed. No tool selected in the tool table ..."))
 
     def mtool_gen_cncjob(self, segx=None, segy=None, use_thread=True):
         """
@@ -4359,7 +4293,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             ymax = self.options['ymax']
         except Exception as e:
             log.debug("FlatCAMObj.FlatCAMGeometry.mtool_gen_cncjob() --> %s\n" % str(e))
-            msg = _("[ERROR] An internal error has ocurred. See shell.\n")
+            msg = _("[ERROR] An internal error has occurred. See shell.\n")
             msg += _('FlatCAMObj.FlatCAMGeometry.mtool_gen_cncjob() --> %s') % str(e)
             msg += traceback.format_exc()
             self.app.inform.emit(msg)
@@ -4369,7 +4303,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # RUNNING ON SEPARATE THREAD!
         def job_init_single_geometry(job_obj, app_obj):
             log.debug("Creating a CNCJob out of a single-geometry")
-
             assert isinstance(job_obj, FlatCAMCNCjob), \
                 "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
 
@@ -4395,10 +4328,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 try:
                     job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.'))
                 except ValueError:
-                    self.app.inform.emit(
-                        _(
-                            '[ERROR_NOTCL] Wrong value format for self.defaults["z_pdepth"] or self.options["z_pdepth"]'
-                        ))
+                    self.app.inform.emit(_('[ERROR_NOTCL] Wrong value format for self.defaults["z_pdepth"] '
+                                           'or self.options["z_pdepth"]'))
 
             try:
                 job_obj.feedrate_probe = float(self.options["feedrate_probe"])
@@ -4407,11 +4338,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 try:
                     job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.'))
                 except ValueError:
-                    self.app.inform.emit(
-                        _(
-                            '[ERROR_NOTCL] Wrong value format for self.defaults["feedrate_probe"] '
-                            'or self.options["feedrate_probe"]'
-                        ))
+                    self.app.inform.emit(_('[ERROR_NOTCL] Wrong value format for self.defaults["feedrate_probe"] '
+                                           'or self.options["feedrate_probe"]'))
 
             for tooluid_key in self.sel_tools:
                 tool_cnt += 1
@@ -4507,20 +4435,15 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                         try:
                             offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.'))
                         except ValueError:
-                            self.app.inform.emit(_(
-                                "[ERROR_NOTCL] Wrong value format entered, "
-                                "use a number."
-                            ))
+                            self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
+                                                   "use a number."))
                             return
                     if offset_value:
                         tool_offset = float(offset_value)
                     else:
-                        self.app.inform.emit(
-                            _(
-                                "[WARNING] Tool Offset is selected in Tool Table but no value is provided.\n"
-                                "Add a Tool Offset or change the Offset Type."
-                            )
-                        )
+                        self.app.inform.emit(_("[WARNING] Tool Offset is selected in Tool Table but "
+                                               "no value is provided.\n"
+                                               "Add a Tool Offset or change the Offset Type."))
                         return
                 dia_cnc_dict.update({
                     'offset_value': tool_offset
@@ -4543,7 +4466,9 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
                 app_obj.progress.emit(40)
 
-                tol = float(self.app.defaults['global_tolerance'])
+                # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
+                # to a value of 0.0005 which is 20 times less than 0.01
+                tol = float(self.app.defaults['global_tolerance']) / 20
                 res = job_obj.generate_from_geometry_2(
                     self, tooldia=tooldia_val, offset=tool_offset, tolerance=tol,
                     z_cut=z_cut, z_move=z_move,
@@ -4582,7 +4507,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # RUNNING ON SEPARATE THREAD!
         def job_init_multi_geometry(job_obj, app_obj):
             log.debug("Creating a CNCJob out of a multi-geometry")
-
             assert isinstance(job_obj, FlatCAMCNCjob), \
                 "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
 
@@ -4610,10 +4534,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 try:
                     job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.'))
                 except ValueError:
-                    self.app.inform.emit(
-                        _(
-                            '[ERROR_NOTCL] Wrong value format for self.defaults["z_pdepth"] or self.options["z_pdepth"]'
-                        ))
+                    self.app.inform.emit(_('[ERROR_NOTCL] Wrong value format for self.defaults["z_pdepth"] '
+                                           'or self.options["z_pdepth"]'))
 
             try:
                 job_obj.feedrate_probe = float(self.options["feedrate_probe"])
@@ -4622,11 +4544,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 try:
                     job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.'))
                 except ValueError:
-                    self.app.inform.emit(
-                        _(
-                            '[ERROR_NOTCL] Wrong value format for self.defaults["feedrate_probe"] '
-                            'or self.options["feedrate_probe"]'
-                        ))
+                    self.app.inform.emit(_('[ERROR_NOTCL] Wrong value format for self.defaults["feedrate_probe"] '
+                                           'or self.options["feedrate_probe"]'))
 
             # make sure that trying to make a CNCJob from an empty file is not creating an app crash
             if not self.solid_geometry:
@@ -4635,9 +4554,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     if self.tools[tooluid_key]['solid_geometry'] is None:
                         a += 1
                 if a == len(self.tools):
-                    self.app.inform.emit(_(
-                        '[ERROR_NOTCL] Cancelled. Empty file, it has no geometry...'
-                    ))
+                    self.app.inform.emit(_('[ERROR_NOTCL] Cancelled. Empty file, it has no geometry...'))
                     return 'fail'
 
             for tooluid_key in self.sel_tools:
@@ -4750,9 +4667,9 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     if offset_value:
                         tool_offset = float(offset_value)
                     else:
-                        self.app.inform.emit(_(
-                            "[WARNING] Tool Offset is selected in Tool Table but no value is provided.\n"
-                            "Add a Tool Offset or change the Offset Type."))
+                        self.app.inform.emit(_("[WARNING] Tool Offset is selected in Tool Table but "
+                                               "no value is provided.\n"
+                                               "Add a Tool Offset or change the Offset Type."))
                         return
                 dia_cnc_dict.update({
                     'offset_value': tool_offset
@@ -4769,9 +4686,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 app_obj.progress.emit(40)
 
                 spindledir = self.app.defaults['geometry_spindledir']
-
                 tool_solid_geometry = self.tools[current_uid]['solid_geometry']
-                tol = float(self.app.defaults['global_tolerance'])
+
+                # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
+                # to a value of 0.0005 which is 20 times less than 0.01
+                tol = float(self.app.defaults['global_tolerance']) / 20
                 res = job_obj.generate_from_multitool_geometry(
                     tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset,
                     tolerance=tol, z_cut=z_cut, z_move=z_move,
@@ -4921,10 +4840,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 try:
                     job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.'))
                 except ValueError:
-                    self.app.inform.emit(
-                        _(
-                            '[ERROR_NOTCL] Wrong value format for self.defaults["z_pdepth"] or self.options["z_pdepth"]'
-                        ))
+                    self.app.inform.emit(_('[ERROR_NOTCL] Wrong value format for self.defaults["z_pdepth"] '
+                                           'or self.options["z_pdepth"]'))
 
             try:
                 job_obj.feedrate_probe = float(self.options["feedrate_probe"])
@@ -4933,18 +4850,17 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 try:
                     job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.'))
                 except ValueError:
-                    self.app.inform.emit(
-                        _(
-                            '[ERROR_NOTCL] Wrong value format for self.defaults["feedrate_probe"] '
-                            'or self.options["feedrate_probe"]'
-                        ))
+                    self.app.inform.emit(_('[ERROR_NOTCL] Wrong value format for self.defaults["feedrate_probe"] '
+                                           'or self.options["feedrate_probe"]'))
 
             job_obj.options['xmin'] = self.options['xmin']
             job_obj.options['ymin'] = self.options['ymin']
             job_obj.options['xmax'] = self.options['xmax']
             job_obj.options['ymax'] = self.options['ymax']
 
-            tol = float(self.app.defaults['global_tolerance'])
+            # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
+            # to a value of 0.0005 which is 20 times less than 0.01
+            tol = float(self.app.defaults['global_tolerance']) / 20
             job_obj.generate_from_geometry_2(self, tooldia=tooldia, offset=offset, tolerance=tol,
                                              z_cut=z_cut, z_move=z_move,
                                              feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
@@ -5887,12 +5803,15 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
                 with open(filename, 'w') as f:
                     for line in lines:
                         f.write(line)
-
             except FileNotFoundError:
                 self.app.inform.emit(_(
                     "[WARNING_NOTCL] No such file or directory"
                 ))
                 return
+            except PermissionError:
+                self.app.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
+                                       "Most likely another app is holding the file open and not accessible."))
+                return 'fail'
         elif to_file is False:
             # Just for adding it to the recent files list.
             if self.app.defaults["global_open_style"] is False:

+ 21 - 1
README.md

@@ -9,6 +9,26 @@ CAD program, and create G-Code for Isolation routing.
 
 =================================================
 
+10.08.2019
+
+- added new feature in NCC Tool: now another object can be used as reference for the area extent to be cleared of copper
+- fixed issue in the latest feature in NCC Tool: now it works also with reference objects made out of LineStrings (tool 'Path' in Geometry Editor)
+- translation files updated for the new strings (Google Translate)
+- RELEASE 8.93
+
+9.08.2019
+
+- added Exception handing for the case when the user is trying to save & overwrite a file already opened in another file
+- finished added 'Area' type of Paint in Paint Tool
+- fixed bug that created a choppy geometry for CNCJob when working in INCH
+- fixed bug that did not asked the user to save the preferences after importing a new set of preferences, after the user is trying to close the Preferences tab window
+
+7.08.2019
+
+- replaced setFixedWidth calls with setMinimumWidth
+- recoded the camlib.Geometry.isolation_geometry() function
+- started to work on Paint Area in Paint Tool
+
 6.08.2019
 
 - fixed bug that crashed the app after creating a new geometry, if a new object is loaded and the new geometry is deleted and then trying to select the just loaded new object
@@ -20,7 +40,7 @@ CAD program, and create G-Code for Isolation routing.
 5.08.2019
 
 - made sure that if using an negative Gerber isolation diameter, the resulting Geometry object will use a tool with positive diameter
-- fixed bug that when isolating a Gerber file made out of a single polygon, an Recurrsion Exception was issued together with inability to create tbe isolation
+- fixed bug that when isolating a Gerber file made out of a single polygon, an RecursionException was issued together with inability to create tbe isolation
 - when applying a new language if there are any changes in the current project, the app will offer to save the project before the reboot
 
 3.08.2019

+ 33 - 28
camlib.py

@@ -555,19 +555,21 @@ class Geometry(object):
                 geo_iso = self.follow_geometry
             else:
                 if corner is None:
-                    if type(self.solid_geometry) is list and len(self.solid_geometry) == 1:
-                        geo_iso = self.solid_geometry[0].buffer(offset, int(int(self.geo_steps_per_circle) / 4))
-                    else:
+                    try:
+                        __ = iter(self.solid_geometry)
                         for el in self.solid_geometry:
                             geo_iso.append(el.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
+                    except TypeError:
+                        geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4))
                 else:
-                    if type(self.solid_geometry) is list and len(self.solid_geometry) == 1:
-                        geo_iso = self.solid_geometry.buffer[0](offset, int(int(self.geo_steps_per_circle) / 4),
-                                                                join_style=corner)
-                    else:
+                    try:
+                        __ = iter(self.solid_geometry)
                         for el in self.solid_geometry:
                             geo_iso.append(el.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
                                                      join_style=corner))
+                    except TypeError:
+                        geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
+                                                             join_style=corner)
 
         # end of replaced block
         if follow:
@@ -5751,11 +5753,11 @@ class CNCjob(Geometry):
                 if pt != geo.coords[0] and pt == geo.coords[-1]:
                     geo.coords = list(geo.coords)[::-1]
 
-                #---------- Single depth/pass --------
+                # ---------- Single depth/pass --------
                 if not multidepth:
                     self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance)
 
-                #--------- Multi-pass ---------
+                # --------- Multi-pass ---------
                 else:
                     self.gcode += self.create_gcode_multi_pass(geo, extracut, tolerance,
                                                                postproc=p, current_point=current_pt)
@@ -5850,8 +5852,8 @@ class CNCjob(Geometry):
                 # solid geometry it's obvious we can't do the offset
                 if -offset > ((c - a) / 2) or -offset > ((d - b) / 2):
                     self.app.inform.emit(_("[ERROR_NOTCL] The Tool Offset value is too negative to use "
-                                         "for the current_geometry.\n"
-                                         "Raise the value (in module) and try again."))
+                                           "for the current_geometry.\n"
+                                           "Raise the value (in module) and try again."))
                     return 'fail'
                 # hack: make offset smaller by 0.0000000001 which is insignificant difference but allow the job
                 # to continue
@@ -5899,7 +5901,7 @@ class CNCjob(Geometry):
                 self.xy_toolchange = [float(eval(a)) for a in toolchangexy.split(",")]
                 if len(self.xy_toolchange) < 2:
                     self.app.inform.emit(_("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be "
-                                         "in the format (x, y) \nbut now there is only one value, not two. "))
+                                           "in the format (x, y) \nbut now there is only one value, not two. "))
                     return 'fail'
         except Exception as e:
             log.debug("camlib.CNCJob.generate_from_geometry_2() --> %s" % str(e))
@@ -5910,19 +5912,19 @@ class CNCjob(Geometry):
 
         if self.z_cut is None:
             self.app.inform.emit(_("[ERROR_NOTCL] Cut_Z parameter is None or zero. Most likely a bad combinations of "
-                                 "other parameters."))
+                                   "other parameters."))
             return 'fail'
 
         if self.z_cut > 0:
             self.app.inform.emit(_("[WARNING] The Cut Z parameter has positive value. "
-                                 "It is the depth value to cut into material.\n"
-                                 "The Cut Z parameter needs to have a negative value, assuming it is a typo "
-                                 "therefore the app will convert the value to negative."
-                                 "Check the resulting CNC code (Gcode etc)."))
+                                   "It is the depth value to cut into material.\n"
+                                   "The Cut Z parameter needs to have a negative value, assuming it is a typo "
+                                   "therefore the app will convert the value to negative."
+                                   "Check the resulting CNC code (Gcode etc)."))
             self.z_cut = -self.z_cut
         elif self.z_cut == 0:
             self.app.inform.emit(_("[WARNING] The Cut Z parameter is zero. "
-                                 "There will be no cut, skipping %s file") % geometry.options['name'])
+                                   "There will be no cut, skipping %s file") % geometry.options['name'])
             return 'fail'
 
         if self.z_move is None:
@@ -5931,14 +5933,14 @@ class CNCjob(Geometry):
 
         if self.z_move < 0:
             self.app.inform.emit(_("[WARNING] The Travel Z parameter has negative value. "
-                                 "It is the height value to travel between cuts.\n"
-                                 "The Z Travel parameter needs to have a positive value, assuming it is a typo "
-                                 "therefore the app will convert the value to positive."
-                                 "Check the resulting CNC code (Gcode etc)."))
+                                   "It is the height value to travel between cuts.\n"
+                                   "The Z Travel parameter needs to have a positive value, assuming it is a typo "
+                                   "therefore the app will convert the value to positive."
+                                   "Check the resulting CNC code (Gcode etc)."))
             self.z_move = -self.z_move
         elif self.z_move == 0:
             self.app.inform.emit(_("[WARNING] The Z Travel parameter is zero. "
-                                 "This is dangerous, skipping %s file") % self.options['name'])
+                                   "This is dangerous, skipping %s file") % self.options['name'])
             return 'fail'
 
         # ## Index first and last points in paths
@@ -6015,11 +6017,11 @@ class CNCjob(Geometry):
                 if pt != geo.coords[0] and pt == geo.coords[-1]:
                     geo.coords = list(geo.coords)[::-1]
 
-                #---------- Single depth/pass --------
+                # ---------- Single depth/pass --------
                 if not multidepth:
                     self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance)
 
-                #--------- Multi-pass ---------
+                # --------- Multi-pass ---------
                 else:
                     self.gcode += self.create_gcode_multi_pass(geo, extracut, tolerance,
                                                                postproc=p, current_point=current_pt)
@@ -6240,7 +6242,8 @@ class CNCjob(Geometry):
                     gcode_multi_pass += self.linear2gcode(geometry, tolerance=tolerance, z_cut=depth, up=False)
                 else:
                     if geometry.is_ring:
-                        gcode_multi_pass += self.linear2gcode_extra(geometry, tolerance=tolerance, z_cut=depth, up=False)
+                        gcode_multi_pass += self.linear2gcode_extra(geometry, tolerance=tolerance, z_cut=depth,
+                                                                    up=False)
                     else:
                         gcode_multi_pass += self.linear2gcode(geometry, tolerance=tolerance, z_cut=depth, up=False)
 
@@ -6414,7 +6417,6 @@ class CNCjob(Geometry):
                 current['G'] = int(gobj['G'])
                 
             if 'X' in gobj or 'Y' in gobj:
-                # TODO: I think there is a problem here, current['X] (and the rest of current[...] are not initialized
                 if 'X' in gobj:
                     x = gobj['X']
                     # current['X'] = x
@@ -6505,6 +6507,9 @@ class CNCjob(Geometry):
         :param color: Color specification.
         :param alpha: Transparency specification.
         :param tool_tolerance: Tolerance when drawing the toolshape.
+        :param obj
+        :param visible
+        :param kind
         :return: None
         """
         # units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
@@ -6556,7 +6561,7 @@ class CNCjob(Geometry):
 
                 if kind == 'all':
                     obj.add_shape(shape=poly, color=color[geo['kind'][0]][1], face_color=color[geo['kind'][0]][0],
-                              visible=visible, layer=1 if geo['kind'][0] == 'C' else 2)
+                                  visible=visible, layer=1 if geo['kind'][0] == 'C' else 2)
                 elif kind == 'travel':
                     if geo['kind'][0] == 'T':
                         obj.add_shape(shape=poly, color=color['T'][1], face_color=color['T'][0],

+ 5 - 5
flatcamEditors/FlatCAMGeoEditor.py

@@ -641,16 +641,16 @@ class TransformEditorTool(FlatCAMTool):
         self.transform_lay.addWidget(title_label)
 
         self.empty_label = QtWidgets.QLabel("")
-        self.empty_label.setFixedWidth(50)
+        self.empty_label.setMinimumWidth(50)
 
         self.empty_label1 = QtWidgets.QLabel("")
-        self.empty_label1.setFixedWidth(70)
+        self.empty_label1.setMinimumWidth(70)
         self.empty_label2 = QtWidgets.QLabel("")
-        self.empty_label2.setFixedWidth(70)
+        self.empty_label2.setMinimumWidth(70)
         self.empty_label3 = QtWidgets.QLabel("")
-        self.empty_label3.setFixedWidth(70)
+        self.empty_label3.setMinimumWidth(70)
         self.empty_label4 = QtWidgets.QLabel("")
-        self.empty_label4.setFixedWidth(70)
+        self.empty_label4.setMinimumWidth(70)
         self.transform_lay.addWidget(self.empty_label)
 
         # Rotate Title

+ 31 - 31
flatcamEditors/FlatCAMGrbEditor.py

@@ -2604,7 +2604,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.pad_array_size_label.setToolTip(
             _("Specify how many pads to be in the array.")
         )
-        self.pad_array_size_label.setFixedWidth(100)
+        self.pad_array_size_label.setMinimumWidth(100)
 
         self.pad_array_size_entry = LengthEntry()
         self.array_form.addRow(self.pad_array_size_label, self.pad_array_size_entry)
@@ -2626,7 +2626,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
               "- 'Y' - vertical axis or \n"
               "- 'Angle' - a custom angle for the array inclination")
         )
-        self.pad_axis_label.setFixedWidth(100)
+        self.pad_axis_label.setMinimumWidth(100)
 
         self.pad_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
                                         {'label': _('Y'), 'value': 'Y'},
@@ -2638,7 +2638,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.pad_pitch_label.setToolTip(
             _("Pitch = Distance between elements of the array.")
         )
-        self.pad_pitch_label.setFixedWidth(100)
+        self.pad_pitch_label.setMinimumWidth(100)
 
         self.pad_pitch_entry = LengthEntry()
         self.linear_form.addRow(self.pad_pitch_label, self.pad_pitch_entry)
@@ -2650,7 +2650,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
               "Min value is: -359.99 degrees.\n"
               "Max value is:  360.00 degrees.")
         )
-        self.linear_angle_label.setFixedWidth(100)
+        self.linear_angle_label.setMinimumWidth(100)
 
         self.linear_angle_spinner = FCDoubleSpinner()
         self.linear_angle_spinner.set_precision(2)
@@ -2669,7 +2669,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
            _("Direction for circular array."
              "Can be CW = clockwise or CCW = counter clockwise.")
         )
-        self.pad_direction_label.setFixedWidth(100)
+        self.pad_direction_label.setMinimumWidth(100)
 
         self.circular_form = QtWidgets.QFormLayout()
         self.circular_box.addLayout(self.circular_form)
@@ -2683,7 +2683,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.pad_angle_label.setToolTip(
             _("Angle at which each element in circular array is placed.")
         )
-        self.pad_angle_label.setFixedWidth(100)
+        self.pad_angle_label.setMinimumWidth(100)
 
         self.pad_angle_entry = LengthEntry()
         self.circular_form.addRow(self.pad_angle_label, self.pad_angle_entry)
@@ -4719,16 +4719,16 @@ class TransformEditorTool(FlatCAMTool):
         self.transform_lay.addWidget(title_label)
 
         self.empty_label = QtWidgets.QLabel("")
-        self.empty_label.setFixedWidth(50)
+        self.empty_label.setMinimumWidth(50)
 
         self.empty_label1 = QtWidgets.QLabel("")
-        self.empty_label1.setFixedWidth(70)
+        self.empty_label1.setMinimumWidth(70)
         self.empty_label2 = QtWidgets.QLabel("")
-        self.empty_label2.setFixedWidth(70)
+        self.empty_label2.setMinimumWidth(70)
         self.empty_label3 = QtWidgets.QLabel("")
-        self.empty_label3.setFixedWidth(70)
+        self.empty_label3.setMinimumWidth(70)
         self.empty_label4 = QtWidgets.QLabel("")
-        self.empty_label4.setFixedWidth(70)
+        self.empty_label4.setMinimumWidth(70)
         self.transform_lay.addWidget(self.empty_label)
 
         # Rotate Title
@@ -4747,7 +4747,7 @@ class TransformEditorTool(FlatCAMTool):
               "Positive numbers for CW motion.\n"
               "Negative numbers for CCW motion.")
         )
-        self.rotate_label.setFixedWidth(50)
+        self.rotate_label.setMinimumWidth(50)
 
         self.rotate_entry = FCEntry()
         # self.rotate_entry.setFixedWidth(60)
@@ -4760,7 +4760,7 @@ class TransformEditorTool(FlatCAMTool):
               "The point of reference is the middle of\n"
               "the bounding box for all selected shapes.")
         )
-        self.rotate_button.setFixedWidth(60)
+        self.rotate_button.setMinimumWidth(60)
 
         form_child.addWidget(self.rotate_entry)
         form_child.addWidget(self.rotate_button)
@@ -4784,7 +4784,7 @@ class TransformEditorTool(FlatCAMTool):
             _("Angle for Skew action, in degrees.\n"
               "Float number between -360 and 359.")
         )
-        self.skewx_label.setFixedWidth(50)
+        self.skewx_label.setMinimumWidth(50)
         self.skewx_entry = FCEntry()
         self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.skewx_entry.setFixedWidth(60)
@@ -4795,14 +4795,14 @@ class TransformEditorTool(FlatCAMTool):
             _("Skew/shear the selected shape(s).\n"
               "The point of reference is the middle of\n"
               "the bounding box for all selected shapes."))
-        self.skewx_button.setFixedWidth(60)
+        self.skewx_button.setMinimumWidth(60)
 
         self.skewy_label = QtWidgets.QLabel(_("Angle Y:"))
         self.skewy_label.setToolTip(
             _("Angle for Skew action, in degrees.\n"
               "Float number between -360 and 359.")
         )
-        self.skewy_label.setFixedWidth(50)
+        self.skewy_label.setMinimumWidth(50)
         self.skewy_entry = FCEntry()
         self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.skewy_entry.setFixedWidth(60)
@@ -4813,7 +4813,7 @@ class TransformEditorTool(FlatCAMTool):
             _("Skew/shear the selected shape(s).\n"
               "The point of reference is the middle of\n"
               "the bounding box for all selected shapes."))
-        self.skewy_button.setFixedWidth(60)
+        self.skewy_button.setMinimumWidth(60)
 
         form1_child_1.addWidget(self.skewx_entry)
         form1_child_1.addWidget(self.skewx_button)
@@ -4840,7 +4840,7 @@ class TransformEditorTool(FlatCAMTool):
         self.scalex_label.setToolTip(
             _("Factor for Scale action over X axis.")
         )
-        self.scalex_label.setFixedWidth(50)
+        self.scalex_label.setMinimumWidth(50)
         self.scalex_entry = FCEntry()
         self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.scalex_entry.setFixedWidth(60)
@@ -4851,13 +4851,13 @@ class TransformEditorTool(FlatCAMTool):
             _("Scale the selected shape(s).\n"
               "The point of reference depends on \n"
               "the Scale reference checkbox state."))
-        self.scalex_button.setFixedWidth(60)
+        self.scalex_button.setMinimumWidth(60)
 
         self.scaley_label = QtWidgets.QLabel(_("Factor Y:"))
         self.scaley_label.setToolTip(
             _("Factor for Scale action over Y axis.")
         )
-        self.scaley_label.setFixedWidth(50)
+        self.scaley_label.setMinimumWidth(50)
         self.scaley_entry = FCEntry()
         self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.scaley_entry.setFixedWidth(60)
@@ -4868,7 +4868,7 @@ class TransformEditorTool(FlatCAMTool):
             _("Scale the selected shape(s).\n"
               "The point of reference depends on \n"
               "the Scale reference checkbox state."))
-        self.scaley_button.setFixedWidth(60)
+        self.scaley_button.setMinimumWidth(60)
 
         self.scale_link_cb = FCCheckBox()
         self.scale_link_cb.set_value(True)
@@ -4876,7 +4876,7 @@ class TransformEditorTool(FlatCAMTool):
         self.scale_link_cb.setToolTip(
             _("Scale the selected shape(s)\n"
               "using the Scale Factor X for both axis."))
-        self.scale_link_cb.setFixedWidth(50)
+        self.scale_link_cb.setMinimumWidth(50)
 
         self.scale_zero_ref_cb = FCCheckBox()
         self.scale_zero_ref_cb.set_value(True)
@@ -4915,7 +4915,7 @@ class TransformEditorTool(FlatCAMTool):
         self.offx_label.setToolTip(
             _("Value for Offset action on X axis.")
         )
-        self.offx_label.setFixedWidth(50)
+        self.offx_label.setMinimumWidth(50)
         self.offx_entry = FCEntry()
         self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.offx_entry.setFixedWidth(60)
@@ -4927,13 +4927,13 @@ class TransformEditorTool(FlatCAMTool):
               "The point of reference is the middle of\n"
               "the bounding box for all selected shapes.\n")
         )
-        self.offx_button.setFixedWidth(60)
+        self.offx_button.setMinimumWidth(60)
 
         self.offy_label = QtWidgets.QLabel(_("Value Y:"))
         self.offy_label.setToolTip(
             _("Value for Offset action on Y axis.")
         )
-        self.offy_label.setFixedWidth(50)
+        self.offy_label.setMinimumWidth(50)
         self.offy_entry = FCEntry()
         self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.offy_entry.setFixedWidth(60)
@@ -4945,7 +4945,7 @@ class TransformEditorTool(FlatCAMTool):
               "The point of reference is the middle of\n"
               "the bounding box for all selected shapes.\n")
         )
-        self.offy_button.setFixedWidth(60)
+        self.offy_button.setMinimumWidth(60)
 
         form3_child_1.addWidget(self.offx_entry)
         form3_child_1.addWidget(self.offx_button)
@@ -4975,7 +4975,7 @@ class TransformEditorTool(FlatCAMTool):
             _("Flip the selected shape(s) over the X axis.\n"
               "Does not create a new shape.")
         )
-        self.flipx_button.setFixedWidth(60)
+        self.flipx_button.setMinimumWidth(60)
 
         self.flipy_button = FCButton()
         self.flipy_button.set_value(_("Flip on Y"))
@@ -4983,7 +4983,7 @@ class TransformEditorTool(FlatCAMTool):
             _("Flip the selected shape(s) over the X axis.\n"
               "Does not create a new shape.")
         )
-        self.flipy_button.setFixedWidth(60)
+        self.flipy_button.setMinimumWidth(60)
 
         self.flip_ref_cb = FCCheckBox()
         self.flip_ref_cb.set_value(True)
@@ -4999,7 +4999,7 @@ class TransformEditorTool(FlatCAMTool):
               "Or enter the coords in format (x, y) in the\n"
               "Point Entry field and click Flip on X(Y)")
         )
-        self.flip_ref_cb.setFixedWidth(50)
+        self.flip_ref_cb.setMinimumWidth(50)
 
         self.flip_ref_label = QtWidgets.QLabel(_("Point:"))
         self.flip_ref_label.setToolTip(
@@ -5007,7 +5007,7 @@ class TransformEditorTool(FlatCAMTool):
               "The 'x' in (x, y) will be used when using Flip on X and\n"
               "the 'y' in (x, y) will be used when using Flip on Y.")
         )
-        self.flip_ref_label.setFixedWidth(50)
+        self.flip_ref_label.setMinimumWidth(50)
         self.flip_ref_entry = EvalEntry2("(0, 0)")
         self.flip_ref_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.flip_ref_entry.setFixedWidth(60)
@@ -5019,7 +5019,7 @@ class TransformEditorTool(FlatCAMTool):
               "left click on canvas together with pressing\n"
               "SHIFT key. Then click Add button to insert.")
         )
-        self.flip_ref_button.setFixedWidth(60)
+        self.flip_ref_button.setMinimumWidth(60)
 
         form4_child_hlay.addStretch()
         form4_child_hlay.addWidget(self.flipx_button)

+ 35 - 22
flatcamGUI/FlatCAMGUI.py

@@ -1656,12 +1656,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.buttonPreview = QtWidgets.QPushButton(_('Print Preview'))
         self.buttonPrint = QtWidgets.QPushButton(_('Print Code'))
         self.buttonFind = QtWidgets.QPushButton(_('Find in Code'))
-        self.buttonFind.setFixedWidth(100)
-        self.buttonPreview.setFixedWidth(100)
+        self.buttonFind.setMinimumWidth(100)
+        self.buttonPreview.setMinimumWidth(100)
         self.entryFind = FCEntry()
         self.entryFind.setMaximumWidth(200)
         self.buttonReplace = QtWidgets.QPushButton(_('Replace With'))
-        self.buttonReplace.setFixedWidth(100)
+        self.buttonReplace.setMinimumWidth(100)
         self.entryReplace = FCEntry()
         self.entryReplace.setMaximumWidth(200)
         self.sel_all_cb = QtWidgets.QCheckBox(_('All'))
@@ -3255,9 +3255,9 @@ class ToolsPreferencesUI(QtWidgets.QWidget):
         self.setLayout(self.layout)
 
         self.tools_ncc_group = ToolsNCCPrefGroupUI()
-        self.tools_ncc_group.setMinimumWidth(200)
+        self.tools_ncc_group.setMinimumWidth(220)
         self.tools_paint_group = ToolsPaintPrefGroupUI()
-        self.tools_paint_group.setMinimumWidth(200)
+        self.tools_paint_group.setMinimumWidth(220)
 
         self.tools_cutout_group = ToolsCutoutPrefGroupUI()
         self.tools_cutout_group.setMinimumWidth(220)
@@ -3417,7 +3417,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         self.pf_color_alpha_slider.setSingleStep(1)
 
         self.pf_color_alpha_spinner = FCSpinner()
-        self.pf_color_alpha_spinner.setFixedWidth(70)
+        self.pf_color_alpha_spinner.setMinimumWidth(70)
         self.pf_color_alpha_spinner.setMinimum(0)
         self.pf_color_alpha_spinner.setMaximum(255)
 
@@ -3467,7 +3467,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         self.sf_color_alpha_slider.setSingleStep(1)
 
         self.sf_color_alpha_spinner = FCSpinner()
-        self.sf_color_alpha_spinner.setFixedWidth(70)
+        self.sf_color_alpha_spinner.setMinimumWidth(70)
         self.sf_color_alpha_spinner.setMinimum(0)
         self.sf_color_alpha_spinner.setMaximum(255)
 
@@ -3517,7 +3517,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         self.alt_sf_color_alpha_slider.setSingleStep(1)
 
         self.alt_sf_color_alpha_spinner = FCSpinner()
-        self.alt_sf_color_alpha_spinner.setFixedWidth(70)
+        self.alt_sf_color_alpha_spinner.setMinimumWidth(70)
         self.alt_sf_color_alpha_spinner.setMinimum(0)
         self.alt_sf_color_alpha_spinner.setMaximum(255)
 
@@ -4291,7 +4291,7 @@ class GerberExpPrefGroupUI(OptionsGroupUI):
         self.format_whole_entry = IntEntry()
         self.format_whole_entry.setMaxLength(1)
         self.format_whole_entry.setAlignment(QtCore.Qt.AlignRight)
-        self.format_whole_entry.setFixedWidth(30)
+        self.format_whole_entry.setMinimumWidth(30)
         self.format_whole_entry.setToolTip(
             _("This numbers signify the number of digits in\n"
               "the whole part of Gerber coordinates.")
@@ -4305,7 +4305,7 @@ class GerberExpPrefGroupUI(OptionsGroupUI):
         self.format_dec_entry = IntEntry()
         self.format_dec_entry.setMaxLength(1)
         self.format_dec_entry.setAlignment(QtCore.Qt.AlignRight)
-        self.format_dec_entry.setFixedWidth(30)
+        self.format_dec_entry.setMinimumWidth(30)
         self.format_dec_entry.setToolTip(
             _("This numbers signify the number of digits in\n"
               "the decimal part of Gerber coordinates.")
@@ -4439,7 +4439,7 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         self.excellon_format_upper_in_entry = IntEntry()
         self.excellon_format_upper_in_entry.setMaxLength(1)
         self.excellon_format_upper_in_entry.setAlignment(QtCore.Qt.AlignRight)
-        self.excellon_format_upper_in_entry.setFixedWidth(30)
+        self.excellon_format_upper_in_entry.setMinimumWidth(30)
         self.excellon_format_upper_in_entry.setToolTip(
            _("This numbers signify the number of digits in\n"
              "the whole part of Excellon coordinates.")
@@ -4453,7 +4453,7 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         self.excellon_format_lower_in_entry = IntEntry()
         self.excellon_format_lower_in_entry.setMaxLength(1)
         self.excellon_format_lower_in_entry.setAlignment(QtCore.Qt.AlignRight)
-        self.excellon_format_lower_in_entry.setFixedWidth(30)
+        self.excellon_format_lower_in_entry.setMinimumWidth(30)
         self.excellon_format_lower_in_entry.setToolTip(
             _("This numbers signify the number of digits in\n"
               "the decimal part of Excellon coordinates.")
@@ -4472,7 +4472,7 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         self.excellon_format_upper_mm_entry = IntEntry()
         self.excellon_format_upper_mm_entry.setMaxLength(1)
         self.excellon_format_upper_mm_entry.setAlignment(QtCore.Qt.AlignRight)
-        self.excellon_format_upper_mm_entry.setFixedWidth(30)
+        self.excellon_format_upper_mm_entry.setMinimumWidth(30)
         self.excellon_format_upper_mm_entry.setToolTip(
             _("This numbers signify the number of digits in\n"
               "the whole part of Excellon coordinates.")
@@ -4486,7 +4486,7 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         self.excellon_format_lower_mm_entry = IntEntry()
         self.excellon_format_lower_mm_entry.setMaxLength(1)
         self.excellon_format_lower_mm_entry.setAlignment(QtCore.Qt.AlignRight)
-        self.excellon_format_lower_mm_entry.setFixedWidth(30)
+        self.excellon_format_lower_mm_entry.setMinimumWidth(30)
         self.excellon_format_lower_mm_entry.setToolTip(
             _("This numbers signify the number of digits in\n"
               "the decimal part of Excellon coordinates.")
@@ -4778,7 +4778,7 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
         # Adding the Excellon Format Defaults Button
         self.excellon_defaults_button = QtWidgets.QPushButton()
         self.excellon_defaults_button.setText(str(_("Defaults")))
-        self.excellon_defaults_button.setFixedWidth(80)
+        self.excellon_defaults_button.setMinimumWidth(80)
         grid4.addWidget(self.excellon_defaults_button, 0, 0, QtCore.Qt.AlignRight)
 
         self.layout.addStretch()
@@ -4945,7 +4945,7 @@ class ExcellonExpPrefGroupUI(OptionsGroupUI):
         self.format_whole_entry = IntEntry()
         self.format_whole_entry.setMaxLength(1)
         self.format_whole_entry.setAlignment(QtCore.Qt.AlignRight)
-        self.format_whole_entry.setFixedWidth(30)
+        self.format_whole_entry.setMinimumWidth(30)
         self.format_whole_entry.setToolTip(
             _("This numbers signify the number of digits in\n"
               "the whole part of Excellon coordinates.")
@@ -4959,7 +4959,7 @@ class ExcellonExpPrefGroupUI(OptionsGroupUI):
         self.format_dec_entry = IntEntry()
         self.format_dec_entry.setMaxLength(1)
         self.format_dec_entry.setAlignment(QtCore.Qt.AlignRight)
-        self.format_dec_entry.setFixedWidth(30)
+        self.format_dec_entry.setMinimumWidth(30)
         self.format_dec_entry.setToolTip(
             _("This numbers signify the number of digits in\n"
               "the decimal part of Excellon coordinates.")
@@ -5074,7 +5074,7 @@ class ExcellonEditorPrefGroupUI(OptionsGroupUI):
         self.drill_array_size_label.setToolTip(
             _("Specify how many drills to be in the array.")
         )
-        # self.drill_array_size_label.setFixedWidth(100)
+        # self.drill_array_size_label.setMinimumWidth(100)
 
         self.drill_array_size_entry = LengthEntry()
 
@@ -5092,7 +5092,7 @@ class ExcellonEditorPrefGroupUI(OptionsGroupUI):
               "- 'Y' - vertical axis or \n"
               "- 'Angle' - a custom angle for the array inclination")
         )
-        # self.drill_axis_label.setFixedWidth(100)
+        # self.drill_axis_label.setMinimumWidth(100)
         self.drill_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
                                           {'label': _('Y'), 'value': 'Y'},
                                           {'label': _('Angle'), 'value': 'A'}])
@@ -5105,7 +5105,7 @@ class ExcellonEditorPrefGroupUI(OptionsGroupUI):
         self.drill_pitch_label.setToolTip(
             _("Pitch = Distance between elements of the array.")
         )
-        # self.drill_pitch_label.setFixedWidth(100)
+        # self.drill_pitch_label.setMinimumWidth(100)
         self.drill_pitch_entry = LengthEntry()
 
         grid0.addWidget(self.drill_pitch_label, 5, 0)
@@ -5875,6 +5875,19 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
         self.ncc_rest_cb = FCCheckBox()
         grid0.addWidget(self.ncc_rest_cb, 6, 1)
 
+        # ## Reference
+        self.reference_radio = RadioSet([{'label': _('Itself'), 'value': 'itself'},
+                                         {'label': _('Box'), 'value': 'box'}])
+        reference_label = QtWidgets.QLabel(_("Reference:"))
+        reference_label.setToolTip(
+            _("When choosing the 'Itself' option the non copper clearing extent\n"
+              "is based on the object that is copper cleared.\n "
+              "Choosing the 'Box' option will do non copper clearing within the box\n"
+              "specified by another object different than the one that is copper cleared.")
+        )
+        grid0.addWidget(reference_label, 7, 0)
+        grid0.addWidget(self.reference_radio, 7, 1)
+
         self.layout.addStretch()
 
 
@@ -6126,8 +6139,8 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(selectlabel, 6, 0)
         self.selectmethod_combo = RadioSet([
             {"label": _("Single"), "value": "single"},
-            {"label": _("All"), "value": "all"},
-            # {"label": "Rectangle", "value": "rectangle"}
+            {"label": _("Area"), "value": "area"},
+            {"label": _("All"), "value": "all"}
         ])
         grid0.addWidget(self.selectmethod_combo, 6, 1)
 

+ 1 - 1
flatcamGUI/GUIElements.py

@@ -1634,7 +1634,7 @@ class Dialog_box(QtWidgets.QWidget):
         self.ok = False
 
         dialog_box = QtWidgets.QInputDialog()
-        dialog_box.setFixedWidth(290)
+        dialog_box.setMinimumWidth(290)
         self.setWindowIcon(icon)
 
         self.location, self.ok = dialog_box.getText(self, title, label, text="0, 0")

+ 20 - 20
flatcamGUI/ObjectUI.py

@@ -101,7 +101,7 @@ class ObjectUI(QtWidgets.QWidget):
         self.scale_button.setToolTip(
             _("Perform scaling operation.")
         )
-        self.scale_button.setFixedWidth(70)
+        self.scale_button.setMinimumWidth(70)
         self.scale_grid.addWidget(self.scale_button, 0, 2)
 
         # ### Offset ####
@@ -128,7 +128,7 @@ class ObjectUI(QtWidgets.QWidget):
         self.offset_button.setToolTip(
             _("Perform the offset operation.")
         )
-        self.offset_button.setFixedWidth(70)
+        self.offset_button.setMinimumWidth(70)
         self.offset_grid.addWidget(self.offset_button, 0, 2)
 
         layout.addStretch()
@@ -148,7 +148,7 @@ class GerberObjectUI(ObjectUI):
         self.custom_box.addLayout(grid0)
 
         self.plot_options_label = QtWidgets.QLabel(_("<b>Plot Options:</b>"))
-        self.plot_options_label.setFixedWidth(90)
+        self.plot_options_label.setMinimumWidth(90)
 
         grid0.addWidget(self.plot_options_label, 0, 0)
 
@@ -157,7 +157,7 @@ class GerberObjectUI(ObjectUI):
         self.solid_cb.setToolTip(
             _("Solid color polygons.")
         )
-        self.solid_cb.setFixedWidth(50)
+        self.solid_cb.setMinimumWidth(50)
         grid0.addWidget(self.solid_cb, 0, 1)
 
         # Multicolored CB
@@ -165,7 +165,7 @@ class GerberObjectUI(ObjectUI):
         self.multicolored_cb.setToolTip(
             _("Draw polygons in different colors.")
         )
-        self.multicolored_cb.setFixedWidth(55)
+        self.multicolored_cb.setMinimumWidth(55)
         grid0.addWidget(self.multicolored_cb, 0, 2)
 
         # Plot CB
@@ -173,7 +173,7 @@ class GerberObjectUI(ObjectUI):
         self.plot_cb.setToolTip(
             _("Plot (show) this object.")
         )
-        self.plot_cb.setFixedWidth(59)
+        self.plot_cb.setMinimumWidth(59)
         grid0.addWidget(self.plot_cb, 0, 3)
 
         # ## Object name
@@ -193,7 +193,7 @@ class GerberObjectUI(ObjectUI):
         self.apertures_table_label.setToolTip(
             _("Apertures Table for the Gerber Object.")
         )
-        self.apertures_table_label.setFixedWidth(90)
+        self.apertures_table_label.setMinimumWidth(90)
 
         hlay_plot.addWidget(self.apertures_table_label)
 
@@ -264,7 +264,7 @@ class GerberObjectUI(ObjectUI):
               "feature, use a negative value for\n"
               "this parameter.")
         )
-        tdlabel.setFixedWidth(90)
+        tdlabel.setMinimumWidth(90)
         grid1.addWidget(tdlabel, 0, 0)
         self.iso_tool_dia_entry = LengthEntry()
         grid1.addWidget(self.iso_tool_dia_entry, 0, 1)
@@ -274,7 +274,7 @@ class GerberObjectUI(ObjectUI):
             _("Width of the isolation gap in\n"
               "number (integer) of tool widths.")
         )
-        passlabel.setFixedWidth(90)
+        passlabel.setMinimumWidth(90)
         grid1.addWidget(passlabel, 1, 0)
         self.iso_width_entry = IntEntry()
         grid1.addWidget(self.iso_width_entry, 1, 1)
@@ -285,7 +285,7 @@ class GerberObjectUI(ObjectUI):
               "Example:\n"
               "A value here of 0.25 means an overlap of 25% from the tool diameter found above.")
         )
-        overlabel.setFixedWidth(90)
+        overlabel.setMinimumWidth(90)
         grid1.addWidget(overlabel, 2, 0)
         self.iso_overlap_entry = FloatEntry()
         grid1.addWidget(self.iso_overlap_entry, 2, 1)
@@ -337,7 +337,7 @@ class GerberObjectUI(ObjectUI):
         self.custom_box.addLayout(hlay_1)
 
         self.padding_area_label = QtWidgets.QLabel('')
-        self.padding_area_label.setFixedWidth(90)
+        self.padding_area_label.setMinimumWidth(90)
         hlay_1.addWidget(self.padding_area_label)
 
         self.generate_iso_button = QtWidgets.QPushButton(_('FULL Geo'))
@@ -346,7 +346,7 @@ class GerberObjectUI(ObjectUI):
               "for isolation routing. It contains both\n"
               "the interiors and exteriors geometry.")
         )
-        self.generate_iso_button.setFixedWidth(90)
+        self.generate_iso_button.setMinimumWidth(90)
         hlay_1.addWidget(self.generate_iso_button, alignment=Qt.AlignLeft)
 
         # hlay_1.addStretch()
@@ -357,7 +357,7 @@ class GerberObjectUI(ObjectUI):
               "for isolation routing containing\n"
               "only the exteriors geometry.")
         )
-        # self.generate_ext_iso_button.setFixedWidth(100)
+        # self.generate_ext_iso_button.setMinimumWidth(100)
         hlay_1.addWidget(self.generate_ext_iso_button)
 
         self.generate_int_iso_button = QtWidgets.QPushButton(_('Int Geo'))
@@ -366,7 +366,7 @@ class GerberObjectUI(ObjectUI):
               "for isolation routing containing\n"
               "only the interiors geometry.")
         )
-        # self.generate_ext_iso_button.setFixedWidth(90)
+        # self.generate_ext_iso_button.setMinimumWidth(90)
         hlay_1.addWidget(self.generate_int_iso_button)
 
         # when the follow checkbox is checked then the exteriors and interiors isolation generation buttons
@@ -383,7 +383,7 @@ class GerberObjectUI(ObjectUI):
             _("Create a Geometry object with\n"
               "toolpaths to cut all non-copper regions.")
         )
-        self.clearcopper_label.setFixedWidth(90)
+        self.clearcopper_label.setMinimumWidth(90)
         grid2.addWidget(self.clearcopper_label, 0, 0)
 
         self.generate_ncc_button = QtWidgets.QPushButton(_('NCC Tool'))
@@ -431,7 +431,7 @@ class GerberObjectUI(ObjectUI):
               "objects with this minimum\n"
               "distance.")
         )
-        bmlabel.setFixedWidth(90)
+        bmlabel.setMinimumWidth(90)
         grid4.addWidget(bmlabel, 0, 0)
         self.noncopper_margin_entry = LengthEntry()
         grid4.addWidget(self.noncopper_margin_entry, 0, 1)
@@ -441,7 +441,7 @@ class GerberObjectUI(ObjectUI):
         self.noncopper_rounded_cb.setToolTip(
             _("Resulting geometry will have rounded corners.")
         )
-        self.noncopper_rounded_cb.setFixedWidth(90)
+        self.noncopper_rounded_cb.setMinimumWidth(90)
         grid4.addWidget(self.noncopper_rounded_cb, 1, 0)
 
         self.generate_noncopper_button = QtWidgets.QPushButton(_('Generate Geo'))
@@ -463,7 +463,7 @@ class GerberObjectUI(ObjectUI):
             _("Distance of the edges of the box\n"
               "to the nearest polygon.")
         )
-        bbmargin.setFixedWidth(90)
+        bbmargin.setMinimumWidth(90)
         grid5.addWidget(bbmargin, 0, 0)
         self.bbmargin_entry = LengthEntry()
         grid5.addWidget(self.bbmargin_entry, 0, 1)
@@ -475,7 +475,7 @@ class GerberObjectUI(ObjectUI):
               "their radius is equal to\n"
               "the margin.")
         )
-        self.bbrounded_cb.setFixedWidth(90)
+        self.bbrounded_cb.setMinimumWidth(90)
         grid5.addWidget(self.bbrounded_cb, 1, 0)
 
         self.generate_bb_button = QtWidgets.QPushButton(_('Generate Geo'))
@@ -957,7 +957,7 @@ class GeometryObjectUI(ObjectUI):
         self.tool_offset_entry = FloatEntry()
         self.tool_offset_entry.setValidator(QtGui.QDoubleValidator(-9999.9999, 9999.9999, 4))
         spacer_lbl = QtWidgets.QLabel(" ")
-        spacer_lbl.setFixedWidth(80)
+        spacer_lbl.setMinimumWidth(80)
 
         self.grid1.addWidget(self.tool_offset_entry, 0, 1)
         self.grid1.addWidget(spacer_lbl, 0, 2)

+ 1 - 1
flatcamGUI/VisPyVisuals.py

@@ -269,7 +269,7 @@ class ShapeCollectionVisual(CompoundVisual):
         # Add data to process pool if pool exists
         try:
             self.results[key] = self.pool.map_async(_update_shape_buffers, [self.data[key]])
-        except:
+        except Exception as e:
             self.data[key] = _update_shape_buffers(self.data[key])
 
         if update:

+ 3 - 3
flatcamTools/ToolCutOut.py

@@ -58,7 +58,7 @@ class CutOut(FlatCAMTool):
               "What is selected here will dictate the kind\n"
               "of objects that will populate the 'Object' combobox.")
         )
-        self.type_obj_combo_label.setFixedWidth(60)
+        self.type_obj_combo_label.setMinimumWidth(60)
         form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
 
         # Object to be cutout
@@ -158,7 +158,7 @@ class CutOut(FlatCAMTool):
               "- 2tb  - 2*top + 2*bottom\n"
               "- 8     - 2*left + 2*right +2*top + 2*bottom")
         )
-        gaps_label.setFixedWidth(60)
+        gaps_label.setMinimumWidth(60)
 
         self.gaps = FCComboBox()
         gaps_items = ['LR', 'TB', '4', '2LR', '2TB', '8']
@@ -232,7 +232,7 @@ class CutOut(FlatCAMTool):
         self.man_object_label.setToolTip(
             _("Geometry object used to create the manual cutout.")
         )
-        self.man_object_label.setFixedWidth(60)
+        self.man_object_label.setMinimumWidth(60)
         # e_lab_0 = QtWidgets.QLabel('')
 
         form_layout_3.addRow(self.man_object_label, self.man_object_combo)

+ 7 - 7
flatcamTools/ToolDblSided.py

@@ -55,7 +55,7 @@ class DblSidedTool(FlatCAMTool):
               "the specified axis. Does not create a new \n"
               "object, but modifies it.")
         )
-        self.mirror_gerber_button.setFixedWidth(60)
+        self.mirror_gerber_button.setMinimumWidth(60)
 
         # grid_lay.addRow("Bottom Layer:", self.object_combo)
         grid_lay.addWidget(self.botlay_label, 0, 0)
@@ -79,7 +79,7 @@ class DblSidedTool(FlatCAMTool):
               "the specified axis. Does not create a new \n"
               "object, but modifies it.")
         )
-        self.mirror_exc_button.setFixedWidth(60)
+        self.mirror_exc_button.setMinimumWidth(60)
 
         # grid_lay.addRow("Bottom Layer:", self.object_combo)
         grid_lay.addWidget(self.excobj_label, 2, 0)
@@ -103,7 +103,7 @@ class DblSidedTool(FlatCAMTool):
               "the specified axis. Does not create a new \n"
               "object, but modifies it.")
         )
-        self.mirror_geo_button.setFixedWidth(60)
+        self.mirror_geo_button.setMinimumWidth(60)
 
         # grid_lay.addRow("Bottom Layer:", self.object_combo)
         grid_lay.addWidget(self.geoobj_label, 4, 0)
@@ -164,15 +164,15 @@ class DblSidedTool(FlatCAMTool):
               "The (x, y) coordinates are captured by pressing SHIFT key\n"
               "and left mouse button click on canvas or you can enter the coords manually.")
         )
-        self.add_point_button.setFixedWidth(60)
+        self.add_point_button.setMinimumWidth(60)
 
         grid_lay2.addWidget(self.pb_label, 10, 0)
         grid_lay2.addLayout(self.point_box_container, 11, 0)
         grid_lay2.addWidget(self.add_point_button, 11, 1)
 
         self.point_entry = EvalEntry()
-
         self.point_box_container.addWidget(self.point_entry)
+
         self.box_combo = QtWidgets.QComboBox()
         self.box_combo.setModel(self.app.collection)
         self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
@@ -214,7 +214,7 @@ class DblSidedTool(FlatCAMTool):
               "- 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.setFixedWidth(60)
+        self.add_drill_point_button.setMinimumWidth(60)
 
         grid_lay3.addWidget(self.alignment_holes, 0, 0)
         grid_lay3.addWidget(self.add_drill_point_button, 0, 1)
@@ -255,7 +255,7 @@ class DblSidedTool(FlatCAMTool):
         self.reset_button.setToolTip(
             _("Resets all the fields.")
         )
-        self.reset_button.setFixedWidth(60)
+        self.reset_button.setMinimumWidth(60)
         hlay2.addWidget(self.reset_button)
 
         self.layout.addStretch()

+ 104 - 5
flatcamTools/ToolNonCopperClear.py

@@ -235,6 +235,51 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.ncc_rest_cb = FCCheckBox()
         grid3.addWidget(self.ncc_rest_cb, 6, 1)
 
+        # ## Reference
+        self.reference_radio = RadioSet([{'label': _('Itself'), 'value': 'itself'},
+                                         {'label': _('Box'), 'value': 'box'}])
+        self.reference_label = QtWidgets.QLabel(_("Reference:"))
+        self.reference_label.setToolTip(
+            _("- 'Itself': the non copper clearing extent\n"
+              "is based on the object that is copper cleared.\n "
+              "- 'Box': will do non copper clearing within the box\n"
+              "specified by the object selected in the Ref. Object combobox.")
+        )
+        grid3.addWidget(self.reference_label, 7, 0)
+        grid3.addWidget(self.reference_radio, 7, 1)
+
+        grid4 = QtWidgets.QGridLayout()
+        self.tools_box.addLayout(grid4)
+
+        self.box_combo_type_label = QtWidgets.QLabel(_("Ref. Type:"))
+        self.box_combo_type_label.setToolTip(
+            _("The type of FlatCAM object to be used as non copper clearing reference.\n"
+              "It can be Gerber, Excellon or Geometry.")
+        )
+        self.box_combo_type = QtWidgets.QComboBox()
+        self.box_combo_type.addItem(_("Gerber   Reference Box Object"))
+        self.box_combo_type.addItem(_("Excellon Reference Box Object"))
+        self.box_combo_type.addItem(_("Geometry Reference Box Object"))
+
+        grid4.addWidget(self.box_combo_type_label, 0, 0)
+        grid4.addWidget(self.box_combo_type, 0, 1)
+
+        self.box_combo_label = QtWidgets.QLabel(_("Ref. Object:"))
+        self.box_combo_label.setToolTip(
+            _("The FlatCAM object to be used as non copper clearing reference.")
+        )
+        self.box_combo = QtWidgets.QComboBox()
+        self.box_combo.setModel(self.app.collection)
+        self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.box_combo.setCurrentIndex(1)
+        grid4.addWidget(self.box_combo_label, 1, 0)
+        grid4.addWidget(self.box_combo, 1, 1)
+
+        self.box_combo.hide()
+        self.box_combo_label.hide()
+        self.box_combo_type.hide()
+        self.box_combo_type_label.hide()
+
         self.generate_ncc_button = QtWidgets.QPushButton(_('Generate Geometry'))
         self.generate_ncc_button.setToolTip(
             _("Create the Geometry Object\n"
@@ -251,6 +296,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.obj_name = ""
         self.ncc_obj = None
 
+        self.bound_obj_name = ""
+        self.bound_obj = None
+
         self.tools_box.addStretch()
 
         self.addtool_btn.clicked.connect(self.on_tool_add)
@@ -258,6 +306,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         self.generate_ncc_button.clicked.connect(self.on_ncc)
 
+        self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
+        self.reference_radio.group_toggle_fn = self.on_toggle_reference
+
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+N', **kwargs)
 
@@ -293,6 +344,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.ncc_connect_cb.set_value(self.app.defaults["tools_nccconnect"])
         self.ncc_contour_cb.set_value(self.app.defaults["tools_ncccontour"])
         self.ncc_rest_cb.set_value(self.app.defaults["tools_nccrest"])
+        self.reference_radio.set_value(self.app.defaults["tools_nccref"])
 
         self.tools_table.setupContextMenu()
         self.tools_table.addContextMenu(
@@ -365,8 +417,12 @@ class NonCopperClear(FlatCAMTool, Gerber):
                     'solid_geometry': []
                 }
             })
+
         self.obj_name = ""
         self.ncc_obj = None
+        self.bound_obj_name = ""
+        self.bound_obj = None
+
         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
 
@@ -464,6 +520,23 @@ class NonCopperClear(FlatCAMTool, Gerber):
         except (TypeError, AttributeError):
             pass
 
+    def on_combo_box_type(self):
+        obj_type = self.box_combo_type.currentIndex()
+        self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.box_combo.setCurrentIndex(0)
+
+    def on_toggle_reference(self):
+        if self.reference_radio.get_value() == "itself":
+            self.box_combo.hide()
+            self.box_combo_label.hide()
+            self.box_combo_type.hide()
+            self.box_combo_type_label.hide()
+        else:
+            self.box_combo.show()
+            self.box_combo_label.show()
+            self.box_combo_type.show()
+            self.box_combo_type_label.show()
+
     def on_tool_add(self, dia=None, muted=None):
 
         self.ui_disconnect()
@@ -647,7 +720,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
                                        "use a number."))
                 return
-        margin = margin if margin else self.app.defaults["tools_nccmargin"]
+        margin = margin if margin is not None else float(self.app.defaults["tools_nccmargin"])
 
         connect = self.ncc_connect_cb.get_value()
         connect = connect if connect else self.app.defaults["tools_nccconnect"]
@@ -661,6 +734,23 @@ class NonCopperClear(FlatCAMTool, Gerber):
         pol_method = self.ncc_method_radio.get_value()
         pol_method = pol_method if pol_method else self.app.defaults["tools_nccmethod"]
 
+        if self.reference_radio.get_value() == 'itself':
+            self.bound_obj_name = self.object_combo.currentText()
+            # Get source object.
+            try:
+                self.bound_obj = self.app.collection.get_by_name(self.bound_obj_name)
+            except Exception as e:
+                self.app.inform.emit(_("[ERROR_NOTCL] Could not retrieve object: %s") % self.obj_name)
+                return "Could not retrieve object: %s" % self.obj_name
+        else:
+            self.bound_obj_name = self.box_combo.currentText()
+            # Get source object.
+            try:
+                self.bound_obj = self.app.collection.get_by_name(self.bound_obj_name)
+            except Exception as e:
+                self.app.inform.emit(_("[ERROR_NOTCL] Could not retrieve object: %s") % self.obj_name)
+                return "Could not retrieve object: %s" % self.obj_name
+
         self.obj_name = self.object_combo.currentText()
         # Get source object.
         try:
@@ -671,10 +761,15 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
         # Prepare non-copper polygons
         try:
-            bounding_box = self.ncc_obj.solid_geometry.envelope.buffer(distance=margin,
-                                                                       join_style=base.JOIN_STYLE.mitre)
-        except AttributeError:
-            self.app.inform.emit(_("[ERROR_NOTCL] No Gerber file available."))
+            if not isinstance(self.bound_obj.solid_geometry, MultiPolygon):
+                env_obj = cascaded_union(self.bound_obj.solid_geometry)
+                env_obj = env_obj.convex_hull
+            else:
+                env_obj = self.bound_obj.solid_geometry.convex_hull
+            bounding_box = env_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
+        except Exception as e:
+            log.debug("NonCopperClear.on_ncc() --> %s" % str(e))
+            self.app.inform.emit(_("[ERROR_NOTCL] No object available."))
             return
 
         # calculate the empty area by subtracting the solid_geometry from the object bounding box geometry
@@ -682,6 +777,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
         if type(empty) is Polygon:
             empty = MultiPolygon([empty])
 
+        if empty.is_empty:
+            self.app.inform.emit(_("[ERROR_NOTCL] Could not get the extent of the area to be non copper cleared."))
+            return
+
         # clear non copper using standard algorithm
         if clearing_method is False:
             self.clear_non_copper(

+ 401 - 4
flatcamTools/ToolPaint.py

@@ -245,8 +245,8 @@ class ToolPaint(FlatCAMTool, Gerber):
         # grid3 = QtWidgets.QGridLayout()
         self.selectmethod_combo = RadioSet([
             {"label": _("Single"), "value": "single"},
-            {"label": _("All"), "value": "all"},
-            # {"label": "Rectangle", "value": "rectangle"}
+            {"label": _("Area"), "value": "area"},
+            {"label": _("All"), "value": "all"}
         ])
         grid3.addWidget(self.selectmethod_combo, 7, 1)
 
@@ -269,6 +269,9 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.units = ''
         self.paint_tools = {}
         self.tooluid = 0
+        self.first_click = False
+        self.cursor_pos = None
+
         # store here the default data for Geometry Data
         self.default_data = {}
         self.default_data.update({
@@ -353,6 +356,10 @@ class ToolPaint(FlatCAMTool, Gerber):
             self.addtool_btn.setDisabled(True)
             self.deltool_btn.setDisabled(True)
             self.tools_table.setContextMenuPolicy(Qt.NoContextMenu)
+        if self.selectmethod_combo.get_value() == 'area':
+            # disable rest-machining for single polygon painting
+            self.rest_cb.set_value(False)
+            self.rest_cb.setDisabled(True)
         else:
             self.rest_cb.setDisabled(False)
             self.addtool_entry.setDisabled(False)
@@ -433,7 +440,6 @@ class ToolPaint(FlatCAMTool, Gerber):
             self.tools_table.setContextMenuPolicy(Qt.NoContextMenu)
 
     def build_ui(self):
-
         try:
             # if connected, disconnect the signal from the slot on item_changed as it creates issues
             self.tools_table.itemChanged.disconnect()
@@ -787,7 +793,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                                 connect=connect,
                                 contour=contour)
 
-        if select_method == "single":
+        elif select_method == "single":
             self.app.inform.emit(_("[WARNING_NOTCL] Click inside the desired polygon."))
 
             # use the first tool in the tool table; get the diameter
@@ -815,6 +821,79 @@ class ToolPaint(FlatCAMTool, Gerber):
             self.app.plotcanvas.vis_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
             self.app.plotcanvas.vis_connect('mouse_press', doit)
 
+        elif select_method == "area":
+            self.app.inform.emit(_("[WARNING_NOTCL] Click the start point of the paint area."))
+
+            # use the first tool in the tool table; get the diameter
+            tooldia = float('%.4f' % float(self.tools_table.item(0, 1).text()))
+
+            # To be called after clicking on the plot.
+            def on_mouse_press(event):
+                # do paint single only for left mouse clicks
+                if event.button == 1:
+                    if not self.first_click:
+                        self.first_click = True
+                        self.app.inform.emit(_("[WARNING_NOTCL] Click the end point of the paint area."))
+
+                        self.cursor_pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
+                        if self.app.grid_status():
+                            self.cursor_pos = self.app.geo_editor.snap(self.cursor_pos[0], self.cursor_pos[1])
+                    else:
+                        self.app.inform.emit(_("Done."))
+                        self.first_click = False
+                        self.app.delete_selection_shape()
+
+                        curr_pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
+                        if self.app.grid_status():
+                            curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
+
+                        x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
+                        x1, y1 = curr_pos[0], curr_pos[1]
+                        pt1 = (x0, y0)
+                        pt2 = (x1, y0)
+                        pt3 = (x1, y1)
+                        pt4 = (x0, y1)
+                        sel_rect = Polygon([pt1, pt2, pt3, pt4])
+
+                        self.paint_poly_area(obj=self.paint_obj,
+                                             sel_obj= sel_rect,
+                                             outname=o_name,
+                                             overlap=overlap,
+                                             connect=connect,
+                                             contour=contour)
+
+                        self.app.plotcanvas.vis_disconnect('mouse_press', on_mouse_press)
+                        self.app.plotcanvas.vis_disconnect('mouse_move', on_mouse_move)
+
+                        self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot)
+                        self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot)
+                        self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
+
+            # called on mouse move
+            def on_mouse_move(event):
+                curr_pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
+                self.app.app_cursor.enabled = False
+
+                if self.app.grid_status():
+                    self.app.app_cursor.enabled = True
+                    # Update cursor
+                    curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
+                    self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
+                                                 symbol='++', edge_color='black', size=20)
+
+                if self.first_click:
+                    self.app.delete_selection_shape()
+                    self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
+                                                         coords=(curr_pos[0], curr_pos[1]),
+                                                         face_alpha=0.0)
+
+            self.app.plotcanvas.vis_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
+            self.app.plotcanvas.vis_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
+            self.app.plotcanvas.vis_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+
+            self.app.plotcanvas.vis_connect('mouse_press', on_mouse_press)
+            self.app.plotcanvas.vis_connect('mouse_move', on_mouse_move)
+
     def paint_poly(self, obj, inside_pt, tooldia, overlap, outname=None, connect=True, contour=True):
         """
         Paints a polygon selected by clicking on its interior.
@@ -1276,5 +1355,323 @@ class ToolPaint(FlatCAMTool, Gerber):
         # Background
         self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
 
+    def paint_poly_area(self, obj, sel_obj, overlap, outname=None, connect=True, contour=True):
+        """
+        Paints all polygons in this object that are within the sel_obj object
+
+        :param obj: painted object
+        :param sel_obj: paint only what is inside this object bounds
+        :param overlap:
+        :param outname:
+        :param connect: Connect lines to avoid tool lifts.
+        :param contour: Paint around the edges.
+        :return:
+        """
+        paint_method = self.paintmethod_combo.get_value()
+
+        try:
+            paint_margin = float(self.paintmargin_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
+                                     "use a number."))
+                return
+
+        proc = self.app.proc_container.new(_("Painting polygon..."))
+        name = outname if outname else self.obj_name + "_paint"
+        over = overlap
+        conn = connect
+        cont = contour
+
+        def recurse(geometry, reset=True):
+            """
+            Creates a list of non-iterable linear geometry objects.
+            Results are placed in self.flat_geometry
+
+            :param geometry: Shapely type or list or list of list of such.
+            :param reset: Clears the contents of self.flat_geometry.
+            """
+
+            if geometry is None:
+                return
+
+            if reset:
+                self.flat_geometry = []
+
+            # ## If iterable, expand recursively.
+            try:
+                for geo in geometry:
+                    if geo is not None:
+                        recurse(geometry=geo, reset=False)
+
+            # ## Not iterable, do the actual indexing and add.
+            except TypeError:
+                if isinstance(geometry, LinearRing):
+                    g = Polygon(geometry)
+                    self.flat_geometry.append(g)
+                else:
+                    self.flat_geometry.append(geometry)
+
+            return self.flat_geometry
+
+        # Initializes the new geometry object
+        def gen_paintarea(geo_obj, app_obj):
+            assert isinstance(geo_obj, FlatCAMGeometry), \
+                "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
+            tool_dia = None
+
+            sorted_tools = []
+            for row in range(self.tools_table.rowCount()):
+                sorted_tools.append(float(self.tools_table.item(row, 1).text()))
+            sorted_tools.sort(reverse=True)
+
+            geo_to_paint = []
+            for poly in obj.solid_geometry:
+                new_pol = poly.intersection(sel_obj)
+                geo_to_paint.append(new_pol)
+
+            try:
+                a, b, c, d = self.paint_bounds(geo_to_paint)
+                geo_obj.options['xmin'] = a
+                geo_obj.options['ymin'] = b
+                geo_obj.options['xmax'] = c
+                geo_obj.options['ymax'] = d
+            except Exception as e:
+                log.debug("ToolPaint.paint_poly.gen_paintarea() bounds error --> %s" % str(e))
+                return
+
+            total_geometry = []
+            current_uid = int(1)
+            geo_obj.solid_geometry = []
+            for tool_dia in sorted_tools:
+                # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry
+                for k, v in self.paint_tools.items():
+                    if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia):
+                        current_uid = int(k)
+                        break
+
+                for geo in recurse(geo_to_paint):
+                    try:
+                        # Polygons are the only really paintable geometries, lines in theory have no area to be painted
+                        if not isinstance(geo, Polygon):
+                            continue
+                        poly_buf = geo.buffer(-paint_margin)
+
+                        if paint_method == "seed":
+                            # Type(cp) == FlatCAMRTreeStorage | None
+                            cp = self.clear_polygon2(poly_buf,
+                                                     tooldia=tool_dia,
+                                                     steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                                     overlap=over,
+                                                     contour=cont,
+                                                     connect=conn)
+
+                        elif paint_method == "lines":
+                            # Type(cp) == FlatCAMRTreeStorage | None
+                            cp = self.clear_polygon3(poly_buf,
+                                                     tooldia=tool_dia,
+                                                     steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                                     overlap=over,
+                                                     contour=cont,
+                                                     connect=conn)
+
+                        else:
+                            # Type(cp) == FlatCAMRTreeStorage | None
+                            cp = self.clear_polygon(poly_buf,
+                                                    tooldia=tool_dia,
+                                                    steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                                    overlap=over,
+                                                    contour=cont,
+                                                    connect=conn)
+
+                        if cp is not None:
+                            total_geometry += list(cp.get_objects())
+                    except Exception as e:
+                        log.debug("Could not Paint the polygons. %s" % str(e))
+                        self.app.inform.emit(
+                            _("[ERROR] Could not do Paint All. Try a different combination of parameters. "
+                              "Or a different Method of paint\n%s") % str(e))
+                        return
+
+                # add the solid_geometry to the current too in self.paint_tools dictionary and then reset the
+                # temporary list that stored that solid_geometry
+                self.paint_tools[current_uid]['solid_geometry'] = deepcopy(total_geometry)
+
+                self.paint_tools[current_uid]['data']['name'] = name
+                total_geometry[:] = []
+
+            geo_obj.options["cnctooldia"] = str(tool_dia)
+            # this turn on the FlatCAMCNCJob plot for multiple tools
+            geo_obj.multigeo = True
+            geo_obj.multitool = True
+            geo_obj.tools.clear()
+            geo_obj.tools = dict(self.paint_tools)
+
+            # test if at least one tool has solid_geometry. If no tool has solid_geometry we raise an Exception
+            has_solid_geo = 0
+            for tooluid in geo_obj.tools:
+                if geo_obj.tools[tooluid]['solid_geometry']:
+                    has_solid_geo += 1
+            if has_solid_geo == 0:
+                self.app.inform.emit(_("[ERROR] There is no Painting Geometry in the file.\n"
+                                       "Usually it means that the tool diameter is too big for the painted geometry.\n"
+                                       "Change the painting parameters and try again."))
+                return
+
+            # Experimental...
+            # print("Indexing...", end=' ')
+            # geo_obj.make_index()
+
+            self.app.inform.emit(_("[success] Paint All Done."))
+
+        # Initializes the new geometry object
+        def gen_paintarea_rest_machining(geo_obj, app_obj):
+            assert isinstance(geo_obj, FlatCAMGeometry), \
+                "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
+
+            tool_dia = None
+            sorted_tools = []
+            for row in range(self.tools_table.rowCount()):
+                sorted_tools.append(float(self.tools_table.item(row, 1).text()))
+            sorted_tools.sort(reverse=True)
+
+            cleared_geo = []
+            current_uid = int(1)
+            geo_obj.solid_geometry = []
+
+            try:
+                a, b, c, d = obj.bounds()
+                geo_obj.options['xmin'] = a
+                geo_obj.options['ymin'] = b
+                geo_obj.options['xmax'] = c
+                geo_obj.options['ymax'] = d
+            except Exception as e:
+                log.debug("ToolPaint.paint_poly.gen_paintarea() bounds error --> %s" % str(e))
+                return
+
+            for tool_dia in sorted_tools:
+                for geo in recurse(obj.solid_geometry):
+                    try:
+                        geo = Polygon(geo) if not isinstance(geo, Polygon) else geo
+                        poly_buf = geo.buffer(-paint_margin)
+                        cp = None
+
+                        if paint_method == "standard":
+                            # Type(cp) == FlatCAMRTreeStorage | None
+                            cp = self.clear_polygon(poly_buf, tooldia=tool_dia,
+                                                    steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                                    overlap=over, contour=cont, connect=conn)
+
+                        elif paint_method == "seed":
+                            # Type(cp) == FlatCAMRTreeStorage | None
+                            cp = self.clear_polygon2(poly_buf, tooldia=tool_dia,
+                                                     steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                                     overlap=over, contour=cont, connect=conn)
+
+                        elif paint_method == "lines":
+                            # Type(cp) == FlatCAMRTreeStorage | None
+                            cp = self.clear_polygon3(poly_buf, tooldia=tool_dia,
+                                                     steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                                     overlap=over, contour=cont, connect=conn)
+
+                        if cp is not None:
+                            cleared_geo += list(cp.get_objects())
+
+                    except Exception as e:
+                        log.debug("Could not Paint the polygons. %s" % str(e))
+                        self.app.inform.emit(
+                            _("[ERROR] Could not do Paint All. Try a different combination of parameters. "
+                              "Or a different Method of paint\n%s") % str(e))
+                        return
+
+                # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry
+                for k, v in self.paint_tools.items():
+                    if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia):
+                        current_uid = int(k)
+                        break
+
+                # add the solid_geometry to the current too in self.paint_tools dictionary and then reset the
+                # temporary list that stored that solid_geometry
+                self.paint_tools[current_uid]['solid_geometry'] = deepcopy(cleared_geo)
+
+                self.paint_tools[current_uid]['data']['name'] = name
+                cleared_geo[:] = []
+
+            geo_obj.options["cnctooldia"] = str(tool_dia)
+            # this turn on the FlatCAMCNCJob plot for multiple tools
+            geo_obj.multigeo = True
+            geo_obj.multitool = True
+            geo_obj.tools.clear()
+            geo_obj.tools = dict(self.paint_tools)
+
+            # test if at least one tool has solid_geometry. If no tool has solid_geometry we raise an Exception
+            has_solid_geo = 0
+            for tooluid in geo_obj.tools:
+                if geo_obj.tools[tooluid]['solid_geometry']:
+                    has_solid_geo += 1
+            if has_solid_geo == 0:
+                self.app.inform.emit(_("[ERROR_NOTCL] There is no Painting Geometry in the file.\n"
+                                       "Usually it means that the tool diameter is too big for the painted geometry.\n"
+                                       "Change the painting parameters and try again."))
+                return
+
+            # Experimental...
+            # print("Indexing...", end=' ')
+            # geo_obj.make_index()
+
+            self.app.inform.emit(_("[success] Paint All with Rest-Machining done."))
+
+        def job_thread(app_obj):
+            try:
+                if self.rest_cb.isChecked():
+                    app_obj.new_object("geometry", name, gen_paintarea_rest_machining)
+                else:
+                    app_obj.new_object("geometry", name, gen_paintarea)
+            except Exception as e:
+                proc.done()
+                traceback.print_stack()
+                return
+            proc.done()
+            # focus on Selected Tab
+            self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+
+        self.app.inform.emit(_("Polygon Paint started ..."))
+
+        # Promise object with the new name
+        self.app.collection.promise(name)
+
+        # Background
+        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+
+    @staticmethod
+    def paint_bounds(geometry):
+        def bounds_rec(o):
+            if type(o) is list:
+                minx = Inf
+                miny = Inf
+                maxx = -Inf
+                maxy = -Inf
+
+                for k in o:
+                    try:
+                        minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                    except Exception as e:
+                        log.debug("ToolPaint.bounds() --> %s" % str(e))
+                        return
+
+                    minx = min(minx, minx_)
+                    miny = min(miny, miny_)
+                    maxx = max(maxx, maxx_)
+                    maxy = max(maxy, maxy_)
+                return minx, miny, maxx, maxy
+            else:
+                # it's a Shapely object, return it's bounds
+                return o.bounds
+
+        return bounds_rec(geometry)
+
     def reset_fields(self):
         self.object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))

+ 4 - 0
flatcamTools/ToolSolderPaste.py

@@ -1396,6 +1396,10 @@ class SolderPaste(FlatCAMTool):
             except FileNotFoundError:
                 self.app.inform.emit(_("[WARNING_NOTCL] No such file or directory"))
                 return
+            except PermissionError:
+                self.app.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
+                                       "Most likely another app is holding the file open and not accessible."))
+                return 'fail'
 
         if self.app.defaults["global_open_style"] is False:
             self.app.file_opened.emit("gcode", filename)

+ 25 - 25
flatcamTools/ToolTransform.py

@@ -44,16 +44,16 @@ class ToolTransform(FlatCAMTool):
         self.transform_lay.addWidget(title_label)
 
         self.empty_label = QtWidgets.QLabel("")
-        self.empty_label.setFixedWidth(70)
+        self.empty_label.setMinimumWidth(70)
 
         self.empty_label1 = QtWidgets.QLabel("")
-        self.empty_label1.setFixedWidth(70)
+        self.empty_label1.setMinimumWidth(70)
         self.empty_label2 = QtWidgets.QLabel("")
-        self.empty_label2.setFixedWidth(70)
+        self.empty_label2.setMinimumWidth(70)
         self.empty_label3 = QtWidgets.QLabel("")
-        self.empty_label3.setFixedWidth(70)
+        self.empty_label3.setMinimumWidth(70)
         self.empty_label4 = QtWidgets.QLabel("")
-        self.empty_label4.setFixedWidth(70)
+        self.empty_label4.setMinimumWidth(70)
         self.transform_lay.addWidget(self.empty_label)
 
         # ## Rotate Title
@@ -72,7 +72,7 @@ class ToolTransform(FlatCAMTool):
               "Positive numbers for CW motion.\n"
               "Negative numbers for CCW motion.")
         )
-        self.rotate_label.setFixedWidth(70)
+        self.rotate_label.setMinimumWidth(70)
 
         self.rotate_entry = FCEntry()
         # self.rotate_entry.setFixedWidth(70)
@@ -85,7 +85,7 @@ class ToolTransform(FlatCAMTool):
               "The point of reference is the middle of\n"
               "the bounding box for all selected objects.")
         )
-        self.rotate_button.setFixedWidth(90)
+        self.rotate_button.setMinimumWidth(90)
 
         form_child.addWidget(self.rotate_entry)
         form_child.addWidget(self.rotate_button)
@@ -109,7 +109,7 @@ class ToolTransform(FlatCAMTool):
             _("Angle for Skew action, in degrees.\n"
               "Float number between -360 and 359.")
         )
-        self.skewx_label.setFixedWidth(70)
+        self.skewx_label.setMinimumWidth(70)
         self.skewx_entry = FCEntry()
         self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.skewx_entry.setFixedWidth(70)
@@ -120,14 +120,14 @@ class ToolTransform(FlatCAMTool):
             _("Skew/shear the selected object(s).\n"
               "The point of reference is the middle of\n"
               "the bounding box for all selected objects."))
-        self.skewx_button.setFixedWidth(90)
+        self.skewx_button.setMinimumWidth(90)
 
         self.skewy_label = QtWidgets.QLabel(_("Angle Y:"))
         self.skewy_label.setToolTip(
             _("Angle for Skew action, in degrees.\n"
               "Float number between -360 and 359.")
         )
-        self.skewy_label.setFixedWidth(70)
+        self.skewy_label.setMinimumWidth(70)
         self.skewy_entry = FCEntry()
         self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.skewy_entry.setFixedWidth(70)
@@ -138,7 +138,7 @@ class ToolTransform(FlatCAMTool):
             _("Skew/shear the selected object(s).\n"
               "The point of reference is the middle of\n"
               "the bounding box for all selected objects."))
-        self.skewy_button.setFixedWidth(90)
+        self.skewy_button.setMinimumWidth(90)
 
         form1_child_1.addWidget(self.skewx_entry)
         form1_child_1.addWidget(self.skewx_button)
@@ -165,7 +165,7 @@ class ToolTransform(FlatCAMTool):
         self.scalex_label.setToolTip(
             _("Factor for Scale action over X axis.")
         )
-        self.scalex_label.setFixedWidth(70)
+        self.scalex_label.setMinimumWidth(70)
         self.scalex_entry = FCEntry()
         self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.scalex_entry.setFixedWidth(70)
@@ -176,13 +176,13 @@ class ToolTransform(FlatCAMTool):
             _("Scale the selected object(s).\n"
               "The point of reference depends on \n"
               "the Scale reference checkbox state."))
-        self.scalex_button.setFixedWidth(90)
+        self.scalex_button.setMinimumWidth(90)
 
         self.scaley_label = QtWidgets.QLabel(_("Factor Y:"))
         self.scaley_label.setToolTip(
             _("Factor for Scale action over Y axis.")
         )
-        self.scaley_label.setFixedWidth(70)
+        self.scaley_label.setMinimumWidth(70)
         self.scaley_entry = FCEntry()
         self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.scaley_entry.setFixedWidth(70)
@@ -193,7 +193,7 @@ class ToolTransform(FlatCAMTool):
             _("Scale the selected object(s).\n"
               "The point of reference depends on \n"
               "the Scale reference checkbox state."))
-        self.scaley_button.setFixedWidth(90)
+        self.scaley_button.setMinimumWidth(90)
 
         self.scale_link_cb = FCCheckBox()
         self.scale_link_cb.set_value(True)
@@ -201,7 +201,7 @@ class ToolTransform(FlatCAMTool):
         self.scale_link_cb.setToolTip(
             _("Scale the selected object(s)\n"
               "using the Scale Factor X for both axis."))
-        self.scale_link_cb.setFixedWidth(70)
+        self.scale_link_cb.setMinimumWidth(70)
 
         self.scale_zero_ref_cb = FCCheckBox()
         self.scale_zero_ref_cb.set_value(True)
@@ -239,7 +239,7 @@ class ToolTransform(FlatCAMTool):
         self.offx_label.setToolTip(
             _("Value for Offset action on X axis.")
         )
-        self.offx_label.setFixedWidth(70)
+        self.offx_label.setMinimumWidth(70)
         self.offx_entry = FCEntry()
         self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.offx_entry.setFixedWidth(70)
@@ -250,13 +250,13 @@ class ToolTransform(FlatCAMTool):
             _("Offset the selected object(s).\n"
               "The point of reference is the middle of\n"
               "the bounding box for all selected objects.\n"))
-        self.offx_button.setFixedWidth(90)
+        self.offx_button.setMinimumWidth(90)
 
         self.offy_label = QtWidgets.QLabel(_("Value Y:"))
         self.offy_label.setToolTip(
             _("Value for Offset action on Y axis.")
         )
-        self.offy_label.setFixedWidth(70)
+        self.offy_label.setMinimumWidth(70)
         self.offy_entry = FCEntry()
         self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.offy_entry.setFixedWidth(70)
@@ -267,7 +267,7 @@ class ToolTransform(FlatCAMTool):
             _("Offset the selected object(s).\n"
               "The point of reference is the middle of\n"
               "the bounding box for all selected objects.\n"))
-        self.offy_button.setFixedWidth(90)
+        self.offy_button.setMinimumWidth(90)
 
         form3_child_1.addWidget(self.offx_entry)
         form3_child_1.addWidget(self.offx_button)
@@ -297,7 +297,7 @@ class ToolTransform(FlatCAMTool):
             _("Flip the selected object(s) over the X axis.\n"
               "Does not create a new object.\n ")
         )
-        self.flipx_button.setFixedWidth(100)
+        self.flipx_button.setMinimumWidth(100)
 
         self.flipy_button = FCButton()
         self.flipy_button.set_value(_("Flip on Y"))
@@ -305,7 +305,7 @@ class ToolTransform(FlatCAMTool):
             _("Flip the selected object(s) over the X axis.\n"
               "Does not create a new object.\n ")
         )
-        self.flipy_button.setFixedWidth(90)
+        self.flipy_button.setMinimumWidth(90)
 
         self.flip_ref_cb = FCCheckBox()
         self.flip_ref_cb.set_value(True)
@@ -320,7 +320,7 @@ class ToolTransform(FlatCAMTool):
               "Then click Add button to insert coordinates.\n"
               "Or enter the coords in format (x, y) in the\n"
               "Point Entry field and click Flip on X(Y)"))
-        self.flip_ref_cb.setFixedWidth(70)
+        self.flip_ref_cb.setMinimumWidth(70)
 
         self.flip_ref_label = QtWidgets.QLabel(_("Point:"))
         self.flip_ref_label.setToolTip(
@@ -328,7 +328,7 @@ class ToolTransform(FlatCAMTool):
               "The 'x' in (x, y) will be used when using Flip on X and\n"
               "the 'y' in (x, y) will be used when using Flip on Y and")
         )
-        self.flip_ref_label.setFixedWidth(70)
+        self.flip_ref_label.setMinimumWidth(70)
         self.flip_ref_entry = EvalEntry2("(0, 0)")
         self.flip_ref_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.flip_ref_entry.setFixedWidth(70)
@@ -339,7 +339,7 @@ class ToolTransform(FlatCAMTool):
             _("The point coordinates can be captured by\n"
               "left click on canvas together with pressing\n"
               "SHIFT key. Then click Add button to insert."))
-        self.flip_ref_button.setFixedWidth(90)
+        self.flip_ref_button.setMinimumWidth(90)
 
         form4_child_hlay.addStretch()
         form4_child_hlay.addWidget(self.flipx_button)

BIN
locale/de/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 196 - 186
locale/de/LC_MESSAGES/strings.po


BIN
locale/en/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 196 - 186
locale/en/LC_MESSAGES/strings.po


BIN
locale/es/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 197 - 186
locale/es/LC_MESSAGES/strings.po


BIN
locale/pt_BR/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 197 - 187
locale/pt_BR/LC_MESSAGES/strings.po


BIN
locale/ro/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 196 - 186
locale/ro/LC_MESSAGES/strings.po


BIN
locale/ru/LC_MESSAGES/strings.mo


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 195 - 185
locale/ru/LC_MESSAGES/strings.po


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 220 - 211
locale_template/strings.pot


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio