Przeglądaj źródła

Merge branch 'Beta' into preferences-refactoring

David Robertson 5 lat temu
rodzic
commit
1c5a6de80d
6 zmienionych plików z 160 dodań i 113 usunięć
  1. 10 0
      CHANGELOG.md
  2. 45 41
      FlatCAMApp.py
  3. 74 64
      camlib.py
  4. 8 3
      flatcamGUI/FlatCAMGUI.py
  5. 22 5
      flatcamGUI/GUIElements.py
  6. 1 0
      flatcamObjects/FlatCAMExcellon.py

+ 10 - 0
CHANGELOG.md

@@ -7,6 +7,16 @@ CHANGELOG for FlatCAM beta
 
 =================================================
 
+5.05.2020
+
+- fixed an issue that made the preprocessors comboxes in Preferences not to load and display the saved value fro the file
+- some PEP8 corrections
+
+4.05.2020
+
+- in detachable tabs, Linux loose the reference of the detached tab and on close of the detachable tabs will gave a 'segmantation fault' error. Solved it by not deleting the reference in case of Unix-like systems
+- some strings added to translation strings
+
 3.05.2020
 
 - small changes to allow making the x86 installer that is made from a Python 3.5 run FlatCAM beta 

+ 45 - 41
FlatCAMApp.py

@@ -543,6 +543,47 @@ class App(QtCore.QObject):
         self.save_project_auto_update()
         self.autosave_timer.timeout.connect(self.save_project_auto)
 
+        # ###########################################################################################################
+        # #################################### LOAD PREPROCESSORS ###################################################
+        # ###########################################################################################################
+
+        # ----------------------------------------- WARNING --------------------------------------------------------
+        # Preprocessors need to be loaded before the Preferences Manager builds the Preferences
+        # That's because the number of preprocessors can vary and here the comboboxes are populated
+        # -----------------------------------------------------------------------------------------------------------
+
+        # a dictionary that have as keys the name of the preprocessor files and the value is the class from
+        # the preprocessor file
+        self.preprocessors = load_preprocessors(self)
+
+        # make sure that always the 'default' preprocessor is the first item in the dictionary
+        if 'default' in self.preprocessors.keys():
+            new_ppp_dict = {}
+
+            # add the 'default' name first in the dict after removing from the preprocessor's dictionary
+            default_pp = self.preprocessors.pop('default')
+            new_ppp_dict['default'] = default_pp
+
+            # then add the rest of the keys
+            for name, val_class in self.preprocessors.items():
+                new_ppp_dict[name] = val_class
+
+            # and now put back the ordered dict with 'default' key first
+            self.preprocessors = new_ppp_dict
+
+        for name in list(self.preprocessors.keys()):
+            # 'Paste' preprocessors are to be used only in the Solder Paste Dispensing Tool
+            if name.partition('_')[0] == 'Paste':
+                self.ui.tools_defaults_form.tools_solderpaste_group.pp_combo.addItem(name)
+                continue
+
+            self.ui.geometry_defaults_form.geometry_opt_group.pp_geometry_name_cb.addItem(name)
+            # HPGL preprocessor is only for Geometry objects therefore it should not be in the Excellon Preferences
+            if name == 'hpgl':
+                continue
+
+            self.ui.excellon_defaults_form.excellon_opt_group.pp_excellon_name_cb.addItem(name)
+
         # ###########################################################################################################
         # ##################################### UPDATE PREFERENCES GUI FORMS ########################################
         # ###########################################################################################################
@@ -558,9 +599,7 @@ class App(QtCore.QObject):
         # ##################################### FIRST RUN SECTION ###################################################
         # ################################ It's done only once after install   #####################################
         # ###########################################################################################################
-
         if self.defaults["first_run"] is True:
-
             # ONLY AT FIRST STARTUP INIT THE GUI LAYOUT TO 'minimal'
             initial_lay = 'minimal'
             layout_field = self.preferencesUiManager.get_form_field("layout")
@@ -583,42 +622,6 @@ class App(QtCore.QObject):
         self.project_filename = None
         self.toggle_units_ignore = False
 
