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

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)
     log.addHandler(handler)
 
 
     # Version
     # Version
-    version = 8.914
-    version_date = "2019/04/23"
+    version = 8.915
+    version_date = "2019/05/1"
     beta = True
     beta = True
 
 
     # current date now
     # current date now
@@ -189,6 +189,8 @@ class App(QtCore.QObject):
 
 
         App.log.info("FlatCAM Starting...")
         App.log.info("FlatCAM Starting...")
 
 
+        self.main_thread = QtWidgets.QApplication.instance().thread()
+
         ###################
         ###################
         ### OS-specific ###
         ### 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_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_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
             # General GUI Settings
             "global_layout": self.ui.general_defaults_form.general_gui_set_group.layout_combo,
             "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_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 General
             "gerber_plot": self.ui.gerber_defaults_form.gerber_gen_group.plot_cb,
             "gerber_plot": self.ui.gerber_defaults_form.gerber_gen_group.plot_cb,
             "gerber_solid": self.ui.gerber_defaults_form.gerber_gen_group.solid_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_alt_sel_line": '#006E20BF',
             "global_draw_color": '#FF0000',
             "global_draw_color": '#FF0000',
             "global_sel_draw_color": '#0000FF',
             "global_sel_draw_color": '#0000FF',
+            "global_proj_item_color": '#000000',
+            "global_proj_item_dis_color": '#b7b7cb',
 
 
             "global_toolbar_view": 511,
             "global_toolbar_view": 511,
 
 
@@ -647,6 +654,7 @@ class App(QtCore.QObject):
 
 
             # General GUI Settings
             # General GUI Settings
             "global_hover": True,
             "global_hover": True,
+            "global_selection_shape": True,
             "global_layout": "compact",
             "global_layout": "compact",
             # Gerber General
             # Gerber General
             "gerber_plot": True,
             "gerber_plot": True,
@@ -1162,7 +1170,8 @@ class App(QtCore.QObject):
             "background-color:%s" % str(self.defaults['global_sel_line'])[:7])
             "background-color:%s" % str(self.defaults['global_sel_line'])[:7])
 
 
         # Init Right-Left Selection colors
         # 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(
         self.ui.general_defaults_form.general_gui_group.alt_sf_color_button.setStyleSheet(
             "background-color:%s" % str(self.defaults['global_alt_sel_fill'])[:7])
             "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(
         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(
         self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_slider.setValue(
             int(self.defaults['global_sel_fill'][7:9], 16))
             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(
         self.ui.general_defaults_form.general_gui_group.alt_sl_color_button.setStyleSheet(
             "background-color:%s" % str(self.defaults['global_alt_sel_line'])[:7])
             "background-color:%s" % str(self.defaults['global_alt_sel_line'])[:7])
 
 
         # Init Draw color and Selection Draw Color
         # 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(
         self.ui.general_defaults_form.general_gui_group.draw_color_button.setStyleSheet(
             "background-color:%s" % str(self.defaults['global_draw_color'])[:7])
             "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(
         self.ui.general_defaults_form.general_gui_group.sel_draw_color_button.setStyleSheet(
             "background-color:%s" % str(self.defaults['global_sel_draw_color'])[:7])
             "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 ####
         #### End of Data ####
 
 
         #### Plot Area ####
         #### 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_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_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_exc.triggered.connect(self.new_excellon_object)
         self.ui.popmenu_new_prj.triggered.connect(self.on_file_new)
         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.zoomfit.triggered.connect(self.on_zoom_fit)
         self.ui.clearplot.triggered.connect(self.clear_plots)
         self.ui.clearplot.triggered.connect(self.clear_plots)
         self.ui.replot.triggered.connect(self.plot_all)
         self.ui.replot.triggered.connect(self.plot_all)
@@ -1418,34 +1423,64 @@ class App(QtCore.QObject):
         ###############################
         ###############################
 
 
         # Setting plot colors signals
         # 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
         # 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
         # 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.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_spinner.valueChanged.connect(
             self.on_alt_sf_color_spinner)
             self.on_alt_sf_color_spinner)
         self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_slider.valueChanged.connect(
         self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_slider.valueChanged.connect(
             self.on_alt_sf_color_slider)
             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
         # 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.wk_cb.currentIndexChanged.connect(self.on_workspace_modified)
         self.ui.general_defaults_form.general_gui_group.workspace_cb.stateChanged.connect(self.on_workspace)
         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.thr2 = QtCore.QThread()
             self.worker_task.emit({'fcn': self.version_check,
             self.worker_task.emit({'fcn': self.version_check,
                                    'params': []})
                                    '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
         # decide if we have a double click or single click
         self.doubleclick = False
         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
         # variable to store if a command is active (then the var is not None) and which one it is
         self.command_active = None
         self.command_active = None
         # variable to store the status of moving selection action
         # 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.film_tool.install(icon=QtGui.QIcon('share/film16.png'))
 
 
         self.paste_tool = SolderPaste(self)
         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 = ToolMove(self)
         self.move_tool.install(icon=QtGui.QIcon('share/move16.png'), pos=self.ui.menuedit,
         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,
         self.paint_tool.install(icon=QtGui.QIcon('share/paint16.png'), pos=self.ui.menutool,
                                   before=self.measurement_tool.menuAction, separator=True)
                                   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 = ToolTransform(self)
         self.transform_tool.install(icon=QtGui.QIcon('share/transform.png'), pos=self.ui.menuoptions, separator=True)
         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.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.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.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.calculators_btn.triggered.connect(lambda: self.calculator_tool.run(toggle=True))
         self.ui.transform_btn.triggered.connect(lambda: self.transform_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
             # set call source to the Editor we go into
             self.call_source = 'grb_editor'
             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
         # delete any selection shape that might be active as they are not relevant in Editor
         self.delete_selection_shape()
         self.delete_selection_shape()
@@ -2178,6 +2218,11 @@ class App(QtCore.QObject):
                 response = msgbox.clickedButton()
                 response = msgbox.clickedButton()
 
 
                 if response == bt_yes:
                 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):
                     if isinstance(edited_obj, FlatCAMGeometry):
                         obj_type = "Geometry"
                         obj_type = "Geometry"
                         if cleanup is None:
                         if cleanup is None:
@@ -2232,6 +2277,11 @@ class App(QtCore.QObject):
 
 
                     self.inform.emit(_("[selected] %s is updated, returning to App...") % obj_type)
                     self.inform.emit(_("[selected] %s is updated, returning to App...") % obj_type)
                 elif response == bt_no:
                 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):
                     if isinstance(edited_obj, FlatCAMGeometry):
                         self.geo_editor.deactivate()
                         self.geo_editor.deactivate()
                     elif isinstance(edited_obj, FlatCAMGerber):
                     elif isinstance(edited_obj, FlatCAMGerber):
@@ -2268,7 +2318,8 @@ class App(QtCore.QObject):
             self.ui.plot_tab_area.protectTab(0)
             self.ui.plot_tab_area.protectTab(0)
 
 
             # make sure that we reenable the selection on Project Tab after returning from Editor Mode:
             # 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):
     def get_last_folder(self):
         return self.defaults["global_last_folder"]
         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):
     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 is, updates the GUI accordingly, any other records and plots it.
         This method is thread-safe.
         This method is thread-safe.
 
 
@@ -2883,7 +2934,7 @@ class App(QtCore.QObject):
         FlatCAMApp.App.log.debug("Moving new object back to main thread.")
         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.
         # 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)
         self.object_created.emit(obj, obj_plot, obj_autoselected)
 
 
         return obj
         return obj
@@ -2911,6 +2962,14 @@ class App(QtCore.QObject):
             grb_obj.follow = False
             grb_obj.follow = False
             grb_obj.apertures = {}
             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)
         self.new_object('gerber', 'new_grb', initialize, plot=False)
 
 
     def on_object_created(self, obj, plot, autoselect):
     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.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
         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):
     def on_deselect_all(self):
         self.collection.set_all_inactive()
         self.collection.set_all_inactive()
         self.delete_selection_shape()
         self.delete_selection_shape()
@@ -4481,7 +4584,7 @@ class App(QtCore.QObject):
         """
         """
         self.report_usage("on_jump_to()")
         self.report_usage("on_jump_to()")
 
 
-        if custom_location is None:
+        if not custom_location:
             dia_box = Dialog_box(title=_("Jump to ..."),
             dia_box = Dialog_box(title=_("Jump to ..."),
                                  label=_("Enter the coordinates in format X,Y:"),
                                  label=_("Enter the coordinates in format X,Y:"),
                                  icon=QtGui.QIcon('share/jump_to16.png'))
                                  icon=QtGui.QIcon('share/jump_to16.png'))
@@ -4627,9 +4730,14 @@ class App(QtCore.QObject):
             if obj.tools:
             if obj.tools:
                 obj_init.tools = 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():
         for obj in self.collection.get_selected():
 
 
@@ -4680,7 +4788,8 @@ class App(QtCore.QObject):
             self.collection.set_active(name)
             self.collection.set_active(name)
             curr_sel_obj = self.collection.get_by_name(name)
             curr_sel_obj = self.collection.get_by_name(name)
             # create the selection box around the selected object
             # 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):
     def on_preferences(self):
 
 
@@ -4915,6 +5024,7 @@ class App(QtCore.QObject):
         # self.plotcanvas.auto_adjust_axes()
         # self.plotcanvas.auto_adjust_axes()
         self.plotcanvas.vispy_canvas.update()           # TODO: Need update canvas?
         self.plotcanvas.vispy_canvas.update()           # TODO: Need update canvas?
         self.on_zoom_fit(None)
         self.on_zoom_fit(None)
+        self.collection.update_view()
 
 
     # TODO: Rework toolbar 'clear', 'replot' functions
     # TODO: Rework toolbar 'clear', 'replot' functions
     def on_toolbar_replot(self):
     def on_toolbar_replot(self):
@@ -4957,9 +5067,9 @@ class App(QtCore.QObject):
             action.triggered.connect(self.set_grid)
             action.triggered.connect(self.set_grid)
 
 
         self.ui.cmenu_gridmenu.addSeparator()
         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_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)
         grid_delete.triggered.connect(self.on_grid_delete)
 
 
     def set_grid(self):
     def set_grid(self):
@@ -4970,8 +5080,8 @@ class App(QtCore.QObject):
         ## Current application units in lower Case
         ## Current application units in lower Case
         units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
         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)
                                        min=0.0000, max=99.9999, decimals=4)
         grid_add_popup.setWindowIcon(QtGui.QIcon('share/plus32.png'))
         grid_add_popup.setWindowIcon(QtGui.QIcon('share/plus32.png'))
 
 
@@ -5126,15 +5236,13 @@ class App(QtCore.QObject):
         self.plotcanvas.vispy_canvas.native.setFocus()
         self.plotcanvas.vispy_canvas.native.setFocus()
         self.pos_jump = event.pos
         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 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
                 return
