Просмотр исходного кода

Merged in test_beta8.915 (pull request #144)

Test beta8.915
Marius Stanciu 6 лет назад
Родитель
Сommit
e7a32c5c90

+ 224 - 110
FlatCAMApp.py

@@ -94,8 +94,8 @@ class App(QtCore.QObject):
     log.addHandler(handler)
 
     # Version
-    version = 8.914
-    version_date = "2019/04/23"
+    version = 8.915
+    version_date = "2019/05/1"
     beta = True
 
     # current date now
@@ -189,6 +189,8 @@ class App(QtCore.QObject):
 
         App.log.info("FlatCAM Starting...")
 
+        self.main_thread = QtWidgets.QApplication.instance().thread()
+
         ###################
         ### OS-specific ###
         ###################
@@ -340,10 +342,13 @@ class App(QtCore.QObject):
             "global_draw_color": self.ui.general_defaults_form.general_gui_group.draw_color_entry,
             "global_sel_draw_color": self.ui.general_defaults_form.general_gui_group.sel_draw_color_entry,
 
+            "global_proj_item_color": self.ui.general_defaults_form.general_gui_group.proj_color_entry,
+            "global_proj_item_dis_color": self.ui.general_defaults_form.general_gui_group.proj_color_dis_entry,
+
             # General GUI Settings
             "global_layout": self.ui.general_defaults_form.general_gui_set_group.layout_combo,
             "global_hover": self.ui.general_defaults_form.general_gui_set_group.hover_cb,
-
+            "global_selection_shape": self.ui.general_defaults_form.general_gui_set_group.selection_cb,
             # Gerber General
             "gerber_plot": self.ui.gerber_defaults_form.gerber_gen_group.plot_cb,
             "gerber_solid": self.ui.gerber_defaults_form.gerber_gen_group.solid_cb,
@@ -612,6 +617,8 @@ class App(QtCore.QObject):
             "global_alt_sel_line": '#006E20BF',
             "global_draw_color": '#FF0000',
             "global_sel_draw_color": '#0000FF',
+            "global_proj_item_color": '#000000',
+            "global_proj_item_dis_color": '#b7b7cb',
 
             "global_toolbar_view": 511,
 
@@ -647,6 +654,7 @@ class App(QtCore.QObject):
 
             # General GUI Settings
             "global_hover": True,
+            "global_selection_shape": True,
             "global_layout": "compact",
             # Gerber General
             "gerber_plot": True,
@@ -1162,7 +1170,8 @@ class App(QtCore.QObject):
             "background-color:%s" % str(self.defaults['global_sel_line'])[:7])
 
         # Init Right-Left Selection colors
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_entry.set_value(self.defaults['global_alt_sel_fill'])
+        self.ui.general_defaults_form.general_gui_group.alt_sf_color_entry.set_value(
+            self.defaults['global_alt_sel_fill'])
         self.ui.general_defaults_form.general_gui_group.alt_sf_color_button.setStyleSheet(
             "background-color:%s" % str(self.defaults['global_alt_sel_fill'])[:7])
         self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_spinner.set_value(
@@ -1170,18 +1179,33 @@ class App(QtCore.QObject):
         self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_slider.setValue(
             int(self.defaults['global_sel_fill'][7:9], 16))
 
-        self.ui.general_defaults_form.general_gui_group.alt_sl_color_entry.set_value(self.defaults['global_alt_sel_line'])
+        self.ui.general_defaults_form.general_gui_group.alt_sl_color_entry.set_value(
+            self.defaults['global_alt_sel_line'])
         self.ui.general_defaults_form.general_gui_group.alt_sl_color_button.setStyleSheet(
             "background-color:%s" % str(self.defaults['global_alt_sel_line'])[:7])
 
         # Init Draw color and Selection Draw Color
-        self.ui.general_defaults_form.general_gui_group.draw_color_entry.set_value(self.defaults['global_draw_color'])
+        self.ui.general_defaults_form.general_gui_group.draw_color_entry.set_value(
+            self.defaults['global_draw_color'])
         self.ui.general_defaults_form.general_gui_group.draw_color_button.setStyleSheet(
             "background-color:%s" % str(self.defaults['global_draw_color'])[:7])
 
-        self.ui.general_defaults_form.general_gui_group.sel_draw_color_entry.set_value(self.defaults['global_sel_draw_color'])
+        self.ui.general_defaults_form.general_gui_group.sel_draw_color_entry.set_value(
+            self.defaults['global_sel_draw_color'])
         self.ui.general_defaults_form.general_gui_group.sel_draw_color_button.setStyleSheet(
             "background-color:%s" % str(self.defaults['global_sel_draw_color'])[:7])
+
+        # Init Project Items color
+        self.ui.general_defaults_form.general_gui_group.proj_color_entry.set_value(
+            self.defaults['global_proj_item_color'])
+        self.ui.general_defaults_form.general_gui_group.proj_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_proj_item_color'])[:7])
+
+        self.ui.general_defaults_form.general_gui_group.proj_color_dis_entry.set_value(
+            self.defaults['global_proj_item_dis_color'])
+        self.ui.general_defaults_form.general_gui_group.proj_color_dis_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_proj_item_dis_color'])[:7])
+
         #### End of Data ####
 
         #### Plot Area ####
@@ -1362,29 +1386,10 @@ class App(QtCore.QObject):
         self.ui.popmenu_disable.triggered.connect(lambda: self.disable_plots(self.collection.get_selected()))
 
         self.ui.popmenu_new_geo.triggered.connect(self.new_geometry_object)
+        self.ui.popmenu_new_grb.triggered.connect(self.new_gerber_object)
         self.ui.popmenu_new_exc.triggered.connect(self.new_excellon_object)
         self.ui.popmenu_new_prj.triggered.connect(self.on_file_new)
 
-        # Geometry Editor
-        self.ui.draw_line.triggered.connect(self.geo_editor.draw_tool_path)
-        self.ui.draw_rect.triggered.connect(self.geo_editor.draw_tool_rectangle)
-        self.ui.draw_cut.triggered.connect(self.geo_editor.cutpath)
-        self.ui.draw_move.triggered.connect(self.geo_editor.on_move)
-
-        # Gerber Editor
-        self.ui.grb_draw_pad.triggered.connect(self.grb_editor.on_pad_add)
-        self.ui.grb_draw_pad_array.triggered.connect(self.grb_editor.on_pad_add_array)
-        self.ui.grb_draw_track.triggered.connect(self.grb_editor.on_track_add)
-        self.ui.grb_draw_region.triggered.connect(self.grb_editor.on_region_add)
-        self.ui.grb_copy.triggered.connect(self.grb_editor.on_copy_button)
-        self.ui.grb_delete.triggered.connect(self.grb_editor.on_delete_btn)
-        self.ui.grb_move.triggered.connect(self.grb_editor.on_move_button)
-
-        # Excellon Editor
-        self.ui.drill.triggered.connect(self.exc_editor.exc_add_drill)
-        self.ui.drill_array.triggered.connect(self.exc_editor.exc_add_drill_array)
-        self.ui.drill_copy.triggered.connect(self.exc_editor.exc_copy_drills)
-
         self.ui.zoomfit.triggered.connect(self.on_zoom_fit)
         self.ui.clearplot.triggered.connect(self.clear_plots)
         self.ui.replot.triggered.connect(self.plot_all)
@@ -1418,34 +1423,64 @@ class App(QtCore.QObject):
         ###############################
 
         # Setting plot colors signals
-        self.ui.general_defaults_form.general_gui_group.pf_color_entry.editingFinished.connect(self.on_pf_color_entry)
-        self.ui.general_defaults_form.general_gui_group.pf_color_button.clicked.connect(self.on_pf_color_button)
-        self.ui.general_defaults_form.general_gui_group.pf_color_alpha_spinner.valueChanged.connect(self.on_pf_color_spinner)
-        self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.valueChanged.connect(self.on_pf_color_slider)
-        self.ui.general_defaults_form.general_gui_group.pl_color_entry.editingFinished.connect(self.on_pl_color_entry)
-        self.ui.general_defaults_form.general_gui_group.pl_color_button.clicked.connect(self.on_pl_color_button)
+        self.ui.general_defaults_form.general_gui_group.pf_color_entry.editingFinished.connect(
+            self.on_pf_color_entry)
+        self.ui.general_defaults_form.general_gui_group.pf_color_button.clicked.connect(
+            self.on_pf_color_button)
+        self.ui.general_defaults_form.general_gui_group.pf_color_alpha_spinner.valueChanged.connect(
+            self.on_pf_color_spinner)
+        self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.valueChanged.connect(
+            self.on_pf_color_slider)
+        self.ui.general_defaults_form.general_gui_group.pl_color_entry.editingFinished.connect(
+            self.on_pl_color_entry)
+        self.ui.general_defaults_form.general_gui_group.pl_color_button.clicked.connect(
+            self.on_pl_color_button)
         # Setting selection (left - right) colors signals
-        self.ui.general_defaults_form.general_gui_group.sf_color_entry.editingFinished.connect(self.on_sf_color_entry)
-        self.ui.general_defaults_form.general_gui_group.sf_color_button.clicked.connect(self.on_sf_color_button)
-        self.ui.general_defaults_form.general_gui_group.sf_color_alpha_spinner.valueChanged.connect(self.on_sf_color_spinner)
-        self.ui.general_defaults_form.general_gui_group.sf_color_alpha_slider.valueChanged.connect(self.on_sf_color_slider)
-        self.ui.general_defaults_form.general_gui_group.sl_color_entry.editingFinished.connect(self.on_sl_color_entry)
-        self.ui.general_defaults_form.general_gui_group.sl_color_button.clicked.connect(self.on_sl_color_button)
+        self.ui.general_defaults_form.general_gui_group.sf_color_entry.editingFinished.connect(
+            self.on_sf_color_entry)
+        self.ui.general_defaults_form.general_gui_group.sf_color_button.clicked.connect(
+            self.on_sf_color_button)
+        self.ui.general_defaults_form.general_gui_group.sf_color_alpha_spinner.valueChanged.connect(
+            self.on_sf_color_spinner)
+        self.ui.general_defaults_form.general_gui_group.sf_color_alpha_slider.valueChanged.connect(
+            self.on_sf_color_slider)
+        self.ui.general_defaults_form.general_gui_group.sl_color_entry.editingFinished.connect(
+            self.on_sl_color_entry)
+        self.ui.general_defaults_form.general_gui_group.sl_color_button.clicked.connect(
+            self.on_sl_color_button)
         # Setting selection (right - left) colors signals
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_entry.editingFinished.connect(self.on_alt_sf_color_entry)
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_button.clicked.connect(self.on_alt_sf_color_button)
+        self.ui.general_defaults_form.general_gui_group.alt_sf_color_entry.editingFinished.connect(
+            self.on_alt_sf_color_entry)
+        self.ui.general_defaults_form.general_gui_group.alt_sf_color_button.clicked.connect(
+            self.on_alt_sf_color_button)
         self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_spinner.valueChanged.connect(
             self.on_alt_sf_color_spinner)
         self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_slider.valueChanged.connect(
             self.on_alt_sf_color_slider)
-        self.ui.general_defaults_form.general_gui_group.alt_sl_color_entry.editingFinished.connect(self.on_alt_sl_color_entry)
-        self.ui.general_defaults_form.general_gui_group.alt_sl_color_button.clicked.connect(self.on_alt_sl_color_button)
+        self.ui.general_defaults_form.general_gui_group.alt_sl_color_entry.editingFinished.connect(
+            self.on_alt_sl_color_entry)
+        self.ui.general_defaults_form.general_gui_group.alt_sl_color_button.clicked.connect(
+            self.on_alt_sl_color_button)
         # Setting Editor Draw colors signals
-        self.ui.general_defaults_form.general_gui_group.draw_color_entry.editingFinished.connect(self.on_draw_color_entry)
-        self.ui.general_defaults_form.general_gui_group.draw_color_button.clicked.connect(self.on_draw_color_button)
-
-        self.ui.general_defaults_form.general_gui_group.sel_draw_color_entry.editingFinished.connect(self.on_sel_draw_color_entry)
-        self.ui.general_defaults_form.general_gui_group.sel_draw_color_button.clicked.connect(self.on_sel_draw_color_button)
+        self.ui.general_defaults_form.general_gui_group.draw_color_entry.editingFinished.connect(
+            self.on_draw_color_entry)
+        self.ui.general_defaults_form.general_gui_group.draw_color_button.clicked.connect(
+            self.on_draw_color_button)
+
+        self.ui.general_defaults_form.general_gui_group.sel_draw_color_entry.editingFinished.connect(
+            self.on_sel_draw_color_entry)
+        self.ui.general_defaults_form.general_gui_group.sel_draw_color_button.clicked.connect(
+            self.on_sel_draw_color_button)
+
+        self.ui.general_defaults_form.general_gui_group.proj_color_entry.editingFinished.connect(
+            self.on_proj_color_entry)
+        self.ui.general_defaults_form.general_gui_group.proj_color_button.clicked.connect(
+            self.on_proj_color_button)
+
+        self.ui.general_defaults_form.general_gui_group.proj_color_dis_entry.editingFinished.connect(
+            self.on_proj_color_dis_entry)
+        self.ui.general_defaults_form.general_gui_group.proj_color_dis_button.clicked.connect(
+            self.on_proj_color_dis_button)
 
         self.ui.general_defaults_form.general_gui_group.wk_cb.currentIndexChanged.connect(self.on_workspace_modified)
         self.ui.general_defaults_form.general_gui_group.workspace_cb.stateChanged.connect(self.on_workspace)
@@ -1772,7 +1807,7 @@ class App(QtCore.QObject):
             self.thr2 = QtCore.QThread()
             self.worker_task.emit({'fcn': self.version_check,
                                    'params': []})
-            self.thr2.start()
+            self.thr2.start(QtCore.QThread.LowPriority)
 
 
         ####################################
@@ -1790,8 +1825,6 @@ class App(QtCore.QObject):
         # decide if we have a double click or single click
         self.doubleclick = False
 
-        # variable to store if there was motion before right mouse button click (panning)
-        self.panning_action = False
         # variable to store if a command is active (then the var is not None) and which one it is
         self.command_active = None
         # variable to store the status of moving selection action
@@ -1977,7 +2010,15 @@ class App(QtCore.QObject):
         self.film_tool.install(icon=QtGui.QIcon('share/film16.png'))
 
         self.paste_tool = SolderPaste(self)
-        self.paste_tool.install(icon=QtGui.QIcon('share/solderpastebis32.png'), separator=True)
+        self.paste_tool.install(icon=QtGui.QIcon('share/solderpastebis32.png'))
+
+        self.calculator_tool = ToolCalculator(self)
+        self.calculator_tool.install(icon=QtGui.QIcon('share/calculator24.png'))
+
+
+        self.sub_tool = ToolSub(self)
+        self.sub_tool.install(icon=QtGui.QIcon('share/sub32.png'), pos=self.ui.menuedit_convert,
+                              before=self.ui.menuedit_convert_sg2mg)
 
         self.move_tool = ToolMove(self)
         self.move_tool.install(icon=QtGui.QIcon('share/move16.png'), pos=self.ui.menuedit,
@@ -1995,9 +2036,6 @@ class App(QtCore.QObject):
         self.paint_tool.install(icon=QtGui.QIcon('share/paint16.png'), pos=self.ui.menutool,
                                   before=self.measurement_tool.menuAction, separator=True)
 
-        self.calculator_tool = ToolCalculator(self)
-        self.calculator_tool.install(icon=QtGui.QIcon('share/calculator24.png'))
-
         self.transform_tool = ToolTransform(self)
         self.transform_tool.install(icon=QtGui.QIcon('share/transform.png'), pos=self.ui.menuoptions, separator=True)
 
@@ -2081,6 +2119,7 @@ class App(QtCore.QObject):
         self.ui.panelize_btn.triggered.connect(lambda: self.panelize_tool.run(toggle=True))
         self.ui.film_btn.triggered.connect(lambda: self.film_tool.run(toggle=True))
         self.ui.solder_btn.triggered.connect(lambda: self.paste_tool.run(toggle=True))
+        self.ui.sub_btn.triggered.connect(lambda: self.sub_tool.run(toggle=True))
 
         self.ui.calculators_btn.triggered.connect(lambda: self.calculator_tool.run(toggle=True))
         self.ui.transform_btn.triggered.connect(lambda: self.transform_tool.run(toggle=True))
@@ -2138,8 +2177,9 @@ class App(QtCore.QObject):
             # set call source to the Editor we go into
             self.call_source = 'grb_editor'
 
-        # make sure that we can't select another object while in Editor Mode:
-        self.collection.view.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
+        # # make sure that we can't select another object while in Editor Mode:
+        # self.collection.view.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
+        self.ui.project_frame.setDisabled(True)
 
         # delete any selection shape that might be active as they are not relevant in Editor
         self.delete_selection_shape()
@@ -2178,6 +2218,11 @@ class App(QtCore.QObject):
                 response = msgbox.clickedButton()
 
                 if response == bt_yes:
+                    # clean the Tools Tab
+                    self.ui.tool_scroll_area.takeWidget()
+                    self.ui.tool_scroll_area.setWidget(QtWidgets.QWidget())
+                    self.ui.notebook.setTabText(2, "Tool")
+
                     if isinstance(edited_obj, FlatCAMGeometry):
                         obj_type = "Geometry"
                         if cleanup is None:
@@ -2232,6 +2277,11 @@ class App(QtCore.QObject):
 
                     self.inform.emit(_("[selected] %s is updated, returning to App...") % obj_type)
                 elif response == bt_no:
+                    # clean the Tools Tab
+                    self.ui.tool_scroll_area.takeWidget()
+                    self.ui.tool_scroll_area.setWidget(QtWidgets.QWidget())
+                    self.ui.notebook.setTabText(2, "Tool")
+
                     if isinstance(edited_obj, FlatCAMGeometry):
                         self.geo_editor.deactivate()
                     elif isinstance(edited_obj, FlatCAMGerber):
@@ -2268,7 +2318,8 @@ class App(QtCore.QObject):
             self.ui.plot_tab_area.protectTab(0)
 
             # make sure that we reenable the selection on Project Tab after returning from Editor Mode:
-            self.collection.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+            # self.collection.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+            self.ui.project_frame.setDisabled(False)
 
     def get_last_folder(self):
         return self.defaults["global_last_folder"]
@@ -2773,7 +2824,7 @@ class App(QtCore.QObject):
 
     def new_object(self, kind, name, initialize, active=True, fit=True, plot=True, autoselected=True):
         """
-        Creates a new specalized FlatCAMObj and attaches it to the application,
+        Creates a new specialized FlatCAMObj and attaches it to the application,
         this is, updates the GUI accordingly, any other records and plots it.
         This method is thread-safe.
 
@@ -2883,7 +2934,7 @@ class App(QtCore.QObject):
         FlatCAMApp.App.log.debug("Moving new object back to main thread.")
 
         # Move the object to the main thread and let the app know that it is available.
-        obj.moveToThread(QtWidgets.QApplication.instance().thread())
+        obj.moveToThread(self.main_thread)
         self.object_created.emit(obj, obj_plot, obj_autoselected)
 
         return obj
@@ -2911,6 +2962,14 @@ class App(QtCore.QObject):
             grb_obj.follow = False
             grb_obj.apertures = {}
 
+            try:
+                grb_obj.options['xmin'] = 0
+                grb_obj.options['ymin'] = 0
+                grb_obj.options['xmax'] = 0
+                grb_obj.options['ymax'] = 0
+            except KeyError:
+                pass
+
         self.new_object('gerber', 'new_grb', initialize, plot=False)
 
     def on_object_created(self, obj, plot, autoselect):
@@ -4024,6 +4083,50 @@ class App(QtCore.QObject):
         self.ui.general_defaults_form.general_gui_group.sel_draw_color_entry.set_value(new_val_sel)
         self.defaults['global_sel_draw_color'] = new_val_sel
 
+    def on_proj_color_entry(self):
+        self.defaults['global_proj_item_color'] = self.ui.general_defaults_form.general_gui_group \
+                                                   .proj_color_entry.get_value()
+        self.ui.general_defaults_form.general_gui_group.proj_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_proj_item_color']))
+
+    def on_proj_color_button(self):
+        current_color = QtGui.QColor(self.defaults['global_proj_item_color'])
+
+        c_dialog = QtWidgets.QColorDialog()
+        proj_color = c_dialog.getColor(initial=current_color)
+
+        if proj_color.isValid() is False:
+            return
+
+        self.ui.general_defaults_form.general_gui_group.proj_color_button.setStyleSheet(
+            "background-color:%s" % str(proj_color.name()))
+
+        new_val_sel = str(proj_color.name())
+        self.ui.general_defaults_form.general_gui_group.proj_color_entry.set_value(new_val_sel)
+        self.defaults['global_proj_item_color'] = new_val_sel
+
+    def on_proj_color_dis_entry(self):
+        self.defaults['global_proj_item_dis_color'] = self.ui.general_defaults_form.general_gui_group \
+                                                   .proj_color_dis_entry.get_value()
+        self.ui.general_defaults_form.general_gui_group.proj_color_dis_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_proj_item_dis_color']))
+
+    def on_proj_color_dis_button(self):
+        current_color = QtGui.QColor(self.defaults['global_proj_item_dis_color'])
+
+        c_dialog = QtWidgets.QColorDialog()
+        proj_color = c_dialog.getColor(initial=current_color)
+
+        if proj_color.isValid() is False:
+            return
+
+        self.ui.general_defaults_form.general_gui_group.proj_color_dis_button.setStyleSheet(
+            "background-color:%s" % str(proj_color.name()))
+
+        new_val_sel = str(proj_color.name())
+        self.ui.general_defaults_form.general_gui_group.proj_color_dis_entry.set_value(new_val_sel)
+        self.defaults['global_proj_item_dis_color'] = new_val_sel
+
     def on_deselect_all(self):
         self.collection.set_all_inactive()
         self.delete_selection_shape()
@@ -4481,7 +4584,7 @@ class App(QtCore.QObject):
         """
         self.report_usage("on_jump_to()")
 
-        if custom_location is None:
+        if not custom_location:
             dia_box = Dialog_box(title=_("Jump to ..."),
                                  label=_("Enter the coordinates in format X,Y:"),
                                  icon=QtGui.QIcon('share/jump_to16.png'))
@@ -4627,9 +4730,14 @@ class App(QtCore.QObject):
             if obj.tools:
                 obj_init.tools = obj.tools
 
-        def initialize_excellon(obj, app):
-            objs = self.collection.get_selected()
-            FlatCAMGeometry.merge(objs, obj)
+        def initialize_excellon(obj_init, app):
+            # objs = self.collection.get_selected()
+            # FlatCAMGeometry.merge(objs, obj)
+            solid_geo = []
+            for tool in obj.tools:
+                for geo in obj.tools[tool]['solid_geometry']:
+                    solid_geo.append(geo)
+            obj_init.solid_geometry = deepcopy(solid_geo)
 
         for obj in self.collection.get_selected():
 
@@ -4680,7 +4788,8 @@ class App(QtCore.QObject):
             self.collection.set_active(name)
             curr_sel_obj = self.collection.get_by_name(name)
             # create the selection box around the selected object
-            self.draw_selection_shape(curr_sel_obj)
+            if self.defaults['global_selection_shape'] is True:
+                self.draw_selection_shape(curr_sel_obj)
 
     def on_preferences(self):
 
@@ -4915,6 +5024,7 @@ class App(QtCore.QObject):
         # self.plotcanvas.auto_adjust_axes()
         self.plotcanvas.vispy_canvas.update()           # TODO: Need update canvas?
         self.on_zoom_fit(None)
+        self.collection.update_view()
 
     # TODO: Rework toolbar 'clear', 'replot' functions
     def on_toolbar_replot(self):
@@ -4957,9 +5067,9 @@ class App(QtCore.QObject):
             action.triggered.connect(self.set_grid)
 
         self.ui.cmenu_gridmenu.addSeparator()
-        grid_add = self.ui.cmenu_gridmenu.addAction(QtGui.QIcon('share/plus32.png'), "Add")
+        grid_add = self.ui.cmenu_gridmenu.addAction(QtGui.QIcon('share/plus32.png'), _("Add"))
+        grid_delete = self.ui.cmenu_gridmenu.addAction(QtGui.QIcon('share/delete32.png'), _("Delete"))
         grid_add.triggered.connect(self.on_grid_add)
-        grid_delete = self.ui.cmenu_gridmenu.addAction(QtGui.QIcon('share/delete32.png'), "Delete")
         grid_delete.triggered.connect(self.on_grid_delete)
 
     def set_grid(self):
@@ -4970,8 +5080,8 @@ class App(QtCore.QObject):
         ## Current application units in lower Case
         units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
 
-        grid_add_popup = FCInputDialog(title="New Grid ...",
-                                       text='Enter a Grid VAlue:',
+        grid_add_popup = FCInputDialog(title=_("New Grid ..."),
+                                       text=_('Enter a Grid Value:'),
                                        min=0.0000, max=99.9999, decimals=4)
         grid_add_popup.setWindowIcon(QtGui.QIcon('share/plus32.png'))
 
@@ -5126,15 +5236,13 @@ class App(QtCore.QObject):
         self.plotcanvas.vispy_canvas.native.setFocus()
         self.pos_jump = event.pos
 
-        if origin_click is True:
-            pass
-        else:
+        self.ui.popMenu.mouse_is_panning = False
+
+        if origin_click != True:
             # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
-            if event.button == 2:
-                self.panning_action = True
+            if event.button == 2 and event.is_dragging == 1:
+                self.ui.popMenu.mouse_is_panning = True
                 return
-            else:
-                self.panning_action = False
 
         if self.rel_point1 is not None:
             try:  # May fail in case mouse not within axes
@@ -5222,12 +5330,12 @@ class App(QtCore.QObject):
         # canvas menu
         try:
             if event.button == 2:  # right click
-                if self.panning_action is True:
-                    self.panning_action = False
-                else:
+                if self.ui.popMenu.mouse_is_panning is False:
+
                     self.cursor = QtGui.QCursor()
                     self.populate_cmenu_grids()
                     self.ui.popMenu.popup(self.cursor.pos())
+
         except Exception as e:
             log.warning("Error: %s" % str(e))
             return
@@ -5294,12 +5402,14 @@ class App(QtCore.QObject):
                     if sel_type is True:
                         if poly_obj.within(poly_selection):
                             # create the selection box around the selected object
-                            self.draw_selection_shape(obj)
+                            if self.defaults['global_selection_shape'] is True:
+                                self.draw_selection_shape(obj)
                             self.collection.set_active(obj.options['name'])
                     else:
                         if poly_selection.intersects(poly_obj):
                             # create the selection box around the selected object
-                            self.draw_selection_shape(obj)
+                            if self.defaults['global_selection_shape'] is True:
+                                self.draw_selection_shape(obj)
                             self.collection.set_active(obj.options['name'])
             except:
                 # the Exception here will happen if we try to select on screen and we have an newly (and empty)
@@ -5349,7 +5459,8 @@ class App(QtCore.QObject):
                         self.collection.set_active(objects_under_the_click_list[0])
                         # create the selection box around the selected object
                         curr_sel_obj = self.collection.get_active()
-                        self.draw_selection_shape(curr_sel_obj)
+                        if self.defaults['global_selection_shape'] is True:
+                            self.draw_selection_shape(curr_sel_obj)
 
                         # self.inform.emit('[selected] %s: %s selected' %
                         #                  (str(curr_sel_obj.kind).capitalize(), str(curr_sel_obj.options['name'])))
@@ -5372,7 +5483,8 @@ class App(QtCore.QObject):
                         self.collection.set_active(objects_under_the_click_list[0])
                         # create the selection box around the selected object
                         curr_sel_obj = self.collection.get_active()
-                        self.draw_selection_shape(curr_sel_obj)
+                        if self.defaults['global_selection_shape'] is True:
+                            self.draw_selection_shape(curr_sel_obj)
 
                         # self.inform.emit('[selected] %s: %s selected' %
                         #                  (str(curr_sel_obj.kind).capitalize(), str(curr_sel_obj.options['name'])))
@@ -5420,7 +5532,8 @@ class App(QtCore.QObject):
                     # delete the possible selection box around a possible selected object
                     self.delete_selection_shape()
                     # create the selection box around the selected object
-                    self.draw_selection_shape(curr_sel_obj)
+                    if self.defaults['global_selection_shape'] is True:
+                        self.draw_selection_shape(curr_sel_obj)
 
                     # self.inform.emit('[selected] %s: %s selected' %
                     #                  (str(curr_sel_obj.kind).capitalize(), str(curr_sel_obj.options['name'])))
@@ -7597,6 +7710,7 @@ class App(QtCore.QObject):
         icons = {
             "gerber": "share/flatcam_icon16.png",
             "excellon": "share/drill16.png",
+            'geometry': "share/geometry16.png",
             "cncjob": "share/cnc16.png",
             "project": "share/project16.png",
             "svg": "share/geometry16.png",
@@ -7868,23 +7982,23 @@ The normal flow when working in FlatCAM is the following:</span></p>
     QObject::connect: Cannot queue arguments of type 'QVector<int>' 
     (Make sure 'QVector<int>' is registered using qRegisterMetaType().
     '''
-    def enable_plots(self, objects, threaded=False):
+    def enable_plots(self, objects, threaded=True):
         if threaded is True:
             def worker_task(app_obj):
-                percentage = 0.1
-                try:
-                    delta = 0.9 / len(objects)
-                except ZeroDivisionError:
-                    self.progress.emit(0)
-                    return
+                # percentage = 0.1
+                # try:
+                #     delta = 0.9 / len(objects)
+                # except ZeroDivisionError:
+                #     self.progress.emit(0)
+                #     return
                 for obj in objects:
                     obj.options['plot'] = True
-                    percentage += delta
-                    self.progress.emit(int(percentage*100))
+                    # percentage += delta
+                    # self.progress.emit(int(percentage*100))
 
-                self.progress.emit(0)
+                # self.progress.emit(0)
                 self.plots_updated.emit()
-                self.collection.update_view()
+                # self.collection.update_view()
 
             # Send to worker
             # self.worker.add_task(worker_task, [self])
@@ -7892,9 +8006,9 @@ The normal flow when working in FlatCAM is the following:</span></p>
         else:
             for obj in objects:
                 obj.options['plot'] = True
-            self.progress.emit(0)
+            # self.progress.emit(0)
             self.plots_updated.emit()
-            self.collection.update_view()
+            # self.collection.update_view()
 
     # TODO: FIX THIS
     '''
@@ -7904,7 +8018,7 @@ The normal flow when working in FlatCAM is the following:</span></p>
     QObject::connect: Cannot queue arguments of type 'QVector<int>' 
     (Make sure 'QVector<int>' is registered using qRegisterMetaType().
     '''
-    def disable_plots(self, objects, threaded=False):
+    def disable_plots(self, objects, threaded=True):
         # TODO: This method is very similar to replot_all. Try to merge.
         """
         Disables plots
@@ -7914,23 +8028,23 @@ The normal flow when working in FlatCAM is the following:</span></p>
         """
 
         if threaded is True:
-            self.progress.emit(10)
+            # self.progress.emit(10)
             def worker_task(app_obj):
-                percentage = 0.1
-                try:
-                    delta = 0.9 / len(objects)
-                except ZeroDivisionError:
-                    self.progress.emit(0)
-                    return
+                # percentage = 0.1
+                # try:
+                #     delta = 0.9 / len(objects)
+                # except ZeroDivisionError:
+                #     self.progress.emit(0)
+                #     return
 
                 for obj in objects:
                     obj.options['plot'] = False
-                    percentage += delta
-                    self.progress.emit(int(percentage*100))
+                    # percentage += delta
+                    # self.progress.emit(int(percentage*100))
 
-                self.progress.emit(0)
+                # self.progress.emit(0)
                 self.plots_updated.emit()
-                self.collection.update_view()
+                # self.collection.update_view()
 
             # Send to worker
             self.worker_task.emit({'fcn': worker_task, 'params': [self]})
@@ -7938,7 +8052,7 @@ The normal flow when working in FlatCAM is the following:</span></p>
             for obj in objects:
                 obj.options['plot'] = False
             self.plots_updated.emit()
-            self.collection.update_view()
+            # self.collection.update_view()
 
     def clear_plots(self):
 

+ 28 - 21
FlatCAMObj.py

@@ -181,17 +181,18 @@ class FlatCAMObj(QtCore.QObject):
         old_name = copy(self.options["name"])
         new_name = self.ui.name_entry.get_value()
 
-        # update the SHELL auto-completer model data
-        try:
-            self.app.myKeywords.remove(old_name)
-            self.app.myKeywords.append(new_name)
-            self.app.shell._edit.set_model_data(self.app.myKeywords)
-            self.app.ui.code_editor.set_model_data(self.app.myKeywords)
-        except:
-            log.debug("on_name_activate() --> Could not remove the old object name from auto-completer model list")
+        if new_name != old_name:
+            # update the SHELL auto-completer model data
+            try:
+                self.app.myKeywords.remove(old_name)
+                self.app.myKeywords.append(new_name)
+                self.app.shell._edit.set_model_data(self.app.myKeywords)
+                self.app.ui.code_editor.set_model_data(self.app.myKeywords)
+            except:
+                log.debug("on_name_activate() --> Could not remove the old object name from auto-completer model list")
 
-        self.options["name"] = self.ui.name_entry.get_value()
-        self.app.inform.emit(_("[success] Name changed from {old} to {new}").format(old=old_name, new=new_name))
+            self.options["name"] = self.ui.name_entry.get_value()
+            self.app.inform.emit(_("[success] Name changed from {old} to {new}").format(old=old_name, new=new_name))
 
     def on_offset_button_click(self):
         self.app.report_usage("obj_on_offset_button")
@@ -358,6 +359,12 @@ class FlatCAMObj(QtCore.QObject):
         except AttributeError:
             pass
 
+        # Not all object types have mark_shapes
+        # try:
+        #     self.mark_shapes.clear(update)
+        # except AttributeError:
+        #     pass
+
     def delete(self):
         # Free resources
         del self.ui
@@ -2717,7 +2724,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # those elements are the ones used for generating GCode
         self.sel_tools = {}
 
-        self.offset_item_options = [_("Path"), _("In"), _("Out"), _("Custom")]
+        self.offset_item_options = ["Path", "In", "Out", "Custom"]
         self.type_item_options = [_("Iso"), _("Rough"), _("Finish")]
         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
 
@@ -2959,7 +2966,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             self.tools.update({
                 self.tooluid: {
                     'tooldia': float(self.options["cnctooldia"]),
-                    'offset': _('Path'),
+                    'offset': ('Path'),
                     'offset_value': 0.0,
                     'type': _('Rough'),
                     'tool_type': 'C1',
@@ -3041,7 +3048,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             tool_offset = self.ui.geo_tools_table.cellWidget(current_row, 2)
             if tool_offset is not None:
                 tool_offset_txt = tool_offset.currentText()
-                if tool_offset_txt == _('Custom'):
+                if tool_offset_txt == ('Custom'):
                     self.ui.tool_offset_entry.show()
                     self.ui.tool_offset_lbl.show()
                 else:
@@ -3246,7 +3253,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             self.tools.update({
                 self.tooluid: {
                     'tooldia': tooldia,
-                    'offset': _('Path'),
+                    'offset': ('Path'),
                     'offset_value': 0.0,
                     'type': _('Rough'),
                     'tool_type': 'C1',
@@ -3615,7 +3622,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 cb_txt = cw.currentText()
                 if cw_col == 2:
                     tooluid_value['offset'] = cb_txt
-                    if cb_txt == _('Custom'):
+                    if cb_txt == ('Custom'):
                         self.ui.tool_offset_entry.show()
                         self.ui.tool_offset_lbl.show()
                     else:
@@ -4088,13 +4095,13 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                             diadict_key: datadict
                         })
 
-                if dia_cnc_dict['offset'] == 'in':
+                if dia_cnc_dict['offset'] == ('in'):
                     tool_offset = -dia_cnc_dict['tooldia'] / 2
                     offset_str = 'inside'
-                elif dia_cnc_dict['offset'].lower() == 'out':
+                elif dia_cnc_dict['offset'].lower() == ('out'):
                     tool_offset = dia_cnc_dict['tooldia']  / 2
                     offset_str = 'outside'
-                elif dia_cnc_dict['offset'].lower() == 'path':
+                elif dia_cnc_dict['offset'].lower() == ('path'):
                     offset_str = 'onpath'
                     tool_offset = 0.0
                 else:
@@ -4321,13 +4328,13 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                             diadict_key: datadict
                         })
 
-                if dia_cnc_dict['offset'] == 'in':
+                if dia_cnc_dict['offset'] == ('in'):
                     tool_offset = -dia_cnc_dict['tooldia'] / 2
                     offset_str = 'inside'
-                elif dia_cnc_dict['offset'].lower() == 'out':
+                elif dia_cnc_dict['offset'].lower() == ('out'):
                     tool_offset = dia_cnc_dict['tooldia']  / 2
                     offset_str = 'outside'
-                elif dia_cnc_dict['offset'].lower() == 'path':
+                elif dia_cnc_dict['offset'].lower() == ('path'):
                     offset_str = 'onpath'
                     tool_offset = 0.0
                 else:

+ 1 - 1
FlatCAMWorkerStack.py

@@ -25,7 +25,7 @@ class WorkerStack(QtCore.QObject):
             thread.started.connect(worker.run)
             worker.task_completed.connect(self.on_task_completed)
 
-            thread.start()
+            thread.start(QtCore.QThread.LowPriority)
 
             self.workers.append(worker)
             self.threads.append(thread)

+ 27 - 21
ObjectCollection.py

@@ -118,13 +118,13 @@ class KeySensitiveListView(QtWidgets.QTreeView):
             event.ignore()
 
 
-class TreeItem:
+class TreeItem(KeySensitiveListView):
     """
     Item of a tree model
     """
 
     def __init__(self, data, icon=None, obj=None, parent_item=None):
-
+        super(TreeItem, self).__init__(parent_item)
         self.parent_item = parent_item
         self.item_data = data  # Columns string data
         self.icon = icon  # Decoration
@@ -378,9 +378,11 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                 return index.internalPointer().data(index.column())
 
         if role == Qt.ForegroundRole:
+            color = QColor(self.app.defaults['global_proj_item_color'])
+            color_disabled = QColor(self.app.defaults['global_proj_item_dis_color'])
             obj = index.internalPointer().obj
             if obj:
-                return QtGui.QBrush(QtCore.Qt.black) if obj.options["plot"] else QtGui.QBrush(QtCore.Qt.darkGray)
+                return QtGui.QBrush(color) if obj.options["plot"] else QtGui.QBrush(color_disabled)
             else:
                 return index.internalPointer().data(index.column())
 
@@ -397,23 +399,25 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         if index.isValid():
             obj = index.internalPointer().obj
             if obj:
-                old_name = obj.options['name']
-                # rename the object
-                obj.options["name"] = str(data)
-                new_name = obj.options['name']
-
-                # update the SHELL auto-completer model data
-                try:
-                    self.app.myKeywords.remove(old_name)
-                    self.app.myKeywords.append(new_name)
-                    self.app.shell._edit.set_model_data(self.app.myKeywords)
-                    self.app.ui.code_editor.set_model_data(self.app.myKeywords)
-                except:
-                    log.debug(
-                        "setData() --> Could not remove the old object name from auto-completer model list")
-
-                obj.build_ui()
-                self.app.inform.emit(_("Object renamed from {old} to {new}").format(old=old_name, new=new_name))
+                old_name = str(obj.options['name'])
+                new_name = str(data)
+                if old_name != new_name and new_name != '':
+                    # rename the object
+                    obj.options["name"] = str(data)
+
+                    # update the SHELL auto-completer model data
+                    try:
+                        self.app.myKeywords.remove(old_name)
+                        self.app.myKeywords.append(new_name)
+                        self.app.shell._edit.set_model_data(self.app.myKeywords)
+                        self.app.ui.code_editor.set_model_data(self.app.myKeywords)
+                    except:
+                        log.debug(
+                            "setData() --> Could not remove the old object name from auto-completer model list")
+
+                    obj.build_ui()
+                    self.app.inform.emit(_("Object renamed from <b>{old}</b> to <b>{new}</b>").format(old=old_name,
+                                                                                                      new=new_name))
 
         return True
 
@@ -681,6 +685,8 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         :param name: Name of the FlatCAM Object
         :return: None
         """
+        log.debug("ObjectCollection.set_inactive()")
+
         obj = self.get_by_name(name)
         item = obj.item
         group = self.group_items[obj.kind]
@@ -721,7 +727,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                     color='red', name=str(obj.options['name'])))
 
         except IndexError:
-            FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
+            # FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
             self.app.inform.emit('')
             try:
                 self.app.ui.selected_scroll_area.takeWidget()

+ 61 - 0
README.md

@@ -9,6 +9,67 @@ CAD program, and create G-Code for Isolation routing.
 
 =================================================
 
+01.05.2019
+
+- the project items color is now controlled from Foreground Role in ObjectCollection.data()
+- made again plot functions threaded but moved the dataChanged signal (update_view() ) to the main thread by using an already existing signal (plots_updated signal) to avoid the errors with register QVector
+- Enable/Disable Object toggle key ("Space" key) will trigger also the datChanged signal for the Project MVC
+- added a new setting for the color of the Project items, the color when they are disabled.
+- fixed a crash when triggering 'Jump To' menu action (shortcut key 'J' worked ok)
+- made some mods to what can be translated as some of the translations interfered with the correct functioning of FlatCAM
+- updated the translations
+- fixed bugs in Excellon Editor
+- Excellon Editor:  made Add Pad tool to work until right click
+- Excellon Editor: fixed mouse right click was always doing popup context menu
+- GUIElements.FCEntry2(): added a try-except clause
+- made sure that the Tools Tab is cleared on Editors exit
+- Geometry Editor: restored the old behavior: a tool is active until it is voluntarily exited: either by using the 'ESC' key, or selecting the Select tool or new: right click on canvas
+- RELEASE 8.915
+
+30.04.2019
+
+- in ObjectCollection class, made sure that renaming an object in Project View does not result in an empty name. If new name is blank the rename is cancelled.
+- made ObjectCollection.TreeItem() inherit KeySensitiveListVIew and implicitly QTreeView (in the hope that the theme applied on app will be applied on the tree items, too (for MacOs new DarkUI theme)
+- renamed SilkScreen Tool to Substract Tool and move it's menu location in Edit -> Conversion
+- started to modify the Substract Tool to work on Geometry objects too
+- progress in the new Substract Tool for Geometry Objects
+- finished the new Substract Tool
+- added new setting for the color of the Project Tree items; it helps in providing contrast when using dark theme like the one in MacOS
+
+29.04.2019
+
+- solved bug in Gerber Editor: the '0' aperture (the region aperture) had no size which created errors. Made the size to be zero.
+- solved bug in editors: the canvas selection shape was not deleted on mouse release if the grid snap was OFF
+- solved bug in Excellon Editor: when selecting a drill hole on canvas the selected row in the Tools Table was not the correct one but the next highest row
+- finished the Silkscreen Tool but there are some limitations (some wires fragments from silkscreen are lost)
+- solved the issue in Silkscreen Tool with losing some fragments of wires from silkscreen
+
+26.04.2019
+
+- small changes in GUI; optimized contextual menu display
+- made sure that the Project Tab is disabled while one of the Editors is active and it is restored after returning to app
+- fixed some bugs recently introduced in Editors due of the changes done to the way mouse panning is detected 
+- cleaned up the context menu's when in Editors; made some structural changes
+- updated the code in camlib.CNCJob.generate_from_excellon_by_tools() to work with the new API from Google OR-Tools
+- all Gerber regions (G36 G37) are stored in the '0' aperture
+- fixed a bug that added geometry with clear polarity in the apertures where was not supposed to be
+
+25.04.2019
+
+- Geometry Editor: modified the intersection (if the selected shapes don't intersects preserve them) and substract functions (delete all shapes that were used in the process)
+- work in the ToolSub
+- for all objects, if in Selected the object name is changed to the same name, the rename is not done (because there is nothing changed)
+- fixed Edit -> Copy as Geom function handler to work for Excellon objects, too
+- made sure that the mouse pointer is restored to default on Editor exit
+- added a toggle button in Preferences to toggle on/off the display of the selection box on canvas when the user is clicking an object or selecting it by mouse dragging.
+
+24.04.2019
+
+- PDF import tool: working in making the PDF layer rendering multithreaded in itself (one layer rendered on each worker)
+- PDF import tool: solved a bug in parsing the rectangle subpath (an extra point was added to the subpath creating nonexisting geometry)
+- PDF import tool: finished layer rendering multithreading
+- New tool: Silkscreen Tool: I am trying to remove the overlapped geo with the soldermask layer from overlay layer; layed out the class and functions - not working yet
+
 23.04.2019
 
 - Gerber Editor: added two new tools: Add Disc and Add SemiDisc (porting of Circle and Arc from Geometry Editor)

+ 60 - 41
camlib.py

@@ -1942,6 +1942,10 @@ class Gerber (Geometry):
         # will store the Gerber geometry's as paths
         self.follow_geometry = []
 
+        # made True when the LPC command is encountered in Gerber parsing
+        # it allows adding data into the clear_geometry key of the self.apertures[aperture] dict
+        self.is_lpc = False
+
         self.source_file = ''
 
         # Attributes to be included in serialization
@@ -2174,10 +2178,6 @@ class Gerber (Geometry):
         # applying a union for every new polygon.
         poly_buffer = []
 
-        # made True when the LPC command is encountered in Gerber parsing
-        # it allows adding data into the clear_geometry key of the self.apertures[aperture] dict
-        self.is_lpc = False
-
         # store here the follow geometry
         follow_buffer = []
 
@@ -2242,6 +2242,8 @@ class Gerber (Geometry):
                 match = self.lpol_re.search(gline)
                 if match:
                     new_polarity = match.group(1)
+                    # log.info("Polarity CHANGE, LPC = %s, poly_buff = %s" % (self.is_lpc, poly_buffer))
+                    self.is_lpc = True if new_polarity == 'C' else False
                     if len(path) > 1 and current_polarity != new_polarity:
 
                         # finish the current path and add it to the storage
@@ -2258,6 +2260,7 @@ class Gerber (Geometry):
                                 self.apertures[last_path_aperture]['follow_geometry'].append(geo)
 
                         geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+
                         if not geo.is_empty:
                             poly_buffer.append(geo)
                             if self.is_lpc is True:
@@ -2280,12 +2283,10 @@ class Gerber (Geometry):
                     # TODO: Remove when bug fixed
                     if len(poly_buffer) > 0:
                         if current_polarity == 'D':
-                            self.is_lpc = True
                             # self.follow_geometry = self.follow_geometry.union(cascaded_union(follow_buffer))
                             self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
 
                         else:
-                            self.is_lpc = False
                             # self.follow_geometry = self.follow_geometry.difference(cascaded_union(follow_buffer))
                             self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
 
@@ -2414,7 +2415,7 @@ class Gerber (Geometry):
                 ### Aperture definitions %ADD...
                 match = self.ad_re.search(gline)
                 if match:
-                    log.info("Found aperture definition. Line %d: %s" % (line_num, gline))
+                    # log.info("Found aperture definition. Line %d: %s" % (line_num, gline))
                     self.aperture_parse(match.group(1), match.group(2), match.group(3))
                     continue
 
@@ -2460,7 +2461,7 @@ class Gerber (Geometry):
                 match = self.tool_re.search(gline)
                 if match:
                     current_aperture = match.group(1)
-                    log.debug("Line %d: Aperture change to (%s)" % (line_num, match.group(1)))
+                    # log.debug("Line %d: Aperture change to (%s)" % (line_num, current_aperture))
 
                     # If the aperture value is zero then make it something quite small but with a non-zero value
                     # so it can be processed by FlatCAM.
@@ -2469,7 +2470,7 @@ class Gerber (Geometry):
                     if self.apertures[current_aperture]["type"] is not "AM":
                         if self.apertures[current_aperture]["size"] == 0:
                             self.apertures[current_aperture]["size"] = 1e-12
-                    log.debug(self.apertures[current_aperture])
+                    # log.debug(self.apertures[current_aperture])
 
                     # Take care of the current path with the previous tool
                     if len(path) > 1:
@@ -2479,7 +2480,6 @@ class Gerber (Geometry):
                         else:
                             # --- Buffered ----
                             width = self.apertures[last_path_aperture]["size"]
-
                             geo = LineString(path)
                             if not geo.is_empty:
                                 follow_buffer.append(geo)
@@ -2551,6 +2551,12 @@ class Gerber (Geometry):
                 if self.regionoff_re.search(gline):
                     making_region = False
 
+                    if '0' not in self.apertures:
+                        self.apertures['0'] = {}
+                        self.apertures['0']['type'] = 'REG'
+                        self.apertures['0']['size'] = 0.0
+                        self.apertures['0']['solid_geometry'] = []
+
                     # if D02 happened before G37 we now have a path with 1 element only so we have to add the current
                     # geo to the poly_buffer otherwise we loose it
                     if current_operation_code == 2:
@@ -2558,24 +2564,24 @@ class Gerber (Geometry):
                             if not geo.is_empty:
                                 follow_buffer.append(geo)
                                 try:
-                                    self.apertures[current_aperture]['follow_geometry'].append(geo)
+                                    self.apertures['0']['follow_geometry'].append(geo)
                                 except KeyError:
-                                    self.apertures[current_aperture]['follow_geometry'] = []
-                                    self.apertures[current_aperture]['follow_geometry'].append(geo)
+                                    self.apertures['0']['follow_geometry'] = []
+                                    self.apertures['0']['follow_geometry'].append(geo)
 
                                 poly_buffer.append(geo)
                                 if self.is_lpc is True:
                                     try:
-                                        self.apertures[current_aperture]['clear_geometry'].append(geo)
+                                        self.apertures['0']['clear_geometry'].append(geo)
                                     except KeyError:
-                                        self.apertures[current_aperture]['clear_geometry'] = []
-                                        self.apertures[current_aperture]['clear_geometry'].append(geo)
+                                        self.apertures['0']['clear_geometry'] = []
+                                        self.apertures['0']['clear_geometry'].append(geo)
                                 else:
                                     try:
-                                        self.apertures[current_aperture]['solid_geometry'].append(geo)
+                                        self.apertures['0']['solid_geometry'].append(geo)
                                     except KeyError:
-                                        self.apertures[current_aperture]['solid_geometry'] = []
-                                        self.apertures[current_aperture]['solid_geometry'].append(geo)
+                                        self.apertures['0']['solid_geometry'] = []
+                                        self.apertures['0']['solid_geometry'].append(geo)
                             continue
 
                     # Only one path defines region?
@@ -2599,10 +2605,10 @@ class Gerber (Geometry):
                     if not region.is_empty:
                         follow_buffer.append(region)
                         try:
-                            self.apertures[current_aperture]['follow_geometry'].append(region)
+                            self.apertures['0']['follow_geometry'].append(region)
                         except KeyError:
-                            self.apertures[current_aperture]['follow_geometry'] = []
-                            self.apertures[current_aperture]['follow_geometry'].append(region)
+                            self.apertures['0']['follow_geometry'] = []
+                            self.apertures['0']['follow_geometry'].append(region)
 
                     region = Polygon(path)
                     if not region.is_valid:
@@ -2613,17 +2619,18 @@ class Gerber (Geometry):
 
                         # we do this for the case that a region is done without having defined any aperture
                         # Allegro does that
-                        if current_aperture:
-                            used_aperture = current_aperture
-                        elif last_path_aperture:
-                            used_aperture = last_path_aperture
-                        else:
-                            if '0' not in self.apertures:
-                                self.apertures['0'] = {}
-                                self.apertures['0']['type'] = 'REG'
-                                self.apertures['0']['solid_geometry'] = []
-                            used_aperture = '0'
-
+                        # if current_aperture:
+                        #     used_aperture = current_aperture
+                        # elif last_path_aperture:
+                        #     used_aperture = last_path_aperture
+                        # else:
+                        #     if '0' not in self.apertures:
+                        #         self.apertures['0'] = {}
+                        #         self.apertures['0']['size'] = 0.0
+                        #         self.apertures['0']['type'] = 'REG'
+                        #         self.apertures['0']['solid_geometry'] = []
+                        #     used_aperture = '0'
+                        used_aperture = '0'
                         if self.is_lpc is True:
                             try:
                                 self.apertures[used_aperture]['clear_geometry'].append(region)
@@ -2727,6 +2734,7 @@ class Gerber (Geometry):
                                 if '0' not in self.apertures:
                                     self.apertures['0'] = {}
                                     self.apertures['0']['type'] = 'REG'
+                                    self.apertures['0']['size'] = 0.0
                                     self.apertures['0']['solid_geometry'] = []
                                 last_path_aperture = '0'
                         else:
@@ -2746,6 +2754,7 @@ class Gerber (Geometry):
                                     if '0' not in self.apertures:
                                         self.apertures['0'] = {}
                                         self.apertures['0']['type'] = 'REG'
+                                        self.apertures['0']['size'] = 0.0
                                         self.apertures['0']['solid_geometry'] = []
                                     last_path_aperture = '0'
                                 geo = Polygon()
@@ -2779,6 +2788,7 @@ class Gerber (Geometry):
                                     if '0' not in self.apertures:
                                         self.apertures['0'] = {}
                                         self.apertures['0']['type'] = 'REG'
+                                        self.apertures['0']['size'] = 0.0
                                         self.apertures['0']['solid_geometry'] = []
                                     last_path_aperture = '0'
                                 elem = [linear_x, linear_y]
@@ -5288,8 +5298,13 @@ class CNCjob(Geometry):
                             y2 = locations[to_node][1]
                             self.matrix[from_node][to_node] = distance_euclidian(x1, y1, x2, y2)
 
-            def Distance(self, from_node, to_node):
-                return int(self.matrix[from_node][to_node])
+            # def Distance(self, from_node, to_node):
+            #     return int(self.matrix[from_node][to_node])
+            def Distance(self, from_index, to_index):
+                # Convert from routing variable Index to distance matrix NodeIndex.
+                from_node = manager.IndexToNode(from_index)
+                to_node = manager.IndexToNode(to_index)
+                return self.matrix[from_node][to_node]
 
         # Create the data.
         def create_data_array():
@@ -5327,8 +5342,9 @@ class CNCjob(Geometry):
                         depot = 0
                         # Create routing model.
                         if tsp_size > 0:
-                            routing = pywrapcp.RoutingModel(tsp_size, num_routes, depot)
-                            search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
+                            manager = pywrapcp.RoutingIndexManager(tsp_size, num_routes, depot)
+                            routing = pywrapcp.RoutingModel(manager)
+                            search_parameters = pywrapcp.DefaultRoutingSearchParameters()
                             search_parameters.local_search_metaheuristic = (
                                 routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
 
@@ -5343,7 +5359,8 @@ class CNCjob(Geometry):
                             # arguments (the from and to node indices) and returns the distance between them.
                             dist_between_locations = CreateDistanceCallback()
                             dist_callback = dist_between_locations.Distance
-                            routing.SetArcCostEvaluatorOfAllVehicles(dist_callback)
+                            transit_callback_index = routing.RegisterTransitCallback(dist_callback)
+                            routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
 
                             # Solve, returns a solution if any.
                             assignment = routing.SolveWithParameters(search_parameters)
@@ -5432,14 +5449,16 @@ class CNCjob(Geometry):
 
                         # Create routing model.
                         if tsp_size > 0:
-                            routing = pywrapcp.RoutingModel(tsp_size, num_routes, depot)
-                            search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
+                            manager = pywrapcp.RoutingIndexManager(tsp_size, num_routes, depot)
+                            routing = pywrapcp.RoutingModel(manager)
+                            search_parameters = pywrapcp.DefaultRoutingSearchParameters()
 
                             # Callback to the distance function. The callback takes two
                             # arguments (the from and to node indices) and returns the distance between them.
                             dist_between_locations = CreateDistanceCallback()
                             dist_callback = dist_between_locations.Distance
-                            routing.SetArcCostEvaluatorOfAllVehicles(dist_callback)
+                            transit_callback_index = routing.RegisterTransitCallback(dist_callback)
+                            routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
 
                             # Solve, returns a solution if any.
                             assignment = routing.SolveWithParameters(search_parameters)

+ 158 - 62
flatcamEditors/FlatCAMExcEditor.py

@@ -44,7 +44,7 @@ class FCDrillAdd(FCShapeTool):
 
         except KeyError:
             self.draw_app.app.inform.emit(_("[WARNING_NOTCL] To add a drill first select a tool"))
-            self.draw_app.select_tool("select")
+            self.draw_app.select_tool("drill_select")
             return
 
         try:
@@ -103,6 +103,7 @@ class FCDrillAdd(FCShapeTool):
 
         self.draw_app.current_storage = self.draw_app.storage_dict[self.selected_dia]
         self.geometry = DrawToolShape(self.util_shape(self.points))
+        self.draw_app.in_action = False
         self.complete = True
         self.draw_app.app.inform.emit(_("[success] Done. Drill added."))
 
@@ -319,7 +320,7 @@ class FCDrillArray(FCShapeTool):
                 self.geometry.append(DrawToolShape(geo))
         self.complete = True
         self.draw_app.app.inform.emit(_("[success] Done. Drill Array added."))
-        self.draw_app.in_action = True
+        self.draw_app.in_action = False
         self.draw_app.array_frame.hide()
         return
 
@@ -428,7 +429,7 @@ class FCDrillResize(FCShapeTool):
         self.complete = True
 
         # MS: always return to the Select Tool
-        self.draw_app.select_tool("select")
+        self.draw_app.select_tool("drill_select")
 
 
 class FCDrillMove(FCShapeTool):
@@ -475,7 +476,7 @@ class FCDrillMove(FCShapeTool):
             self.make()
 
             # MS: always return to the Select Tool
-            self.draw_app.select_tool("select")
+            self.draw_app.select_tool("drill_select")
             return
 
     def make(self):
@@ -643,8 +644,11 @@ class FCDrillSelect(DrawTool):
                         sel_tools.add(storage)
 
             for storage in sel_tools:
-                self.exc_editor_app.tools_table_exc.selectRow(int(storage))
-                self.draw_app.last_tool_selected = int(storage)
+                for k, v in self.draw_app.tool2tooldia.items():
+                    if v == storage:
+                        self.exc_editor_app.tools_table_exc.selectRow(int(k) - 1)
+                        self.draw_app.last_tool_selected = int(k)
+                        break
 
             self.exc_editor_app.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
 
@@ -931,7 +935,7 @@ class FlatCAMExcEditor(QtCore.QObject):
 
         self.drill_axis_radio = RadioSet([{'label': 'X', 'value': 'X'},
                                           {'label': 'Y', 'value': 'Y'},
-                                          {'label': _('Angle'), 'value': 'A'}])
+                                          {'label': 'Angle', 'value': 'A'}])
         self.drill_axis_radio.set_value('X')
         self.linear_form.addRow(self.drill_axis_label, self.drill_axis_radio)
 
@@ -999,7 +1003,7 @@ class FlatCAMExcEditor(QtCore.QObject):
 
         ## Toolbar events and properties
         self.tools_exc = {
-            "select": {"button": self.app.ui.select_drill_btn,
+            "drill_select": {"button": self.app.ui.select_drill_btn,
                        "constructor": FCDrillSelect},
             "drill_add": {"button": self.app.ui.add_drill_btn,
                     "constructor": FCDrillAdd},
@@ -1016,6 +1020,8 @@ class FlatCAMExcEditor(QtCore.QObject):
         ### Data
         self.active_tool = None
 
+        self.in_action = False
+
         self.storage_dict = {}
         self.current_storage = []
 
@@ -1067,7 +1073,6 @@ class FlatCAMExcEditor(QtCore.QObject):
 
         self.app.ui.exc_move_drill_menuitem.triggered.connect(self.exc_move_drills)
 
-
         # Init GUI
         self.drill_array_size_entry.set_value(5)
         self.drill_pitch_entry.set_value(2.54)
@@ -1646,6 +1651,13 @@ class FlatCAMExcEditor(QtCore.QObject):
         if self.app.ui.grid_snap_btn.isChecked() is False:
             self.app.ui.grid_snap_btn.trigger()
 
+        self.app.ui.popmenu_disable.setVisible(False)
+        self.app.ui.cmenu_newmenu.menuAction().setVisible(False)
+        self.app.ui.popmenu_properties.setVisible(False)
+        self.app.ui.e_editor_cmenu.menuAction().setVisible(True)
+        self.app.ui.g_editor_cmenu.menuAction().setVisible(False)
+        self.app.ui.grb_editor_cmenu.menuAction().setVisible(False)
+
         # Tell the App that the editor is active
         self.editor_active = True
 
@@ -1653,6 +1665,11 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.drills_frame.show()
 
     def deactivate(self):
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         # adjust the status of the menu entries related to the editor
         self.app.ui.menueditedit.setDisabled(False)
         self.app.ui.menueditok.setDisabled(True)
@@ -1706,8 +1723,12 @@ class FlatCAMExcEditor(QtCore.QObject):
 
         self.app.ui.update_obj_btn.setEnabled(False)
 
-        self.app.ui.g_editor_cmenu.setEnabled(False)
-        self.app.ui.e_editor_cmenu.setEnabled(False)
+        self.app.ui.popmenu_disable.setVisible(True)
+        self.app.ui.cmenu_newmenu.menuAction().setVisible(True)
+        self.app.ui.popmenu_properties.setVisible(True)
+        self.app.ui.g_editor_cmenu.menuAction().setVisible(False)
+        self.app.ui.e_editor_cmenu.menuAction().setVisible(False)
+        self.app.ui.grb_editor_cmenu.menuAction().setVisible(False)
 
         # Show original geometry
         if self.exc_obj:
@@ -1733,6 +1754,18 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.app.plotcanvas.vis_disconnect('mouse_double_click', self.app.on_double_click_over_plot)
         self.app.collection.view.clicked.disconnect()
 
+        self.app.ui.popmenu_copy.triggered.disconnect()
+        self.app.ui.popmenu_delete.triggered.disconnect()
+        self.app.ui.popmenu_move.triggered.disconnect()
+
+        self.app.ui.popmenu_copy.triggered.connect(self.exc_copy_drills)
+        self.app.ui.popmenu_delete.triggered.connect(self.on_delete_btn)
+        self.app.ui.popmenu_move.triggered.connect(self.exc_move_drills)
+
+        # Excellon Editor
+        self.app.ui.drill.triggered.connect(self.exc_add_drill)
+        self.app.ui.drill_array.triggered.connect(self.exc_add_drill_array)
+
     def disconnect_canvas_event_handlers(self):
         # we restore the key and mouse control to FlatCAMApp method
         # first connect to new, then disconnect the old handlers
@@ -1747,6 +1780,36 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.canvas.vis_disconnect('mouse_move', self.on_canvas_move)
         self.canvas.vis_disconnect('mouse_release', self.on_exc_click_release)
 
+        try:
+            self.app.ui.popmenu_copy.triggered.disconnect(self.exc_copy_drills)
+        except TypeError:
+            pass
+
+        try:
+            self.app.ui.popmenu_delete.triggered.disconnect(self.on_delete_btn)
+        except TypeError:
+            pass
+
+        try:
+            self.app.ui.popmenu_move.triggered.disconnect(self.exc_move_drills)
+        except TypeError:
+            pass
+
+        self.app.ui.popmenu_copy.triggered.connect(self.app.on_copy_object)
+        self.app.ui.popmenu_delete.triggered.connect(self.app.on_delete)
+        self.app.ui.popmenu_move.triggered.connect(self.app.obj_move)
+
+        # Excellon Editor
+        try:
+            self.app.ui.drill.triggered.disconnect(self.exc_add_drill)
+        except TypeError:
+            pass
+
+        try:
+            self.app.ui.drill_array.triggered.disconnect(self.exc_add_drill_array)
+        except TypeError:
+            pass
+
     def clear(self):
         self.active_tool = None
         # self.shape_buffer = []
@@ -1786,7 +1849,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         # Set selection tolerance
         # DrawToolShape.tolerance = fc_excellon.drawing_tolerance * 10
 
-        self.select_tool("select")
+        self.select_tool("drill_select")
 
         self.set_ui()
 
@@ -2002,10 +2065,10 @@ class FlatCAMExcEditor(QtCore.QObject):
 
         self.app.log.debug("on_tool_select('%s')" % tool)
 
-        if self.last_tool_selected is None and current_tool is not 'select':
-            # self.draw_app.select_tool('select')
+        if self.last_tool_selected is None and current_tool is not 'drill_select':
+            # self.draw_app.select_tool('drill_select')
             self.complete = True
-            current_tool = 'select'
+            current_tool = 'drill_select'
             self.app.inform.emit(_("[WARNING_NOTCL] Cancelled. There is no Tool/Drill selected"))
 
         # This is to make the group behave as radio group
@@ -2024,7 +2087,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                 for t in self.tools_exc:
                     self.tools_exc[t]["button"].setChecked(False)
 
-                self.select_tool('select')
+                self.select_tool('drill_select')
                 self.active_tool = FCDrillSelect(self)
 
     def on_row_selected(self, row, col):
@@ -2042,7 +2105,7 @@ class FlatCAMExcEditor(QtCore.QObject):
 
             try:
                 selected_dia = self.tool2tooldia[self.tools_table_exc.currentRow() + 1]
-                self.last_tool_selected = copy(self.tools_table_exc.currentRow()) + 1
+                self.last_tool_selected = int(self.tools_table_exc.currentRow()) + 1
                 for obj in self.storage_dict[selected_dia].get_objects():
                     self.selected.append(obj)
             except Exception as e:
@@ -2060,23 +2123,29 @@ class FlatCAMExcEditor(QtCore.QObject):
     def on_canvas_click(self, event):
         """
         event.x and .y have canvas coordinates
-        event.xdaya and .ydata have plot coordinates
+        event.xdata and .ydata have plot coordinates
 
-        :param event: Event object dispatched by Matplotlib
+        :param event: Event object dispatched by VisPy
         :return: None
         """
 
+        self.pos = self.canvas.vispy_canvas.translate_coords(event.pos)
+
+        if self.app.grid_status():
+            self.pos  = self.app.geo_editor.snap(self.pos[0], self.pos[1])
+            self.app.app_cursor.enabled = True
+            # Update cursor
+            self.app.app_cursor.set_data(np.asarray([(self.pos[0], self.pos[1])]), symbol='++', edge_color='black',
+                                         size=20)
+        else:
+            self.pos = (self.pos[0], self.pos[1])
+            self.app.app_cursor.enabled = False
+
         if event.button is 1:
             self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
                                                    "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
             self.pos = self.canvas.vispy_canvas.translate_coords(event.pos)
 
-            ### Snap coordinates
-            x, y = self.app.geo_editor.snap(self.pos[0], self.pos[1])
-
-            self.pos = (x, y)
-            # print(self.active_tool)
-
             # Selection with left mouse button
             if self.active_tool is not None and event.button is 1:
                 # Dispatch event to active_tool
@@ -2100,7 +2169,11 @@ class FlatCAMExcEditor(QtCore.QObject):
                     if key_modifier == modifier_to_use:
                         self.select_tool(self.active_tool.name)
                     else:
-                        self.select_tool("select")
+                        # return to Select tool but not for FCPad
+                        if isinstance(self.active_tool, FCDrillAdd):
+                            self.select_tool(self.active_tool.name)
+                        else:
+                            self.select_tool("drill_select")
                         return
 
                 if isinstance(self.active_tool, FCDrillSelect):
@@ -2187,10 +2260,9 @@ class FlatCAMExcEditor(QtCore.QObject):
             self.storage.insert(shape)  # TODO: Check performance
 
     def on_exc_click_release(self, event):
-        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
-
         self.modifiers = QtWidgets.QApplication.keyboardModifiers()
 
+        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
         if self.app.grid_status():
             pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
         else:
@@ -2200,11 +2272,29 @@ class FlatCAMExcEditor(QtCore.QObject):
         # canvas menu
         try:
             if event.button == 2:  # right click
-                if self.app.panning_action is True:
-                    self.app.panning_action = False
-                else:
-                    self.app.cursor = QtGui.QCursor()
-                    self.app.ui.popMenu.popup(self.app.cursor.pos())
+                if self.app.ui.popMenu.mouse_is_panning is False:
+                    try:
+                        QtGui.QGuiApplication.restoreOverrideCursor()
+                    except:
+                        pass
+                    if self.active_tool.complete is False and not isinstance(self.active_tool, FCDrillSelect):
+                        self.active_tool.complete = True
+                        self.in_action = False
+                        self.delete_utility_geometry()
+                        self.app.inform.emit(_("[success] Done."))
+                        self.select_tool('drill_select')
+                    else:
+                        if isinstance(self.active_tool, FCDrillAdd):
+                            self.active_tool.complete = True
+                            self.in_action = False
+                            self.delete_utility_geometry()
+                            self.app.inform.emit(_("[success] Done."))
+                            self.select_tool('drill_select')
+
+                        self.app.cursor = QtGui.QCursor()
+                        self.app.populate_cmenu_grids()
+                        self.app.ui.popMenu.popup(self.app.cursor.pos())
+
         except Exception as e:
             log.warning("Error: %s" % str(e))
             raise
@@ -2216,13 +2306,13 @@ class FlatCAMExcEditor(QtCore.QObject):
                 if self.app.selection_type is not None:
                     self.draw_selection_area_handler(self.pos, pos, self.app.selection_type)
                     self.app.selection_type = None
+
                 elif isinstance(self.active_tool, FCDrillSelect):
-                    # Dispatch event to active_tool
-                    # msg = self.active_tool.click(self.app.geo_editor.snap(event.xdata, event.ydata))
-                    # msg = self.active_tool.click_release((self.pos[0], self.pos[1]))
-                    # self.app.inform.emit(msg)
                     self.active_tool.click_release((self.pos[0], self.pos[1]))
-                    self.replot()
+
+                    # if there are selected objects then plot them
+                    if self.selected:
+                        self.replot()
         except Exception as e:
             log.warning("Error: %s" % str(e))
             raise
@@ -2264,7 +2354,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                         if self.tool2tooldia[key] == storage:
                             item = self.tools_table_exc.item((key - 1), 1)
                             self.tools_table_exc.setCurrentItem(item)
-                            self.last_tool_selected = key
+                            self.last_tool_selected = int(key)
 
         self.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
 
@@ -2287,16 +2377,12 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.x = event.xdata
         self.y = event.ydata
 
-        # Prevent updates on pan
-        # if len(event.buttons) > 0:
-        #     return
+        self.app.ui.popMenu.mouse_is_panning = False
 
         # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
-        if event.button == 2:
-            self.app.panning_action = True
+        if event.button == 2 and event.is_dragging == 1:
+            self.app.ui.popMenu.mouse_is_panning = True
             return
-        else:
-            self.app.panning_action = False
 
         try:
             x = float(event.xdata)
@@ -2308,7 +2394,13 @@ class FlatCAMExcEditor(QtCore.QObject):
             return
 
         ### Snap coordinates
-        x, y = self.app.geo_editor.app.geo_editor.snap(x, y)
+        if self.app.grid_status():
+            x, y = self.app.geo_editor.snap(x, y)
+            self.app.app_cursor.enabled = True
+            # Update cursor
+            self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color='black', size=20)
+        else:
+            self.app.app_cursor.enabled = False
 
         self.snap_x = x
         self.snap_y = y
@@ -2330,23 +2422,27 @@ class FlatCAMExcEditor(QtCore.QObject):
         geo = self.active_tool.utility_geometry(data=(x, y))
 
         if isinstance(geo, DrawToolShape) and geo.geo is not None:
-
             # Remove any previous utility shape
             self.tool_shape.clear(update=True)
             self.draw_utility_geometry(geo=geo)
 
         ### Selection area on canvas section ###
-        dx = pos[0] - self.pos[0]
         if event.is_dragging == 1 and event.button == 1:
-            self.app.delete_selection_shape()
-            if dx < 0:
-                self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x,y),
-                     color=self.app.defaults["global_alt_sel_line"],
-                     face_color=self.app.defaults['global_alt_sel_fill'])
-                self.app.selection_type = False
+            # I make an exception for FCDrillAdd and FCDrillArray because clicking and dragging while making regions
+            # can create strange issues
+            if isinstance(self.active_tool, FCDrillAdd) or isinstance(self.active_tool, FCDrillArray):
+                pass
             else:
-                self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x,y))
-                self.app.selection_type = True
+                dx = pos[0] - self.pos[0]
+                self.app.delete_selection_shape()
+                if dx < 0:
+                    self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x,y),
+                         color=self.app.defaults["global_alt_sel_line"],
+                         face_color=self.app.defaults['global_alt_sel_fill'])
+                    self.app.selection_type = False
+                else:
+                    self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x,y))
+                    self.app.selection_type = True
         else:
             self.app.selection_type = None
 
@@ -2587,21 +2683,21 @@ class FlatCAMExcEditor(QtCore.QObject):
             self.linear_angle_label.hide()
 
     def exc_add_drill(self):
-        self.select_tool('add')
+        self.select_tool('drill_add')
         return
 
     def exc_add_drill_array(self):
-        self.select_tool('add_array')
+        self.select_tool('drill_array')
         return
 
     def exc_resize_drills(self):
-        self.select_tool('resize')
+        self.select_tool('drill_resize')
         return
 
     def exc_copy_drills(self):
-        self.select_tool('copy')
+        self.select_tool('drill_copy')
         return
 
     def exc_move_drills(self):
-        self.select_tool('move')
+        self.select_tool('drill_move')
         return

+ 224 - 74
flatcamEditors/FlatCAMGeoEditor.py

@@ -18,7 +18,7 @@ from FlatCAMTool import FlatCAMTool
 from flatcamGUI.ObjectUI import LengthEntry, RadioSet
 
 from shapely.geometry import LineString, LinearRing, MultiLineString
-from shapely.ops import cascaded_union
+from shapely.ops import cascaded_union, unary_union
 import shapely.affinity as affinity
 
 from numpy import arctan2, Inf, array, sqrt, sign, dot
@@ -475,9 +475,9 @@ class PaintOptionsTool(FlatCAMTool):
         )
         grid.addWidget(methodlabel, 3, 0)
         self.paintmethod_combo = RadioSet([
-            {"label": _("Standard"), "value": "standard"},
-            {"label": _("Seed-based"), "value": "seed"},
-            {"label": _("Straight lines"), "value": "lines"}
+            {"label": "Standard", "value": "standard"},
+            {"label": "Seed-based", "value": "seed"},
+            {"label": "Straight lines", "value": "lines"}
         ], orientation='vertical', stretch=False)
         grid.addWidget(self.paintmethod_combo, 3, 1)
 
@@ -2875,8 +2875,12 @@ class FlatCAMGeoEditor(QtCore.QObject):
         def gridx_changed(goption, gentry):
             entry2option(option=goption, entry=gentry)
             # if the grid link is checked copy the value in the GridX field to GridY
+            try:
+                val = float(self.app.ui.grid_gap_x_entry.get_value())
+            except ValueError:
+                return
             if self.app.ui.grid_gap_link_cb.isChecked():
-                self.app.ui.grid_gap_y_entry.set_value(self.app.ui.grid_gap_x_entry.get_value())
+                self.app.ui.grid_gap_y_entry.set_value(val)
 
         self.app.ui.grid_gap_x_entry.setValidator(QtGui.QDoubleValidator())
         self.app.ui.grid_gap_x_entry.textChanged.connect(
@@ -2970,6 +2974,11 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         self.app.ui.snap_toolbar.setDisabled(False)
 
+        self.app.ui.popmenu_disable.setVisible(False)
+        self.app.ui.cmenu_newmenu.menuAction().setVisible(False)
+        self.app.ui.popmenu_properties.setVisible(False)
+        self.app.ui.g_editor_cmenu.menuAction().setVisible(True)
+
         # prevent the user to change anything in the Selected Tab while the Geo Editor is active
         sel_tab_widget_list = self.app.ui.selected_tab.findChildren(QtWidgets.QWidget)
         for w in sel_tab_widget_list:
@@ -2979,6 +2988,11 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.editor_active = True
 
     def deactivate(self):
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         # adjust the status of the menu entries related to the editor
         self.app.ui.menueditedit.setDisabled(False)
         self.app.ui.menueditok.setDisabled(True)
@@ -3033,6 +3047,13 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # Tell the app that the editor is no longer active
         self.editor_active = False
 
+        self.app.ui.popmenu_disable.setVisible(True)
+        self.app.ui.cmenu_newmenu.menuAction().setVisible(True)
+        self.app.ui.popmenu_properties.setVisible(True)
+        self.app.ui.grb_editor_cmenu.menuAction().setVisible(False)
+        self.app.ui.e_editor_cmenu.menuAction().setVisible(False)
+        self.app.ui.g_editor_cmenu.menuAction().setVisible(False)
+
         try:
             # re-enable all the widgets in the Selected Tab that were disabled after entering in Edit Geometry Mode
             sel_tab_widget_list = self.app.ui.selected_tab.findChildren(QtWidgets.QWidget)
@@ -3061,7 +3082,21 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app.plotcanvas.vis_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
         self.app.plotcanvas.vis_disconnect('mouse_double_click', self.app.on_double_click_over_plot)
 
-        self.app.collection.view.clicked.disconnect()
+        # self.app.collection.view.clicked.disconnect()
+        self.app.ui.popmenu_copy.triggered.disconnect()
+        self.app.ui.popmenu_delete.triggered.disconnect()
+        self.app.ui.popmenu_move.triggered.disconnect()
+
+        self.app.ui.popmenu_copy.triggered.connect(lambda: self.select_tool('copy'))
+        self.app.ui.popmenu_delete.triggered.connect(self.on_delete_btn)
+        self.app.ui.popmenu_move.triggered.connect(lambda: self.select_tool('move'))
+
+        # Geometry Editor
+        self.app.ui.draw_line.triggered.connect(self.draw_tool_path)
+        self.app.ui.draw_rect.triggered.connect(self.draw_tool_rectangle)
+        self.app.ui.draw_cut.triggered.connect(self.cutpath)
+        self.app.ui.draw_move.triggered.connect(self.on_move)
+
 
     def disconnect_canvas_event_handlers(self):
         # we restore the key and mouse control to FlatCAMApp method
@@ -3071,12 +3106,50 @@ class FlatCAMGeoEditor(QtCore.QObject):
         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)
         self.app.plotcanvas.vis_connect('mouse_double_click', self.app.on_double_click_over_plot)
-        self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
+        # self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
 
         self.canvas.vis_disconnect('mouse_press', self.on_canvas_click)
         self.canvas.vis_disconnect('mouse_move', self.on_canvas_move)
         self.canvas.vis_disconnect('mouse_release', self.on_geo_click_release)
 
+        try:
+            self.app.ui.popmenu_copy.triggered.disconnect(lambda: self.select_tool('copy'))
+        except TypeError:
+            pass
+        try:
+            self.app.ui.popmenu_delete.triggered.disconnect(self.on_delete_btn)
+        except TypeError:
+            pass
+        try:
+            self.app.ui.popmenu_move.triggered.disconnect(lambda: self.select_tool('move'))
+        except TypeError:
+            pass
+
+        self.app.ui.popmenu_copy.triggered.connect(self.app.on_copy_object)
+        self.app.ui.popmenu_delete.triggered.connect(self.app.on_delete)
+        self.app.ui.popmenu_move.triggered.connect(self.app.obj_move)
+
+        # Geometry Editor
+        try:
+            self.app.ui.draw_line.triggered.disconnect(self.draw_tool_path)
+        except TypeError:
+            pass
+
+        try:
+            self.app.ui.draw_rect.triggered.disconnect(self.draw_tool_rectangle)
+        except TypeError:
+            pass
+
+        try:
+            self.app.ui.draw_cut.triggered.disconnect(self.cutpath)
+        except TypeError:
+            pass
+
+        try:
+            self.app.ui.draw_move.triggered.disconnect(self.on_move)
+        except TypeError:
+            pass
+
     def add_shape(self, shape):
         """
         Adds a shape to the shape storage.
@@ -3117,31 +3190,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.tool_shape.clear(update=True)
         self.tool_shape.redraw()
 
-    def cutpath(self):
-        selected = self.get_selected()
-        tools = selected[1:]
-        toolgeo = cascaded_union([shp.geo for shp in tools])
-
-        target = selected[0]
-        if type(target.geo) == Polygon:
-            for ring in poly2rings(target.geo):
-                self.add_shape(DrawToolShape(ring.difference(toolgeo)))
-            self.delete_shape(target)
-        elif type(target.geo) == LineString or type(target.geo) == LinearRing:
-            self.add_shape(DrawToolShape(target.geo.difference(toolgeo)))
-            self.delete_shape(target)
-        elif type(target.geo) == MultiLineString:
-            try:
-                for linestring in target.geo:
-                    self.add_shape(DrawToolShape(linestring.difference(toolgeo)))
-            except:
-                self.app.log.warning("Current LinearString does not intersect the target")
-            self.delete_shape(target)
-        else:
-            self.app.log.warning("Not implemented. Object type: %s" % str(type(target.geo)))
-
-        self.replot()
-
     def toolbar_tool_toggle(self, key):
         self.options[key] = self.sender().isChecked()
         if self.options[key] == True:
@@ -3268,15 +3316,21 @@ class FlatCAMGeoEditor(QtCore.QObject):
         :return: None
         """
 
+        self.pos = self.canvas.vispy_canvas.translate_coords(event.pos)
+
+        if self.app.grid_status():
+            self.pos  = self.app.geo_editor.snap(self.pos[0], self.pos[1])
+            self.app.app_cursor.enabled = True
+            # Update cursor
+            self.app.app_cursor.set_data(np.asarray([(self.pos[0], self.pos[1])]), symbol='++', edge_color='black',
+                                         size=20)
+        else:
+            self.pos = (self.pos[0], self.pos[1])
+            self.app.app_cursor.enabled = False
+
         if event.button is 1:
             self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
                                                    "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
-            self.pos = self.canvas.vispy_canvas.translate_coords(event.pos)
-
-            ### Snap coordinates
-            x, y = self.snap(self.pos[0], self.pos[1])
-
-            self.pos = (x, y)
 
             modifiers = QtWidgets.QApplication.keyboardModifiers()
             # If the SHIFT key is pressed when LMB is clicked then the coordinates are copied to clipboard
@@ -3288,7 +3342,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
             # Selection with left mouse button
             if self.active_tool is not None and event.button is 1:
                 # Dispatch event to active_tool
-                # msg = self.active_tool.click(self.snap(event.xdata, event.ydata))
                 msg = self.active_tool.click(self.snap(self.pos[0], self.pos[1]))
 
                 # If it is a shape generating tool
@@ -3303,13 +3356,19 @@ class FlatCAMGeoEditor(QtCore.QObject):
                     else:
                         modifier_to_use = Qt.ShiftModifier
 
+                    if isinstance(self.active_tool, FCText):
+                        self.select_tool("select")
+                    else:
+                        self.select_tool(self.active_tool.name)
+
+
                     # if modifier key is pressed then we add to the selected list the current shape but if
                     # it's already in the selected list, we removed it. Therefore first click selects, second deselects.
-                    if key_modifier == modifier_to_use:
-                        self.select_tool(self.active_tool.name)
-                    else:
-                        self.select_tool("select")
-                        return
+                    # if key_modifier == modifier_to_use:
+                    #     self.select_tool(self.active_tool.name)
+                    # else:
+                    #     self.select_tool("select")
+                    #     return
 
                 if isinstance(self.active_tool, FCSelect):
                     # self.app.log.debug("Replotting after click.")
@@ -3334,16 +3393,12 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.x = event.xdata
         self.y = event.ydata
 
-        # Prevent updates on pan
-        # if len(event.buttons) > 0:
-        #     return
+        self.app.ui.popMenu.mouse_is_panning = False
 
         # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
-        if event.button == 2:
-            self.app.panning_action = True
+        if event.button == 2 and event.is_dragging == 1:
+            self.app.ui.popMenu.mouse_is_panning = True
             return
-        else:
-            self.app.panning_action = False
 
         try:
             x = float(event.xdata)
@@ -3355,7 +3410,13 @@ class FlatCAMGeoEditor(QtCore.QObject):
             return
 
         ### Snap coordinates
-        x, y = self.snap(x, y)
+        if self.app.grid_status():
+            x, y = self.snap(x, y)
+            self.app.app_cursor.enabled = True
+            # Update cursor
+            self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color='black', size=20)
+        else:
+            self.app.app_cursor.enabled = False
 
         self.snap_x = x
         self.snap_y = y
@@ -3396,9 +3457,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
         else:
             self.app.selection_type = None
 
-        # Update cursor
-        self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color='black', size=20)
-
     def on_geo_click_release(self, event):
         pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
 
@@ -3411,12 +3469,23 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # canvas menu
         try:
             if event.button == 2:  # right click
-                if self.app.panning_action is True:
-                    self.app.panning_action = False
-                else:
+                if self.app.ui.popMenu.mouse_is_panning is False:
                     if self.in_action is False:
-                        self.app.cursor = QtGui.QCursor()
-                        self.app.ui.popMenu.popup(self.app.cursor.pos())
+                        try:
+                            QtGui.QGuiApplication.restoreOverrideCursor()
+                        except:
+                            pass
+
+                        if self.active_tool.complete is False and not isinstance(self.active_tool, FCSelect):
+                            self.active_tool.complete = True
+                            self.in_action = False
+                            self.delete_utility_geometry()
+                            self.app.inform.emit(_("[success] Done."))
+                            self.select_tool('select')
+                        else:
+                            self.app.cursor = QtGui.QCursor()
+                            self.app.populate_cmenu_grids()
+                            self.app.ui.popMenu.popup(self.app.cursor.pos())
                     else:
                         # if right click on canvas and the active tool need to be finished (like Path or Polygon)
                         # right mouse click will finish the action
@@ -3426,19 +3495,20 @@ class FlatCAMGeoEditor(QtCore.QObject):
                             if self.active_tool.complete:
                                 self.on_shape_complete()
                                 self.app.inform.emit(_("[success] Done."))
+                                self.select_tool(self.active_tool.name)
 
                                 # MS: always return to the Select Tool if modifier key is not pressed
                                 # else return to the current tool
-                                key_modifier = QtWidgets.QApplication.keyboardModifiers()
-                                if self.app.defaults["global_mselect_key"] == 'Control':
-                                    modifier_to_use = Qt.ControlModifier
-                                else:
-                                    modifier_to_use = Qt.ShiftModifier
-
-                                if key_modifier == modifier_to_use:
-                                    self.select_tool(self.active_tool.name)
-                                else:
-                                    self.select_tool("select")
+                                # key_modifier = QtWidgets.QApplication.keyboardModifiers()
+                                # if self.app.defaults["global_mselect_key"] == 'Control':
+                                #     modifier_to_use = Qt.ControlModifier
+                                # else:
+                                #     modifier_to_use = Qt.ShiftModifier
+                                #
+                                # if key_modifier == modifier_to_use:
+                                #     self.select_tool(self.active_tool.name)
+                                # else:
+                                #     self.select_tool("select")
 
         except Exception as e:
             log.warning("Error: %s" % str(e))
@@ -3781,7 +3851,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         :return: None.
         """
 
-        results = cascaded_union([t.geo for t in self.get_selected()])
+        results = unary_union([t.geo for t in self.get_selected()])
 
         # Delete originals.
         for_deletion = [s for s in self.get_selected()]
@@ -3795,9 +3865,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         self.replot()
 
-    def intersection(self):
+    def intersection_2(self):
         """
-        Makes intersectino of selected polygons. Original polygons are deleted.
+        Makes intersection of selected polygons. Original polygons are deleted.
 
         :return: None
         """
@@ -3827,11 +3897,67 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         self.replot()
 
+    def intersection(self):
+        """
+        Makes intersection of selected polygons. Original polygons are deleted.
+
+        :return: None
+        """
+
+        shapes = self.get_selected()
+        results = []
+        intact = []
+
+        try:
+            intersector = shapes[0].geo
+        except Exception as e:
+            log.debug("FlatCAMGeoEditor.intersection() --> %s" % str(e))
+            self.app.inform.emit(_("[WARNING_NOTCL] A selection of at least 2 geo items is required to do Intersection."))
+            self.select_tool('select')
+            return
+
+        for shape in shapes[1:]:
+            if intersector.intersects(shape.geo):
+                results.append(intersector.intersection(shape.geo))
+            else:
+                intact.append(shape)
+
+        if len(results) != 0:
+            # Delete originals.
+            for_deletion = [s for s in self.get_selected()]
+            for shape in for_deletion:
+                if shape not in intact:
+                    self.delete_shape(shape)
+
+            for geo in results:
+                self.add_shape(DrawToolShape(geo))
+
+        # Selected geometry is now gone!
+        self.selected = []
+        self.replot()
+
     def subtract(self):
         selected = self.get_selected()
         try:
             tools = selected[1:]
-            toolgeo = cascaded_union([shp.geo for shp in tools])
+            toolgeo = unary_union([shp.geo for shp in tools])
+            result = selected[0].geo.difference(toolgeo)
+
+            for_deletion = [s for s in self.get_selected()]
+            for shape in for_deletion:
+                self.delete_shape(shape)
+
+            self.add_shape(DrawToolShape(result))
+
+            self.replot()
+        except Exception as e:
+            log.debug(str(e))
+
+    def subtract_2(self):
+        selected = self.get_selected()
+        try:
+            tools = selected[1:]
+            toolgeo = unary_union([shp.geo for shp in tools])
             result = selected[0].geo.difference(toolgeo)
 
             self.delete_shape(selected[0])
@@ -3841,6 +3967,30 @@ class FlatCAMGeoEditor(QtCore.QObject):
         except Exception as e:
             log.debug(str(e))
 
+    def cutpath(self):
+        selected = self.get_selected()
+        tools = selected[1:]
+        toolgeo = unary_union([shp.geo for shp in tools])
+
+        target = selected[0]
+        if type(target.geo) == Polygon:
+            for ring in poly2rings(target.geo):
+                self.add_shape(DrawToolShape(ring.difference(toolgeo)))
+        elif type(target.geo) == LineString or type(target.geo) == LinearRing:
+            self.add_shape(DrawToolShape(target.geo.difference(toolgeo)))
+        elif type(target.geo) == MultiLineString:
+            try:
+                for linestring in target.geo:
+                    self.add_shape(DrawToolShape(linestring.difference(toolgeo)))
+            except:
+                self.app.log.warning("Current LinearString does not intersect the target")
+        else:
+            self.app.log.warning("Not implemented. Object type: %s" % str(type(target.geo)))
+            return
+
+        self.delete_shape(target)
+        self.replot()
+
     def buffer(self, buf_distance, join_style):
         selected = self.get_selected()
 

+ 169 - 83
flatcamEditors/FlatCAMGrbEditor.py

@@ -1613,22 +1613,25 @@ class FCApertureSelect(DrawTool):
         key_modifier = QtWidgets.QApplication.keyboardModifiers()
 
         for storage in self.grb_editor_app.storage_dict:
-            for shape in self.grb_editor_app.storage_dict[storage]['solid_geometry']:
-                if Point(point).within(shape.geo):
-                    if (self.grb_editor_app.app.defaults["global_mselect_key"] == 'Control' and
-                        key_modifier == Qt.ControlModifier) or \
-                            (self.grb_editor_app.app.defaults["global_mselect_key"] == 'Shift' and
-                             key_modifier == Qt.ShiftModifier):
-
-                        if shape in self.draw_app.selected:
-                            self.draw_app.selected.remove(shape)
+            try:
+                for shape in self.grb_editor_app.storage_dict[storage]['solid_geometry']:
+                    if Point(point).within(shape.geo):
+                        if (self.grb_editor_app.app.defaults["global_mselect_key"] == 'Control' and
+                            key_modifier == Qt.ControlModifier) or \
+                                (self.grb_editor_app.app.defaults["global_mselect_key"] == 'Shift' and
+                                 key_modifier == Qt.ShiftModifier):
+
+                            if shape in self.draw_app.selected:
+                                self.draw_app.selected.remove(shape)
+                            else:
+                                # add the object to the selected shapes
+                                self.draw_app.selected.append(shape)
+                                sel_aperture.add(storage)
                         else:
-                            # add the object to the selected shapes
                             self.draw_app.selected.append(shape)
                             sel_aperture.add(storage)
-                    else:
-                        self.draw_app.selected.append(shape)
-                        sel_aperture.add(storage)
+            except KeyError:
+                pass
 
         # select the aperture in the Apertures Table that is associated with the selected shape
         try:
@@ -1980,7 +1983,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         self.pad_axis_radio = RadioSet([{'label': 'X', 'value': 'X'},
                                           {'label': 'Y', 'value': 'Y'},
-                                          {'label': _('Angle'), 'value': 'A'}])
+                                          {'label': 'Angle', 'value': 'A'}])
         self.pad_axis_radio.set_value('X')
         self.linear_form.addRow(self.pad_axis_label, self.pad_axis_radio)
 
@@ -2174,11 +2177,12 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         # flag to show if the object was modified
         self.is_modified = False
-
         self.edited_obj_name = ""
-
         self.tool_row = 0
 
+        # A QTimer
+        self.plot_thread = None
+
         # store the status of the editor so the Delete at object level will not work until the edit is finished
         self.editor_active = False
 
@@ -2629,14 +2633,15 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.edited_obj_name = self.name_entry.get_value()
 
     def on_aptype_changed(self, current_text):
+        # 'O' is letter O not zero.
         if current_text == 'R' or current_text == 'O':
             self.apdim_lbl.show()
             self.apdim_entry.show()
-            self.apsize_entry.setReadOnly(True)
+            self.apsize_entry.setDisabled(True)
         else:
             self.apdim_lbl.hide()
             self.apdim_entry.hide()
-            self.apsize_entry.setReadOnly(False)
+            self.apsize_entry.setDisabled(False)
 
     def activate_grb_editor(self):
         # adjust the status of the menu entries related to the editor
@@ -2684,10 +2689,20 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.app.ui.popmenu_edit.setVisible(False)
         self.app.ui.popmenu_save.setVisible(True)
 
+        self.app.ui.popmenu_disable.setVisible(False)
+        self.app.ui.cmenu_newmenu.menuAction().setVisible(False)
+        self.app.ui.popmenu_properties.setVisible(False)
+        self.app.ui.grb_editor_cmenu.menuAction().setVisible(True)
+
         # Tell the App that the editor is active
         self.editor_active = True
 
     def deactivate_grb_editor(self):
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         # adjust the status of the menu entries related to the editor
         self.app.ui.menueditedit.setDisabled(False)
         self.app.ui.menueditok.setDisabled(True)
@@ -2741,14 +2756,17 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         self.app.ui.update_obj_btn.setEnabled(False)
 
-        self.app.ui.g_editor_cmenu.setEnabled(False)
-        self.app.ui.grb_editor_cmenu.setEnabled(False)
-        self.app.ui.e_editor_cmenu.setEnabled(False)
-
         # adjust the visibility of some of the canvas context menu
         self.app.ui.popmenu_edit.setVisible(True)
         self.app.ui.popmenu_save.setVisible(False)
 
+        self.app.ui.popmenu_disable.setVisible(True)
+        self.app.ui.cmenu_newmenu.menuAction().setVisible(True)
+        self.app.ui.popmenu_properties.setVisible(True)
+        self.app.ui.g_editor_cmenu.menuAction().setVisible(False)
+        self.app.ui.e_editor_cmenu.menuAction().setVisible(False)
+        self.app.ui.grb_editor_cmenu.menuAction().setVisible(False)
+
         # Show original geometry
         if self.gerber_obj:
             self.gerber_obj.visible = True
@@ -2771,6 +2789,20 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.canvas.vis_disconnect('mouse_double_click', self.app.on_double_click_over_plot)
         self.app.collection.view.clicked.disconnect()
 
+        self.app.ui.popmenu_copy.triggered.disconnect()
+        self.app.ui.popmenu_delete.triggered.disconnect()
+        self.app.ui.popmenu_move.triggered.disconnect()
+
+        self.app.ui.popmenu_copy.triggered.connect(self.on_copy_button)
+        self.app.ui.popmenu_delete.triggered.connect(self.on_delete_btn)
+        self.app.ui.popmenu_move.triggered.connect(self.on_move_button)
+
+        # Gerber Editor
+        self.app.ui.grb_draw_pad.triggered.connect(self.on_pad_add)
+        self.app.ui.grb_draw_pad_array.triggered.connect(self.on_pad_add_array)
+        self.app.ui.grb_draw_track.triggered.connect(self.on_track_add)
+        self.app.ui.grb_draw_region.triggered.connect(self.on_region_add)
+
     def disconnect_canvas_event_handlers(self):
 
         # we restore the key and mouse control to FlatCAMApp method
@@ -2786,6 +2818,47 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.canvas.vis_disconnect('mouse_move', self.on_canvas_move)
         self.canvas.vis_disconnect('mouse_release', self.on_grb_click_release)
 
+        try:
+            self.app.ui.popmenu_copy.triggered.disconnect(self.on_copy_button)
+        except TypeError:
+            pass
+
+        try:
+            self.app.ui.popmenu_delete.triggered.disconnect(self.on_delete_btn)
+        except TypeError:
+            pass
+
+        try:
+            self.app.ui.popmenu_move.triggered.disconnect(self.on_move_button)
+        except TypeError:
+            pass
+
+        self.app.ui.popmenu_copy.triggered.connect(self.app.on_copy_object)
+        self.app.ui.popmenu_delete.triggered.connect(self.app.on_delete)
+        self.app.ui.popmenu_move.triggered.connect(self.app.obj_move)
+
+        # Gerber Editor
+
+        try:
+            self.app.ui.grb_draw_pad.triggered.disconnect(self.on_pad_add)
+        except TypeError:
+            pass
+
+        try:
+            self.app.ui.grb_draw_pad_array.triggered.disconnect(self.on_pad_add_array)
+        except TypeError:
+            pass
+
+        try:
+            self.app.ui.grb_draw_track.triggered.disconnect(self.on_track_add)
+        except TypeError:
+            pass
+
+        try:
+            self.app.ui.grb_draw_region.triggered.disconnect(self.on_region_add)
+        except TypeError:
+            pass
+
     def clear(self):
         self.active_tool = None
         # self.shape_buffer = []
@@ -3160,26 +3233,31 @@ class FlatCAMGrbEditor(QtCore.QObject):
     def on_canvas_click(self, event):
         """
         event.x and .y have canvas coordinates
-        event.xdaya and .ydata have plot coordinates
+        event.xdata and .ydata have plot coordinates
 
-        :param event: Event object dispatched by Matplotlib
+        :param event: Event object dispatched by VisPy
         :return: None
         """
 
+        self.pos = self.canvas.vispy_canvas.translate_coords(event.pos)
+
+        if self.app.grid_status():
+            self.pos  = self.app.geo_editor.snap(self.pos[0], self.pos[1])
+            self.app.app_cursor.enabled = True
+            # Update cursor
+            self.app.app_cursor.set_data(np.asarray([(self.pos[0], self.pos[1])]), symbol='++', edge_color='black',
+                                         size=20)
+        else:
+            self.pos = (self.pos[0], self.pos[1])
+            self.app.app_cursor.enabled = False
+
         if event.button is 1:
             self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
                                                    "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
-            self.pos = self.canvas.vispy_canvas.translate_coords(event.pos)
-
-            ### Snap coordinates
-            x, y = self.app.geo_editor.snap(self.pos[0], self.pos[1])
-
-            self.pos = (x, y)
 
             # Selection with left mouse button
             if self.active_tool is not None and event.button is 1:
                 # Dispatch event to active_tool
-                # msg = self.active_tool.click(self.app.geo_editor.snap(event.xdata, event.ydata))
                 msg = self.active_tool.click(self.app.geo_editor.snap(self.pos[0], self.pos[1]))
 
                 # If it is a shape generating tool
@@ -3213,10 +3291,9 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 self.app.log.debug("No active tool to respond to click!")
 
     def on_grb_click_release(self, event):
-        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
-
         self.modifiers = QtWidgets.QApplication.keyboardModifiers()
 
+        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
         if self.app.grid_status():
             pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
         else:
@@ -3226,9 +3303,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # canvas menu
         try:
             if event.button == 2:  # right click
-                if self.app.panning_action is True:
-                    self.app.panning_action = False
-                else:
+                if self.app.ui.popMenu.mouse_is_panning is False:
                     if self.in_action is False:
                         try:
                             QtGui.QGuiApplication.restoreOverrideCursor()
@@ -3243,6 +3318,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                             self.select_tool('select')
                         else:
                             self.app.cursor = QtGui.QCursor()
+                            self.app.populate_cmenu_grids()
                             self.app.ui.popMenu.popup(self.app.cursor.pos())
                     else:
                         # if right click on canvas and the active tool need to be finished (like Path or Polygon)
@@ -3282,10 +3358,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
                     self.app.selection_type = None
 
                 elif isinstance(self.active_tool, FCApertureSelect):
-                    # Dispatch event to active_tool
-                    # msg = self.active_tool.click(self.app.geo_editor.snap(event.xdata, event.ydata))
-                    # msg = self.active_tool.click_release((self.pos[0], self.pos[1]))
-                    # self.app.inform.emit(msg)
                     self.active_tool.click_release((self.pos[0], self.pos[1]))
 
                     # if there are selected objects then plot them
@@ -3303,26 +3375,29 @@ class FlatCAMGrbEditor(QtCore.QObject):
         :type Bool
         :return:
         """
+
         poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
         sel_aperture = set()
         self.apertures_table.clearSelection()
 
         self.app.delete_selection_shape()
         for storage in self.storage_dict:
-            for obj in self.storage_dict[storage]['solid_geometry']:
-                if (sel_type is True and poly_selection.contains(obj.geo)) or \
-                        (sel_type is False and poly_selection.intersects(obj.geo)):
-                    if self.key == self.app.defaults["global_mselect_key"]:
-                        if obj in self.selected:
-                            self.selected.remove(obj)
+            try:
+                for obj in self.storage_dict[storage]['solid_geometry']:
+                    if (sel_type is True and poly_selection.contains(obj.geo)) or \
+                            (sel_type is False and poly_selection.intersects(obj.geo)):
+                        if self.key == self.app.defaults["global_mselect_key"]:
+                            if obj in self.selected:
+                                self.selected.remove(obj)
+                            else:
+                                # add the object to the selected shapes
+                                self.selected.append(obj)
+                                sel_aperture.add(storage)
                         else:
-                            # add the object to the selected shapes
                             self.selected.append(obj)
                             sel_aperture.add(storage)
-                    else:
-                        self.selected.append(obj)
-                        sel_aperture.add(storage)
-
+            except KeyError:
+                pass
         try:
             self.apertures_table.cellPressed.disconnect()
         except:
@@ -3349,22 +3424,18 @@ class FlatCAMGrbEditor(QtCore.QObject):
         :return: None
         """
 
-        pos = self.canvas.vispy_canvas.translate_coords(event.pos)
-        event.xdata, event.ydata = pos[0], pos[1]
+        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
+        event.xdata, event.ydata = pos_canvas[0], pos_canvas[1]
 
         self.x = event.xdata
         self.y = event.ydata
 
-        # Prevent updates on pan
-        # if len(event.buttons) > 0:
-        #     return
+        self.app.ui.popMenu.mouse_is_panning = False
 
         # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
-        if event.button == 2:
-            self.app.panning_action = True
+        if event.button == 2 and event.is_dragging == 1:
+            self.app.ui.popMenu.mouse_is_panning = True
             return
-        else:
-            self.app.panning_action = False
 
         try:
             x = float(event.xdata)
@@ -3376,7 +3447,13 @@ class FlatCAMGrbEditor(QtCore.QObject):
             return
 
         ### Snap coordinates
-        x, y = self.app.geo_editor.app.geo_editor.snap(x, y)
+        if self.app.grid_status():
+            x, y = self.app.geo_editor.snap(x, y)
+            self.app.app_cursor.enabled = True
+            # Update cursor
+            self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color='black', size=20)
+        else:
+            self.app.app_cursor.enabled = False
 
         self.snap_x = x
         self.snap_y = y
@@ -3398,7 +3475,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
         geo = self.active_tool.utility_geometry(data=(x, y))
 
         if isinstance(geo, DrawToolShape) and geo.geo is not None:
-
             # Remove any previous utility shape
             self.tool_shape.clear(update=True)
             self.draw_utility_geometry(geo=geo)
@@ -3410,7 +3486,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             if isinstance(self.active_tool, FCRegion) or isinstance(self.active_tool, FCTrack):
                 pass
             else:
-                dx = pos[0] - self.pos[0]
+                dx = pos_canvas[0] - self.pos[0]
                 self.app.delete_selection_shape()
                 if dx < 0:
                     self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x,y),
@@ -3423,9 +3499,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
         else:
             self.app.selection_type = None
 
-        # Update cursor
-        self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color='black', size=20)
-
     def on_canvas_key_release(self, event):
         self.key = None
 
@@ -3459,15 +3532,18 @@ class FlatCAMGrbEditor(QtCore.QObject):
             self.shapes.clear(update=True)
 
             for storage in self.storage_dict:
-                for shape in self.storage_dict[storage]['solid_geometry']:
-                    if shape.geo is None:
-                        continue
-
-                    if shape in self.selected:
-                        self.plot_shape(geometry=shape.geo, color=self.app.defaults['global_sel_draw_color'],
-                                        linewidth=2)
-                        continue
-                    self.plot_shape(geometry=shape.geo, color=self.app.defaults['global_draw_color'])
+                try:
+                    for shape in self.storage_dict[storage]['solid_geometry']:
+                        if shape.geo is None:
+                            continue
+
+                        if shape in self.selected:
+                            self.plot_shape(geometry=shape.geo, color=self.app.defaults['global_sel_draw_color'],
+                                            linewidth=2)
+                            continue
+                        self.plot_shape(geometry=shape.geo, color=self.app.defaults['global_draw_color'])
+                except KeyError:
+                    pass
 
             for shape in self.utility:
                 self.plot_shape(geometry=shape.geo, linewidth=1)
@@ -3498,6 +3574,13 @@ class FlatCAMGrbEditor(QtCore.QObject):
             self.shapes.add(shape=geometry, color=color, face_color=color+'AF', layer=0)
 
     def start_delayed_plot(self, check_period):
+        """
+        This function starts an QTImer and it will periodically check if all the workers finish the plotting functions
+
+        :param check_period: time at which to check periodically if all plots finished to be plotted
+        :return:
+        """
+
         # self.plot_thread = threading.Thread(target=lambda: self.check_plot_finished(check_period))
         # self.plot_thread.start()
         log.debug("FlatCAMGrbEditor --> Delayed Plot started.")
@@ -3507,7 +3590,12 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.plot_thread.start()
 
     def check_plot_finished(self):
-        # print(self.grb_plot_promises)
+        """
+        If all the promises made are finished then all the shapes are in shapes_storage and can be plotted safely and
+        then the UI is rebuilt accordingly.
+        :return:
+        """
+
         try:
             if not self.grb_plot_promises:
                 self.plot_thread.stop()
@@ -3571,13 +3659,11 @@ class FlatCAMGrbEditor(QtCore.QObject):
             return
 
         for storage in self.storage_dict:
-            # try:
-            #     self.storage_dict[storage].remove(shape)
-            # except:
-            #     pass
-            if shape in self.storage_dict[storage]['solid_geometry']:
-                self.storage_dict[storage]['solid_geometry'].remove(shape)
-
+            try:
+                if shape in self.storage_dict[storage]['solid_geometry']:
+                    self.storage_dict[storage]['solid_geometry'].remove(shape)
+            except KeyError:
+                pass
         if shape in self.selected:
             self.selected.remove(shape)  # TODO: Check performance
 

+ 134 - 59
flatcamGUI/FlatCAMGUI.py

@@ -638,6 +638,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.panelize_btn = self.toolbartools.addAction(QtGui.QIcon('share/panel16.png'), _("Panel Tool"))
         self.film_btn = self.toolbartools.addAction(QtGui.QIcon('share/film16.png'),_( "Film Tool"))
         self.solder_btn = self.toolbartools.addAction(QtGui.QIcon('share/solderpastebis32.png'), _("SolderPaste Tool"))
+        self.sub_btn = self.toolbartools.addAction(QtGui.QIcon('share/sub32.png'), _("Substract Tool"))
+
         self.toolbartools.addSeparator()
 
         self.calculators_btn = self.toolbartools.addAction(QtGui.QIcon('share/calculator24.png'), _("Calculators Tool"))
@@ -699,7 +701,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.grb_convert_poly_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/poligonize32.png'),
                                                                     _("Poligonize"))
 
-
         self.grb_add_semidisc_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/semidisc32.png'), _("SemiDisc"))
         self.grb_add_disc_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/disc32.png'), _("Disc"))
         self.grb_edit_toolbar.addSeparator()
@@ -752,12 +753,27 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         ################
 
         ### Project ###
+        # self.project_tab = QtWidgets.QWidget()
+        # self.project_tab.setObjectName("project_tab")
+        # # project_tab.setMinimumWidth(250)  # Hack
+        # self.project_tab_layout = QtWidgets.QVBoxLayout(self.project_tab)
+        # self.project_tab_layout.setContentsMargins(2, 2, 2, 2)
+        # self.notebook.addTab(self.project_tab,_( "Project"))
+
         self.project_tab = QtWidgets.QWidget()
         self.project_tab.setObjectName("project_tab")
-        # project_tab.setMinimumWidth(250)  # Hack
-        self.project_tab_layout = QtWidgets.QVBoxLayout(self.project_tab)
+
+        self.project_frame_lay = QtWidgets.QVBoxLayout(self.project_tab)
+        self.project_frame_lay.setContentsMargins(0, 0, 0, 0)
+
+        self.project_frame = QtWidgets.QFrame()
+        self.project_frame.setContentsMargins(0, 0, 0, 0)
+        self.project_frame_lay.addWidget(self.project_frame)
+
+        self.project_tab_layout = QtWidgets.QVBoxLayout(self.project_frame)
         self.project_tab_layout.setContentsMargins(2, 2, 2, 2)
-        self.notebook.addTab(self.project_tab,_( "Project"))
+        self.notebook.addTab(self.project_tab, _("Project"))
+        self.project_frame.setDisabled(False)
 
         ### Selected ###
         self.selected_tab = QtWidgets.QWidget()
@@ -1545,12 +1561,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         ##############################################################
         ### HERE WE BUILD THE CONTEXT MENU FOR RMB CLICK ON CANVAS ###
         ##############################################################
-        self.popMenu = QtWidgets.QMenu()
+        self.popMenu = FCMenu()
 
         self.popmenu_disable = self.popMenu.addAction(QtGui.QIcon('share/clear_plot32.png'), _("Disable"))
         self.popMenu.addSeparator()
         self.cmenu_newmenu = self.popMenu.addMenu(QtGui.QIcon('share/file32.png'), _("New"))
         self.popmenu_new_geo = self.cmenu_newmenu.addAction(QtGui.QIcon('share/new_geo32_bis.png'), _("Geometry"))
+        self.popmenu_new_grb = self.cmenu_newmenu.addAction(QtGui.QIcon('share/flatcam_icon32.png'), "Gerber")
         self.popmenu_new_exc = self.cmenu_newmenu.addAction(QtGui.QIcon('share/new_exc32.png'), _("Excellon"))
         self.cmenu_newmenu.addSeparator()
         self.popmenu_new_prj = self.cmenu_newmenu.addAction(QtGui.QIcon('share/file16.png'), _("Project"))
@@ -1576,15 +1593,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.grb_draw_pad_array = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/padarray32.png'), _("Pad Array"))
         self.grb_draw_track = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/track32.png'), _("Track"))
         self.grb_draw_region = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/polygon32.png'), _("Region"))
-        self.grb_editor_cmenu.addSeparator()
-        self.grb_copy = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/copy.png'), _("Copy"))
-        self.grb_delete = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/trash32.png'), _("Delete"))
-        self.grb_move = self.grb_editor_cmenu.addAction(QtGui.QIcon('share/move32.png'), _("Move"))
 
         self.e_editor_cmenu = self.popMenu.addMenu(QtGui.QIcon('share/drill32.png'), _("Exc Editor"))
         self.drill = self.e_editor_cmenu.addAction(QtGui.QIcon('share/drill32.png'), _("Add Drill"))
         self.drill_array = self.e_editor_cmenu.addAction(QtGui.QIcon('share/addarray32.png'), _("Add Drill Array"))
-        self.drill_copy = self.e_editor_cmenu.addAction(QtGui.QIcon('share/copy32.png'), _("Copy Drill(s)"))
 
         self.popMenu.addSeparator()
         self.popmenu_copy = self.popMenu.addAction(QtGui.QIcon('share/copy32.png'), _("Copy"))
@@ -1724,9 +1736,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # start with GRID activated
         self.grid_snap_btn.trigger()
 
-        self.g_editor_cmenu.setEnabled(False)
-        self.grb_editor_cmenu.setEnabled(False)
-        self.e_editor_cmenu.setEnabled(False)
+        self.g_editor_cmenu.menuAction().setVisible(False)
+        self.grb_editor_cmenu.menuAction().setVisible(False)
+        self.e_editor_cmenu.menuAction().setVisible(False)
 
         self.general_defaults_form = GeneralPreferencesUI()
         self.gerber_defaults_form = GerberPreferencesUI()
@@ -1844,13 +1856,15 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.film_btn = self.toolbartools.addAction(QtGui.QIcon('share/film16.png'), _("Film Tool"))
         self.solder_btn = self.toolbartools.addAction(QtGui.QIcon('share/solderpastebis32.png'),
                                                       _("SolderPaste Tool"))
+        self.sub_btn = self.toolbartools.addAction(QtGui.QIcon('share/sub32.png'), _("Substract Tool"))
+
         self.toolbartools.addSeparator()
 
         self.calculators_btn = self.toolbartools.addAction(QtGui.QIcon('share/calculator24.png'),
                                                            _("Calculators Tool"))
         self.transform_btn = self.toolbartools.addAction(QtGui.QIcon('share/transform.png'), _("Transform Tool"))
 
-        ### Drill Editor Toolbar ###
+        ### Excellon Editor Toolbar ###
         self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), _("Select"))
         self.add_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/plus16.png'), _('Add Drill Hole'))
         self.add_drill_array_btn = self.exc_edit_toolbar.addAction(
@@ -1906,6 +1920,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.add_pad_ar_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/padarray32.png'), _('Add Pad Array'))
         self.grb_add_track_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/track32.png'), _("Add Track"))
         self.grb_add_region_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/polygon32.png'), _("Add Region"))
+        self.grb_convert_poly_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/poligonize32.png'),
+                                                                    _("Poligonize"))
+
+        self.grb_add_semidisc_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/semidisc32.png'), _("SemiDisc"))
+        self.grb_add_disc_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/disc32.png'), _("Disc"))
         self.grb_edit_toolbar.addSeparator()
 
         self.aperture_buffer_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/buffer16-2.png'), _('Buffer'))
@@ -1974,7 +1993,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 self.exc_edit_toolbar.setDisabled(True)
                 self.geo_edit_toolbar.setVisible(True)
                 self.geo_edit_toolbar.setDisabled(True)
-                self.grb_edit_toolbar.setVisible(False)
+                self.grb_edit_toolbar.setVisible(True)
                 self.grb_edit_toolbar.setDisabled(True)
 
                 self.corner_snap_btn.setVisible(True)
@@ -2146,6 +2165,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.cutout_tool.run(toggle=True)
                     return
 
+                # Substract Tool
+                if key == QtCore.Qt.Key_W:
+                    self.app.sub_tool.run(toggle=True)
+                    return
+
                 # Panelize Tool
                 if key == QtCore.Qt.Key_Z:
                     self.app.panelize_tool.run(toggle=True)
@@ -2214,6 +2238,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == QtCore.Qt.Key_Space:
                     for select in selected:
                         select.ui.plot_cb.toggle()
+                    self.app.collection.update_view()
                     self.app.delete_selection_shape()
 
                 # New Geometry
@@ -2807,7 +2832,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.exc_editor.replot()
                     # self.select_btn.setChecked(True)
                     # self.on_tool_select('select')
-                    self.app.exc_editor.select_tool('select')
+                    self.app.exc_editor.select_tool('drill_select')
                     return
 
                 # Delete selected object if delete key event comes out of canvas
@@ -2943,7 +2968,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == QtCore.Qt.Key_T or key == 'T':
                     self.app.exc_editor.launched_from_shortcuts = True
                     ## Current application units in Upper Case
-                    self.units = self.general_defaults_group.general_app_group.units_radio.get_value().upper()
+                    self.units = self.general_defaults_form.general_app_group.units_radio.get_value().upper()
                     tool_add_popup = FCInputDialog(title=_("New Tool ..."),
                                                    text=_('Enter a Tool Diameter:'),
                                                    min=0.0000, max=99.9999, decimals=4)
@@ -3047,6 +3072,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                                                    'params': [self.filename, object_type, None]})
 
                     if extension in self.app.pdf_list:
+                        self.app.pdf_tool.periodic_check(1000)
                         self.app.worker_task.emit({'fcn': self.app.pdf_tool.open_pdf,
                                                    'params': [self.filename]})
 
@@ -3483,6 +3509,34 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         self.form_box_child_11.addWidget(self.sel_draw_color_button)
         self.form_box_child_11.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
 
+        # Project Tab items color
+        self.proj_color_label = QtWidgets.QLabel(_('Project Items:'))
+        self.proj_color_label.setToolTip(
+            _("Set the color of the items in Project Tab Tree.")
+        )
+        self.proj_color_entry = FCEntry()
+        self.proj_color_button = QtWidgets.QPushButton()
+        self.proj_color_button.setFixedSize(15, 15)
+
+        self.form_box_child_12 = QtWidgets.QHBoxLayout()
+        self.form_box_child_12.addWidget(self.proj_color_entry)
+        self.form_box_child_12.addWidget(self.proj_color_button)
+        self.form_box_child_12.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        self.proj_color_dis_label = QtWidgets.QLabel(_('Proj. Dis. Items:'))
+        self.proj_color_dis_label.setToolTip(
+            _("Set the color of the items in Project Tab Tree,\n"
+              "for the case when the items are disabled.")
+        )
+        self.proj_color_dis_entry = FCEntry()
+        self.proj_color_dis_button = QtWidgets.QPushButton()
+        self.proj_color_dis_button.setFixedSize(15, 15)
+
+        self.form_box_child_13 = QtWidgets.QHBoxLayout()
+        self.form_box_child_13.addWidget(self.proj_color_dis_entry)
+        self.form_box_child_13.addWidget(self.proj_color_dis_button)
+        self.form_box_child_13.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
         # Just to add empty rows
         self.spacelabel = QtWidgets.QLabel('')
 
@@ -3507,6 +3561,9 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         self.form_box.addRow(self.alt_sl_color_label, self.form_box_child_9)
         self.form_box.addRow(self.draw_color_label, self.form_box_child_10)
         self.form_box.addRow(self.sel_draw_color_label, self.form_box_child_11)
+        self.form_box.addRow(QtWidgets.QLabel(""))
+        self.form_box.addRow(self.proj_color_label, self.form_box_child_12)
+        self.form_box.addRow(self.proj_color_dis_label, self.form_box_child_13)
 
         self.form_box.addRow(self.spacelabel, self.spacelabel)
 
@@ -3589,6 +3646,16 @@ class GeneralGUISetGroupUI(OptionsGroupUI):
         )
         self.hover_cb = FCCheckBox()
 
+        # Enable Selection box
+        self.selection_label = QtWidgets.QLabel(_('Sel. Shape:'))
+        self.selection_label.setToolTip(
+            _("Enable the display of a selection shape for FlatCAM objects.\n"
+              "It is displayed whenever the mouse selects an object\n"
+              "either by clicking or dragging mouse from left to right or\n"
+              "right to left.")
+        )
+        self.selection_cb = FCCheckBox()
+
         # Just to add empty rows
         self.spacelabel = QtWidgets.QLabel('')
 
@@ -3600,6 +3667,7 @@ class GeneralGUISetGroupUI(OptionsGroupUI):
         self.form_box.addRow(self.hdpi_label, self.hdpi_cb)
         self.form_box.addRow(self.clear_label, self.clear_btn)
         self.form_box.addRow(self.hover_label, self.hover_cb)
+        self.form_box.addRow(self.selection_label, self.selection_cb)
 
         # Add the QFormLayout that holds the Application general defaults
         # to the main layout of this TAB
@@ -3667,8 +3735,8 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
                                         "ADVANCED level -> full functionality.\n\n"
                                         "The choice here will influence the parameters in\n"
                                         "the Selected Tab for all kinds of FlatCAM objects."))
-        self.app_level_radio = RadioSet([{'label': _('Basic'), 'value': 'b'},
-                                         {'label': _('Advanced'), 'value': 'a'}])
+        self.app_level_radio = RadioSet([{'label': 'Basic', 'value': 'b'},
+                                         {'label': 'Advanced', 'value': 'a'}])
 
         # Languages for FlatCAM
         self.languagelabel = QtWidgets.QLabel(_('<b>Languages:</b>'))
@@ -3676,6 +3744,13 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
         self.language_cb = FCComboBox()
         self.languagespace = QtWidgets.QLabel('')
         self.language_apply_btn = FCButton(_("Apply Language"))
+        self.language_apply_btn.setToolTip(_("Set the language used throughout FlatCAM.\n"
+                                             "The app will restart after click."
+                                             "Windows: When FlatCAM is installed in Program Files\n"
+                                             "directory, it is possible that the app will not\n"
+                                             "restart after the button is clicked due of Windows\n"
+                                             "security features. In this case the language will be\n"
+                                             "applied at the next app start."))
 
         # Shell StartUp CB
         self.shell_startup_label = QtWidgets.QLabel(_('Shell at StartUp:'))
@@ -3720,14 +3795,14 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
         self.panbuttonlabel.setToolTip(_("Select the mouse button to use for panning:\n"
                                        "- MMB --> Middle Mouse Button\n"
                                        "- RMB --> Right Mouse Button"))
-        self.pan_button_radio = RadioSet([{'label': _('MMB'), 'value': '3'},
-                                     {'label': _('RMB'), 'value': '2'}])
+        self.pan_button_radio = RadioSet([{'label': 'MMB', 'value': '3'},
+                                     {'label': 'RMB', 'value': '2'}])
 
         # Multiple Selection Modifier Key
         self.mselectlabel = QtWidgets.QLabel(_('<b>Multiple Sel:</b>'))
         self.mselectlabel.setToolTip(_("Select the key used for multiple selection."))
-        self.mselect_radio = RadioSet([{'label': _('CTRL'), 'value': 'Control'},
-                                     {'label': _('SHIFT'), 'value': 'Shift'}])
+        self.mselect_radio = RadioSet([{'label': 'CTRL', 'value': 'Control'},
+                                     {'label': 'SHIFT', 'value': 'Shift'}])
 
         # Project at StartUp CB
         self.project_startup_label = QtWidgets.QLabel(_('Project at StartUp:'))
@@ -3955,8 +4030,8 @@ class GerberOptPrefGroupUI(OptionsGroupUI):
             "- conventional / useful when there is no backlash compensation")
         )
         grid0.addWidget(milling_type_label, 3, 0)
-        self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
-                                            {'label': _('Conv.'), 'value': 'cv'}])
+        self.milling_type_radio = RadioSet([{'label': 'Climb', 'value': 'cl'},
+                                            {'label': 'Conv.', 'value': 'cv'}])
         grid0.addWidget(self.milling_type_radio, 3, 1)
 
         # Combine passes
@@ -4224,8 +4299,8 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         )
         hlay3.addWidget(self.excellon_zeros_label)
 
-        self.excellon_zeros_radio = RadioSet([{'label': _('LZ'), 'value': 'L'},
-                                     {'label': _('TZ'), 'value': 'T'}])
+        self.excellon_zeros_radio = RadioSet([{'label': 'LZ', 'value': 'L'},
+                                     {'label': 'TZ', 'value': 'T'}])
         self.excellon_zeros_radio.setToolTip(
             _("This sets the default type of Excellon zeros.\n"
             "If it is not detected in the parsed file the value here\n"
@@ -4252,8 +4327,8 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         )
         hlay4.addWidget(self.excellon_units_label)
 
-        self.excellon_units_radio = RadioSet([{'label': _('INCH'), 'value': 'INCH'},
-                                              {'label': _('MM'), 'value': 'METRIC'}])
+        self.excellon_units_radio = RadioSet([{'label': 'INCH', 'value': 'INCH'},
+                                              {'label': 'MM', 'value': 'METRIC'}])
         self.excellon_units_radio.setToolTip(
             _("This sets the units of Excellon files.\n"
             "Some Excellon files don't have an header\n"
@@ -4291,8 +4366,8 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
             "Travelling Salesman algorithm for path optimization.")
         )
 
-        self.excellon_optimization_radio = RadioSet([{'label': _('MH'), 'value': 'M'},
-                                     {'label': _('Basic'), 'value': 'B'}])
+        self.excellon_optimization_radio = RadioSet([{'label': 'MH', 'value': 'M'},
+                                     {'label': 'Basic', 'value': 'B'}])
         self.excellon_optimization_radio.setToolTip(
             _("This sets the optimization type for the Excellon drill path.\n"
             "If MH is checked then Google OR-Tools algorithm with MetaHeuristic\n"
@@ -4457,9 +4532,9 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
             "When choosing 'Slots' or 'Both', slots will be\n"
             "converted to drills.")
         )
-        self.excellon_gcode_type_radio = RadioSet([{'label': _('Drills'), 'value': 'drills'},
-                                          {'label': _('Slots'), 'value': 'slots'},
-                                          {'label': _('Both'), 'value': 'both'}])
+        self.excellon_gcode_type_radio = RadioSet([{'label': 'Drills', 'value': 'drills'},
+                                          {'label': 'Slots', 'value': 'slots'},
+                                          {'label': 'Both', 'value': 'both'}])
         grid2.addWidget(excellon_gcode_type_label, 9, 0)
         grid2.addWidget(self.excellon_gcode_type_radio, 9, 1)
 
@@ -4643,8 +4718,8 @@ class ExcellonExpPrefGroupUI(OptionsGroupUI):
             _("The units used in the Excellon file.")
         )
 
-        self.excellon_units_radio = RadioSet([{'label': _('INCH'), 'value': 'INCH'},
-                                              {'label': _('MM'), 'value': 'METRIC'}])
+        self.excellon_units_radio = RadioSet([{'label': 'INCH', 'value': 'INCH'},
+                                              {'label': 'MM', 'value': 'METRIC'}])
         self.excellon_units_radio.setToolTip(
             _("The units used in the Excellon file.")
         )
@@ -4699,8 +4774,8 @@ class ExcellonExpPrefGroupUI(OptionsGroupUI):
             "Also it will have to be specified if LZ = leading zeros are kept\n"
             "or TZ = trailing zeros are kept.")
         )
-        self.format_radio = RadioSet([{'label': _('Decimal'), 'value': 'dec'},
-                                      {'label': _('No-Decimal'), 'value': 'ndec'}])
+        self.format_radio = RadioSet([{'label': 'Decimal', 'value': 'dec'},
+                                      {'label': 'No-Decimal', 'value': 'ndec'}])
         self.format_radio.setToolTip(
             _("Select the kind of coordinates format used.\n"
             "Coordinates can be saved with decimal point or without.\n"
@@ -4723,8 +4798,8 @@ class ExcellonExpPrefGroupUI(OptionsGroupUI):
             "and Leading Zeros are removed.")
         )
 
-        self.zeros_radio = RadioSet([{'label': _('LZ'), 'value': 'LZ'},
-                                     {'label': _('TZ'), 'value': 'TZ'}])
+        self.zeros_radio = RadioSet([{'label': 'LZ', 'value': 'LZ'},
+                                     {'label': 'TZ', 'value': 'TZ'}])
         self.zeros_radio.setToolTip(
             _("This sets the default type of Excellon zeros.\n"
             "If LZ then Leading Zeros are kept and\n"
@@ -5105,9 +5180,9 @@ class CNCJobGenPrefGroupUI(OptionsGroupUI):
         )
 
         self.cncplot_method_radio = RadioSet([
-            {"label": _("All"), "value": "all"},
-            {"label": _("Travel"), "value": "travel"},
-            {"label": _("Cut"), "value": "cut"}
+            {"label": "All", "value": "all"},
+            {"label": "Travel", "value": "travel"},
+            {"label": "Cut", "value": "cut"}
         ], stretch=False)
 
         grid0.addWidget(self.cncplot_method_label, 1, 0)
@@ -5341,9 +5416,9 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
         )
         grid0.addWidget(methodlabel, 3, 0)
         self.ncc_method_radio = RadioSet([
-            {"label": _("Standard"), "value": "standard"},
-            {"label": _("Seed-based"), "value": "seed"},
-            {"label": _("Straight lines"), "value": "lines"}
+            {"label": "Standard", "value": "standard"},
+            {"label": "Seed-based", "value": "seed"},
+            {"label": "Straight lines", "value": "lines"}
         ], orientation='vertical', stretch=False)
         grid0.addWidget(self.ncc_method_radio, 3, 1)
 
@@ -5490,8 +5565,8 @@ class Tools2sidedPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.drill_dia_entry, 0, 1)
 
         ## Axis
-        self.mirror_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
-                                     {'label': _('Y'), 'value': 'Y'}])
+        self.mirror_axis_radio = RadioSet([{'label': 'X', 'value': 'X'},
+                                     {'label': 'Y', 'value': 'Y'}])
         self.mirax_label = QtWidgets.QLabel(_("Mirror Axis:"))
         self.mirax_label.setToolTip(
             _("Mirror vertically (X) or horizontally (Y).")
@@ -5503,8 +5578,8 @@ class Tools2sidedPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.mirror_axis_radio, 2, 1)
 
         ## Axis Location
-        self.axis_location_radio = RadioSet([{'label': _('Point'), 'value': 'point'},
-                                             {'label': _('Box'), 'value': 'box'}])
+        self.axis_location_radio = RadioSet([{'label': 'Point', 'value': 'point'},
+                                             {'label': 'Box', 'value': 'box'}])
         self.axloc_label = QtWidgets.QLabel(_("Axis Ref:"))
         self.axloc_label.setToolTip(
             _("The axis should pass through a <b>point</b> or cut\n "
@@ -5581,9 +5656,9 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         )
         grid0.addWidget(methodlabel, 3, 0)
         self.paintmethod_combo = RadioSet([
-            {"label": _("Standard"), "value": "standard"},
-            {"label": _("Seed-based"), "value": "seed"},
-            {"label": _("Straight lines"), "value": "lines"}
+            {"label": "Standard", "value": "standard"},
+            {"label": "Seed-based", "value": "seed"},
+            {"label": "Straight lines", "value": "lines"}
         ], orientation='vertical', stretch=False)
         grid0.addWidget(self.paintmethod_combo, 3, 1)
 
@@ -5614,8 +5689,8 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         )
         grid0.addWidget(selectlabel, 6, 0)
         self.selectmethod_combo = RadioSet([
-            {"label": _("Single"), "value": "single"},
-            {"label": _("All"), "value": "all"},
+            {"label": "Single", "value": "single"},
+            {"label": "All", "value": "all"},
             # {"label": "Rectangle", "value": "rectangle"}
         ])
         grid0.addWidget(self.selectmethod_combo, 6, 1)
@@ -5642,8 +5717,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0 = QtWidgets.QGridLayout()
         self.layout.addLayout(grid0)
 
-        self.film_type_radio = RadioSet([{'label': _('Pos'), 'value': 'pos'},
-                                         {'label': _('Neg'), 'value': 'neg'}])
+        self.film_type_radio = RadioSet([{'label': 'Pos', 'value': 'pos'},
+                                         {'label': 'Neg', 'value': 'neg'}])
         ftypelbl = QtWidgets.QLabel(_('Film Type:'))
         ftypelbl.setToolTip(
             _("Generate a Positive black film or a Negative film.\n"
@@ -5742,8 +5817,8 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.prows, 3, 1)
 
         ## Type of resulting Panel object
-        self.panel_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'gerber'},
-                                          {'label': _('Geo'), 'value': 'geometry'}])
+        self.panel_type_radio = RadioSet([{'label': 'Gerber', 'value': 'gerber'},
+                                          {'label': 'Geo', 'value': 'geometry'}])
         self.panel_type_label = QtWidgets.QLabel(_("Panel Type:"))
         self.panel_type_label.setToolTip(
            _( "Choose the type of object for the panel object:\n"

+ 15 - 1
flatcamGUI/GUIElements.py

@@ -367,7 +367,11 @@ class FCEntry2(FCEntry):
         self.readyToEdit = True
 
     def set_value(self, val):
-        self.setText('%.4f' % float(val))
+        try:
+            fval = float(val)
+        except ValueError:
+            return
+        self.setText('%.4f' % fval)
 
 
 class EvalEntry(QtWidgets.QLineEdit):
@@ -676,6 +680,16 @@ class FCButton(QtWidgets.QPushButton):
         self.setText(str(val))
 
 
+class FCMenu(QtWidgets.QMenu):
+    def __init__(self):
+        super().__init__()
+        self.mouse_is_panning = False
+
+    def popup(self, pos, action=None):
+        self.mouse_is_panning = False
+        super().popup(pos)
+
+
 class FCTab(QtWidgets.QTabWidget):
     def __init__(self, parent=None):
         super(FCTab, self).__init__(parent)

+ 10 - 10
flatcamGUI/ObjectUI.py

@@ -153,7 +153,7 @@ class GerberObjectUI(ObjectUI):
         grid0.addWidget(self.plot_options_label, 0, 0)
 
         # Solid CB
-        self.solid_cb = FCCheckBox(label=_('Solid   '))
+        self.solid_cb = FCCheckBox(label=_('Solid'))
         self.solid_cb.setToolTip(
             _("Solid color polygons.")
         )
@@ -161,7 +161,7 @@ class GerberObjectUI(ObjectUI):
         grid0.addWidget(self.solid_cb, 0, 1)
 
         # Multicolored CB
-        self.multicolored_cb = FCCheckBox(label=_('M-Color   '))
+        self.multicolored_cb = FCCheckBox(label=_('M-Color'))
         self.multicolored_cb.setToolTip(
             _("Draw polygons in different colors.")
         )
@@ -299,8 +299,8 @@ class GerberObjectUI(ObjectUI):
             "- conventional / useful when there is no backlash compensation")
         )
         grid1.addWidget(self.milling_type_label, 3, 0)
-        self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
-                                            {'label': _('Conv.'), 'value': 'cv'}])
+        self.milling_type_radio = RadioSet([{'label': 'Climb', 'value': 'cl'},
+                                            {'label': 'Conv.', 'value': 'cv'}])
         grid1.addWidget(self.milling_type_radio, 3, 1)
 
         # combine all passes CB
@@ -749,9 +749,9 @@ class ExcellonObjectUI(ObjectUI):
             "When choosing 'Slots' or 'Both', slots will be\n"
             "converted to a series of drills.")
         )
-        self.excellon_gcode_type_radio = RadioSet([{'label': _('Drills'), 'value': 'drills'},
-                                                   {'label': _('Slots'), 'value': 'slots'},
-                                                   {'label': _('Both'), 'value': 'both'}])
+        self.excellon_gcode_type_radio = RadioSet([{'label': 'Drills', 'value': 'drills'},
+                                                   {'label': 'Slots', 'value': 'slots'},
+                                                   {'label': 'Both', 'value': 'both'}])
         gcode_box.addRow(gcode_type_label, self.excellon_gcode_type_radio)
         self.tools_box.addLayout(gcode_box)
 
@@ -1355,9 +1355,9 @@ class CNCObjectUI(ObjectUI):
         )
 
         self.cncplot_method_combo = RadioSet([
-            {"label": _("All"), "value": "all"},
-            {"label": _("Travel"), "value": "travel"},
-            {"label": _("Cut"), "value": "cut"}
+            {"label": "All", "value": "all"},
+            {"label": "Travel", "value": "travel"},
+            {"label": "Cut", "value": "cut"}
         ], stretch=False)
 
         ## Object name

+ 1 - 1
flatcamTools/ToolCalculators.py

@@ -12,9 +12,9 @@ import math
 
 import gettext
 import FlatCAMTranslation as fcTranslate
+import builtins
 
 fcTranslate.apply_language('strings')
-import builtins
 if '_' not in builtins.__dict__:
     _ = gettext.gettext
 

+ 231 - 128
flatcamTools/ToolPDF.py

@@ -2,7 +2,7 @@
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
-# Date: 3/10/2019                                          #
+# Date: 4/23/2019                                          #
 # MIT Licence                                              #
 ############################################################
 
@@ -18,6 +18,7 @@ import numpy as np
 
 import zlib
 import re
+import time
 
 import gettext
 import FlatCAMTranslation as fcTranslate
@@ -106,9 +107,18 @@ class ToolPDF(FlatCAMTool):
         self.gs['transform'] = []
         self.gs['line_width'] = []   # each element is a float
 
-        self.obj_dict = dict()
-        self.pdf_parsed = ''
-        self.parsed_obj_dict = dict()
+        self.pdf_decompressed = {}
+
+        # key = file name and extension
+        # value is a dict to store the parsed content of the PDF
+        self.pdf_parsed = {}
+
+        # QTimer for periodic check
+        self.check_thread = QtCore.QTimer()
+
+        # Every time a parser is started we add a promise; every time a parser finished we remove a promise
+        # when empty we start the layer rendering
+        self.parsing_promises = []
 
         # conversion factor to INCH
         self.point_to_unit_factor = 0.01388888888
@@ -148,16 +158,22 @@ class ToolPDF(FlatCAMTool):
         if len(filenames) == 0:
             self.app.inform.emit(_("[WARNING_NOTCL] Open PDF cancelled."))
         else:
+            # start the parsing timer with a period of 1 second
+            self.periodic_check(1000)
+
             for filename in filenames:
                 if filename != '':
-                    self.app.worker_task.emit({'fcn': self.open_pdf, 'params': [filename]})
+                    self.app.worker_task.emit({'fcn': self.open_pdf,
+                                               'params': [filename]})
 
     def open_pdf(self, filename):
-        new_name = filename.split('/')[-1].split('\\')[-1]
-        self.obj_dict.clear()
-        self.pdf_parsed = ''
-        self.parsed_obj_dict = {}
-        obj_type = 'gerber'
+        short_name = filename.split('/')[-1].split('\\')[-1]
+        self.parsing_promises.append(short_name)
+        self.pdf_parsed[short_name] = {}
+        self.pdf_parsed[short_name]['pdf'] = {}
+        self.pdf_parsed[short_name]['filename'] = filename
+
+        self.pdf_decompressed[short_name] = ''
 
         # the UNITS in PDF files are points and here we set the factor to convert them to real units (either MM or INCH)
         if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
@@ -177,102 +193,183 @@ class ToolPDF(FlatCAMTool):
                 log.debug(" PDF STREAM: %d\n" % stream_nr)
                 s = s.strip(b'\r\n')
                 try:
-                    self.pdf_parsed += (zlib.decompress(s).decode('UTF-8') + '\r\n')
+                    self.pdf_decompressed[short_name] += (zlib.decompress(s).decode('UTF-8') + '\r\n')
                 except Exception as e:
                     log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(e))
 
-            self.parsed_obj_dict = self.parse_pdf(pdf_content=self.pdf_parsed)
-
-        for k in self.parsed_obj_dict:
-            ap_dict = deepcopy(self.parsed_obj_dict[k])
-            if ap_dict:
-                if k == 0:
-                    # Excellon
-                    obj_type = 'excellon'
-
-                    new_name = new_name + "_exc"
-                    # store the points here until reconstitution: keys are diameters and values are list of (x,y) coords
-                    points = {}
-
-                    def obj_init(exc_obj, app_obj):
-                        # print(self.parsed_obj_dict[0])
-
-                        for geo in self.parsed_obj_dict[0]['0']['solid_geometry']:
-                            xmin, ymin, xmax, ymax = geo.bounds
-                            center = (((xmax - xmin) / 2) + xmin, ((ymax - ymin) / 2) + ymin)
-
-                            # for drill bits, even in INCH, it's enough 3 decimals
-                            correction_factor = 0.974
-                            dia = (xmax - xmin) * correction_factor
-                            dia = round(dia, 3)
-                            if dia in points:
-                                points[dia].append(center)
-                            else:
-                                points[dia] = [center]
-
-                        sorted_dia = sorted(points.keys())
-
-                        name_tool = 0
-                        for dia in sorted_dia:
-                            name_tool += 1
-
-                            # create tools dictionary
-                            spec = {"C": dia}
-                            spec['solid_geometry'] = []
-                            exc_obj.tools[str(name_tool)] = spec
-
-                            # create drill list of dictionaries
-                            for dia_points in points:
-                                if dia == dia_points:
-                                    for pt in points[dia_points]:
-                                        exc_obj.drills.append({'point': Point(pt), 'tool': str(name_tool)})
-                                    break
-
-                        ret = exc_obj.create_geometry()
-                        if ret == 'fail':
-                            log.debug("Could not create geometry for Excellon object.")
-                            return "fail"
-                        for tool in exc_obj.tools:
-                            if exc_obj.tools[tool]['solid_geometry']:
-                                return
-                        app_obj.inform.emit(_("[ERROR_NOTCL] No geometry found in file: %s") % new_name)
-                        return "fail"
+            self.pdf_parsed[short_name]['pdf'] = self.parse_pdf(pdf_content=self.pdf_decompressed[short_name])
+            # we used it, now we delete it
+            self.pdf_decompressed[short_name] = ''
+
+        # removal from list is done in a multithreaded way therefore not always the removal can be done
+        # try to remove until it's done
+        try:
+            while True:
+                self.parsing_promises.remove(short_name)
+                time.sleep(0.1)
+        except:
+            pass
+        self.app.inform.emit(_("[success] Opened: %s") % filename)
+
+    def layer_rendering_as_excellon(self, filename, ap_dict, layer_nr):
+        outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
+
+        # store the points here until reconstitution:
+        # keys are diameters and values are list of (x,y) coords
+        points = {}
+
+        def obj_init(exc_obj, app_obj):
+
+            for geo in ap_dict['0']['solid_geometry']:
+                xmin, ymin, xmax, ymax = geo.bounds
+                center = (((xmax - xmin) / 2) + xmin, ((ymax - ymin) / 2) + ymin)
+
+                # for drill bits, even in INCH, it's enough 3 decimals
+                correction_factor = 0.974
+                dia = (xmax - xmin) * correction_factor
+                dia = round(dia, 3)
+                if dia in points:
+                    points[dia].append(center)
                 else:
-                    # Gerber
-                    obj_type = 'gerber'
-
-                    def obj_init(grb_obj, app_obj):
-
-                        grb_obj.apertures = ap_dict
-
-                        poly_buff = []
-                        for ap in grb_obj.apertures:
-                            for k in grb_obj.apertures[ap]:
-                                if k == 'solid_geometry':
-                                    poly_buff += ap_dict[ap][k]
-
-                        poly_buff = unary_union(poly_buff)
-                        try:
-                            poly_buff = poly_buff.buffer(0.0000001)
-                        except ValueError:
-                            pass
-                        try:
-                            poly_buff = poly_buff.buffer(-0.0000001)
-                        except ValueError:
-                            pass
-
-                        grb_obj.solid_geometry = deepcopy(poly_buff)
-
-                with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % (int(k) - 2)):
-
-                    ret = self.app.new_object(obj_type, new_name, obj_init, autoselected=False)
-                    if ret == 'fail':
-                        self.app.inform.emit(_('[ERROR_NOTCL] Open PDF file failed.'))
-                        return
-                    # Register recent file
-                    self.app.file_opened.emit(obj_type, filename)
-                    # GUI feedback
-                    self.app.inform.emit(_("[success] Opened: %s") % filename)
+                    points[dia] = [center]
+
+            sorted_dia = sorted(points.keys())
+
+            name_tool = 0
+            for dia in sorted_dia:
+                name_tool += 1
+
+                # create tools dictionary
+                spec = {"C": dia}
+                spec['solid_geometry'] = []
+                exc_obj.tools[str(name_tool)] = spec
+
+                # create drill list of dictionaries
+                for dia_points in points:
+                    if dia == dia_points:
+                        for pt in points[dia_points]:
+                            exc_obj.drills.append({'point': Point(pt), 'tool': str(name_tool)})
+                        break
+
+            ret = exc_obj.create_geometry()
+            if ret == 'fail':
+                log.debug("Could not create geometry for Excellon object.")
+                return "fail"
+            for tool in exc_obj.tools:
+                if exc_obj.tools[tool]['solid_geometry']:
+                    return
+            app_obj.inform.emit(_("[ERROR_NOTCL] No geometry found in file: %s") % outname)
+            return "fail"
+
+        with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
+
+            ret = self.app.new_object("excellon", outname, obj_init, autoselected=False)
+            if ret == 'fail':
+                self.app.inform.emit(_('[ERROR_NOTCL] Open PDF file failed.'))
+                return
+            # Register recent file
+            self.app.file_opened.emit("excellon", filename)
+            # GUI feedback
+            self.app.inform.emit(_("[success] Rendered: %s") % outname)
+
+    def layer_rendering_as_gerber(self, filename, ap_dict, layer_nr):
+        outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
+
+        def obj_init(grb_obj, app_obj):
+
+            grb_obj.apertures = ap_dict
+
+            poly_buff = []
+            for ap in grb_obj.apertures:
+                for k in grb_obj.apertures[ap]:
+                    if k == 'solid_geometry':
+                        poly_buff += ap_dict[ap][k]
+
+            poly_buff = unary_union(poly_buff)
+            try:
+                poly_buff = poly_buff.buffer(0.0000001)
+            except ValueError:
+                pass
+            try:
+                poly_buff = poly_buff.buffer(-0.0000001)
+            except ValueError:
+                pass
+
+            grb_obj.solid_geometry = deepcopy(poly_buff)
+
+        with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
+
+            ret = self.app.new_object('gerber', outname, obj_init, autoselected=False)
+            if ret == 'fail':
+                self.app.inform.emit(_('[ERROR_NOTCL] Open PDF file failed.'))
+                return
+            # Register recent file
+            self.app.file_opened.emit('gerber', filename)
+            # GUI feedback
+            self.app.inform.emit(_("[success] Rendered: %s") % outname)
+
+    def periodic_check(self, check_period):
+        """
+        This function starts an QTimer and it will periodically check if parsing was done
+
+        :param check_period: time at which to check periodically if all plots finished to be plotted
+        :return:
+        """
+
+        # self.plot_thread = threading.Thread(target=lambda: self.check_plot_finished(check_period))
+        # self.plot_thread.start()
+        log.debug("ToolPDF --> Periodic Check started.")
+
+        try:
+            self.check_thread.stop()
+        except:
+            pass
+
+        self.check_thread.setInterval(check_period)
+        try:
+            self.check_thread.timeout.disconnect(self.periodic_check_handler)
+        except:
+            pass
+
+        self.check_thread.timeout.connect(self.periodic_check_handler)
+        self.check_thread.start(QtCore.QThread.HighPriority)
+
+    def periodic_check_handler(self):
+        """
+        If the parsing worker finished then start multithreaded rendering
+        :return:
+        """
+        # log.debug("checking parsing --> %s" % str(self.parsing_promises))
+
+        try:
+            if not self.parsing_promises:
+                self.check_thread.stop()
+                # parsing finished start the layer rendering
+                if self.pdf_parsed:
+                    obj_to_delete = []
+                    for object_name in self.pdf_parsed:
+                        filename = deepcopy(self.pdf_parsed[object_name]['filename'])
+                        pdf_content = deepcopy(self.pdf_parsed[object_name]['pdf'])
+                        obj_to_delete.append(object_name)
+                        for k in pdf_content:
+                            ap_dict = pdf_content[k]
+                            if ap_dict:
+                                layer_nr = k
+                                if k == 0:
+                                    self.app.worker_task.emit({'fcn': self.layer_rendering_as_excellon,
+                                                               'params': [filename, ap_dict, layer_nr]})
+                                else:
+                                    self.app.worker_task.emit({'fcn': self.layer_rendering_as_gerber,
+                                                               'params': [filename, ap_dict, layer_nr]})
+                    # delete the object already processed so it will not be processed again for other objects
+                    # that were opened at the same time; like in drag & drop on GUI
+                    for obj_name in obj_to_delete:
+                        if obj_name in self.pdf_parsed:
+                            self.pdf_parsed.pop(obj_name)
+
+                log.debug("ToolPDF --> Periodic check finished.")
+        except Exception:
+            traceback.print_exc()
 
     def parse_pdf(self, pdf_content):
         path = dict()
@@ -301,9 +398,10 @@ class ToolPDF(FlatCAMTool):
 
         # store the objects to be transformed into Gerbers
         object_dict = {}
-
         # will serve as key in the object_dict
-        object_nr = 1
+        layer_nr = 1
+        # create first object
+        object_dict[layer_nr] = {}
 
         # store the apertures here
         apertures_dict = {}
@@ -320,15 +418,11 @@ class ToolPDF(FlatCAMTool):
         clear_apertures_dict['0']['type'] = 'C'
         clear_apertures_dict['0']['solid_geometry'] = []
 
-        # create first object
-        object_dict[object_nr] = apertures_dict
-        object_nr += 1
-
         # on stroke color change we create a new apertures dictionary and store the old one in a storage from where
         # it will be transformed into Gerber object
         old_color = [None, None ,None]
 
-        # signal that we have clear geometry and the geometry will be added to a special object_nr = 0
+        # signal that we have clear geometry and the geometry will be added to a special layer_nr = 0
         flag_clear_geo = False
 
         line_nr = 0
@@ -350,11 +444,12 @@ class ToolPDF(FlatCAMTool):
                     # same color, do nothing
                     continue
                 else:
-                    object_dict[object_nr] = deepcopy(apertures_dict)
-                    object_nr += 1
+                    if apertures_dict:
+                        object_dict[layer_nr] = deepcopy(apertures_dict)
+                        apertures_dict.clear()
+                        layer_nr += 1
 
-                    object_dict[object_nr] = dict()
-                    apertures_dict = {}
+                        object_dict[layer_nr] = dict()
                 old_color = copy(color)
                 # we make sure that the following geometry is added to the right storage
                 flag_clear_geo = False
@@ -536,7 +631,6 @@ class ToolPDF(FlatCAMTool):
                         y * self.point_to_unit_factor * scale_geo[1])
 
                 subpath['bezier'].append([start, c1, stop, stop])
-                print(subpath['bezier'])
                 current_point = stop
                 continue
 
@@ -747,18 +841,18 @@ class ToolPDF(FlatCAMTool):
                     if path['rectangle']:
                         for subp in path['rectangle']:
                             geo = copy(subp)
-                            # close the subpath if it was not closed already
-                            if close_subpath is False and start_point is not None:
-                                geo.append(start_point)
+                            # # close the subpath if it was not closed already
+                            # if close_subpath is False and start_point is not None:
+                            #     geo.append(start_point)
                             geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
                             path_geo.append(geo_el)
                         # the path was painted therefore initialize it
                         path['rectangle'] = []
                     else:
                         geo = copy(subpath['rectangle'])
-                        # close the subpath if it was not closed already
-                        if close_subpath is False and start_point is not None:
-                            geo.append(start_point)
+                        # # close the subpath if it was not closed already
+                        # if close_subpath is False and start_point is not None:
+                        #     geo.append(start_point)
                         geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
                         path_geo.append(geo_el)
                         subpath['rectangle'] = []
@@ -869,9 +963,9 @@ class ToolPDF(FlatCAMTool):
                         # fill
                         for subp in path['rectangle']:
                             geo = copy(subp)
-                            # close the subpath if it was not closed already
-                            if close_subpath is False:
-                                geo.append(geo[0])
+                            # # close the subpath if it was not closed already
+                            # if close_subpath is False:
+                            #     geo.append(geo[0])
                             geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
                             fill_geo.append(geo_el)
                         # stroke
@@ -884,9 +978,9 @@ class ToolPDF(FlatCAMTool):
                     else:
                         # fill
                         geo = copy(subpath['rectangle'])
-                        # close the subpath if it was not closed already
-                        if close_subpath is False:
-                            geo.append(start_point)
+                        # # close the subpath if it was not closed already
+                        # if close_subpath is False:
+                        #     geo.append(start_point)
                         geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
                         fill_geo.append(geo_el)
                         # stroke
@@ -940,11 +1034,20 @@ class ToolPDF(FlatCAMTool):
 
         # tidy up. copy the current aperture dict to the object dict but only if it is not empty
         if apertures_dict:
-            object_dict[object_nr] = deepcopy(apertures_dict)
+            object_dict[layer_nr] = deepcopy(apertures_dict)
 
         if clear_apertures_dict['0']['solid_geometry']:
             object_dict[0] = deepcopy(clear_apertures_dict)
 
+        # delete keys (layers) with empty values
+        empty_layers = []
+        for layer in object_dict:
+            if not object_dict[layer]:
+                empty_layers.append(layer)
+        for x in empty_layers:
+            if x in object_dict:
+                object_dict.pop(x)
+
         return object_dict
 
     def bezier_to_points(self, start, c1, c2, stop):
@@ -958,7 +1061,7 @@ class ToolPDF(FlatCAMTool):
         # with the final point P3. Intermediate values of t generate intermediate points along the curve.
         # The curve does not, in general, pass through the two control points P1 and P2
 
-        :return: LineString geometry
+        :return: A list of point coordinates tuples (x, y)
         """
 
         # here we store the geometric points

+ 553 - 0
flatcamTools/ToolSub.py

@@ -0,0 +1,553 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 4/24/2019                                          #
+# MIT Licence                                              #
+############################################################
+
+
+from FlatCAMTool import FlatCAMTool
+# from copy import copy, deepcopy
+from ObjectCollection import *
+import time
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class ToolSub(FlatCAMTool):
+
+    toolName = _("Substract Tool")
+
+    def __init__(self, app):
+        self.app = app
+
+        FlatCAMTool.__init__(self, app)
+
+        self.tools_frame = QtWidgets.QFrame()
+        self.tools_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.tools_frame)
+        self.tools_box = QtWidgets.QVBoxLayout()
+        self.tools_box.setContentsMargins(0, 0, 0, 0)
+        self.tools_frame.setLayout(self.tools_box)
+
+        # Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                        QLabel
+                        {
+                            font-size: 16px;
+                            font-weight: bold;
+                        }
+                        """)
+        self.tools_box.addWidget(title_label)
+
+        # Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.tools_box.addLayout(form_layout)
+
+        self.gerber_title = QtWidgets.QLabel(_("<b>Gerber Objects</b>"))
+        form_layout.addRow(self.gerber_title)
+
+        # Target Gerber Object
+        self.target_gerber_combo = QtWidgets.QComboBox()
+        self.target_gerber_combo.setModel(self.app.collection)
+        self.target_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.target_gerber_combo.setCurrentIndex(1)
+
+        self.target_gerber_label = QtWidgets.QLabel(_("Target:"))
+        self.target_gerber_label.setToolTip(
+            _("Gerber object from which to substract\n"
+              "the substractor Gerber object.")
+        )
+
+        form_layout.addRow(self.target_gerber_label, self.target_gerber_combo)
+
+        # Substractor Gerber Object
+        self.sub_gerber_combo = QtWidgets.QComboBox()
+        self.sub_gerber_combo.setModel(self.app.collection)
+        self.sub_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.sub_gerber_combo.setCurrentIndex(1)
+
+        self.sub_gerber_label = QtWidgets.QLabel(_("Substractor:"))
+        self.sub_gerber_label.setToolTip(
+            _("Gerber object that will be substracted\n"
+              "from the target Gerber object.")
+        )
+        e_lab_1 = QtWidgets.QLabel('')
+
+        form_layout.addRow(self.sub_gerber_label, self.sub_gerber_combo)
+
+        self.intersect_btn = FCButton(_('Substract Gerber'))
+        self.intersect_btn.setToolTip(
+            _("Will remove the area occupied by the substractor\n"
+              "Gerber from the Target Gerber.\n"
+              "Can be used to remove the overlapping silkscreen\n"
+              "over the soldermask.")
+        )
+        self.tools_box.addWidget(self.intersect_btn)
+        self.tools_box.addWidget(e_lab_1)
+
+        # Form Layout
+        form_geo_layout = QtWidgets.QFormLayout()
+        self.tools_box.addLayout(form_geo_layout)
+
+        self.geo_title = QtWidgets.QLabel(_("<b>Geometry Objects</b>"))
+        form_geo_layout.addRow(self.geo_title)
+
+        # Target Geometry Object
+        self.target_geo_combo = QtWidgets.QComboBox()
+        self.target_geo_combo.setModel(self.app.collection)
+        self.target_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
+        self.target_geo_combo.setCurrentIndex(1)
+
+        self.target_geo_label = QtWidgets.QLabel(_("Target:"))
+        self.target_geo_label.setToolTip(
+            _("Geometry object from which to substract\n"
+              "the substractor Geometry object.")
+        )
+
+        form_geo_layout.addRow(self.target_geo_label, self.target_geo_combo)
+
+        # Substractor Geometry Object
+        self.sub_geo_combo = QtWidgets.QComboBox()
+        self.sub_geo_combo.setModel(self.app.collection)
+        self.sub_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
+        self.sub_geo_combo.setCurrentIndex(1)
+
+        self.sub_geo_label = QtWidgets.QLabel(_("Substractor:"))
+        self.sub_geo_label.setToolTip(
+            _("Geometry object that will be substracted\n"
+              "from the target Geometry object.")
+        )
+        e_lab_1 = QtWidgets.QLabel('')
+
+        form_geo_layout.addRow(self.sub_geo_label, self.sub_geo_combo)
+
+        self.intersect_geo_btn = FCButton(_('Substract Geometry'))
+        self.intersect_geo_btn.setToolTip(
+            _("Will remove the area occupied by the substractor\n"
+              "Geometry from the Target Geometry.")
+        )
+        self.tools_box.addWidget(self.intersect_geo_btn)
+        self.tools_box.addWidget(e_lab_1)
+
+        self.tools_box.addStretch()
+
+        # QTimer for periodic check
+        self.check_thread = QtCore.QTimer()
+        # Every time an intersection job is started we add a promise; every time an intersection job is finished
+        # we remove a promise.
+        # When empty we start the layer rendering
+        self.promises = []
+
+        self.new_apertures = {}
+        self.new_tools = {}
+        self.new_solid_geometry = []
+
+        self.sub_union = None
+
+        self.sub_grb_obj = None
+        self.sub_grb_obj_name = None
+        self.target_grb_obj = None
+        self.target_grb_obj_name = None
+
+        self.sub_geo_obj = None
+        self.sub_geo_obj_name = None
+        self.target_geo_obj = None
+        self.target_geo_obj_name = None
+
+        # signal which type of substraction to do: "geo" or "gerber"
+        self.sub_type = None
+
+        # store here the options from target_obj
+        self.target_options = {}
+
+        try:
+            self.intersect_btn.clicked.disconnect(self.on_grb_intersection_click)
+        except:
+            pass
+        self.intersect_btn.clicked.connect(self.on_grb_intersection_click)
+
+        try:
+            self.intersect_geo_btn.clicked.disconnect()
+        except:
+            pass
+        self.intersect_geo_btn.clicked.connect(self.on_geo_intersection_click)
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+W', **kwargs)
+
+    def run(self, toggle=True):
+        self.app.report_usage("ToolSub()")
+
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+
+        FlatCAMTool.run(self)
+        self.set_tool_ui()
+
+        self.new_apertures.clear()
+        self.new_tools.clear()
+        self.new_solid_geometry = []
+        self.target_options.clear()
+
+        self.app.ui.notebook.setTabText(2, _("Sub Tool"))
+
+    def set_tool_ui(self):
+        self.tools_frame.show()
+
+    def on_grb_intersection_click(self):
+        # reset previous values
+        self.new_apertures.clear()
+        self.new_solid_geometry = []
+        self.sub_union = []
+
+        self.sub_type = "gerber"
+
+        self.target_grb_obj_name = self.target_gerber_combo.currentText()
+        if self.target_grb_obj_name == '':
+            self.app.inform.emit(_("[ERROR_NOTCL] No Target object loaded."))
+            return
+
+        # Get source object.
+        try:
+            self.target_grb_obj = self.app.collection.get_by_name(self.target_grb_obj_name)
+        except:
+            self.app.inform.emit(_("[ERROR_NOTCL] Could not retrieve object: %s") % self.obj_name)
+            return "Could not retrieve object: %s" % self.target_grb_obj_name
+
+        self.sub_grb_obj_name = self.sub_gerber_combo.currentText()
+        if self.sub_grb_obj_name == '':
+            self.app.inform.emit(_("[ERROR_NOTCL] No Substractor object loaded."))
+            return
+
+        # Get source object.
+        try:
+            self.sub_grb_obj = self.app.collection.get_by_name(self.sub_grb_obj_name)
+        except:
+            self.app.inform.emit(_("[ERROR_NOTCL] Could not retrieve object: %s") % self.obj_name)
+            return "Could not retrieve object: %s" % self.sub_grb_obj_name
+
+        # crate the new_apertures dict structure
+        for apid in self.target_grb_obj.apertures:
+            self.new_apertures[apid] = {}
+            self.new_apertures[apid]['type'] = 'C'
+            self.new_apertures[apid]['size'] = self.target_grb_obj.apertures[apid]['size']
+            self.new_apertures[apid]['solid_geometry'] = []
+
+        geo_union_list = []
+        for apid1 in self.sub_grb_obj.apertures:
+            geo_union_list += self.sub_grb_obj.apertures[apid1]['solid_geometry']
+        self.sub_union = cascaded_union(geo_union_list)
+
+        # add the promises
+        for apid in self.target_grb_obj.apertures:
+            self.promises.append(apid)
+
+        # start the QTimer to check for promises with 1 second period check
+        self.periodic_check(500, reset=True)
+
+        for apid in self.target_grb_obj.apertures:
+            geo = self.target_grb_obj.apertures[apid]['solid_geometry']
+            self.app.worker_task.emit({'fcn': self.aperture_intersection,
+                                       'params': [apid, geo]})
+
+    def aperture_intersection(self, apid, geo):
+        new_solid_geometry = []
+        log.debug("Working on promise: %s" % str(apid))
+
+        with self.app.proc_container.new(_("Parsing aperture %s geometry ..." % str(apid))):
+            for geo_silk in geo:
+                if geo_silk.intersects(self.sub_union):
+                    new_geo = geo_silk.difference(self.sub_union)
+                    new_geo = new_geo.buffer(0)
+                    if new_geo:
+                        if not new_geo.is_empty:
+                            new_solid_geometry.append(new_geo)
+                        else:
+                            new_solid_geometry.append(geo_silk)
+                    else:
+                        new_solid_geometry.append(geo_silk)
+                else:
+                    new_solid_geometry.append(geo_silk)
+
+        if new_solid_geometry:
+            while not self.new_apertures[apid]['solid_geometry']:
+                self.new_apertures[apid]['solid_geometry'] = deepcopy(new_solid_geometry)
+                time.sleep(0.5)
+
+        while True:
+            # removal from list is done in a multithreaded way therefore not always the removal can be done
+            # so we keep trying until it's done
+            if apid not in self.promises:
+                break
+
+            self.promises.remove(apid)
+            time.sleep(0.5)
+
+        log.debug("Promise fulfilled: %s" % str(apid))
+
+    def new_gerber_object(self, outname):
+
+        def obj_init(grb_obj, app_obj):
+
+            grb_obj.apertures = deepcopy(self.new_apertures)
+
+            poly_buff = []
+            for ap in self.new_apertures:
+                for poly in self.new_apertures[ap]['solid_geometry']:
+                    poly_buff.append(poly)
+
+            work_poly_buff = cascaded_union(poly_buff)
+            try:
+                poly_buff = work_poly_buff.buffer(0.0000001)
+            except ValueError:
+                pass
+            try:
+                poly_buff = poly_buff.buffer(-0.0000001)
+            except ValueError:
+                pass
+
+            grb_obj.solid_geometry = deepcopy(poly_buff)
+
+        with self.app.proc_container.new(_("Generating new object ...")):
+            ret = self.app.new_object('gerber', outname, obj_init, autoselected=False)
+            if ret == 'fail':
+                self.app.inform.emit(_('[ERROR_NOTCL] Generating new object failed.'))
+                return
+            # Register recent file
+            self.app.file_opened.emit('gerber', outname)
+            # GUI feedback
+            self.app.inform.emit(_("[success] Created: %s") % outname)
+
+            # cleanup
+            self.new_apertures.clear()
+            self.new_solid_geometry[:] = []
+            self.sub_union[:] = []
+
+    def on_geo_intersection_click(self):
+        # reset previous values
+        self.new_tools.clear()
+        self.target_options.clear()
+        self.new_solid_geometry = []
+        self.sub_union = []
+
+        self.sub_type = "geo"
+
+        self.target_geo_obj_name = self.target_geo_combo.currentText()
+        if self.target_geo_obj_name == '':
+            self.app.inform.emit(_("[ERROR_NOTCL] No Target object loaded."))
+            return
+
+        # Get source object.
+        try:
+            self.target_geo_obj = self.app.collection.get_by_name(self.target_geo_obj_name)
+        except:
+            self.app.inform.emit(_("[ERROR_NOTCL] Could not retrieve object: %s") % self.target_geo_obj_name)
+            return "Could not retrieve object: %s" % self.target_grb_obj_name
+
+        self.sub_geo_obj_name = self.sub_geo_combo.currentText()
+        if self.sub_geo_obj_name == '':
+            self.app.inform.emit(_("[ERROR_NOTCL] No Substractor object loaded."))
+            return
+
+        # Get source object.
+        try:
+            self.sub_geo_obj = self.app.collection.get_by_name(self.sub_geo_obj_name)
+        except:
+            self.app.inform.emit(_("[ERROR_NOTCL] Could not retrieve object: %s") % self.sub_geo_obj_name)
+            return "Could not retrieve object: %s" % self.sub_geo_obj_name
+
+        if self.sub_geo_obj.multigeo:
+            self.app.inform.emit(_("[ERROR_NOTCL] Currently, the Substractor geometry cannot be of type Multigeo."))
+            return
+
+        # create the target_options obj
+        self.target_options = {}
+        for opt in self.target_geo_obj.options:
+            if opt != 'name':
+                self.target_options[opt] = deepcopy(self.target_geo_obj.options[opt])
+
+        # crate the new_tools dict structure
+        for tool in self.target_geo_obj.tools:
+            self.new_tools[tool] = {}
+            for key in self.target_geo_obj.tools[tool]:
+                if key == 'solid_geometry':
+                    self.new_tools[tool][key] = []
+                else:
+                    self.new_tools[tool][key] = deepcopy(self.target_geo_obj.tools[tool][key])
+
+        # add the promises
+        if self.target_geo_obj.multigeo:
+            for tool in self.target_geo_obj.tools:
+                self.promises.append(tool)
+        else:
+            self.promises.append("single")
+
+        self.sub_union = cascaded_union(self.sub_geo_obj.solid_geometry)
+
+        # start the QTimer to check for promises with 0.5 second period check
+        self.periodic_check(500, reset=True)
+
+        if self.target_geo_obj.multigeo:
+            for tool in self.target_geo_obj.tools:
+                geo = self.target_geo_obj.tools[tool]['solid_geometry']
+                self.app.worker_task.emit({'fcn': self.toolgeo_intersection,
+                                           'params': [tool, geo]})
+        else:
+            geo = self.target_geo_obj.solid_geometry
+            self.app.worker_task.emit({'fcn': self.toolgeo_intersection,
+                                       'params': ["single", geo]})
+
+    def toolgeo_intersection(self, tool, geo):
+        new_geometry = []
+        log.debug("Working on promise: %s" % str(tool))
+
+        if tool == "single":
+            text = _("Parsing solid_geometry ...")
+        else:
+            text = _("Parsing tool %s geometry ...") % str(tool)
+
+        with self.app.proc_container.new(text):
+            new_geo = (cascaded_union(geo)).difference(self.sub_union)
+            if new_geo:
+                if not new_geo.is_empty:
+                    new_geometry.append(new_geo)
+
+        if new_geometry:
+            if tool == "single":
+                while not self.new_solid_geometry:
+                    self.new_solid_geometry = deepcopy(new_geometry)
+                    time.sleep(0.5)
+            else:
+                while not self.new_tools[tool]['solid_geometry']:
+                    self.new_tools[tool]['solid_geometry'] = deepcopy(new_geometry)
+                    time.sleep(0.5)
+
+        while True:
+            # removal from list is done in a multithreaded way therefore not always the removal can be done
+            # so we keep trying until it's done
+            if tool not in self.promises:
+                break
+
+            self.promises.remove(tool)
+            time.sleep(0.5)
+        log.debug("Promise fulfilled: %s" % str(tool))
+
+    def new_geo_object(self, outname):
+        def obj_init(geo_obj, app_obj):
+
+            geo_obj.options = deepcopy(self.target_options)
+            geo_obj.options['name'] = outname
+
+            if self.target_geo_obj.multigeo:
+                geo_obj.tools = deepcopy(self.new_tools)
+                # this turn on the FlatCAMCNCJob plot for multiple tools
+                geo_obj.multigeo = True
+                geo_obj.multitool = True
+            else:
+                geo_obj.solid_geometry = deepcopy(self.new_solid_geometry)
+                try:
+                    geo_obj.tools = deepcopy(self.new_tools)
+                    for tool in geo_obj.tools:
+                        geo_obj.tools[tool]['solid_geometry'] = deepcopy(self.new_solid_geometry)
+                except:
+                    pass
+
+        with self.app.proc_container.new(_("Generating new object ...")):
+            ret = self.app.new_object('geometry', outname, obj_init, autoselected=False)
+            if ret == 'fail':
+                self.app.inform.emit(_('[ERROR_NOTCL] Generating new object failed.'))
+                return
+            # Register recent file
+            self.app.file_opened.emit('geometry', outname)
+            # GUI feedback
+            self.app.inform.emit(_("[success] Created: %s") % outname)
+
+            # cleanup
+            self.new_tools.clear()
+            self.new_solid_geometry[:] = []
+            self.sub_union[:] = []
+
+    def periodic_check(self, check_period, reset=False):
+        """
+        This function starts an QTimer and it will periodically check if intersections are done
+
+        :param check_period: time at which to check periodically
+        :param reset: will reset the timer
+        :return:
+        """
+
+        log.debug("ToolSub --> Periodic Check started.")
+
+        try:
+            self.check_thread.stop()
+        except Exception as e:
+            pass
+
+        if reset:
+            self.check_thread.setInterval(check_period)
+            try:
+                self.check_thread.timeout.disconnect(self.periodic_check_handler)
+            except Exception as e:
+                pass
+
+        self.check_thread.timeout.connect(self.periodic_check_handler)
+        self.check_thread.start(QtCore.QThread.HighPriority)
+
+    def periodic_check_handler(self):
+        """
+        If the intersections workers finished then start creating the solid_geometry
+        :return:
+        """
+        # log.debug("checking parsing --> %s" % str(self.parsing_promises))
+
+
+        try:
+            if not self.promises:
+                self.check_thread.stop()
+                if self.sub_type == "gerber":
+                    outname = self.target_gerber_combo.currentText() + '_sub'
+
+                    # intersection jobs finished, start the creation of solid_geometry
+                    self.app.worker_task.emit({'fcn': self.new_gerber_object,
+                                               'params': [outname]})
+                else:
+                    outname = self.target_geo_combo.currentText() + '_sub'
+
+                    # intersection jobs finished, start the creation of solid_geometry
+                    self.app.worker_task.emit({'fcn': self.new_geo_object,
+                                               'params': [outname]})
+
+                # reset the type of substraction for next time
+                self.sub_type = None
+
+                log.debug("ToolSub --> Periodic check finished.")
+        except Exception as e:
+            log.debug("ToolSub().periodic_check_handler() --> %s" % str(e))
+            traceback.print_exc()
+
+    def reset_fields(self):
+        self.target_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.sub_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+
+        self.target_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
+        self.sub_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))

+ 2 - 0
flatcamTools/__init__.py

@@ -16,5 +16,7 @@ from flatcamTools.ToolTransform import ToolTransform
 from flatcamTools.ToolSolderPaste import SolderPaste
 from flatcamTools.ToolPcbWizard import PcbWizard
 from flatcamTools.ToolPDF import ToolPDF
+from flatcamTools.ToolSub import ToolSub
+
 
 from flatcamTools.ToolShell import FCShell

BIN
locale/de/LC_MESSAGES/strings.mo


Разница между файлами не показана из-за своего большого размера
+ 225 - 198
locale/de/LC_MESSAGES/strings.po


BIN
locale/en/LC_MESSAGES/strings.mo


Разница между файлами не показана из-за своего большого размера
+ 229 - 200
locale/en/LC_MESSAGES/strings.po


BIN
locale/ro/LC_MESSAGES/strings.mo


Разница между файлами не показана из-за своего большого размера
+ 225 - 198
locale/ro/LC_MESSAGES/strings.po


Разница между файлами не показана из-за своего большого размера
+ 229 - 202
locale_template/strings.pot


+ 0 - 1
setup_ubuntu.sh

@@ -20,7 +20,6 @@ pip3 install --upgrade Shapely
 pip3 install --upgrade vispy
 pip3 install --upgrade rtree
 pip3 install --upgrade pyopengl
-pip3 install --upgrade pyopengl-accelerate
 pip3 install --upgrade setuptools
 pip3 install --upgrade svg.path
 pip3 install --upgrade ortools

BIN
share/sub32.png


Некоторые файлы не были показаны из-за большого количества измененных файлов