-        # ###########################################################################################################
-        # #################################### LOAD PREPROCESSORS ###################################################
-        # ###########################################################################################################
-
-        # a dictionary that have as keys the name of the preprocessor files and the value is the class from
-        # the preprocessor file
-        self.preprocessors = load_preprocessors(self)
-
-        # make sure that always the 'default' preprocessor is the first item in the dictionary
-        if 'default' in self.preprocessors.keys():
-            new_ppp_dict = {}
-
-            # add the 'default' name first in the dict after removing from the preprocessor's dictionary
-            default_pp = self.preprocessors.pop('default')
-            new_ppp_dict['default'] = default_pp
-
-            # then add the rest of the keys
-            for name, val_class in self.preprocessors.items():
-                new_ppp_dict[name] = val_class
-
-            # and now put back the ordered dict with 'default' key first
-            self.preprocessors = new_ppp_dict
-
-        for name in list(self.preprocessors.keys()):
-            # 'Paste' preprocessors are to be used only in the Solder Paste Dispensing Tool
-            if name.partition('_')[0] == 'Paste':
-                self.ui.tools_defaults_form.tools_solderpaste_group.pp_combo.addItem(name)
-                continue
-
-            self.ui.geometry_defaults_form.geometry_opt_group.pp_geometry_name_cb.addItem(name)
-            # HPGL preprocessor is only for Geometry objects therefore it should not be in the Excellon Preferences
-            if name == 'hpgl':
-                continue
-
-            self.ui.excellon_defaults_form.excellon_opt_group.pp_excellon_name_cb.addItem(name)
-
         # ###########################################################################################################
         # ########################################## LOAD LANGUAGES  ################################################
         # ###########################################################################################################
@@ -2578,7 +2581,7 @@ class App(QtCore.QObject):
         self.date = self.date.replace(' ', '_')
 
         filter__ = "HTML File .html (*.html);;TXT File .txt (*.txt);;All Files (*.*)"
-        path_to_save = self.defaults["global_last_save_folder"] if\
+        path_to_save = self.defaults["global_last_save_folder"] if \
             self.defaults["global_last_save_folder"] is not None else self.data_path
         try:
             filename, _f = FCFileSaveDialog.get_saved_filename(
@@ -3252,7 +3255,7 @@ class App(QtCore.QObject):
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('<b>%s</b>' % _("E-mail")), 0, 2)
 
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Juan Pablo Caram"), 1, 0)
-                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Program Author"), 1, 1)
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % _("Program Author")), 1, 1)
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "<>"), 1, 2)
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Denis Hayrullin"), 2, 0)
                 self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Kamil Sopko"), 3, 0)
@@ -3348,7 +3351,7 @@ class App(QtCore.QObject):
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Italian"), 4, 0)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Golfetto Massimiliano"), 4, 1)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "), 4, 2)
-                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "pcb@golfetto.eu"), 4, 3)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "<golfetto.pcb@gmail.com>"), 4, 3)
 
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "German"), 5, 0)
                 self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Marius Stanciu (Google-Tr)"), 5, 1)
@@ -3605,6 +3608,7 @@ class App(QtCore.QObject):
         # quit app by signalling for self.kill_app() method
         # self.close_app_signal.emit()
         QtWidgets.qApp.quit()
+        # QtCore.QCoreApplication.exit()
 
         # When the main event loop is not started yet in which case the qApp.quit() will do nothing
         # we use the following command

+ 74 - 64
camlib.py

@@ -249,7 +249,7 @@ class ApertureMacro:
 
         pol, dia, x, y = ApertureMacro.default2zero(4, mods)
 
-        return {"pol": int(pol), "geometry": Point(x, y).buffer(dia/2)}
+        return {"pol": int(pol), "geometry": Point(x, y).buffer(dia / 2)}
 
     @staticmethod
     def make_vectorline(mods):
@@ -262,7 +262,7 @@ class ApertureMacro:
         pol, width, xs, ys, xe, ye, angle = ApertureMacro.default2zero(7, mods)
 
         line = LineString([(xs, ys), (xe, ye)])
-        box = line.buffer(width/2, cap_style=2)
+        box = line.buffer(width / 2, cap_style=2)
         box_rotated = affinity.rotate(box, angle, origin=(0, 0))
 
         return {"pol": int(pol), "geometry": box_rotated}