-            else:
-                self.panning_action = False
 
 
         if self.rel_point1 is not None:
         if self.rel_point1 is not None:
             try:  # May fail in case mouse not within axes
             try:  # May fail in case mouse not within axes
@@ -5222,12 +5330,12 @@ class App(QtCore.QObject):
         # canvas menu
         # canvas menu
         try:
         try:
             if event.button == 2:  # right click
             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.cursor = QtGui.QCursor()
                     self.populate_cmenu_grids()
                     self.populate_cmenu_grids()
                     self.ui.popMenu.popup(self.cursor.pos())
                     self.ui.popMenu.popup(self.cursor.pos())
+
         except Exception as e:
         except Exception as e:
             log.warning("Error: %s" % str(e))
             log.warning("Error: %s" % str(e))
             return
             return
@@ -5294,12 +5402,14 @@ class App(QtCore.QObject):
                     if sel_type is True:
                     if sel_type is True:
                         if poly_obj.within(poly_selection):
                         if poly_obj.within(poly_selection):
                             # create the selection box around the selected object
                             # 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'])
                             self.collection.set_active(obj.options['name'])
                     else:
                     else:
                         if poly_selection.intersects(poly_obj):
                         if poly_selection.intersects(poly_obj):
                             # create the selection box around the selected object
                             # 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'])
                             self.collection.set_active(obj.options['name'])
             except:
             except:
                 # the Exception here will happen if we try to select on screen and we have an newly (and empty)
                 # 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])
                         self.collection.set_active(objects_under_the_click_list[0])
                         # create the selection box around the selected object
                         # create the selection box around the selected object
                         curr_sel_obj = self.collection.get_active()
                         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' %
                         # self.inform.emit('[selected] %s: %s selected' %
                         #                  (str(curr_sel_obj.kind).capitalize(), str(curr_sel_obj.options['name'])))
                         #                  (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])
                         self.collection.set_active(objects_under_the_click_list[0])
                         # create the selection box around the selected object
                         # create the selection box around the selected object
                         curr_sel_obj = self.collection.get_active()
                         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' %
                         # self.inform.emit('[selected] %s: %s selected' %
                         #                  (str(curr_sel_obj.kind).capitalize(), str(curr_sel_obj.options['name'])))
                         #                  (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
                     # delete the possible selection box around a possible selected object
                     self.delete_selection_shape()
                     self.delete_selection_shape()
                     # create the selection box around the selected object
                     # 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' %
                     # self.inform.emit('[selected] %s: %s selected' %
                     #                  (str(curr_sel_obj.kind).capitalize(), str(curr_sel_obj.options['name'])))
                     #                  (str(curr_sel_obj.kind).capitalize(), str(curr_sel_obj.options['name'])))
@@ -7597,6 +7710,7 @@ class App(QtCore.QObject):
         icons = {
         icons = {
             "gerber": "share/flatcam_icon16.png",
             "gerber": "share/flatcam_icon16.png",
             "excellon": "share/drill16.png",
             "excellon": "share/drill16.png",
+            'geometry': "share/geometry16.png",
             "cncjob": "share/cnc16.png",
             "cncjob": "share/cnc16.png",
             "project": "share/project16.png",
             "project": "share/project16.png",
             "svg": "share/geometry16.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>' 
     QObject::connect: Cannot queue arguments of type 'QVector<int>' 
     (Make sure 'QVector<int>' is registered using qRegisterMetaType().
     (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:
         if threaded is True:
             def worker_task(app_obj):
             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:
                 for obj in objects:
                     obj.options['plot'] = True
                     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.plots_updated.emit()
-                self.collection.update_view()
+                # self.collection.update_view()
 
 
             # Send to worker
             # Send to worker
             # self.worker.add_task(worker_task, [self])
             # self.worker.add_task(worker_task, [self])
@@ -7892,9 +8006,9 @@ The normal flow when working in FlatCAM is the following:</span></p>
         else:
         else:
             for obj in objects:
             for obj in objects:
                 obj.options['plot'] = True
                 obj.options['plot'] = True
-            self.progress.emit(0)
+            # self.progress.emit(0)
             self.plots_updated.emit()
             self.plots_updated.emit()
-            self.collection.update_view()
+            # self.collection.update_view()
 
 
     # TODO: FIX THIS
     # 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>' 
     QObject::connect: Cannot queue arguments of type 'QVector<int>' 
     (Make sure 'QVector<int>' is registered using qRegisterMetaType().
     (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.
         # TODO: This method is very similar to replot_all. Try to merge.
         """
         """
         Disables plots
         Disables plots
@@ -7914,23 +8028,23 @@ The normal flow when working in FlatCAM is the following:</span></p>
         """
         """
 
 
         if threaded is True:
         if threaded is True:
-            self.progress.emit(10)
+            # self.progress.emit(10)
             def worker_task(app_obj):
             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:
                 for obj in objects:
                     obj.options['plot'] = False
                     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.plots_updated.emit()
-                self.collection.update_view()
+                # self.collection.update_view()
 
 
             # Send to worker
             # Send to worker
             self.worker_task.emit({'fcn': worker_task, 'params': [self]})
             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:
             for obj in objects:
                 obj.options['plot'] = False
                 obj.options['plot'] = False
             self.plots_updated.emit()
             self.plots_updated.emit()
-            self.collection.update_view()
+            # self.collection.update_view()
 
 
     def clear_plots(self):
     def clear_plots(self):
 
 

+ 28 - 21
FlatCAMObj.py

@@ -181,17 +181,18 @@ class FlatCAMObj(QtCore.QObject):
         old_name = copy(self.options["name"])
         old_name = copy(self.options["name"])
         new_name = self.ui.name_entry.get_value()
         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):
     def on_offset_button_click(self):
         self.app.report_usage("obj_on_offset_button")
         self.app.report_usage("obj_on_offset_button")
@@ -358,6 +359,12 @@ class FlatCAMObj(QtCore.QObject):
         except AttributeError:
         except AttributeError:
             pass
             pass
 
 
+        # Not all object types have mark_shapes
+        # try:
+        #     self.mark_shapes.clear(update)
+        # except AttributeError:
+        #     pass
+
     def delete(self):
     def delete(self):
         # Free resources
         # Free resources
         del self.ui
         del self.ui
@@ -2717,7 +2724,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # those elements are the ones used for generating GCode
         # those elements are the ones used for generating GCode
         self.sel_tools = {}
         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.type_item_options = [_("Iso"), _("Rough"), _("Finish")]
         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
 
 
@@ -2959,7 +2966,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             self.tools.update({
             self.tools.update({
                 self.tooluid: {
                 self.tooluid: {
                     'tooldia': float(self.options["cnctooldia"]),
                     'tooldia': float(self.options["cnctooldia"]),
-                    'offset': _('Path'),
+                    'offset': ('Path'),
                     'offset_value': 0.0,
                     'offset_value': 0.0,
                     'type': _('Rough'),
                     'type': _('Rough'),
                     'tool_type': 'C1',
                     'tool_type': 'C1',
@@ -3041,7 +3048,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             tool_offset = self.ui.geo_tools_table.cellWidget(current_row, 2)
             tool_offset = self.ui.geo_tools_table.cellWidget(current_row, 2)
             if tool_offset is not None:
             if tool_offset is not None:
                 tool_offset_txt = tool_offset.currentText()
                 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_entry.show()
                     self.ui.tool_offset_lbl.show()
                     self.ui.tool_offset_lbl.show()
                 else:
                 else:
@@ -3246,7 +3253,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             self.tools.update({
             self.tools.update({
                 self.tooluid: {
                 self.tooluid: {
                     'tooldia': tooldia,
                     'tooldia': tooldia,
-                    'offset': _('Path'),
+                    'offset': ('Path'),
                     'offset_value': 0.0,
                     'offset_value': 0.0,
                     'type': _('Rough'),
                     'type': _('Rough'),
                     'tool_type': 'C1',
                     'tool_type': 'C1',
@@ -3615,7 +3622,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 cb_txt = cw.currentText()
                 cb_txt = cw.currentText()
                 if cw_col == 2:
                 if cw_col == 2:
                     tooluid_value['offset'] = cb_txt
                     tooluid_value['offset'] = cb_txt
-                    if cb_txt == _('Custom'):
+                    if cb_txt == ('Custom'):
                         self.ui.tool_offset_entry.show()
                         self.ui.tool_offset_entry.show()
                         self.ui.tool_offset_lbl.show()
                         self.ui.tool_offset_lbl.show()
                     else:
                     else:
@@ -4088,13 +4095,13 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                             diadict_key: datadict
                             diadict_key: datadict
                         })
                         })
 
 
-                if dia_cnc_dict['offset'] == 'in':
+                if dia_cnc_dict['offset'] == ('in'):
                     tool_offset = -dia_cnc_dict['tooldia'] / 2
                     tool_offset = -dia_cnc_dict['tooldia'] / 2
                     offset_str = 'inside'
                     offset_str = 'inside'
-                elif dia_cnc_dict['offset'].lower() == 'out':
+                elif dia_cnc_dict['offset'].lower() == ('out'):
                     tool_offset = dia_cnc_dict['tooldia']  / 2
                     tool_offset = dia_cnc_dict['tooldia']  / 2
                     offset_str = 'outside'
                     offset_str = 'outside'
-                elif dia_cnc_dict['offset'].lower() == 'path':
+                elif dia_cnc_dict['offset'].lower() == ('path'):
                     offset_str = 'onpath'
                     offset_str = 'onpath'
                     tool_offset = 0.0
                     tool_offset = 0.0
                 else:
                 else:
@@ -4321,13 +4328,13 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                             diadict_key: datadict
                             diadict_key: datadict
                         })
                         })
 
 
-                if dia_cnc_dict['offset'] == 'in':
+                if dia_cnc_dict['offset'] == ('in'):
                     tool_offset = -dia_cnc_dict['tooldia'] / 2
                     tool_offset = -dia_cnc_dict['tooldia'] / 2
                     offset_str = 'inside'
                     offset_str = 'inside'
-                elif dia_cnc_dict['offset'].lower() == 'out':
+                elif dia_cnc_dict['offset'].lower() == ('out'):
                     tool_offset = dia_cnc_dict['tooldia']  / 2
                     tool_offset = dia_cnc_dict['tooldia']  / 2
                     offset_str = 'outside'
                     offset_str = 'outside'
-                elif dia_cnc_dict['offset'].lower() == 'path':
+                elif dia_cnc_dict['offset'].lower() == ('path'):
                     offset_str = 'onpath'
                     offset_str = 'onpath'
                     tool_offset = 0.0
                     tool_offset = 0.0
                 else:
                 else:

+ 1 - 1
FlatCAMWorkerStack.py

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

+ 27 - 21
ObjectCollection.py

@@ -118,13 +118,13 @@ class KeySensitiveListView(QtWidgets.QTreeView):
             event.ignore()
             event.ignore()
 
 
 
 
-class TreeItem:
+class TreeItem(KeySensitiveListView):
     """
     """
     Item of a tree model
     Item of a tree model
     """
     """
 
 
     def __init__(self, data, icon=None, obj=None, parent_item=None):
     def __init__(self, data, icon=None, obj=None, parent_item=None):
-
+        super(TreeItem, self).__init__(parent_item)
         self.parent_item = parent_item
         self.parent_item = parent_item
         self.item_data = data  # Columns string data
         self.item_data = data  # Columns string data
         self.icon = icon  # Decoration
         self.icon = icon  # Decoration
@@ -378,9 +378,11 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                 return index.internalPointer().data(index.column())
                 return index.internalPointer().data(index.column())
 
 
         if role == Qt.ForegroundRole:
         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
             obj = index.internalPointer().obj
             if 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:
             else:
                 return index.internalPointer().data(index.column())
                 return index.internalPointer().data(index.column())
 
 
@@ -397,23 +399,25 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         if index.isValid():
         if index.isValid():
             obj = index.internalPointer().obj
             obj = index.internalPointer().obj
             if 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
         return True
 
 
@@ -681,6 +685,8 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         :param name: Name of the FlatCAM Object
         :param name: Name of the FlatCAM Object
         :return: None
         :return: None
         """
         """
+        log.debug("ObjectCollection.set_inactive()")
+
         obj = self.get_by_name(name)
         obj = self.get_by_name(name)
         item = obj.item
         item = obj.item
         group = self.group_items[obj.kind]
         group = self.group_items[obj.kind]
@@ -721,7 +727,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                     color='red', name=str(obj.options['name'])))
                     color='red', name=str(obj.options['name'])))
 
 
         except IndexError:
         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('')
             self.app.inform.emit('')
             try:
             try:
                 self.app.ui.selected_scroll_area.takeWidget()
                 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
 23.04.2019
 
 
 - Gerber Editor: added two new tools: Add Disc and Add SemiDisc (porting of Circle and Arc from Geometry Editor)
 - 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
         # will store the Gerber geometry's as paths
         self.follow_geometry = []
         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 = ''
         self.source_file = ''
 
 
         # Attributes to be included in serialization
         # Attributes to be included in serialization
@@ -2174,10 +2178,6 @@ class Gerber (Geometry):
         # applying a union for every new polygon.
         # applying a union for every new polygon.
         poly_buffer = []
         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
         # store here the follow geometry
         follow_buffer = []
         follow_buffer = []
 
 
@@ -2242,6 +2242,8 @@ class Gerber (Geometry):
                 match = self.lpol_re.search(gline)
                 match = self.lpol_re.search(gline)
                 if match:
                 if match:
                     new_polarity = match.group(1)
                     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:
                     if len(path) > 1 and current_polarity != new_polarity:
 
 
                         # finish the current path and add it to the storage
                         # 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)
                                 self.apertures[last_path_aperture]['follow_geometry'].append(geo)
 
 
                         geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
                         geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+
                         if not geo.is_empty:
                         if not geo.is_empty:
                             poly_buffer.append(geo)
                             poly_buffer.append(geo)
                             if self.is_lpc is True:
                             if self.is_lpc is True:
@@ -2280,12 +2283,10 @@ class Gerber (Geometry):
                     # TODO: Remove when bug fixed
                     # TODO: Remove when bug fixed
                     if len(poly_buffer) > 0:
                     if len(poly_buffer) > 0:
                         if current_polarity == 'D':
                         if current_polarity == 'D':
-                            self.is_lpc = True
                             # self.follow_geometry = self.follow_geometry.union(cascaded_union(follow_buffer))
                             # self.follow_geometry = self.follow_geometry.union(cascaded_union(follow_buffer))
                             self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
                             self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
 
 
                         else:
                         else:
-                            self.is_lpc = False
                             # self.follow_geometry = self.follow_geometry.difference(cascaded_union(follow_buffer))
                             # self.follow_geometry = self.follow_geometry.difference(cascaded_union(follow_buffer))
                             self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
                             self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
 
 
@@ -2414,7 +2415,7 @@ class Gerber (Geometry):
                 ### Aperture definitions %ADD...
                 ### Aperture definitions %ADD...
                 match = self.ad_re.search(gline)
                 match = self.ad_re.search(gline)
                 if match:
                 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))
                     self.aperture_parse(match.group(1), match.group(2), match.group(3))
                     continue
                     continue
 
 
@@ -2460,7 +2461,7 @@ class Gerber (Geometry):
                 match = self.tool_re.search(gline)
                 match = self.tool_re.search(gline)
                 if match:
                 if match:
                     current_aperture = match.group(1)
                     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
                     # 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.
                     # 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]["type"] is not "AM":
                         if self.apertures[current_aperture]["size"] == 0:
                         if self.apertures[current_aperture]["size"] == 0:
                             self.apertures[current_aperture]["size"] = 1e-12
                             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
                     # Take care of the current path with the previous tool
                     if len(path) > 1:
                     if len(path) > 1:
@@ -2479,7 +2480,6 @@ class Gerber (Geometry):
                         else:
                         else:
                             # --- Buffered ----
                             # --- Buffered ----
                             width = self.apertures[last_path_aperture]["size"]
                             width = self.apertures[last_path_aperture]["size"]
-
                             geo = LineString(path)
                             geo = LineString(path)
                             if not geo.is_empty:
                             if not geo.is_empty:
                                 follow_buffer.append(geo)
                                 follow_buffer.append(geo)
@@ -2551,6 +2551,12 @@ class Gerber (Geometry):
                 if self.regionoff_re.search(gline):
                 if self.regionoff_re.search(gline):
                     making_region = False
                     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
                     # 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
                     # geo to the poly_buffer otherwise we loose it
                     if current_operation_code == 2:
                     if current_operation_code == 2:
@@ -2558,24 +2564,24 @@ class Gerber (Geometry):
                             if not geo.is_empty:
                             if not geo.is_empty:
                                 follow_buffer.append(geo)
                                 follow_buffer.append(geo)
                                 try:
                                 try:
-                                    self.apertures[current_aperture]['follow_geometry'].append(geo)
+                                    self.apertures['0']['follow_geometry'].append(geo)
                                 except KeyError:
                                 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)
                                 poly_buffer.append(geo)
                                 if self.is_lpc is True:
                                 if self.is_lpc is True:
                                     try:
                                     try:
-                                        self.apertures[current_aperture]['clear_geometry'].append(geo)
+                                        self.apertures['0']['clear_geometry'].append(geo)
                                     except KeyError:
                                     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:
                                 else:
                                     try:
                                     try:
-                                        self.apertures[current_aperture]['solid_geometry'].append(geo)
+                                        self.apertures['0']['solid_geometry'].append(geo)
                                     except KeyError:
                                     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
                             continue
 
 
                     # Only one path defines region?
                     # Only one path defines region?
@@ -2599,10 +2605,10 @@ class Gerber (Geometry):
                     if not region.is_empty:
                     if not region.is_empty:
                         follow_buffer.append(region)
                         follow_buffer.append(region)
                         try:
                         try:
-                            self.apertures[current_aperture]['follow_geometry'].append(region)
+                            self.apertures['0']['follow_geometry'].append(region)
                         except KeyError:
                         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)
                     region = Polygon(path)
                     if not region.is_valid:
                     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
                         # we do this for the case that a region is done without having defined any aperture
                         # Allegro does that
                         # 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:
                         if self.is_lpc is True:
                             try:
                             try:
                                 self.apertures[used_aperture]['clear_geometry'].append(region)
                                 self.apertures[used_aperture]['clear_geometry'].append(region)
@@ -2727,6 +2734,7 @@ class Gerber (Geometry):
                                 if '0' not in self.apertures:
                                 if '0' not in self.apertures:
                                     self.apertures['0'] = {}
                                     self.apertures['0'] = {}
                                     self.apertures['0']['type'] = 'REG'
                                     self.apertures['0']['type'] = 'REG'
+                                    self.apertures['0']['size'] = 0.0
                                     self.apertures['0']['solid_geometry'] = []
                                     self.apertures['0']['solid_geometry'] = []
                                 last_path_aperture = '0'
                                 last_path_aperture = '0'
                         else:
                         else:
@@ -2746,6 +2754,7 @@ class Gerber (Geometry):
                                     if '0' not in self.apertures:
                                     if '0' not in self.apertures:
                                         self.apertures['0'] = {}
                                         self.apertures['0'] = {}
                                         self.apertures['0']['type'] = 'REG'
                                         self.apertures['0']['type'] = 'REG'
+                                        self.apertures['0']['size'] = 0.0
                                         self.apertures['0']['solid_geometry'] = []
                                         self.apertures['0']['solid_geometry'] = []
                                     last_path_aperture = '0'
                                     last_path_aperture = '0'
                                 geo = Polygon()
                                 geo = Polygon()
@@ -2779,6 +2788,7 @@ class Gerber (Geometry):
                                     if '0' not in self.apertures:
                                     if '0' not in self.apertures:
                                         self.apertures['0'] = {}
                                         self.apertures['0'] = {}
                                         self.apertures['0']['type'] = 'REG'
                                         self.apertures['0']['type'] = 'REG'
+                                        self.apertures['0']['size'] = 0.0
                                         self.apertures['0']['solid_geometry'] = []
                                         self.apertures['0']['solid_geometry'] = []
                                     last_path_aperture = '0'
                                     last_path_aperture = '0'
                                 elem = [linear_x, linear_y]
                                 elem = [linear_x, linear_y]
@@ -5288,8 +5298,13 @@ class CNCjob(Geometry):
                             y2 = locations[to_node][1]
                             y2 = locations[to_node][1]
                             self.matrix[from_node][to_node] = distance_euclidian(x1, y1, x2, y2)
                             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.
         # Create the data.
         def create_data_array():
         def create_data_array():
@@ -5327,8 +5342,9 @@ class CNCjob(Geometry):
                         depot = 0
                         depot = 0
                         # Create routing model.
                         # Create routing model.
                         if tsp_size > 0:
                         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 = (
                             search_parameters.local_search_metaheuristic = (
                                 routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
                                 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.
                             # arguments (the from and to node indices) and returns the distance between them.
                             dist_between_locations = CreateDistanceCallback()
                             dist_between_locations = CreateDistanceCallback()
                             dist_callback = dist_between_locations.Distance
                             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.
                             # Solve, returns a solution if any.
                             assignment = routing.SolveWithParameters(search_parameters)
                             assignment = routing.SolveWithParameters(search_parameters)
@@ -5432,14 +5449,16 @@ class CNCjob(Geometry):
 
 
                         # Create routing model.
                         # Create routing model.
                         if tsp_size > 0:
                         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
                             # Callback to the distance function. The callback takes two
                             # arguments (the from and to node indices) and returns the distance between them.
                             # arguments (the from and to node indices) and returns the distance between them.
                             dist_between_locations = CreateDistanceCallback()
                             dist_between_locations = CreateDistanceCallback()
                             dist_callback = dist_between_locations.Distance
                             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.
                             # Solve, returns a solution if any.
                             assignment = routing.SolveWithParameters(search_parameters)
                             assignment = routing.SolveWithParameters(search_parameters)

+ 158 - 62
flatcamEditors/FlatCAMExcEditor.py

@@ -44,7 +44,7 @@ class FCDrillAdd(FCShapeTool):
 
 
         except KeyError:
         except KeyError:
             self.draw_app.app.inform.emit(_("[WARNING_NOTCL] To add a drill first select a tool"))
             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
             return
 
 
         try:
         try:
@@ -103,6 +103,7 @@ class FCDrillAdd(FCShapeTool):
 
 
         self.draw_app.current_storage = self.draw_app.storage_dict[self.selected_dia]
         self.draw_app.current_storage = self.draw_app.storage_dict[self.selected_dia]
         self.geometry = DrawToolShape(self.util_shape(self.points))
         self.geometry = DrawToolShape(self.util_shape(self.points))
+        self.draw_app.in_action = False
         self.complete = True
         self.complete = True
         self.draw_app.app.inform.emit(_("[success] Done. Drill added."))
         self.draw_app.app.inform.emit(_("[success] Done. Drill added."))
 
 
@@ -319,7 +320,7 @@ class FCDrillArray(FCShapeTool):
                 self.geometry.append(DrawToolShape(geo))
                 self.geometry.append(DrawToolShape(geo))
         self.complete = True
         self.complete = True
         self.draw_app.app.inform.emit(_("[success] Done. Drill Array added."))
         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()
         self.draw_app.array_frame.hide()
         return
         return
 
 
@@ -428,7 +429,7 @@ class FCDrillResize(FCShapeTool):
         self.complete = True
         self.complete = True
 
 
         # MS: always return to the Select Tool
         # MS: always return to the Select Tool
-        self.draw_app.select_tool("select")
+        self.draw_app.select_tool("drill_select")
 
 
 
 
 class FCDrillMove(FCShapeTool):
 class FCDrillMove(FCShapeTool):
@@ -475,7 +476,7 @@ class FCDrillMove(FCShapeTool):
             self.make()
             self.make()
 
 
             # MS: always return to the Select Tool
             # MS: always return to the Select Tool
-            self.draw_app.select_tool("select")
+            self.draw_app.select_tool("drill_select")
             return
             return
 
 
     def make(self):
     def make(self):
@@ -643,8 +644,11 @@ class FCDrillSelect(DrawTool):
                         sel_tools.add(storage)
                         sel_tools.add(storage)
 
 
             for storage in sel_tools:
             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)
             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'},
         self.drill_axis_radio = RadioSet([{'label': 'X', 'value': 'X'},
                                           {'label': 'Y', 'value': 'Y'},
                                           {'label': 'Y', 'value': 'Y'},
-                                          {'label': _('Angle'), 'value': 'A'}])
+                                          {'label': 'Angle', 'value': 'A'}])
         self.drill_axis_radio.set_value('X')
         self.drill_axis_radio.set_value('X')
         self.linear_form.addRow(self.drill_axis_label, self.drill_axis_radio)
         self.linear_form.addRow(self.drill_axis_label, self.drill_axis_radio)
 
 