@@ -278,7 +278,7 @@ class ApertureMacro:
 
         pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
 
-        box = shply_box(x-width/2, y-height/2, x+width/2, y+height/2)
+        box = shply_box(x - width / 2, y - height / 2, x + width / 2, y + height / 2)
         box_rotated = affinity.rotate(box, angle, origin=(0, 0))
 
         return {"pol": int(pol), "geometry": box_rotated}
@@ -294,7 +294,7 @@ class ApertureMacro:
 
         pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
 
-        box = shply_box(x, y, x+width, y+height)
+        box = shply_box(x, y, x + width, y + height)
         box_rotated = affinity.rotate(box, angle, origin=(0, 0))
 
         return {"pol": int(pol), "geometry": box_rotated}
@@ -309,12 +309,12 @@ class ApertureMacro:
 
         pol = mods[0]
         n = mods[1]
-        points = [(0, 0)]*(n+1)
+        points = [(0, 0)] * (n + 1)
 
-        for i in range(n+1):
-            points[i] = mods[2*i + 2:2*i + 4]
+        for i in range(n + 1):
+            points[i] = mods[2 * i + 2:2 * i + 4]
 
-        angle = mods[2*n + 4]
+        angle = mods[2 * n + 4]
 
         poly = Polygon(points)
         poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
@@ -333,11 +333,11 @@ class ApertureMacro:
         """
 
         pol, nverts, x, y, dia, angle = ApertureMacro.default2zero(6, mods)
-        points = [(0, 0)]*nverts
+        points = [(0, 0)] * nverts
 
         for i in range(nverts):
-            points[i] = (x + 0.5 * dia * np.cos(2*np.pi * i/nverts),
-                         y + 0.5 * dia * np.sin(2*np.pi * i/nverts))
+            points[i] = (x + 0.5 * dia * np.cos(2 * np.pi * i / nverts),
+                         y + 0.5 * dia * np.sin(2 * np.pi * i / nverts))
 
         poly = Polygon(points)
         poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
@@ -358,9 +358,9 @@ class ApertureMacro:
 
         x, y, dia, thickness, gap, nrings, cross_th, cross_len, angle = ApertureMacro.default2zero(9, mods)
 
-        r = dia/2 - thickness/2
-        result = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
-        ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)  # Need a copy!
+        r = dia / 2 - thickness / 2
+        result = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)
+        ring = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)  # Need a copy!
 
         i = 1  # Number of rings created so far
 
@@ -370,13 +370,13 @@ class ApertureMacro:
             r -= thickness + gap
             if r <= 0:
                 break
-            ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
+            ring = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)
             result = cascaded_union([result, ring])
             i += 1
 
         # ## Crosshair
-        hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th/2.0, cap_style=2)
-        ver = LineString([(x, y-cross_len), (x, y + cross_len)]).buffer(cross_th/2.0, cap_style=2)
+        hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th / 2.0, cap_style=2)
+        ver = LineString([(x, y - cross_len), (x, y + cross_len)]).buffer(cross_th / 2.0, cap_style=2)
         result = cascaded_union([result, hor, ver])
 
         return {"pol": 1, "geometry": result}
@@ -394,9 +394,9 @@ class ApertureMacro:
 
         x, y, dout, din, t, angle = ApertureMacro.default2zero(6, mods)
 
-        ring = Point((x, y)).buffer(dout/2.0).difference(Point((x, y)).buffer(din/2.0))
-        hline = LineString([(x - dout/2.0, y), (x + dout/2.0, y)]).buffer(t/2.0, cap_style=3)
-        vline = LineString([(x, y - dout/2.0), (x, y + dout/2.0)]).buffer(t/2.0, cap_style=3)
+        ring = Point((x, y)).buffer(dout / 2.0).difference(Point((x, y)).buffer(din / 2.0))
+        hline = LineString([(x - dout / 2.0, y), (x + dout / 2.0, y)]).buffer(t / 2.0, cap_style=3)
+        vline = LineString([(x, y - dout / 2.0), (x, y + dout / 2.0)]).buffer(t / 2.0, cap_style=3)
         thermal = ring.difference(hline.union(vline))
 
         return {"pol": 1, "geometry": thermal}
@@ -920,14 +920,16 @@ class Geometry(object):
         Creates contours around geometry at a given
         offset distance.
 
-        :param offset: Offset distance.
-        :type offset: float
-        :param iso_type: type of isolation, can be 0 = exteriors or 1 = interiors or 2 = both (complete)
-        :param corner: type of corner for the isolation: 0 = round; 1 = square; 2= beveled (line that connects the ends)
-        :param follow: whether the geometry to be isolated is a follow_geometry
-        :param passes: current pass out of possible multiple passes for which the isolation is done
-        :return: The buffered geometry.
-        :rtype: Shapely.MultiPolygon or Shapely.Polygon
+        :param offset:      Offset distance.
+        :type offset:       float
+        :param geometry     The geometry to work with
+        :param iso_type:    type of isolation, can be 0 = exteriors or 1 = interiors or 2 = both (complete)
+        :param corner:      type of corner for the isolation:
+                            0 = round; 1 = square; 2= beveled (line that connects the ends)
+        :param follow:      whether the geometry to be isolated is a follow_geometry
+        :param passes:      current pass out of possible multiple passes for which the isolation is done
+        :return:            The buffered geometry.
+        :rtype:             Shapely.MultiPolygon or Shapely.Polygon
         """
 
         if self.app.abort_flag:
@@ -1194,7 +1196,7 @@ class Geometry(object):
             return 0
         bounds = self.bounds()
         return bounds[2] - bounds[0], bounds[3] - bounds[1]
-        
+
     def get_empty_area(self, boundary=None):
         """
         Returns the complement of self.solid_geometry within
@@ -1886,6 +1888,7 @@ class Geometry(object):
         # ## Index first and last points in paths
         def get_pts(o):
             return [o.coords[0], o.coords[-1]]
+
         #
         # storage = FlatCAMRTreeStorage()
         # storage.get_points = get_pts
@@ -1982,10 +1985,10 @@ class Geometry(object):
         the geometry appropriately. This call ``scale()``. Don't call
         it again in descendents.
 
-        :param units: "IN" or "MM"
-        :type units: str
-        :return: Scaling factor resulting from unit change.
-        :rtype: float
+        :param obj_units:   "IN" or "MM"
+        :type units:        str
+        :return:            Scaling factor resulting from unit change.
+        :rtype:             float
         """
 
         if obj_units.upper() == self.units.upper():
@@ -2013,8 +2016,8 @@ class Geometry(object):
         Returns a representation of the object as a dictionary.
         Attributes to include are listed in ``self.ser_attrs``.
 
-        :return: A dictionary-encoded copy of the object.
-        :rtype: dict
+        :return:    A dictionary-encoded copy of the object.
+        :rtype:     dict
         """
         d = {}
         for attr in self.ser_attrs:
@@ -2030,9 +2033,9 @@ class Geometry(object):
         be present. Use only for deserializing saved
         objects.
 
-        :param d: Dictionary of attributes to set in the object.
-        :type d: dict
-        :return: None
+        :param d:   Dictionary of attributes to set in the object.
+        :type d:    dict
+        :return:    None
         """
         for attr in self.ser_attrs:
             setattr(self, attr, d[attr])
@@ -2432,7 +2435,7 @@ class CNCjob(Geometry):
                  pp_geometry_name='default', pp_excellon_name='default',
                  depthpercut=0.1, z_pdepth=-0.02,
                  spindlespeed=None, spindledir='CW', dwell=True, dwelltime=1000,
-                 toolchangez=0.787402, toolchange_xy=[0.0, 0.0],
+                 toolchangez=0.787402, toolchange_xy='0.0,0.0',
                  endz=2.0, endxy='',
                  segx=None,
                  segy=None,
@@ -2441,7 +2444,8 @@ class CNCjob(Geometry):
         self.decimals = self.app.decimals
 
         # Used when parsing G-code arcs
-        self.steps_per_circle = int(self.app.defaults['cncjob_steps_per_circle'])
+        self.steps_per_circle = steps_per_circle if steps_per_circle is not None else \
+            int(self.app.defaults['cncjob_steps_per_circle'])
 
         Geometry.__init__(self, geo_steps_per_circle=self.steps_per_circle)
 