@@ -999,7 +1003,7 @@ class FlatCAMExcEditor(QtCore.QObject):
 
 
         ## Toolbar events and properties
         ## Toolbar events and properties
         self.tools_exc = {
         self.tools_exc = {
-            "select": {"button": self.app.ui.select_drill_btn,
+            "drill_select": {"button": self.app.ui.select_drill_btn,
                        "constructor": FCDrillSelect},
                        "constructor": FCDrillSelect},
             "drill_add": {"button": self.app.ui.add_drill_btn,
             "drill_add": {"button": self.app.ui.add_drill_btn,
                     "constructor": FCDrillAdd},
                     "constructor": FCDrillAdd},
@@ -1016,6 +1020,8 @@ class FlatCAMExcEditor(QtCore.QObject):
         ### Data
         ### Data
         self.active_tool = None
         self.active_tool = None
 
 
+        self.in_action = False
+
         self.storage_dict = {}
         self.storage_dict = {}
         self.current_storage = []
         self.current_storage = []
 
 
@@ -1067,7 +1073,6 @@ class FlatCAMExcEditor(QtCore.QObject):
 
 
         self.app.ui.exc_move_drill_menuitem.triggered.connect(self.exc_move_drills)
         self.app.ui.exc_move_drill_menuitem.triggered.connect(self.exc_move_drills)
 
 
-
         # Init GUI
         # Init GUI
         self.drill_array_size_entry.set_value(5)
         self.drill_array_size_entry.set_value(5)
         self.drill_pitch_entry.set_value(2.54)
         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:
         if self.app.ui.grid_snap_btn.isChecked() is False:
             self.app.ui.grid_snap_btn.trigger()
             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
         # Tell the App that the editor is active
         self.editor_active = True
         self.editor_active = True
 
 
@@ -1653,6 +1665,11 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.drills_frame.show()
         self.drills_frame.show()
 
 
     def deactivate(self):
     def deactivate(self):
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         # adjust the status of the menu entries related to the editor
         # adjust the status of the menu entries related to the editor
         self.app.ui.menueditedit.setDisabled(False)
         self.app.ui.menueditedit.setDisabled(False)
         self.app.ui.menueditok.setDisabled(True)
         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.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
         # Show original geometry
         if self.exc_obj:
         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.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(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):
     def disconnect_canvas_event_handlers(self):
         # we restore the key and mouse control to FlatCAMApp method
         # we restore the key and mouse control to FlatCAMApp method
         # first connect to new, then disconnect the old handlers
         # 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_move', self.on_canvas_move)
         self.canvas.vis_disconnect('mouse_release', self.on_exc_click_release)
         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):
     def clear(self):
         self.active_tool = None
         self.active_tool = None
         # self.shape_buffer = []
         # self.shape_buffer = []
@@ -1786,7 +1849,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         # Set selection tolerance
         # Set selection tolerance
         # DrawToolShape.tolerance = fc_excellon.drawing_tolerance * 10
         # DrawToolShape.tolerance = fc_excellon.drawing_tolerance * 10
 
 
-        self.select_tool("select")
+        self.select_tool("drill_select")
 
 
         self.set_ui()
         self.set_ui()
 
 
@@ -2002,10 +2065,10 @@ class FlatCAMExcEditor(QtCore.QObject):
 
 
         self.app.log.debug("on_tool_select('%s')" % tool)
         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
             self.complete = True
-            current_tool = 'select'
+            current_tool = 'drill_select'
             self.app.inform.emit(_("[WARNING_NOTCL] Cancelled. There is no Tool/Drill selected"))
             self.app.inform.emit(_("[WARNING_NOTCL] Cancelled. There is no Tool/Drill selected"))
 
 
         # This is to make the group behave as radio group
         # This is to make the group behave as radio group
@@ -2024,7 +2087,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                 for t in self.tools_exc:
                 for t in self.tools_exc:
                     self.tools_exc[t]["button"].setChecked(False)
                     self.tools_exc[t]["button"].setChecked(False)
 
 
-                self.select_tool('select')
+                self.select_tool('drill_select')
                 self.active_tool = FCDrillSelect(self)
                 self.active_tool = FCDrillSelect(self)
 
 
     def on_row_selected(self, row, col):
     def on_row_selected(self, row, col):
@@ -2042,7 +2105,7 @@ class FlatCAMExcEditor(QtCore.QObject):
 
 
             try:
             try:
                 selected_dia = self.tool2tooldia[self.tools_table_exc.currentRow() + 1]
                 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():
                 for obj in self.storage_dict[selected_dia].get_objects():
                     self.selected.append(obj)
                     self.selected.append(obj)
             except Exception as e:
             except Exception as e:
@@ -2060,23 +2123,29 @@ class FlatCAMExcEditor(QtCore.QObject):
     def on_canvas_click(self, event):
     def on_canvas_click(self, event):
         """
         """
         event.x and .y have canvas coordinates
         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
         :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:
         if event.button is 1:
             self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
             self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
                                                    "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
                                                    "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
             self.pos = self.canvas.vispy_canvas.translate_coords(event.pos)
             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
             # Selection with left mouse button
             if self.active_tool is not None and event.button is 1:
             if self.active_tool is not None and event.button is 1:
                 # Dispatch event to active_tool
                 # Dispatch event to active_tool
@@ -2100,7 +2169,11 @@ class FlatCAMExcEditor(QtCore.QObject):
                     if key_modifier == modifier_to_use:
                     if key_modifier == modifier_to_use:
                         self.select_tool(self.active_tool.name)
                         self.select_tool(self.active_tool.name)
                     else:
                     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
                         return
 
 
                 if isinstance(self.active_tool, FCDrillSelect):
                 if isinstance(self.active_tool, FCDrillSelect):
@@ -2187,10 +2260,9 @@ class FlatCAMExcEditor(QtCore.QObject):
             self.storage.insert(shape)  # TODO: Check performance
             self.storage.insert(shape)  # TODO: Check performance
 
 
     def on_exc_click_release(self, event):
     def on_exc_click_release(self, event):
-        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
-
         self.modifiers = QtWidgets.QApplication.keyboardModifiers()
         self.modifiers = QtWidgets.QApplication.keyboardModifiers()
 
 
+        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
         if self.app.grid_status():
         if self.app.grid_status():
             pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
             pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
         else:
         else:
@@ -2200,11 +2272,29 @@ class FlatCAMExcEditor(QtCore.QObject):
         # canvas menu
         # canvas menu
         try:
         try:
             if event.button == 2:  # right click
             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:
         except Exception as e:
             log.warning("Error: %s" % str(e))
             log.warning("Error: %s" % str(e))
             raise
             raise
@@ -2216,13 +2306,13 @@ class FlatCAMExcEditor(QtCore.QObject):
                 if self.app.selection_type is not None:
                 if self.app.selection_type is not None:
                     self.draw_selection_area_handler(self.pos, pos, self.app.selection_type)
                     self.draw_selection_area_handler(self.pos, pos, self.app.selection_type)
                     self.app.selection_type = None
                     self.app.selection_type = None
+
                 elif isinstance(self.active_tool, FCDrillSelect):
                 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.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:
         except Exception as e:
             log.warning("Error: %s" % str(e))
             log.warning("Error: %s" % str(e))
             raise
             raise
@@ -2264,7 +2354,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                         if self.tool2tooldia[key] == storage:
                         if self.tool2tooldia[key] == storage:
                             item = self.tools_table_exc.item((key - 1), 1)
                             item = self.tools_table_exc.item((key - 1), 1)
                             self.tools_table_exc.setCurrentItem(item)
                             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)
         self.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
 
 
@@ -2287,16 +2377,12 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.x = event.xdata
         self.x = event.xdata
         self.y = event.ydata
         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 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
             return
-        else:
-            self.app.panning_action = False
 
 
         try:
         try:
             x = float(event.xdata)
             x = float(event.xdata)
@@ -2308,7 +2394,13 @@ class FlatCAMExcEditor(QtCore.QObject):
             return
             return
 
 
         ### Snap coordinates
         ### 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_x = x
         self.snap_y = y
         self.snap_y = y
@@ -2330,23 +2422,27 @@ class FlatCAMExcEditor(QtCore.QObject):
         geo = self.active_tool.utility_geometry(data=(x, y))
         geo = self.active_tool.utility_geometry(data=(x, y))
 
 
         if isinstance(geo, DrawToolShape) and geo.geo is not None:
         if isinstance(geo, DrawToolShape) and geo.geo is not None:
-
             # Remove any previous utility shape
             # Remove any previous utility shape
             self.tool_shape.clear(update=True)
             self.tool_shape.clear(update=True)
             self.draw_utility_geometry(geo=geo)
             self.draw_utility_geometry(geo=geo)
 
 
         ### Selection area on canvas section ###
         ### Selection area on canvas section ###
-        dx = pos[0] - self.pos[0]
         if event.is_dragging == 1 and event.button == 1:
         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:
             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:
         else:
             self.app.selection_type = None
             self.app.selection_type = None
 
 
@@ -2587,21 +2683,21 @@ class FlatCAMExcEditor(QtCore.QObject):
             self.linear_angle_label.hide()
             self.linear_angle_label.hide()
 
 
     def exc_add_drill(self):
     def exc_add_drill(self):
-        self.select_tool('add')
+        self.select_tool('drill_add')
         return
         return
 
 
     def exc_add_drill_array(self):
     def exc_add_drill_array(self):
-        self.select_tool('add_array')
+        self.select_tool('drill_array')
         return
         return
 
 
     def exc_resize_drills(self):
     def exc_resize_drills(self):
-        self.select_tool('resize')
+        self.select_tool('drill_resize')
         return
         return
 
 
     def exc_copy_drills(self):
     def exc_copy_drills(self):
-        self.select_tool('copy')
+        self.select_tool('drill_copy')
         return
         return
 
 
     def exc_move_drills(self):
     def exc_move_drills(self):
-        self.select_tool('move')
+        self.select_tool('drill_move')
         return
         return

+ 224 - 74
flatcamEditors/FlatCAMGeoEditor.py

@@ -18,7 +18,7 @@ from FlatCAMTool import FlatCAMTool
 from flatcamGUI.ObjectUI import LengthEntry, RadioSet
 from flatcamGUI.ObjectUI import LengthEntry, RadioSet
 
 
 from shapely.geometry import LineString, LinearRing, MultiLineString
 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
 import shapely.affinity as affinity
 
 
 from numpy import arctan2, Inf, array, sqrt, sign, dot
 from numpy import arctan2, Inf, array, sqrt, sign, dot
@@ -475,9 +475,9 @@ class PaintOptionsTool(FlatCAMTool):
         )
         )
         grid.addWidget(methodlabel, 3, 0)
         grid.addWidget(methodlabel, 3, 0)
         self.paintmethod_combo = RadioSet([
         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)
         ], orientation='vertical', stretch=False)
         grid.addWidget(self.paintmethod_combo, 3, 1)
         grid.addWidget(self.paintmethod_combo, 3, 1)
 
 
@@ -2875,8 +2875,12 @@ class FlatCAMGeoEditor(QtCore.QObject):
         def gridx_changed(goption, gentry):
         def gridx_changed(goption, gentry):
             entry2option(option=goption, entry=gentry)
             entry2option(option=goption, entry=gentry)
             # if the grid link is checked copy the value in the GridX field to GridY
             # 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():
             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.setValidator(QtGui.QDoubleValidator())
         self.app.ui.grid_gap_x_entry.textChanged.connect(
         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.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
         # 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)
         sel_tab_widget_list = self.app.ui.selected_tab.findChildren(QtWidgets.QWidget)
         for w in sel_tab_widget_list:
         for w in sel_tab_widget_list:
@@ -2979,6 +2988,11 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.editor_active = True
         self.editor_active = True
 
 
     def deactivate(self):
     def deactivate(self):
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         # adjust the status of the menu entries related to the editor
         # adjust the status of the menu entries related to the editor
         self.app.ui.menueditedit.setDisabled(False)
         self.app.ui.menueditedit.setDisabled(False)
         self.app.ui.menueditok.setDisabled(True)
         self.app.ui.menueditok.setDisabled(True)
@@ -3033,6 +3047,13 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # Tell the app that the editor is no longer active
         # Tell the app that the editor is no longer active
         self.editor_active = False
         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:
         try:
             # re-enable all the widgets in the Selected Tab that were disabled after entering in Edit Geometry Mode
             # 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)
             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_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.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):
     def disconnect_canvas_event_handlers(self):
         # we restore the key and mouse control to FlatCAMApp method
         # 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_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_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.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_press', self.on_canvas_click)
         self.canvas.vis_disconnect('mouse_move', self.on_canvas_move)
         self.canvas.vis_disconnect('mouse_move', self.on_canvas_move)
         self.canvas.vis_disconnect('mouse_release', self.on_geo_click_release)
         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):
     def add_shape(self, shape):
         """
         """
         Adds a shape to the shape storage.
         Adds a shape to the shape storage.
@@ -3117,31 +3190,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.tool_shape.clear(update=True)
         self.tool_shape.clear(update=True)
         self.tool_shape.redraw()
         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):
     def toolbar_tool_toggle(self, key):
         self.options[key] = self.sender().isChecked()
         self.options[key] = self.sender().isChecked()
         if self.options[key] == True:
         if self.options[key] == True:
@@ -3268,15 +3316,21 @@ class FlatCAMGeoEditor(QtCore.QObject):
         :return: None
         :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:
         if event.button is 1:
             self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
             self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
                                                    "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
                                                    "%.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()
             modifiers = QtWidgets.QApplication.keyboardModifiers()
             # If the SHIFT key is pressed when LMB is clicked then the coordinates are copied to clipboard
             # 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
             # Selection with left mouse button
             if self.active_tool is not None and event.button is 1:
             if self.active_tool is not None and event.button is 1:
                 # Dispatch event to active_tool
                 # 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]))
                 msg = self.active_tool.click(self.snap(self.pos[0], self.pos[1]))
 
 
                 # If it is a shape generating tool
                 # If it is a shape generating tool
@@ -3303,13 +3356,19 @@ class FlatCAMGeoEditor(QtCore.QObject):
                     else:
                     else:
                         modifier_to_use = Qt.ShiftModifier
                         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
                     # 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.
                     # 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):
                 if isinstance(self.active_tool, FCSelect):
                     # self.app.log.debug("Replotting after click.")
                     # self.app.log.debug("Replotting after click.")
@@ -3334,16 +3393,12 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.x = event.xdata
         self.x = event.xdata
         self.y = event.ydata
         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 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
             return
-        else:
-            self.app.panning_action = False
 
 
         try:
         try:
             x = float(event.xdata)
             x = float(event.xdata)
@@ -3355,7 +3410,13 @@ class FlatCAMGeoEditor(QtCore.QObject):
             return
             return
 
 
         ### Snap coordinates
         ### 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_x = x
         self.snap_y = y
         self.snap_y = y
@@ -3396,9 +3457,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
         else:
         else:
             self.app.selection_type = None
             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):
     def on_geo_click_release(self, event):
         pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
         pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
 
 
@@ -3411,12 +3469,23 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # canvas menu
         # canvas menu
         try:
         try:
             if event.button == 2:  # right click
             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:
                     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:
                     else:
                         # if right click on canvas and the active tool need to be finished (like Path or Polygon)
                         # if right click on canvas and the active tool need to be finished (like Path or Polygon)
                         # right mouse click will finish the action
                         # right mouse click will finish the action
@@ -3426,19 +3495,20 @@ class FlatCAMGeoEditor(QtCore.QObject):
                             if self.active_tool.complete:
                             if self.active_tool.complete:
                                 self.on_shape_complete()
                                 self.on_shape_complete()
                                 self.app.inform.emit(_("[success] Done."))
                                 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
                                 # MS: always return to the Select Tool if modifier key is not pressed
                                 # else return to the current tool
                                 # 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:
         except Exception as e:
             log.warning("Error: %s" % str(e))
             log.warning("Error: %s" % str(e))
@@ -3781,7 +3851,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         :return: None.
         :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.
         # Delete originals.
         for_deletion = [s for s in self.get_selected()]
         for_deletion = [s for s in self.get_selected()]
@@ -3795,9 +3865,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         self.replot()
         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
         :return: None
         """
         """
@@ -3827,11 +3897,67 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         self.replot()
         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):
     def subtract(self):
         selected = self.get_selected()
         selected = self.get_selected()
         try:
         try:
             tools = selected[1:]
             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)
             result = selected[0].geo.difference(toolgeo)
 
 
             self.delete_shape(selected[0])
             self.delete_shape(selected[0])