@@ -2667,6 +2671,7 @@ class CNCjob(Geometry):
             if self.xy_toolchange == '':
                 self.xy_toolchange = None
             else:
+                self.xy_toolchange = re.sub('[()\[\]]', '', str(self.xy_toolchange))
                 self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",") if self.xy_toolchange != '']
                 if self.xy_toolchange and len(self.xy_toolchange) < 2:
                     self.app.inform.emit('[ERROR]%s' %
@@ -2677,6 +2682,7 @@ class CNCjob(Geometry):
             log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> %s" % str(e))
             pass
 
+        self.xy_end = re.sub('[()\[\]]', '', str(self.xy_end))
         self.xy_end = [float(eval(a)) for a in self.xy_end.split(",") if self.xy_end != '']
         if self.xy_end and len(self.xy_end) < 2:
             self.app.inform.emit('[ERROR]  %s' % _("The End Move X,Y field in Edit -> Preferences has to be "
@@ -2689,7 +2695,7 @@ class CNCjob(Geometry):
         log.debug("Creating CNC Job from Excellon...")
 
         # Tools
-        
+
         # sort the tools list by the second item in tuple (here we have a dict with diameter of the tool)
         # so we actually are sorting the tools by diameter
         # sorted_tools = sorted(exobj.tools.items(), key=lambda t1: t1['C'])
@@ -2700,7 +2706,7 @@ class CNCjob(Geometry):
         sorted_tools = sorted(sort, key=lambda t1: t1[1])
 
         if tools == "all":
-            tools = [i[0] for i in sorted_tools]   # we get a array of ordered tools
+            tools = [i[0] for i in sorted_tools]  # we get a array of ordered tools
             log.debug("Tools 'all' and sorted are: %s" % str(tools))
         else:
             selected_tools = [x.strip() for x in tools.split(",")]  # we strip spaces and also separate the tools by ','
@@ -3101,7 +3107,7 @@ class CNCjob(Geometry):
                             raise grace
 
                         self.tool = tool
-                        self.postdata['toolC']=exobj.tools[tool]["C"]
+                        self.postdata['toolC'] = exobj.tools[tool]["C"]
                         self.tooldia = exobj.tools[tool]["C"]
 
                         if self.use_ui:
@@ -3577,7 +3583,8 @@ class CNCjob(Geometry):
         self.startz = float(startz) if startz is not None else None
         self.z_end = float(endz) if endz is not None else None
 
-        self.xy_end = [float(eval(a)) for a in endxy.split(",") if endxy != '']
+        self.xy_end = re.sub('[()\[\]]', '', str(endxy))
+        self.xy_end = [float(eval(a)) for a in self.xy_end.split(",") if endxy != '']
         if self.xy_end and len(self.xy_end) < 2:
             self.app.inform.emit('[ERROR]  %s' % _("The End Move X,Y field in Edit -> Preferences has to be "
                                                    "in the format (x, y) but now there is only one value, not two."))
@@ -3595,7 +3602,8 @@ class CNCjob(Geometry):
             if toolchangexy == '':
                 self.xy_toolchange = None
             else:
-                self.xy_toolchange = [float(eval(a)) for a in toolchangexy.split(",")]
+                self.xy_toolchange = re.sub('[()\[\]]', '', str(toolchangexy))
+                self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")]
                 if len(self.xy_toolchange) < 2:
                     self.app.inform.emit('[ERROR]  %s' % _("The Toolchange X,Y field in Edit -> Preferences has to be "
                                                            "in the format (x, y) \n"
@@ -3693,7 +3701,7 @@ class CNCjob(Geometry):
 
         self.gcode = self.doformat(p.start_code)
 
-        self.gcode += self.doformat(p.feedrate_code)        # sets the feed rate
+        self.gcode += self.doformat(p.feedrate_code)  # sets the feed rate
 
         if toolchange is False:
             self.gcode += self.doformat(p.lift_code, x=0, y=0)  # Move (up) to travel height
@@ -3707,19 +3715,19 @@ class CNCjob(Geometry):
             self.gcode += self.doformat(p.toolchange_code)
 
             if 'laser' not in self.pp_geometry_name:
-                self.gcode += self.doformat(p.spindle_code)     # Spindle start
+                self.gcode += self.doformat(p.spindle_code)  # Spindle start
             else:
                 # for laser this will disable the laser
                 self.gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy)  # Move (up) to travel height
 
             if self.dwell is True:
-                self.gcode += self.doformat(p.dwell_code)   # Dwell time
+                self.gcode += self.doformat(p.dwell_code)  # Dwell time
         else:
             if 'laser' not in self.pp_geometry_name:
                 self.gcode += self.doformat(p.spindle_code)  # Spindle start
 
             if self.dwell is True:
-                self.gcode += self.doformat(p.dwell_code)   # Dwell time
+                self.gcode += self.doformat(p.dwell_code)  # Dwell time
 
         total_travel = 0.0
         total_cut = 0.0
@@ -3788,7 +3796,7 @@ class CNCjob(Geometry):
                 total_travel = total_travel + abs(distance(pt1=current_pt, pt2=pt))
                 current_pt = geo.coords[-1]
 
-                pt, geo = storage.nearest(current_pt)   # Next
+                pt, geo = storage.nearest(current_pt)  # Next
 
                 disp_number = int(np.interp(path_count, [0, geo_len], [0, 100]))
                 if old_disp_number < disp_number <= 100:
@@ -3961,7 +3969,9 @@ class CNCjob(Geometry):
 
         self.startz = float(startz) if startz is not None else self.app.defaults["geometry_startz"]
         self.z_end = float(endz) if endz is not None else self.app.defaults["geometry_endz"]
+
         self.xy_end = endxy if endxy != '' else self.app.defaults["geometry_endxy"]
+        self.xy_end = re.sub('[()\[\]]', '', str(self.xy_end))
         self.xy_end = [float(eval(a)) for a in self.xy_end.split(",") if self.xy_end != '']
         if self.xy_end and len(self.xy_end) < 2:
             self.app.inform.emit('[ERROR]  %s' % _("The End Move X,Y field in Edit -> Preferences has to be "
@@ -3978,7 +3988,8 @@ class CNCjob(Geometry):
             if toolchangexy == '':
                 self.xy_toolchange = None
             else:
-                self.xy_toolchange = [float(eval(a)) for a in toolchangexy.split(",")]
+                self.xy_toolchange = re.sub('[()\[\]]', '', str(toolchangexy))
+                self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")]
                 if len(self.xy_toolchange) < 2:
                     self.app.inform.emit(
                         '[ERROR] %s' %
@@ -4085,7 +4096,7 @@ class CNCjob(Geometry):
 
         self.gcode = self.doformat(p.start_code)
 
-        self.gcode += self.doformat(p.feedrate_code)        # sets the feed rate
+        self.gcode += self.doformat(p.feedrate_code)  # sets the feed rate
 
         if toolchange is False:
             self.gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy)  # Move (up) to travel height
@@ -4099,19 +4110,19 @@ class CNCjob(Geometry):
             self.gcode += self.doformat(p.toolchange_code)
 
             if 'laser' not in self.pp_geometry_name:
-                self.gcode += self.doformat(p.spindle_code)     # Spindle start
+                self.gcode += self.doformat(p.spindle_code)  # Spindle start
             else:
                 # for laser this will disable the laser
                 self.gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy)  # Move (up) to travel height
 
             if self.dwell is True:
-                self.gcode += self.doformat(p.dwell_code)   # Dwell time
+                self.gcode += self.doformat(p.dwell_code)  # Dwell time
         else:
             if 'laser' not in self.pp_geometry_name:
                 self.gcode += self.doformat(p.spindle_code)  # Spindle start
 
             if self.dwell is True:
-                self.gcode += self.doformat(p.dwell_code)   # Dwell time
+                self.gcode += self.doformat(p.dwell_code)  # Dwell time
 
         total_travel = 0.0
         total_cut = 0.0
@@ -4553,7 +4564,7 @@ class CNCjob(Geometry):
         kind = ["C", "F"]  # T=travel, C=cut, F=fast, S=slow
 
         # Results go here
-        geometry = []        
+        geometry = []
 
         # Last known instruction
         current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
@@ -4636,7 +4647,7 @@ class CNCjob(Geometry):
                                 kind = ['C', 'F']
                                 geometry.append(
                                     {
-                                        "geom": Point(current_drill_point_coords).buffer(dia/2.0).exterior,
+                                        "geom": Point(current_drill_point_coords).buffer(dia / 2.0).exterior,
                                         "kind": kind
                                     }
                                 )
@@ -4644,14 +4655,14 @@ class CNCjob(Geometry):
 
             if 'G' in gobj:
                 current['G'] = int(gobj['G'])
-                
+
             if 'X' in gobj or 'Y' in gobj:
                 if 'X' in gobj:
                     x = gobj['X']
                     # current['X'] = x
                 else:
                     x = current['X']
-                
+
                 if 'Y' in gobj:
                     y = gobj['Y']
                 else:
@@ -4670,7 +4681,7 @@ class CNCjob(Geometry):
                 arcdir = [None, None, "cw", "ccw"]
                 if current['G'] in [2, 3]:  # arc
                     center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
-                    radius = np.sqrt(gobj['I']**2 + gobj['J']**2)
+                    radius = np.sqrt(gobj['I'] ** 2 + gobj['J'] ** 2)
                     start = np.arctan2(-gobj['J'], -gobj['I'])
                     stop = np.arctan2(-center[1] + y, -center[0] + x)
                     path += arc(center, radius, start, stop, arcdir[current['G']], int(self.steps_per_circle))
@@ -5592,7 +5603,7 @@ class CNCjob(Geometry):
                             new_nr = float(nr) * xfactor
                             # replace the updated string
                             line = line.replace(nr, ('%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_nr))
-                            )
+                                                )
 
                 # this scales all the X and Y and Z and F values and also the Tool Dia in the toolchange message
                 if header_stop is True:
@@ -5993,9 +6004,9 @@ def arc(center, radius, start, stop, direction, steps_per_circ):
         stop += 2 * np.pi
     if direction == "cw" and stop >= start:
         stop -= 2 * np.pi
-    
+
     angle = abs(stop - start)
-        
+
     # angle = stop-start
     steps = max([int(np.ceil(angle / (2 * np.pi) * steps_per_circ)), 2])
     delta_angle = da_sign[direction] * angle * 1.0 / steps
@@ -6578,7 +6589,6 @@ class FlatCAMRTreeStorage(FlatCAMRTree):
         tidx = super(FlatCAMRTreeStorage, self).nearest(pt)
         return (tidx.bbox[0], tidx.bbox[1]), self.objects[tidx.object]
 
-
 # class myO:
 #     def __init__(self, coords):
 #         self.coords = coords

+ 8 - 3
flatcamGUI/FlatCAMGUI.py

@@ -767,7 +767,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.setCentralWidget(self.splitter)
 
         # self.notebook = QtWidgets.QTabWidget()
-        self.notebook = FCDetachableTab(protect=True)
+        self.notebook = FCDetachableTab(protect=True, parent=self)
         self.notebook.setTabsClosable(False)
         self.notebook.useOldIndex(True)
 
@@ -1174,7 +1174,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ########################################################################
         # ########################## PLOT AREA Tab # #############################
         # ########################################################################
-        self.plot_tab_area = FCDetachableTab2(protect=False, protect_by_name=[_('Plot Area')])
+        self.plot_tab_area = FCDetachableTab2(protect=False, protect_by_name=[_('Plot Area')], parent=self)
         self.plot_tab_area.useOldIndex(True)
 
         self.right_lay.addWidget(self.plot_tab_area)
@@ -1372,7 +1372,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.sh_tab_layout.addLayout(self.sh_hlay)
 
         self.app_sh_msg = (
-            '''<b>General Shortcut list</b><br>
+            '''<b>%s</b><br>
             <table border="0" cellpadding="0" cellspacing="0" style="width:283px">
                 <tbody>
                     <tr height="20">
@@ -1716,6 +1716,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
             </table>
             ''' %
             (
+                _("General Shortcut list"),
                 _("SHOW SHORTCUT LIST"), _("Switch to Project Tab"), _("Switch to Selected Tab"),
                 _("Switch to Tool Tab"),
                 _("New Gerber"), _("Edit Object (if selected)"), _("Grid On/Off"), _("Jump to Coordinates"),
@@ -2446,6 +2447,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.grid_snap_btn.triggered.connect(self.on_grid_snap_triggered)
         self.snap_infobar_label.clicked.connect(self.on_grid_icon_snap_clicked)
 
+        # to be used in the future
+        # self.plot_tab_area.tab_attached.connect(lambda x: print(x))
+        # self.plot_tab_area.tab_detached.connect(lambda x: print(x))
+
         # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
         # %%%%%%%%%%%%%%%%% GUI Building FINISHED %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
         # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

+ 22 - 5
flatcamGUI/GUIElements.py

@@ -20,6 +20,7 @@ from copy import copy
 import re
 import logging
 import html
+import sys
 
 import gettext
 import FlatCAMTranslation as fcTranslate
@@ -1431,7 +1432,8 @@ class FCComboBox(QtWidgets.QComboBox):
         return str(self.currentText())
 
     def set_value(self, val):
-        self.setCurrentIndex(self.findText(str(val)))
+        idx = self.findText(str(val))
+        self.setCurrentIndex(idx)
 
     @property
     def is_last(self):
@@ -1575,9 +1577,11 @@ class FCDetachableTab(QtWidgets.QTabWidget):
     From here:
     https://stackoverflow.com/questions/47267195/in-pyqt4-is-it-possible-to-detach-tabs-from-a-qtabwidget
     """
+    tab_detached = QtCore.pyqtSignal(str)
+    tab_attached = QtCore.pyqtSignal(str)
 
     def __init__(self, protect=None, protect_by_name=None, parent=None):
-        super().__init__()
+        super().__init__(parent=parent)
 
         self.tabBar = self.FCTabBar(self)
         self.tabBar.onMoveTabSignal.connect(self.moveTab)
@@ -1714,7 +1718,7 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         self.insertTab(toIndex, widget, icon, text)
         self.setCurrentIndex(toIndex)
 
-    @pyqtSlot(int, QtCore.QPoint)
+    # @pyqtSlot(int, QtCore.QPoint)
     def detachTab(self, index, point):
         """
         Detach the tab by removing it's contents and placing them in
@@ -1751,6 +1755,8 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         # Create a reference to maintain access to the detached tab
         self.detachedTabs[name] = detachedTab
 
+        self.tab_detached.emit(name)
+
     def attachTab(self, contentWidget, name, icon, insertAt=None):
         """
         Re-attach the tab by removing the content from the DetachedTab window,
@@ -1763,11 +1769,11 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         :return:
         """
 
+        old_name = name
+
         # Make the content widget a child of this widget
         contentWidget.setParent(self)
 
-        # Remove the reference
-        del self.detachedTabs[name]
         # make sure that we strip the 'FlatCAM' part of the detached name otherwise the tab name will be too long
         name = name.partition(' ')[2]
 
@@ -1807,6 +1813,9 @@ class FCDetachableTab(QtWidgets.QTabWidget):
             else:
                 index = self.insertTab(insert_index, contentWidget, icon, name)
 
+        obj_name = contentWidget.objectName()
+        self.tab_attached.emit(obj_name)
+
         # on reattaching the tab if protect is true then the closure button is not added
         if self.protect_tab is True:
             self.protectTab(index)
@@ -1822,6 +1831,14 @@ class FCDetachableTab(QtWidgets.QTabWidget):
             if index > -1:
                 self.setCurrentIndex(insert_index) if self.use_old_index else self.setCurrentIndex(index)
 
+        # Remove the reference
+        # Unix-like OS's crash with segmentation fault after this. FOr whatever reason, they loose reference
+        if sys.platform == 'win32':
+            try:
+                del self.detachedTabs[old_name]
+            except KeyError:
+                pass
+
     def removeTabByName(self, name):
         """
         Remove the tab with the given name, even if it is detached

+ 1 - 0
flatcamObjects/FlatCAMExcellon.py

@@ -1115,6 +1115,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
                     else:
                         geo_obj.solid_geometry.append(
                             Point(hole['point']).buffer(buffer_value).exterior)
+
         if use_thread:
             def geo_thread(app_obj):
                 app_obj.new_object("geometry", outname, geo_init, plot=plot)