@@ -3841,6 +3967,30 @@ class FlatCAMGeoEditor(QtCore.QObject):
         except Exception as e:
         except Exception as e:
             log.debug(str(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):
     def buffer(self, buf_distance, join_style):
         selected = self.get_selected()
         selected = self.get_selected()
 
 

+ 169 - 83
flatcamEditors/FlatCAMGrbEditor.py

@@ -1613,22 +1613,25 @@ class FCApertureSelect(DrawTool):
         key_modifier = QtWidgets.QApplication.keyboardModifiers()
         key_modifier = QtWidgets.QApplication.keyboardModifiers()
 
 
         for storage in self.grb_editor_app.storage_dict:
         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:
                         else:
-                            # add the object to the selected shapes
                             self.draw_app.selected.append(shape)
                             self.draw_app.selected.append(shape)
                             sel_aperture.add(storage)
                             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
         # select the aperture in the Apertures Table that is associated with the selected shape
         try:
         try:
@@ -1980,7 +1983,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
 
         self.pad_axis_radio = RadioSet([{'label': 'X', 'value': 'X'},
         self.pad_axis_radio = RadioSet([{'label': 'X', 'value': 'X'},
                                           {'label': 'Y', 'value': 'Y'},
                                           {'label': 'Y', 'value': 'Y'},
-                                          {'label': _('Angle'), 'value': 'A'}])
+                                          {'label': 'Angle', 'value': 'A'}])
         self.pad_axis_radio.set_value('X')
         self.pad_axis_radio.set_value('X')
         self.linear_form.addRow(self.pad_axis_label, self.pad_axis_radio)
         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
         # flag to show if the object was modified
         self.is_modified = False
         self.is_modified = False
-
         self.edited_obj_name = ""
         self.edited_obj_name = ""
-
         self.tool_row = 0
         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
         # store the status of the editor so the Delete at object level will not work until the edit is finished
         self.editor_active = False
         self.editor_active = False
 
 
@@ -2629,14 +2633,15 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.edited_obj_name = self.name_entry.get_value()
         self.edited_obj_name = self.name_entry.get_value()
 
 
     def on_aptype_changed(self, current_text):
     def on_aptype_changed(self, current_text):
+        # 'O' is letter O not zero.
         if current_text == 'R' or current_text == 'O':
         if current_text == 'R' or current_text == 'O':
             self.apdim_lbl.show()
             self.apdim_lbl.show()
             self.apdim_entry.show()
             self.apdim_entry.show()
-            self.apsize_entry.setReadOnly(True)
+            self.apsize_entry.setDisabled(True)
         else:
         else:
             self.apdim_lbl.hide()
             self.apdim_lbl.hide()
             self.apdim_entry.hide()
             self.apdim_entry.hide()
-            self.apsize_entry.setReadOnly(False)
+            self.apsize_entry.setDisabled(False)
 
 
     def activate_grb_editor(self):
     def activate_grb_editor(self):
         # adjust the status of the menu entries related to the editor
         # 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_edit.setVisible(False)
         self.app.ui.popmenu_save.setVisible(True)
         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
         # Tell the App that the editor is active
         self.editor_active = True
         self.editor_active = True
 
 
     def deactivate_grb_editor(self):
     def deactivate_grb_editor(self):
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         # adjust the status of the menu entries related to the editor
         # adjust the status of the menu entries related to the editor
         self.app.ui.menueditedit.setDisabled(False)
         self.app.ui.menueditedit.setDisabled(False)
         self.app.ui.menueditok.setDisabled(True)
         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.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
         # adjust the visibility of some of the canvas context menu
         self.app.ui.popmenu_edit.setVisible(True)
         self.app.ui.popmenu_edit.setVisible(True)
         self.app.ui.popmenu_save.setVisible(False)
         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
         # Show original geometry
         if self.gerber_obj:
         if self.gerber_obj:
             self.gerber_obj.visible = True
             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.canvas.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(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):
     def disconnect_canvas_event_handlers(self):
 
 
         # we restore the key and mouse control to FlatCAMApp method
         # 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_move', self.on_canvas_move)
         self.canvas.vis_disconnect('mouse_release', self.on_grb_click_release)
         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):
     def clear(self):
         self.active_tool = None
         self.active_tool = None
         # self.shape_buffer = []
         # self.shape_buffer = []
@@ -3160,26 +3233,31 @@ class FlatCAMGrbEditor(QtCore.QObject):
     def on_canvas_click(self, event):
     def on_canvas_click(self, event):
         """
         """
         event.x and .y have canvas coordinates
         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
         :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:
         if event.button is 1:
             self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
             self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
                                                    "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
                                                    "%.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
             # Selection with left mouse button
             if self.active_tool is not None and event.button is 1:
             if self.active_tool is not None and event.button is 1:
                 # Dispatch event to active_tool
                 # 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]))
                 msg = self.active_tool.click(self.app.geo_editor.snap(self.pos[0], self.pos[1]))
 
 
                 # If it is a shape generating tool
                 # 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!")
                 self.app.log.debug("No active tool to respond to click!")
 
 
     def on_grb_click_release(self, event):
     def on_grb_click_release(self, event):
-        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
-
         self.modifiers = QtWidgets.QApplication.keyboardModifiers()
         self.modifiers = QtWidgets.QApplication.keyboardModifiers()
 
 
+        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
         if self.app.grid_status():
         if self.app.grid_status():
             pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
             pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
         else:
         else:
@@ -3226,9 +3303,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # canvas menu
         # canvas menu
         try:
         try:
             if event.button == 2:  # right click
             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:
                     if self.in_action is False:
                         try:
                         try:
                             QtGui.QGuiApplication.restoreOverrideCursor()
                             QtGui.QGuiApplication.restoreOverrideCursor()
@@ -3243,6 +3318,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                             self.select_tool('select')
                             self.select_tool('select')
                         else:
                         else:
                             self.app.cursor = QtGui.QCursor()
                             self.app.cursor = QtGui.QCursor()
+                            self.app.populate_cmenu_grids()
                             self.app.ui.popMenu.popup(self.app.cursor.pos())
                             self.app.ui.popMenu.popup(self.app.cursor.pos())
                     else:
                     else:
                         # if right click on canvas and the active tool need to be finished (like Path or Polygon)
                         # 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
                     self.app.selection_type = None
 
 
                 elif isinstance(self.active_tool, FCApertureSelect):
                 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]))
                     self.active_tool.click_release((self.pos[0], self.pos[1]))
 
 
                     # if there are selected objects then plot them
                     # if there are selected objects then plot them
@@ -3303,26 +3375,29 @@ class FlatCAMGrbEditor(QtCore.QObject):
         :type Bool
         :type Bool
         :return:
         :return:
         """
         """
+
         poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
         poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
         sel_aperture = set()
         sel_aperture = set()
         self.apertures_table.clearSelection()
         self.apertures_table.clearSelection()
 
 
         self.app.delete_selection_shape()
         self.app.delete_selection_shape()
         for storage in self.storage_dict:
         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:
                         else:
-                            # add the object to the selected shapes
                             self.selected.append(obj)
                             self.selected.append(obj)
                             sel_aperture.add(storage)
                             sel_aperture.add(storage)
-                    else:
-                        self.selected.append(obj)
-                        sel_aperture.add(storage)
-
+            except KeyError:
+                pass
         try:
         try:
             self.apertures_table.cellPressed.disconnect()
             self.apertures_table.cellPressed.disconnect()
         except:
         except:
@@ -3349,22 +3424,18 @@ class FlatCAMGrbEditor(QtCore.QObject):
         :return: None
         :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.x = event.xdata
         self.y = event.ydata
         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 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
             return
-        else:
-            self.app.panning_action = False
 
 
         try:
         try:
             x = float(event.xdata)
             x = float(event.xdata)
@@ -3376,7 +3447,13 @@ class FlatCAMGrbEditor(QtCore.QObject):
             return
             return
 
 
         ### Snap coordinates
         ### 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_x = x
         self.snap_y = y
         self.snap_y = y
@@ -3398,7 +3475,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
         geo = self.active_tool.utility_geometry(data=(x, y))
         geo = self.active_tool.utility_geometry(data=(x, y))
 
 
         if isinstance(geo, DrawToolShape) and geo.geo is not None:
         if isinstance(geo, DrawToolShape) and geo.geo is not None:
-
             # Remove any previous utility shape
             # Remove any previous utility shape
             self.tool_shape.clear(update=True)
             self.tool_shape.clear(update=True)
             self.draw_utility_geometry(geo=geo)
             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):
             if isinstance(self.active_tool, FCRegion) or isinstance(self.active_tool, FCTrack):
                 pass
                 pass
             else:
             else:
-                dx = pos[0] - self.pos[0]
+                dx = pos_canvas[0] - self.pos[0]
                 self.app.delete_selection_shape()
                 self.app.delete_selection_shape()
                 if dx < 0:
                 if dx < 0:
                     self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x,y),
                     self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x,y),
@@ -3423,9 +3499,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
         else:
         else:
             self.app.selection_type = None
             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):
     def on_canvas_key_release(self, event):
         self.key = None
         self.key = None
 
 
@@ -3459,15 +3532,18 @@ class FlatCAMGrbEditor(QtCore.QObject):
             self.shapes.clear(update=True)
             self.shapes.clear(update=True)
 
 
             for storage in self.storage_dict:
             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:
             for shape in self.utility:
                 self.plot_shape(geometry=shape.geo, linewidth=1)
                 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)
             self.shapes.add(shape=geometry, color=color, face_color=color+'AF', layer=0)
 
 
     def start_delayed_plot(self, check_period):
     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 = threading.Thread(target=lambda: self.check_plot_finished(check_period))
         # self.plot_thread.start()
         # self.plot_thread.start()
         log.debug("FlatCAMGrbEditor --> Delayed Plot started.")
         log.debug("FlatCAMGrbEditor --> Delayed Plot started.")
@@ -3507,7 +3590,12 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.plot_thread.start()
         self.plot_thread.start()
 
 
     def check_plot_finished(self):
     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:
         try:
             if not self.grb_plot_promises:
             if not self.grb_plot_promises:
                 self.plot_thread.stop()
                 self.plot_thread.stop()
@@ -3571,13 +3659,11 @@ class FlatCAMGrbEditor(QtCore.QObject):
             return
             return
 
 
         for storage in self.storage_dict:
         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:
         if shape in self.selected:
             self.selected.remove(shape)  # TODO: Check performance
             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.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.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.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.toolbartools.addSeparator()
 
 
         self.calculators_btn = self.toolbartools.addAction(QtGui.QIcon('share/calculator24.png'), _("Calculators Tool"))
         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'),
         self.grb_convert_poly_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/poligonize32.png'),
                                                                     _("Poligonize"))
                                                                     _("Poligonize"))
 
 
-
         self.grb_add_semidisc_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/semidisc32.png'), _("SemiDisc"))
         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_add_disc_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/disc32.png'), _("Disc"))
         self.grb_edit_toolbar.addSeparator()
         self.grb_edit_toolbar.addSeparator()
@@ -752,12 +753,27 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         ################
         ################
 
 
         ### Project ###
         ### 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 = QtWidgets.QWidget()
         self.project_tab.setObjectName("project_tab")
         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.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 ###
         ### Selected ###
         self.selected_tab = QtWidgets.QWidget()
         self.selected_tab = QtWidgets.QWidget()
@@ -1545,12 +1561,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         ##############################################################
         ##############################################################
         ### HERE WE BUILD THE CONTEXT MENU FOR RMB CLICK ON CANVAS ###
         ### 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_disable = self.popMenu.addAction(QtGui.QIcon('share/clear_plot32.png'), _("Disable"))
         self.popMenu.addSeparator()
         self.popMenu.addSeparator()
         self.cmenu_newmenu = self.popMenu.addMenu(QtGui.QIcon('share/file32.png'), _("New"))
         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_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.popmenu_new_exc = self.cmenu_newmenu.addAction(QtGui.QIcon('share/new_exc32.png'), _("Excellon"))
         self.cmenu_newmenu.addSeparator()
         self.cmenu_newmenu.addSeparator()
         self.popmenu_new_prj = self.cmenu_newmenu.addAction(QtGui.QIcon('share/file16.png'), _("Project"))
         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_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_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_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.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 = 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_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.addSeparator()
         self.popmenu_copy = self.popMenu.addAction(QtGui.QIcon('share/copy32.png'), _("Copy"))
         self.popmenu_copy = self.popMenu.addAction(QtGui.QIcon('share/copy32.png'), _("Copy"))
@@ -1724,9 +1736,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # start with GRID activated
         # start with GRID activated
         self.grid_snap_btn.trigger()
         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.general_defaults_form = GeneralPreferencesUI()
         self.gerber_defaults_form = GerberPreferencesUI()
         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.film_btn = self.toolbartools.addAction(QtGui.QIcon('share/film16.png'), _("Film Tool"))
         self.solder_btn = self.toolbartools.addAction(QtGui.QIcon('share/solderpastebis32.png'),
         self.solder_btn = self.toolbartools.addAction(QtGui.QIcon('share/solderpastebis32.png'),
                                                       _("SolderPaste Tool"))
                                                       _("SolderPaste Tool"))
+        self.sub_btn = self.toolbartools.addAction(QtGui.QIcon('share/sub32.png'), _("Substract Tool"))
+
         self.toolbartools.addSeparator()
         self.toolbartools.addSeparator()
 
 
         self.calculators_btn = self.toolbartools.addAction(QtGui.QIcon('share/calculator24.png'),
         self.calculators_btn = self.toolbartools.addAction(QtGui.QIcon('share/calculator24.png'),
                                                            _("Calculators Tool"))
                                                            _("Calculators Tool"))
         self.transform_btn = self.toolbartools.addAction(QtGui.QIcon('share/transform.png'), _("Transform 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.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_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/plus16.png'), _('Add Drill Hole'))
         self.add_drill_array_btn = self.exc_edit_toolbar.addAction(
         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.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_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_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.grb_edit_toolbar.addSeparator()
 
 
         self.aperture_buffer_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/buffer16-2.png'), _('Buffer'))
         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.exc_edit_toolbar.setDisabled(True)
                 self.geo_edit_toolbar.setVisible(True)
                 self.geo_edit_toolbar.setVisible(True)
                 self.geo_edit_toolbar.setDisabled(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.grb_edit_toolbar.setDisabled(True)
 
 
                 self.corner_snap_btn.setVisible(True)
                 self.corner_snap_btn.setVisible(True)
@@ -2146,6 +2165,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.cutout_tool.run(toggle=True)
                     self.app.cutout_tool.run(toggle=True)
                     return
                     return
 
 
+                # Substract Tool
+                if key == QtCore.Qt.Key_W:
+                    self.app.sub_tool.run(toggle=True)
+                    return
+
                 # Panelize Tool
                 # Panelize Tool
                 if key == QtCore.Qt.Key_Z:
                 if key == QtCore.Qt.Key_Z:
                     self.app.panelize_tool.run(toggle=True)
                     self.app.panelize_tool.run(toggle=True)
@@ -2214,6 +2238,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == QtCore.Qt.Key_Space:
                 if key == QtCore.Qt.Key_Space:
                     for select in selected:
                     for select in selected:
                         select.ui.plot_cb.toggle()
                         select.ui.plot_cb.toggle()
+                    self.app.collection.update_view()
                     self.app.delete_selection_shape()
                     self.app.delete_selection_shape()
 
 
                 # New Geometry
                 # New Geometry
@@ -2807,7 +2832,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.exc_editor.replot()
                     self.app.exc_editor.replot()
                     # self.select_btn.setChecked(True)
                     # self.select_btn.setChecked(True)
                     # self.on_tool_select('select')
                     # self.on_tool_select('select')
-                    self.app.exc_editor.select_tool('select')
+                    self.app.exc_editor.select_tool('drill_select')
                     return
                     return
 
 
                 # Delete selected object if delete key event comes out of canvas
                 # 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':
                 if key == QtCore.Qt.Key_T or key == 'T':
                     self.app.exc_editor.launched_from_shortcuts = True
                     self.app.exc_editor.launched_from_shortcuts = True
                     ## Current application units in Upper Case
                     ## 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 ..."),
                     tool_add_popup = FCInputDialog(title=_("New Tool ..."),
                                                    text=_('Enter a Tool Diameter:'),
                                                    text=_('Enter a Tool Diameter:'),
                                                    min=0.0000, max=99.9999, decimals=4)
                                                    min=0.0000, max=99.9999, decimals=4)
@@ -3047,6 +3072,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                                                    'params': [self.filename, object_type, None]})
                                                    'params': [self.filename, object_type, None]})
 
 
                     if extension in self.app.pdf_list:
                     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,
                         self.app.worker_task.emit({'fcn': self.app.pdf_tool.open_pdf,
                                                    'params': [self.filename]})
                                                    '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.addWidget(self.sel_draw_color_button)
         self.form_box_child_11.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
         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
         # Just to add empty rows
         self.spacelabel = QtWidgets.QLabel('')
         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.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.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(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)
         self.form_box.addRow(self.spacelabel, self.spacelabel)
 
 
@@ -3589,6 +3646,16 @@ class GeneralGUISetGroupUI(OptionsGroupUI):
         )
         )
         self.hover_cb = FCCheckBox()
         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
         # Just to add empty rows
         self.spacelabel = QtWidgets.QLabel('')
         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.hdpi_label, self.hdpi_cb)
         self.form_box.addRow(self.clear_label, self.clear_btn)
         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.hover_label, self.hover_cb)
+        self.form_box.addRow(self.selection_label, self.selection_cb)
 
 
         # Add the QFormLayout that holds the Application general defaults
         # Add the QFormLayout that holds the Application general defaults
         # to the main layout of this TAB
         # to the main layout of this TAB
@@ -3667,8 +3735,8 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
                                         "ADVANCED level -> full functionality.\n\n"
                                         "ADVANCED level -> full functionality.\n\n"
                                         "The choice here will influence the parameters in\n"
                                         "The choice here will influence the parameters in\n"
                                         "the Selected Tab for all kinds of FlatCAM objects."))
                                         "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
         # Languages for FlatCAM
         self.languagelabel = QtWidgets.QLabel(_('<b>Languages:</b>'))
         self.languagelabel = QtWidgets.QLabel(_('<b>Languages:</b>'))
@@ -3676,6 +3744,13 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
         self.language_cb = FCComboBox()
         self.language_cb = FCComboBox()
         self.languagespace = QtWidgets.QLabel('')
         self.languagespace = QtWidgets.QLabel('')
         self.language_apply_btn = FCButton(_("Apply Language"))
         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
         # Shell StartUp CB
         self.shell_startup_label = QtWidgets.QLabel(_('Shell at StartUp:'))
         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"
         self.panbuttonlabel.setToolTip(_("Select the mouse button to use for panning:\n"
                                        "- MMB --> Middle Mouse Button\n"
                                        "- MMB --> Middle Mouse Button\n"
                                        "- RMB --> Right Mouse Button"))
                                        "- 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
         # Multiple Selection Modifier Key
         self.mselectlabel = QtWidgets.QLabel(_('<b>Multiple Sel:</b>'))
         self.mselectlabel = QtWidgets.QLabel(_('<b>Multiple Sel:</b>'))
         self.mselectlabel.setToolTip(_("Select the key used for multiple selection."))
         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
         # Project at StartUp CB
         self.project_startup_label = QtWidgets.QLabel(_('Project at StartUp:'))
         self.project_startup_label = QtWidgets.QLabel(_('Project at StartUp:'))
@@ -3955,8 +4030,8 @@ class GerberOptPrefGroupUI(OptionsGroupUI):
             "- conventional / useful when there is no backlash compensation")
             "- conventional / useful when there is no backlash compensation")
         )
         )
         grid0.addWidget(milling_type_label, 3, 0)
         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)
         grid0.addWidget(self.milling_type_radio, 3, 1)
 
 
         # Combine passes
         # Combine passes
@@ -4224,8 +4299,8 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         )
         )
         hlay3.addWidget(self.excellon_zeros_label)
         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(
         self.excellon_zeros_radio.setToolTip(
             _("This sets the default type of Excellon zeros.\n"
             _("This sets the default type of Excellon zeros.\n"
             "If it is not detected in the parsed file the value here\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)
         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(
         self.excellon_units_radio.setToolTip(
             _("This sets the units of Excellon files.\n"
             _("This sets the units of Excellon files.\n"
             "Some Excellon files don't have an header\n"
             "Some Excellon files don't have an header\n"
@@ -4291,8 +4366,8 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
             "Travelling Salesman algorithm for path optimization.")
             "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(
         self.excellon_optimization_radio.setToolTip(
             _("This sets the optimization type for the Excellon drill path.\n"
             _("This sets the optimization type for the Excellon drill path.\n"
             "If MH is checked then Google OR-Tools algorithm with MetaHeuristic\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"
             "When choosing 'Slots' or 'Both', slots will be\n"
             "converted to drills.")
             "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(excellon_gcode_type_label, 9, 0)
         grid2.addWidget(self.excellon_gcode_type_radio, 9, 1)
         grid2.addWidget(self.excellon_gcode_type_radio, 9, 1)
 
 
@@ -4643,8 +4718,8 @@ class ExcellonExpPrefGroupUI(OptionsGroupUI):
             _("The units used in the Excellon file.")
             _("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(
         self.excellon_units_radio.setToolTip(
             _("The units used in the Excellon file.")
             _("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"
             "Also it will have to be specified if LZ = leading zeros are kept\n"
             "or TZ = trailing zeros are kept.")
             "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(
         self.format_radio.setToolTip(
             _("Select the kind of coordinates format used.\n"
             _("Select the kind of coordinates format used.\n"
             "Coordinates can be saved with decimal point or without.\n"
             "Coordinates can be saved with decimal point or without.\n"
@@ -4723,8 +4798,8 @@ class ExcellonExpPrefGroupUI(OptionsGroupUI):
             "and Leading Zeros are removed.")
             "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(
         self.zeros_radio.setToolTip(
             _("This sets the default type of Excellon zeros.\n"
             _("This sets the default type of Excellon zeros.\n"
             "If LZ then Leading Zeros are kept and\n"
             "If LZ then Leading Zeros are kept and\n"
@@ -5105,9 +5180,9 @@ class CNCJobGenPrefGroupUI(OptionsGroupUI):
         )
         )
 
 
         self.cncplot_method_radio = RadioSet([
         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)
         ], stretch=False)
 
 
         grid0.addWidget(self.cncplot_method_label, 1, 0)
         grid0.addWidget(self.cncplot_method_label, 1, 0)
@@ -5341,9 +5416,9 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
         )
         )
         grid0.addWidget(methodlabel, 3, 0)
         grid0.addWidget(methodlabel, 3, 0)
         self.ncc_method_radio = RadioSet([
         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)
         ], orientation='vertical', stretch=False)
         grid0.addWidget(self.ncc_method_radio, 3, 1)
         grid0.addWidget(self.ncc_method_radio, 3, 1)
 
 
@@ -5490,8 +5565,8 @@ class Tools2sidedPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.drill_dia_entry, 0, 1)
         grid0.addWidget(self.drill_dia_entry, 0, 1)
 
 
         ## Axis
         ## 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 = QtWidgets.QLabel(_("Mirror Axis:"))
         self.mirax_label.setToolTip(
         self.mirax_label.setToolTip(
             _("Mirror vertically (X) or horizontally (Y).")
             _("Mirror vertically (X) or horizontally (Y).")
@@ -5503,8 +5578,8 @@ class Tools2sidedPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.mirror_axis_radio, 2, 1)
         grid0.addWidget(self.mirror_axis_radio, 2, 1)
 
 
         ## Axis Location
         ## 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 = QtWidgets.QLabel(_("Axis Ref:"))
         self.axloc_label.setToolTip(
         self.axloc_label.setToolTip(
             _("The axis should pass through a <b>point</b> or cut\n "
             _("The axis should pass through a <b>point</b> or cut\n "
@@ -5581,9 +5656,9 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         )
         )
         grid0.addWidget(methodlabel, 3, 0)
         grid0.addWidget(methodlabel, 3, 0)
         self.paintmethod_combo = RadioSet([
         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)
         ], orientation='vertical', stretch=False)
         grid0.addWidget(self.paintmethod_combo, 3, 1)
         grid0.addWidget(self.paintmethod_combo, 3, 1)
 
 
@@ -5614,8 +5689,8 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         )
         )
         grid0.addWidget(selectlabel, 6, 0)
         grid0.addWidget(selectlabel, 6, 0)
         self.selectmethod_combo = RadioSet([
         self.selectmethod_combo = RadioSet([
-            {"label": _("Single"), "value": "single"},
-            {"label": _("All"), "value": "all"},
+            {"label": "Single", "value": "single"},
+            {"label": "All", "value": "all"},
             # {"label": "Rectangle", "value": "rectangle"}
             # {"label": "Rectangle", "value": "rectangle"}
         ])
         ])
         grid0.addWidget(self.selectmethod_combo, 6, 1)
         grid0.addWidget(self.selectmethod_combo, 6, 1)
@@ -5642,8 +5717,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0 = QtWidgets.QGridLayout()
         grid0 = QtWidgets.QGridLayout()
         self.layout.addLayout(grid0)
         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 = QtWidgets.QLabel(_('Film Type:'))
         ftypelbl.setToolTip(
         ftypelbl.setToolTip(
             _("Generate a Positive black film or a Negative film.\n"
             _("Generate a Positive black film or a Negative film.\n"
@@ -5742,8 +5817,8 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.prows, 3, 1)
         grid0.addWidget(self.prows, 3, 1)
 
 
         ## Type of resulting Panel object
         ## 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 = QtWidgets.QLabel(_("Panel Type:"))
         self.panel_type_label.setToolTip(
         self.panel_type_label.setToolTip(
            _( "Choose the type of object for the panel object:\n"
            _( "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
         self.readyToEdit = True
 
 
     def set_value(self, val):
     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):
 class EvalEntry(QtWidgets.QLineEdit):
@@ -676,6 +680,16 @@ class FCButton(QtWidgets.QPushButton):
         self.setText(str(val))
         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):
 class FCTab(QtWidgets.QTabWidget):
     def __init__(self, parent=None):
     def __init__(self, parent=None):
         super(FCTab, self).__init__(parent)
         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)
         grid0.addWidget(self.plot_options_label, 0, 0)
 
 
         # Solid CB
         # Solid CB
-        self.solid_cb = FCCheckBox(label=_('Solid   '))
+        self.solid_cb = FCCheckBox(label=_('Solid'))
         self.solid_cb.setToolTip(
         self.solid_cb.setToolTip(
             _("Solid color polygons.")
             _("Solid color polygons.")
         )
         )
@@ -161,7 +161,7 @@ class GerberObjectUI(ObjectUI):
         grid0.addWidget(self.solid_cb, 0, 1)
         grid0.addWidget(self.solid_cb, 0, 1)
 
 
         # Multicolored CB
         # Multicolored CB
-        self.multicolored_cb = FCCheckBox(label=_('M-Color   '))
+        self.multicolored_cb = FCCheckBox(label=_('M-Color'))
         self.multicolored_cb.setToolTip(
         self.multicolored_cb.setToolTip(
             _("Draw polygons in different colors.")
             _("Draw polygons in different colors.")
         )
         )
@@ -299,8 +299,8 @@ class GerberObjectUI(ObjectUI):
             "- conventional / useful when there is no backlash compensation")
             "- conventional / useful when there is no backlash compensation")
         )
         )
         grid1.addWidget(self.milling_type_label, 3, 0)
         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)
         grid1.addWidget(self.milling_type_radio, 3, 1)
 
 
         # combine all passes CB
         # combine all passes CB
@@ -749,9 +749,9 @@ class ExcellonObjectUI(ObjectUI):
             "When choosing 'Slots' or 'Both', slots will be\n"
             "When choosing 'Slots' or 'Both', slots will be\n"
             "converted to a series of drills.")
             "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)
         gcode_box.addRow(gcode_type_label, self.excellon_gcode_type_radio)
         self.tools_box.addLayout(gcode_box)
         self.tools_box.addLayout(gcode_box)
 
 
@@ -1355,9 +1355,9 @@ class CNCObjectUI(ObjectUI):
         )
         )
 
 
         self.cncplot_method_combo = RadioSet([
         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)
         ], stretch=False)
 
 
         ## Object name
         ## Object name

+ 1 - 1
flatcamTools/ToolCalculators.py

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

+ 231 - 128
flatcamTools/ToolPDF.py

@@ -2,7 +2,7 @@
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
-# Date: 3/10/2019                                          #
+# Date: 4/23/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
 ############################################################
 ############################################################
 
 
@@ -18,6 +18,7 @@ import numpy as np
 
 
 import zlib
 import zlib
 import re
 import re
+import time
 
 
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
@@ -106,9 +107,18 @@ class ToolPDF(FlatCAMTool):
         self.gs['transform'] = []
         self.gs['transform'] = []
         self.gs['line_width'] = []   # each element is a float
         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
         # conversion factor to INCH
         self.point_to_unit_factor = 0.01388888888
         self.point_to_unit_factor = 0.01388888888
@@ -148,16 +158,22 @@ class ToolPDF(FlatCAMTool):
         if len(filenames) == 0:
         if len(filenames) == 0:
             self.app.inform.emit(_("[WARNING_NOTCL] Open PDF cancelled."))
             self.app.inform.emit(_("[WARNING_NOTCL] Open PDF cancelled."))
         else:
         else:
+            # start the parsing timer with a period of 1 second
+            self.periodic_check(1000)
+
             for filename in filenames:
             for filename in filenames:
                 if filename != '':
                 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):
     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)
         # 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':
         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)
                 log.debug(" PDF STREAM: %d\n" % stream_nr)
                 s = s.strip(b'\r\n')
                 s = s.strip(b'\r\n')
                 try:
                 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:
                 except Exception as e:
                     log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(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:
                 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):
     def parse_pdf(self, pdf_content):
         path = dict()
         path = dict()
@@ -301,9 +398,10 @@ class ToolPDF(FlatCAMTool):
 
 
         # store the objects to be transformed into Gerbers
         # store the objects to be transformed into Gerbers
         object_dict = {}
         object_dict = {}
-
         # will serve as key in the 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
         # store the apertures here
         apertures_dict = {}
         apertures_dict = {}
@@ -320,15 +418,11 @@ class ToolPDF(FlatCAMTool):
         clear_apertures_dict['0']['type'] = 'C'
         clear_apertures_dict['0']['type'] = 'C'
         clear_apertures_dict['0']['solid_geometry'] = []
         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
         # 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
         # it will be transformed into Gerber object
         old_color = [None, None ,None]
         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
         flag_clear_geo = False
 
 
         line_nr = 0
         line_nr = 0
@@ -350,11 +444,12 @@ class ToolPDF(FlatCAMTool):
                     # same color, do nothing
                     # same color, do nothing
                     continue
                     continue
                 else:
                 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)
                 old_color = copy(color)
                 # we make sure that the following geometry is added to the right storage
                 # we make sure that the following geometry is added to the right storage
                 flag_clear_geo = False
                 flag_clear_geo = False
@@ -536,7 +631,6 @@ class ToolPDF(FlatCAMTool):
                         y * self.point_to_unit_factor * scale_geo[1])
                         y * self.point_to_unit_factor * scale_geo[1])
 
 
                 subpath['bezier'].append([start, c1, stop, stop])
                 subpath['bezier'].append([start, c1, stop, stop])
-                print(subpath['bezier'])
                 current_point = stop
                 current_point = stop
                 continue
                 continue
 
 
@@ -747,18 +841,18 @@ class ToolPDF(FlatCAMTool):
                     if path['rectangle']:
                     if path['rectangle']:
                         for subp in path['rectangle']:
                         for subp in path['rectangle']:
                             geo = copy(subp)
                             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)
                             geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
                             path_geo.append(geo_el)
                             path_geo.append(geo_el)
                         # the path was painted therefore initialize it
                         # the path was painted therefore initialize it
                         path['rectangle'] = []
                         path['rectangle'] = []
                     else:
                     else:
                         geo = copy(subpath['rectangle'])
                         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)
                         geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
                         path_geo.append(geo_el)
                         path_geo.append(geo_el)
                         subpath['rectangle'] = []
                         subpath['rectangle'] = []
@@ -869,9 +963,9 @@ class ToolPDF(FlatCAMTool):
                         # fill
                         # fill
                         for subp in path['rectangle']:
                         for subp in path['rectangle']:
                             geo = copy(subp)
                             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)
                             geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
                             fill_geo.append(geo_el)
                             fill_geo.append(geo_el)
                         # stroke
                         # stroke
@@ -884,9 +978,9 @@ class ToolPDF(FlatCAMTool):
                     else:
                     else:
                         # fill
                         # fill
                         geo = copy(subpath['rectangle'])
                         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)
                         geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
                         fill_geo.append(geo_el)
                         fill_geo.append(geo_el)
                         # stroke
                         # 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
         # tidy up. copy the current aperture dict to the object dict but only if it is not empty
         if apertures_dict:
         if apertures_dict:
-            object_dict[object_nr] = deepcopy(apertures_dict)
+            object_dict[layer_nr] = deepcopy(apertures_dict)
 
 
         if clear_apertures_dict['0']['solid_geometry']:
         if clear_apertures_dict['0']['solid_geometry']:
             object_dict[0] = deepcopy(clear_apertures_dict)
             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
         return object_dict
 
 
     def bezier_to_points(self, start, c1, c2, stop):
     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.
         # 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
         # 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
         # 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.ToolSolderPaste import SolderPaste
 from flatcamTools.ToolPcbWizard import PcbWizard
 from flatcamTools.ToolPcbWizard import PcbWizard
 from flatcamTools.ToolPDF import ToolPDF
 from flatcamTools.ToolPDF import ToolPDF
+from flatcamTools.ToolSub import ToolSub
+
 
 
 from flatcamTools.ToolShell import FCShell
 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 vispy
 pip3 install --upgrade rtree
 pip3 install --upgrade rtree
 pip3 install --upgrade pyopengl
 pip3 install --upgrade pyopengl
-pip3 install --upgrade pyopengl-accelerate
 pip3 install --upgrade setuptools
 pip3 install --upgrade setuptools
 pip3 install --upgrade svg.path
 pip3 install --upgrade svg.path
 pip3 install --upgrade ortools
 pip3 install --upgrade ortools

BIN
share/sub32.png


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