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

Merged Beta_8.994 into AutolevellingFeature

Marius Stanciu 5 лет назад
Родитель
Сommit
971c3f383f
100 измененных файлов с 29407 добавлено и 21053 удалено
  1. 549 5
      CHANGELOG.md
  2. 131 5
      appCommon/Common.py
  3. 119 0
      appCommon/bilinear.py
  4. 740 274
      appDatabase.py
  5. 2304 2198
      appEditors/AppExcEditor.py
  6. 129 107
      appEditors/AppGeoEditor.py
  7. 74 57
      appEditors/AppGerberEditor.py
  8. 54 14
      appEditors/AppTextEditor.py
  9. 788 0
      appEditors/appGCodeEditor.py
  10. 561 64
      appGUI/GUIElements.py
  11. 226 70
      appGUI/MainGUI.py
  12. 180 686
      appGUI/ObjectUI.py
  13. 30 12
      appGUI/PlotCanvasLegacy.py
  14. 35 3
      appGUI/VisPyVisuals.py
  15. 101 65
      appGUI/preferences/PreferencesUIManager.py
  16. 158 122
      appGUI/preferences/cncjob/CNCJobAdvOptPrefGroupUI.py
  17. 79 0
      appGUI/preferences/cncjob/CNCJobEditorPrefGroupUI.py
  18. 0 30
      appGUI/preferences/cncjob/CNCJobGenPrefGroupUI.py
  19. 31 30
      appGUI/preferences/cncjob/CNCJobOptPrefGroupUI.py
  20. 11 2
      appGUI/preferences/cncjob/CNCJobPreferencesUI.py
  21. 15 108
      appGUI/preferences/excellon/ExcellonAdvOptPrefGroupUI.py
  22. 31 29
      appGUI/preferences/excellon/ExcellonGenPrefGroupUI.py
  23. 0 195
      appGUI/preferences/excellon/ExcellonOptPrefGroupUI.py
  24. 1 1
      appGUI/preferences/excellon/ExcellonPreferencesUI.py
  25. 7 7
      appGUI/preferences/general/GeneralAppPrefGroupUI.py
  26. 16 2
      appGUI/preferences/general/GeneralAppSettingsGroupUI.py
  27. 9 5
      appGUI/preferences/general/GeneralGUIPrefGroupUI.py
  28. 2 2
      appGUI/preferences/geometry/GeometryEditorPrefGroupUI.py
  29. 89 5
      appGUI/preferences/geometry/GeometryGenPrefGroupUI.py
  30. 1 4
      appGUI/preferences/gerber/GerberAdvOptPrefGroupUI.py
  31. 31 9
      appGUI/preferences/gerber/GerberGenPrefGroupUI.py
  32. 1 1
      appGUI/preferences/gerber/GerberPreferencesUI.py
  33. 18 7
      appGUI/preferences/tools/Tools2sidedPrefGroupUI.py
  34. 81 13
      appGUI/preferences/tools/ToolsCutoutPrefGroupUI.py
  35. 392 0
      appGUI/preferences/tools/ToolsDrillPrefGroupUI.py
  36. 15 5
      appGUI/preferences/tools/ToolsPanelizePrefGroupUI.py
  37. 12 6
      appGUI/preferences/tools/ToolsPreferencesUI.py
  38. 119 29
      appObjects/AppObject.py
  39. 1410 37
      appObjects/FlatCAMCNCJob.py
  40. 8 10
      appObjects/FlatCAMDocument.py
  41. 208 616
      appObjects/FlatCAMExcellon.py
  42. 326 180
      appObjects/FlatCAMGeometry.py
  43. 144 108
      appObjects/FlatCAMGerber.py
  44. 10 6
      appObjects/FlatCAMObj.py
  45. 4 7
      appObjects/FlatCAMScript.py
  46. 3 1
      appObjects/ObjectCollection.py
  47. 17 5
      appParsers/ParseDXF.py
  48. 9 10
      appParsers/ParseDXF_Spline.py
  49. 285 227
      appParsers/ParseExcellon.py
  50. 116 27
      appParsers/ParseGerber.py
  51. 63 43
      appParsers/ParseHPGL2.py
  52. 1 1
      appParsers/ParsePDF.py
  53. 134 71
      appParsers/ParseSVG.py
  54. 199 162
      appTools/ToolAlignObjects.py
  55. 158 125
      appTools/ToolCalculators.py
  56. 1125 1100
      appTools/ToolCalibration.py
  57. 1313 1284
      appTools/ToolCopperThieving.py
  58. 208 173
      appTools/ToolCorners.py
  59. 708 554
      appTools/ToolCutOut.py
  60. 277 692
      appTools/ToolDblSided.py
  61. 179 145
      appTools/ToolDistance.py
  62. 149 117
      appTools/ToolDistanceMin.py
  63. 2687 0
      appTools/ToolDrilling.py
  64. 253 217
      appTools/ToolEtchCompensation.py
  65. 552 501
      appTools/ToolExtractDrills.py
  66. 449 412
      appTools/ToolFiducials.py
  67. 1064 1032
      appTools/ToolFilm.py
  68. 170 132
      appTools/ToolImage.py
  69. 162 154
      appTools/ToolInvertGerber.py
  70. 502 1007
      appTools/ToolIsolation.py
  71. 2354 0
      appTools/ToolMilling.py
  72. 972 1338
      appTools/ToolNCC.py
  73. 285 254
      appTools/ToolOptimal.py
  74. 776 1190
      appTools/ToolPaint.py
  75. 478 373
      appTools/ToolPanelize.py
  76. 183 151
      appTools/ToolPcbWizard.py
  77. 45 29
      appTools/ToolProperties.py
  78. 536 504
      appTools/ToolPunchGerber.py
  79. 394 355
      appTools/ToolQRCode.py
  80. 1362 1327
      appTools/ToolRulesCheck.py
  81. 3 0
      appTools/ToolShell.py
  82. 530 972
      appTools/ToolSolderPaste.py
  83. 212 186
      appTools/ToolSub.py
  84. 793 762
      appTools/ToolTransform.py
  85. 2 0
      appTools/__init__.py
  86. 2 1
      appTranslation.py
  87. 447 283
      app_Main.py
  88. BIN
      assets/Shapely-1.8.dev0-py2.py3-none-any.whl
  89. BIN
      assets/resources/apply32.png
  90. BIN
      assets/resources/apply_red32.png
  91. BIN
      assets/resources/dark_resources/apply32.png
  92. BIN
      assets/resources/dark_resources/apply_red32.png
  93. BIN
      assets/resources/dark_resources/down-arrow32.png
  94. BIN
      assets/resources/dark_resources/drilling_tool32.png
  95. BIN
      assets/resources/dark_resources/find32.png
  96. BIN
      assets/resources/dark_resources/gaps32.png
  97. BIN
      assets/resources/dark_resources/geometry16.png
  98. BIN
      assets/resources/dark_resources/geometry32.png
  99. BIN
      assets/resources/dark_resources/irregular32.png
  100. BIN
      assets/resources/dark_resources/left_arrow32.png

+ 549 - 5
CHANGELOG.md

@@ -7,6 +7,550 @@ CHANGELOG for FlatCAM beta
 
 =================================================
 
+26.09.2020
+
+- the Selected Tab is now Properties Tab for FlatCAM objects
+- modified the Properties Tab for various FlatCAM objects preparing the move of Properties Tool data into the Properties Tab
+- if the Properties tab is in focus (selected) when a new object is created then it is automatically selected therefore it's properties will be populated
+
+25.09.2020
+
+- minor GUI change in Isolation Tool
+
+24.09.2020
+
+- fixed a bug where end_xy parameter in Drilling Tool was not used
+- fixed an issue in Delete All method in the app_Main.py
+
+23.09.2020
+
+- added support for virtual units in SVG parser; warning: it may require the support for units which is not implemented yet
+- fixed canvas selection such that when selecting shape fails to be displayed with rounded corners a square selection shape is used
+- fixed canvas selection for the case when the selected object is a single line or a line made from multiple segments
+
+22.09.2020
+
+- fixed an error in importing SVG that has a single line
+- updated the POT file and the PO/MO files for Turkish language
+- working to add virtual units to SVG parser
+
+20.09.2020
+
+- in CNCJob UI Autolevelling: on manual add of probe points, only voronoi diagram is calculated
+- in SVG parser: made sure that the minimum number of steps to approximate an arc/circle/bezier is 10
+
+19.09.2020
+
+- removed some brackets in the GRBL laser preprocessor due of GRBL firmware interpreting the first closing bracket as the comment end
+
+3.09.2020
+
+- in CNCJob UI Autolevelling: changed the UI a bit
+- added a bilinear interpolation calculation class from: https://github.com/pmav99/interpolation
+- in CNCJob UI Autolevelling: made sure that the grid can't have less than 2 rows and 2 columns when using the bilinear interpolation or 1 row and 1 column when using the Voronoi polygons
+- in CNCJob UI Autolevelling: prepared the app for bilinear interpolation
+- in CNCJob UI Autolevelling: fixes in the UI
+
+2.09.2020
+
+- in CNCJob UI Autolevelling: solved some small errors: when manual adding probe points dragging the mouse with left button pressed created selection rectangles; detection of click inside the solid geometry was failing
+- in CNCJob UI Autolevelling: in manual adding of probe points make sure you always add a first probe point in origin
+- in CNCJob UI Autolevelling: first added point when manual adding of probe points is auto added in origin before adding first point
+- in CNCJob UI Autolevelling: temp geo for adding points in manual mode is now painted in solid black color and with a smaller diameter
+- in CNCJob UI Autolevelling - GRBL controller - added a way to save a GRBL height map
+- in CNCJob UI Autolevelling: added the UI for choosing the method used for the interpolation used in autolevelling
+
+31.08.2020
+
+- updated the Italian translation files by Massimiliano Golfetto
+- in CNCJob UI Autolevelling: made sure that plotting a Voronoi polygon is done only for non-None polygons
+- in CNCJob UI Autolevelling: in manual mode, Points can be chosen only when clicking inside the object to be probed
+- in CNCJob UI Autolevelling: made sure that plotting a Voronoi polygon is done only for non-None polygons
+- in CNCJob UI Autolevelling: remade the probing points generation so they could allow bilinear interpolation
+
+29.08.2020
+
+- 2Sided Tool - fixed newly introduced issues in the Alignment section
+- 2Sided Tool - modified the UI such that some of the fields will allow only numbers and some special characters ([,],(,),/,*,,,+,-,%)
+- Cutout Tool - working on adding mouse bites for the Freeform cutout
+- updated the translation files to the current state of the app
+- Cutout Tool - rectangular and freeform cutouts are done in a threaded way
+- Cutout Tool - added the Mouse Bites feature for the Rectangular and Freeform cutouts and right now it fails in case of using a Geometry object and Freeform cutout (weird result)
+- some changes in camlib due of warnings for future changes in Shapely 2.0
+- Cutout Tool - fixed mouse bites feature in case of using a Geometry object and Freeform cutout
+- Cutout Tool - can do cutouts on multigeo Geometry objects: it will automatically select the geometry of first tool
+- Geometry Editor - fixed exception raised when trying to move and there is no shape to move
+- Cutout Tool - finished adding the Mouse Bites feature by adding mouse bites for manual cutouts
+
+28.08.2020
+
+- Paint Tool - upgraded the UI and added the functionality that now adding a new tool is done by first searching the Tools DB for a suitable tool and if fails then it adds an default tool
+- Paint Tool - on start will attempt to search in the Tools DB for the default tools and if found will load them from the DB
+- NCC Tool - upgraded the UI and added the functionality that now adding a new tool is done by first searching the Tools DB for a suitable tool and if fails then it adds an default tool
+- NCC Tool - on start will attempt to search in the Tools DB for the default tools and if found will load them from the DB
+- fixes in NCC, Paint and Isolation Tool due of recent changes
+- modified the Tools Database and Preferences with the new parameters from CutOut Tool
+- changes in Tool Cutout: now on Cutout Tool start the app will look into Tools Database and search for a tool with same diameter (or within the set tolerance) as the one from Preferences and load it if found or load a default tool if not
+- Tool Cutout - this Tool can now load tools from Tools Database through buttons in the Cutout Tool
+
+27.08.2020
+
+- fixed the Tcl commands AddCircle, AddPolygon, AddPolyline and AddRectangle to have stored bounds therefore making them movable/selectable on canvas
+- in Tool Cutout, when using the Thin Gaps feature, the resulting geometry loose the extra color by toggling tool plot in Geometry UI Tools Table- fixed
+- in Tool Cutout fixed manual adding of gaps with thin gaps and plotting
+- in Tool Cutout, when using fix gaps made sure that this feature is not activated if the value is zero
+- in Tool Cutout: modified the UI in preparation for adding the Mouse Bites feature
+- Turkish translation strings were updated by the translator, Mehmet Kaya
+- Film Tool - moved the Tool UI in its own class
+- in Tools: Image, InvertGerber, Optimal, PcbWizard - moved the Tool UI in its own class
+- Tool Isolation - made sure that the app can load from Tools Database only tools marked for Isolation tool
+- Tool Isolation - on Tool start it will attempt to load the Preferences set tools by diameter from Tools Database. If it can't find one there it will add a default tool.
+- in Tools: Transform, SUb, RulesCheck, DistanceMin, Distance - moved the Tool UI in its own class
+- some small fixes
+- fixed a borderline issue in CNCJob UI Autolevelling - Voronoi polygons calculations
+
+26.08.2020
+
+- fix for issue nr 2 in case of Drilling Tool. Need to check Isolation Tool, Paint Tool, NCC Tool
+- Drilling Tool - UI changes
+- Geometry object - now plotting color for an individual tool can be specified
+- in CutOut Tool - when using  'thin gaps' option then the cut parts are colored differently than the rest of the geometry in the Geometry object
+- solved some deprecation warnings (Shapely module)
+- Drilling Tool - when replacing Tools if more than one tool for one diameter is found, the application exit the process and display an error in status bar; some minor fixes
+- Isolation Tool - remade the UI
+- Isolation Tool - modified the add new tool method to search first in Tools Database  for a suitable tool
+- Isolation Tool - added ability to find the tool diameter that will guarantee total isolation of the currently selected Gerber object
+- NCC Tool - UI change: if the operation is Isolation then some of the tool parameters are disabled
+- fixed issue when plotting a CNCJob object with multiple tools and annotations on by plotting annotations after all the tools geometries are plotted
+- fixed crash in Properties Tool, when applied on a CNCJob object made out of an Excellon object (fixed issue #444)
+- in Properties Tool, for CNCJob objects made out of Excellon objects, added the information's from Tool Data
+- in Properties Tool made sure that the set color for the Tree parents depends on the fact if the gray icons set are used (dark theme) or not
+- Properties Tool - properties for a Gerber objects has the Tool Data now at the end of the information's
+- in Gerber UI done some optimizations
+
+25.08.2020
+
+- in CNCJob UI Autolevelling - made the Voronoi calculations work even in the scenarios that previously did not work; it need a newer version of Shapely, currently I installed the GIT version
+- in CNCJob UI Autolevelling - Voronoi polygons are now plotted
+- in CNCJob UI Autolevelling - adding manual probe points now show some geometry (circles) for the added points until the adding is finished
+- 2Sided Tool - finished the feature that allows picking an Excellon drill hole center as a Point mirror reference
+- Tool Align Objects - moved the Tool Ui into its own class
+- for Tools: Calculators, Calibration, Copper Thieving, Corners, Fiducials - moved the Tool UI in its own class
+
+24.08.2020
+
+- fixed issues in units conversion
+- in CNCJob UI Autolevelling - changed how the probing code is generated and when
+- changed some strings in CNCJob UI Autolevelling
+- made sure that when doing units conversion keep only the decimals specified in the application decimals setting (should differentiate between values and display?)
+- in CNCJob UI Autolevelling - some UI changes
+- in CNCJob UI Autolevelling - GRBL controller - added the probing method
+- in CNCJob UI Autolevelling - GRBL controller - fixed the send_grbl_command() method
+
+23.08.2020
+
+- in CNCJob UI Autolevelling - autolevelling is made to be not available for cnc code generated with Roland or HPGL preprocessors
+- in CNCJob UI Autolevelling - added a save dialog for the probing GCode
+- added a new GUI element, a DoubleSlider
+- in CNCJob UI Autolevelling - GRBL controller - Control: trying to add DoubleSlider + DoubleSpinner combo controls
+- in GUI element FCDoubleSpinner fixed an range issue
+
+21.08.2020
+
+- in CNCJob UI Autolevelling - GRBL controller - Control: added a Origin button; changed the UI to have rounded rectangles 
+- in CNCJob UI Autolevelling - GRBL controller - Control: added feedrate and step size controls and added them in Preferences
+- in CNCJob UI Autolevelling - GRBL controller - added handlers for the Zeroing and for Homing and for Pause/Resume; some UI optimizations
+
+19.08.2020
+
+- in CNCJob UI Autolevelling - sending GCode/GRBL commands is now threaded
+- in CNCJob UI Autolevelling - Grbl Connect tab colors will change with the connection status
+- in CNCJob UI Autolevelling - GRBL Control and Sender tabs are disabled when the serial port is disconnected
+- in CNCJob UI Autolevelling - GRBL Sender - now only a single command can be sent 
+- in CNCJob UI Autolevelling - GRBL controller - changed the UI
+- in CNCJob UI Autolevelling - added some VOronoi poly calculations
+
+18.08.2020
+
+- in Doublesided Tool added some UI for Excellon hole snapping
+- in Doublesided Tool cleaned up the UI
+- in CNCJob UI Autolevelling - in COntrol section added  buttons for Jog an individual axes zeroing
+- in CNCJob UI Autolevelling - added handlers for: jogging, reset, sending commands
+- in CNCJob UI Autolevelling - added handlers for GRBL report and for getting GRBL parameters
+
+17.08.2020
+
+- in CNCJob UI Autolevelling - GRBL GUI controls are now organized in a tab widget
+
+16.08.2020
+
+- in CNCJob UI Autolevelling - updated the UI with controls for probing GCode parameters and added signals and slots for the UI
+- in CNCJob UI Autolevelling - added a mini gcode sender for the GRBL to be able to send the probing GCode and get the height map (I may make a small and light app for that so it does not need to have FlatCAM on the GCode sender PC)
+- in CNCJob UI Autolevelling finished the probing GCode generation for MACH/LinuxCNC controllers; this GCode can also be viewed
+- in CNCJob UI Autolevelling - Probing GCode has now a header
+- in CNCJob UI Autolevelling - Added entries in Preferences
+- in CNCJob UI Autolevelling - finished the Import Height Map method
+- in CNCJob UI Autolevelling - made autolevelling checkbox state persistent between app restarts
+
+14.08.2020
+
+- in CNCJob UI worked on the UI for the Autolevelling
+- in CNCJob UI finished working on adding test points in Grid mode
+- in CNCJob UI finished working on adding test points in Manual mode
+
+13.08.2020
+
+- in CNCJob UI added GUI for an eventual Autolevelling feature 
+- in CNCJob UI updated the GUI for Autolevelling
+- Cutout Tool - finished handler for gaps thickness control for the manual gaps
+- CNCJob object - working in generating Voronoi diagram for autolevelling
+
+11.08.2020
+
+- CutOut Tool - finished handler for gaps thickness control for the free form cutout
+
+9.08.2020
+
+- small fix so the cx_freeze 6.2 module will work in building a frozen version of FlatCAM
+
+7.08.2020
+
+- all Geometry objects resulted from Isolation Tool are now of type multi-geo
+- fixed minor glitch in the Isolation Tool UI
+- added an extra check when doing selection on canvas
+- fixed an UI problem in Gerber Editor
+
+5.08.2020
+
+- Tool Cutout - more work in gaps thickness control feature
+- Tool Cutout - added some icons to buttons
+- Tool Cutout - done handling the gaps thickness control for the rectangular cutout; TODO: check all app for the usage of geometry_spindledir and geometry_optimization_type defaults in tools and in options
+- Tool Cutout - some work in gaps thickness control for the free form cutout
+
+4.08.2020
+
+- removed the Toolchange Macro feature (in the future it will be replaced by full preprocessor customization)
+- modified GUI in Preferences
+- Tool Cutout - working in adding gaps thickness control feature; added the UI in the Tool
+
+3.08.2020
+
+- GCode Editor - GCode tool selection when clicking on tool in Tools table is working. The only issue is that the first tool gcode includes the start gcode which confuse the algorithm
+- GCode Editor - can not delete objects while in the Editor; can not close the Code Editor Tab except on Editor exit; activated the shortcut keys (for now only CTRL+S is working)
+- added a way to remember the old state of Tools toolbar before and after entering an Editor
+- GCode Editor - modified the UI
+
+2.08.2020
+
+- GCode Editor - closing the Editor will close also the Code Editor Tab
+- cleanup of the CNCJob UI; added a checkbox to signal if any append/prepend gcode was set in Preferences (unchecking it will override and disable the usage of the append/prepend GCode)
+- the start Gcode is now stored in the CNCJob object attribute gc_start
+- GCode Editor - finished adding the ability to select a row in the Tools table and select the related GCode
+- GCode Editor - working on GCode tool selection - not OK
+
+1.08.2020
+
+- Tools Database: added a Cutout Tool Parameters section
+- GCode Editor - work in the UI
+
+31.07.2020
+
+- minor work in GCode Editor
+
+29.07.2020
+
+- fixed an exception that was raised in Geometry object when using an Offset
+
+27.07.2020
+
+- Gerber parser - a single move with pen up D2 followed by a pen down D1 at the same location is now treated as a Flash; fixed issue #441
+
+25.07.2020
+
+- Tools Tab is hidden when entering into a Editor and showed on exit (this needs to be remade such that the toolbars state should be restored to whatever it was before entering in the Editor)
+
+22.07.2020
+
+- working on a proper GCode Editor
+- wip in the GCode Editor
+- added a Laser preprocessor named 'Z_laser' which will change the Z to the Travel Z on each ToolChange event allowing therefore control of the dot size
+- by default now a new blank Geometry object created by FlatCAM is of type multigeo
+- made sure that optimizations of lines when importing SVG or DXF as lines will not encounter polygons but only LinesStrings or LinearRings, otherwise having crashes
+- fixed the import SVG and import DXF, when importing as Geometry to be imported as multigeo tool
+- fixed the import SVG and import DXF, the source files will be saved as loaded into the source_file attribute of the resulting object (be it Geometry or Gerber)
+- in import SVG and import DXF methods made sure that any polygons that are imported as polygons will survive and only the lines are optimized (changed the behavior of the above made modification)
+
+21.07.2020
+
+- updated the FCRadio class with a method that allow disabling certain options
+- the Path optimization options for Excellon and Geometry objects are now available depending on the OS platform used (32bit vs 64bit)
+- fixed MultiColor checkbox in Excellon Object to work in Legacy Mode (2D)
+- modified the visibility change in Excellon UI to no longer do plot() when doing visibility toggle for one of the tools but only a visibility change in the shapes properties
+- Excellon UI in Legacy Mode (2D): fixed the Solid checkbox functionality
+- Excellon UI: fixed plot checkbox performing an extra plot function which was not required
+- Excellon UI: added a column which will color each row/tool of that column in the color used when checking Multicolor checkbox
+- Excellon UI: made sure that when the Multicolor checkbox is unchecked, the color is updated in the Color column of the tools table
+- made sure that the Preferences files are deleted on new version install, while the application is in Beta status
+- fixed issues with detecting older Preferences files
+- fixed some issues in Excellon Editor due of recent changes
+- moved the Gerber colors fill in the AppObject.on_object_created() slot and fixed some minor issues here
+- made sure there are no issues when plotting the Excellon object in one thread and trying to build the UI in another by using a signal
+
+20.07.2020
+
+- fixed a bug in the FlatCAMGerber.on_mark_cb_click_table() method when moving a Gerber object
+- added a way to remember the colors set for the Gerber objects; it will remember the order that they were loaded and set a color previously given
+- added a control in Preferences -> Gerber Tab for Gerber colors storage usage
+- made sure that the defaults on first install will set the number of workers to half the number of CPU's on the system but no less than 2
+
+18.07.2020
+
+- added some icons in the Code Editor
+- replaced some icons in the app
+- in Code Editor, when changing text, the Save Code button will change color (text and icon) to red and after save it will revert the color to the default one
+- in Code Editor some methods rework
+
+16.07.2020
+
+- added a new method for GCode generation for Geometry objects
+- added multiple algorithms for path optimization when generating GCode from an Geometry object beside the original Rtree algorithm: TSA, OR-Tools Basic, OR-Tools metaheuristics
+- added controls for Geometry object path optimization in Preferences
+
+15.07.2020
+
+- added icons to some of the push buttons
+- Tool Drilling - automatically switch to the Selected Tab after job finished
+- added Editor Push buttons in Geometry and CNCJob UI's
+- Tool Drilling - brushing through code and solved the report on estimation of execution time
+- Tool Drilling - more optimizations regarding of using Toolchange as opposed to not using it
+- modified the preprocessors to work with the new properties for Excellon objects
+- added to preprocessors information regarding the X,Y position at the end of the job
+- Tool Drilling made sure that on Toolchange event after toolchange event the tool feedrate is set
+- added some icons to more push buttons inside the app
+- a change of layout in Tools Database
+- a new icon for Search in DB
+
+14.07.2020
+
+- Drilling Tool - now there is an Excellon preference that control the autoload of tools from the Tools Database
+- Tools Database - remade the UI
+- made sure that the serializable attributes are added correctly and only once (self.ser_attrs)
+- Tools Database - some fixes in the UI (some of the widgets had duplicated names)
+- Tools Database - made sure the on save the tools are saved only with the properties that relate to their targeted area of the app
+- Tools Database - changes can be done only for one tool at a time
+- Tool Database - more changes to the UI
+
+13.07.2020
+
+- fixed a bug in Tools Database: due of not disconnecting the signals it created a race that was concluded into a RuntimeError exception (an dict changed size during iteration)
+- Drilling Tool - working in adding tools auto-load from Tools DB
+- some updates to the Excellon Object options
+- Drilling Tool - manual add from Tools DB is working
+- Drilling Tool - now slots are converted to drills if the checkbox is ON for the tool investigated
+- Drilling Tool - fixes due of changes in properties (preferences)
+- fixed the Drillcncjob TCL command
+- Multiple Tools fix - fixed issue with converting slots to drills selection being cleared when toggling all rows by clicking on the header
+- Multiple Tools fix - fixes for when having multiple tools selected which created issues in tool tables for many tools
+
+12.07.2020
+
+- when creating a new FlatCAM object, the options will be updated with FlatCAM tools properties that relate to them
+- updated the Tools DB class by separating the Tools DB UI into it's own class
+- Tools DB - added the parameters for Drilling Tool
+
+11.07.2020
+
+- moved all Excellon Advanced Preferences to Drilling Tool Preferences
+- updated Drilling Tool to use the new settings
+- updated the Excellon Editor: the default_data dict is populated now on Editor entry
+- Excellon Editor: added a new functionality: conversion of slots to drills
+- Excellon UI: added a new feature that is grouped in Advanced Settings: a toggle tools table visibility checkbox 
+- Drilling Tool - minor fixes
+- Drilling Tool - changes in UI
+- Isolation Tool - modified the UI; preparing to add new feature of polishing at the end of the milling job
+- Tool Paint - fixed an issue when launching the tool and an object other than Geometry or Excellon is selected
+- Geometry UI - moved the UI for polishing from Isolation Tool to Geometry UI (actually in the future Milling Tool) where it belongs
+- Gerber UI - optimized the mark shapes to use only one ShapeCollection
+
+10.07.2020
+
+- Tool Drilling - moved some of the Excellon Preferences related to drilling operation to it's own group Drilling Tool Options
+- optimized the CNCJob UI to look like other parts of the app 
+- in Gerber and Excellon UI added buttons to start the Editor
+- in all Editors Selected Tab added a button to Exit the Editor
+- Tool Drilling - fixed incorrect annotations in CNCJob objects generated; one drawback is that now each tool (when Toolchange is ON) has it's own annotation order which lead to overlapping in the start point of one tool and the end of previous tool
+- Tool Drilling - refactoring methods and optimizations
+
+9.07.2020
+
+- Tool Drilling - remade the methods used to generate GCode from Excellon, to parse the GCode. Now the GCode and GCode_parsed are stored individually for each tool and also they are plotted individually
+- Tool Drilling now works - I still need to add the method for converting slots to drill holes
+- CNCJob object - now it is possible for CNCJob objects originated from Excellon objects, to toggle the plot for a selection of tools
+- working in cleaning up the Excellon UI (Selected Tab)
+- finished the clean-up in Excellon UI
+- Tool Drilling - added new feature to drill the slots
+
+8.07.2020
+
+- Tool Drilling - working on the UI
+- Tool Drilling - added more tool parameters; laying the ground for adding "Drilling Slots" feature
+- added as ToolTip for the the Preprocessor combobox items, the actual name of the items
+- working on Tool Drilling - remaking the way that the GCode is stored, each tool will store it's own GCode
+- working on Tool Drilling
+
+7.07.2020
+
+- updated the Panelize Tool to save the source code for the panelized Excellon objects so it can be saved from the Save project tab context menu entry
+- updated the Panelize Tool to save the source code for the panelized Geometry objects as DXF file
+- fixed the Panelize Tool so the box object stay as selected on new objects are loaded; any selection shape on canvas is deleted when clicking Panelize
+
+6.07.2020
+
+- Convert Any to Excellon. Finished Gerber object conversion to Excellon. Flash's are converted to drills. Traces in the form of a linear LineString (no changes in direction) are converted to slots.
+- Turkish translation updated by Mehmet Kaya for the 8.993 version of strings
+
+2.07.2020
+
+- trying to optimize the resulting geometry in DXF import (and in SVG import) by merging contiguous lines; reduced the lines to about one third of the original
+- fixed importing DXF file as Gerber method such that now the resulting Gerber object is correctly created having the geometry attributes like self.apertures and self.follow_geometry
+- added Turkish translation - courtesy of Mehmet Kaya
+- modified the Gerber export method to take care of the situation where the exported Gerber file is a SVG/DXF file imported as Gerber
+- working in making a new functionality: Convert Any to Excellon. Finished Geometry object conversion to Excellon.
+
+30.06.2020
+
+- fixed the SVG parser so the SVG files with no information regarding the 'height' can be opened in FlatCAM; fixed issue #433
+
+29.06.2020
+
+- fixed the DXF parser to work with the latest version of ezdxf module (issues for the ellipse entity and modified attribute name for the knots_values to knots)
+- fixed the DXF parser to parse correctly the b-splines by not adding automatically a knot value 0f (0, 0) when the spline is not closed
+
+27.06.2020
+
+- Drilling Tool - UI is working as expected; I will have to propagate the changes to other tools too, to increase likeness between different parts of the app
+
+25.06.2020
+
+- made sure that when trying to view the source but no object is selected, the messages are correct
+- wip for Tool Drilling
+
+23.06.2020
+
+- working on Tool Drilling
+
+21.06.2020
+
+- wip
+
+18.06.2020
+
+- fixed bug in the Cutout Tool that did not allowed the manual cutous to be added on a Geometry created in the Tool
+- fixed bug in Cutout Tool that made the selection box show in the stage of adding manual gaps
+- updated Cutout Tool UI
+- Cutout Tool - in manual gap adding there is now an option to automatically turn on the big cursor which could help
+- Cutout Tool - fixed errors when trying to add a manual gap without having a geometry object selected in the combobox
+- Cutout Tool - made sure that all the paths generated by this tool are contiguous which means that two lines that meet at one end will become only one line therefore reducing unnecessary Z moves
+- Panelize Tool - added a new option for the panels of type Geometry named Path Optimization. If the checkbox is checked then all the LineStrings that are overlapped in the resulting multigeo Geometry panel object will keep only one of the paths thus minimizing the tool cuts.
+- Panelize Tool - fixed to work for panelizing Excellon objects with the new data structure storing drills and tools in the obj.tools dictionary
+- put the bases for a new Tool: Milling Holes Tool
+
+17.06.2020
+
+- added the multi-save capability if multiple CNCJob objects are selected in Project tab but only if all are of type CNCJob
+- added fuse tools control in Preferences UI for the Excellon objects: if checked the app will try to see if there are tools with same diameter and merge the drills for those tools; if not the tools will just be added to the new combined Excellon
+- modified generate_from_excellon_by_tool() method in camlib.CNCJob() such that when Toolchange option is False, since the drills will be drilled with one tool only, all tools will be optimized together
+
+16.06.2020
+
+- changed the data structure for the Excellon object; modified the Excellon parser and the Excellon object class
+- fixed partially the Excellon Editor to work with the new data structure
+- fixed Excellon export to work with the new data structure
+- fixed all transformations in the Excellon object attributes; still need to fix the App Tools that creates or use Excellon objects
+- fixed some problems (typos, missing data) generated by latest changes
+- more typos fixed in Excellon parser, slots processing
+- fixed Extract Drills Tool to work with the new Excellon data format
+- minor fix in App Tools that were updated to have UI in a separate class
+- Tool Punch Gerber - updated the UI
+- Tool Panelize - updated the UI
+- Tool Extract Drills - updated the UI
+- Tool QRcode - updated the UI
+- Tool SolderPaste - updated the UI
+- Tool DblSided - updated the UI
+
+15.06.2020
+
+- in Paint Tool and NCC Tool updated the way the selected tools were processed and made sure that the Tools Table rows are counted only once in the processing
+- modified the UI in Paint Tool such that in case of using rest machining the offset will apply for all tools
+- Paint Tool - made the rest machining function for the paint single polygon method
+- Paint Tool - refurbished the 'rest machining' for the entire tool
+- Isolation Tool - fixed to work with selection of tools in the Tool Table (previously it always used all the tools in the Tool Table)
+- Tools Database - added a context menu action to Save the changes to the database even if it's not in the Administration mode
+- Tool Isolation - fixed a UI minor issue: 'forced rest' checkbox state at startup was always enabled
+- started working in moving the Excellon drilling in its own Application Tool
+- created a new App Tool named Drilling Tool where I will move the drilling out of the Excellon UI
+- working on the Drilling Tool - started to create a new data structure that will hold the Excellon object data
+
+14.06.2020
+
+- made sure that clicking the icons in the status bar works only for the left mouse click
+- if clicking the activity icon in the status bar and there is no object selected then the effect will be a plot_all with fit_view
+- modified the FCLabel GUI element
+- NCC Tool - remade and optimized the copper clearing with rest machining: now it works as expected with a reasonable performance
+- fixed issue #428 - Cutout Tool -> Freeform geometry was not generated due of trying to get the bounds of the solid_geometry before it was available
+- NCC Tool - now the tools can be reordered (if the order UI radio is set to 'no')
+- remade the UI in Paint Tool and the tools in tools table ca now be reordered (if the order UI radio is set to 'no')
+- some updates in NCC Tool using code from Paint Tool
+- in Paint and NCC Tools made sure that using the key ESCAPE to cancel the tool will not create mouse events issues
+- some updates in Tcl commands Paint and CopperClear data dicts
+- modified the Isolation Tool UI: now the tools can be reordered (if the order UI radio is set to 'no')
+- modified the Paint, NCC and Isolation Tools that when no tools is selected in the Tools Table, a message will show that no Tool is selected and the Geometry generation button is disabled
+
+13.06.2020
+
+- modified the Tools Database such that there is now a way to mark a tool as meant to be used in a certain part of the application; it will disable or enable parts of the parameters of the tool
+- updated the FCTable GUI element to work correctly when doing drag&drop for the rows
+- updated the Geometry UI to work with the new FCTable
+- made the coordinates / delta coordinates / grid toolbar / actions toolbar visibility an option, controlled from the infobar (Status bar) context menu. How it's at app shutdown it's restored at the next application start
+- moved the init of activity view in the MainGUI file from the APP.__init__()
+- added a new string in the tooltip for the button that adds tool from database specifying the tools database administration is done in the menu
+- when opening a new tab in the PlotTabArea the coordinates toolbars will be hidden and shown after the tab is closed
+
+12.06.2020
+
+- NCC Tool optimization - moved the UI in its own class
+- NCC Tool optimization - optimized the Tool edit method
+- NCC Tool - allow no tool at NCC Tool start (the Preferences have no tool)
+- NCC Tool - optimized tool reset code
+- NCC Tool - fixed the non-rest copper clearing to work as expected: each tool in the tool table will make it's own copper clearing without interference from the rest of the tools 
+- Geometry UI - made again the header clickable and first click selects all rows, second click will deselect all rows.
+- Geometry UI - minor updates in the layout; moved the warning text to the tooltip of the generate_cncjob button
+- Geometry UI - working in making the modification of tool parameters such that if there is a selection of tools the modification in the Tool parameters will be applied to all selected
+
+11.06.2020
+
+- finished tool reordering in Geometry UI
+
+10.06.2020
+
+- fixed bug in the Isolation Tool that in certain cases an empty geometry was present in the solid_geometry which mae the CNCJob object generation to fail. It happen for Gerber objects created in the Gerber Editor
+- working on the tool reordering in the Geometry UI
+- continue - work in tool reordering in Geometry UI
+
+9.06.2020
+
+- fixed a possible problem in generating bounds value for a solid_geometry that have empty geo elements
+- added ability to merge tools when merging Geometry objects if they share the same attributes like: diameter, tool_type or type
+- added a control in Edit -> Preferences -> Geometry to control if to merge/fuse tools during Geometry merging
+
+8.06.2020
+
+- minor changes in the way that the tools are installed and connected
+- renamed the GeoEditor class/file to AppGeoEditor from FlatCAMGeoEditor making it easier to see in the IDE tree structure
+- some refactoring that lead to a working solution when using the Python 3.8 + PyQt 5.15
+- more refactoring in the app Editors
+- added a protection when trying to edit a Geometry object that have multiple tools but no tool is selected
+
 7.06.2020
 
 - refactoring in camlib.py. Made sure that some conditions are met, if some of the parameters are None then return failure. Modifications in generate_from_geometry_2 and generate_from_multitool_geometry methods
@@ -1327,7 +1871,7 @@ RELEASE 8.993
 13.11.2019
 
 - trying to improve the performance of View CNC Code command by using QPlainTextEdit; made the mods for it
-- when using the Find function in the TextEditor and the result reach the bottom of the document, the next find will be the first in the document (before it defaulted to the beginning of the document)
+- when using the Find function in the AppTextEditor and the result reach the bottom of the document, the next find will be the first in the document (before it defaulted to the beginning of the document)
 - finished improving the show of text files in FlatCAM (CNC Code, Source files)
 - fixed an issue in the FlatCAMObj.GerberObject.convert_units() which needed to be updated after changes elsewhere
 
@@ -1630,7 +2174,7 @@ RELEASE 8.993
 
 - fixed bug in Geometry Editor that did not allow the copy of geometric elements
 - created a new class that holds all the Code Editor functionality and integrated as a Editor in FlatCAM, the location is in flatcamEditors folder
-- remade all the functions for view_source, scripts and view_code to use the new TextEditor class; now all the Code Editor tabs are being kept alive, before only one could be in an open state
+- remade all the functions for view_source, scripts and view_code to use the new AppTextEditor class; now all the Code Editor tabs are being kept alive, before only one could be in an open state
 - changed the name of the new object FlatCAMNotes to a more general one DocumentObject
 - changed the way a new ScriptObject object is made, the method that is processing the Tcl commands when the Run button is clicked is moved to the FlatCAMObj.ScriptObject() class
 - reused the Multiprocessing Pool declared in the App for the ToolRulesCheck() class
@@ -2486,7 +3030,7 @@ RELEASE 8.993
 
 4.06.2019
 
-- PEP8 updates in FlatCAMExcEditor.py
+- PEP8 updates in AppExcEditor.py
 - added the Excellon Editor parameters to the Edit -> Preferences -> Excellon GUI
 - fixed a small bug in Excellon Editor
 - PEP8 cleanup in FlatCAMGui
@@ -2558,7 +3102,7 @@ RELEASE 8.993
 - fixed the Circle Steps parameter for both Gerber and Geometry objects not being applied and instead the app internal defaults were used.
 - fixed the Tcl command Geocutout issue that gave an error when using the 4 or 8 value for gaps parameter
 - made wider the '#' column for Apertures Table for Gerber Object and for Gerber Editor; in this way numbers with 3 digits can be seen
-- PEP8 corrections in FlatCAMGrbEditor.py
+- PEP8 corrections in AppGerberEditor.py
 - added a selection limit parameter for Geometry Editor
 - added entries in Edit -> Preferences for the new parameter Selection limit for both the Gerber and Geometry Editors.
 - set the buttons in the lower part of the Preferences Window to have a preferred minimum width instead of fixed width
@@ -2835,7 +3379,7 @@ RELEASE 8.993
 - Gerber Editor: fixed error when adding an aperture with code value lower than the ones that already exists
 - when adding an aperture with code '0' (zero) it will automatically be set with size zero and type: 'REG' (from region); here we store all the regions from a Gerber file, the ones without a declared aperture
 - Gerber Editor: added support for Gerber polarity change commands (LPD, LPC)
-- moved the polarity change processing from FlatCAMGrbEditor() class to camlib.Gerber().parse_lines()
+- moved the polarity change processing from AppGerberEditor() class to camlib.Gerber().parse_lines()
 - made optional the saving of an edited object. Now the user can cancel the changes to the object.
 - replaced the standard buttons in the QMessageBox's used in the app with custom ones that can have text translated
 - updated the POT translation file and the MO/PO files for English and Romanian language

+ 131 - 5
Common.py → appCommon/Common.py

@@ -12,15 +12,19 @@
 # ##########################################################
 from PyQt5 import QtCore
 
-from shapely.geometry import Polygon, Point, LineString
+from shapely.geometry import Polygon, Point, LineString, MultiPoint
 from shapely.ops import unary_union
 
 from appGUI.VisPyVisuals import ShapeCollection
 from appTool import AppTool
 
 from copy import deepcopy
+import collections
+import traceback
 
 import numpy as np
+# from voronoi import Voronoi
+# from voronoi import Polygon as voronoi_polygon
 
 import gettext
 import appTranslation as fcTranslate
@@ -81,6 +85,84 @@ class LoudDict(dict):
         self.callback = callback
 
 
+class LoudUniqueList(list, collections.MutableSequence):
+    """
+    A List with a callback for item changes, callback which returns the index where the items are added/modified.
+    A List that will allow adding only items that are not in the list.
+    """
+
+    def __init__(self, arg=None):
+        super().__init__()
+        self.callback = lambda x: None
+
+        if not arg is None:
+            if isinstance(arg, list):
+                self.extend(arg)
+            else:
+                self.extend([arg])
+
+    def insert(self, i, v):
+        if v in self:
+            raise ValueError("One of the added items is already in the list.")
+        self.callback(i)
+        return super().insert(i, v)
+
+    def append(self, v):
+        if v in self:
+            raise ValueError("One of the added items is already in the list.")
+        l = len(self)
+        self.callback(l)
+        return super().append(v)
+
+    def extend(self, t):
+        for v in t:
+            if v in self:
+                raise ValueError("One of the added items is already in the list.")
+        l = len(self)
+        self.callback(l)
+        return super().extend(t)
+
+    def __add__(self, t):  # This is for something like `LoudUniqueList([1, 2, 3]) + list([4, 5, 6])`...
+        for v in t:
+            if v in self:
+                raise ValueError("One of the added items is already in the list.")
+        l = len(self)
+        self.callback(l)
+        return super().__add__(t)
+
+    def __iadd__(self, t):  # This is for something like `l = LoudUniqueList(); l += [1, 2, 3]`
+        for v in t:
+            if v in self:
+                raise ValueError("One of the added items is already in the list.")
+        l = len(self)
+        self.callback(l)
+        return super().__iadd__(t)
+
+    def __setitem__(self, i, v):
+        try:
+            for v1 in v:
+                if v1 in self:
+                    raise ValueError("One of the modified items is already in the list.")
+        except TypeError:
+            if v in self:
+                raise ValueError("One of the modified items is already in the list.")
+        if not v is None:
+            self.callback(i)
+        return super().__setitem__(i, v)
+
+    def set_callback(self, callback):
+        """
+        Assigns a function as callback on item change. The callback
+        will receive the index of the object that was changed.
+
+        :param callback: Function to call on item change.
+        :type callback: func
+        :return: None
+        """
+
+        self.callback = callback
+
+
 class FCSignal:
     """
     Taken from here: https://blog.abstractfactory.io/dynamic-signals-in-pyqt/
@@ -406,6 +488,7 @@ class ExclusionAreas(QtCore.QObject):
             # since the exclusion areas should apply to all objects in the app collection, this check is limited to
             # only the current object therefore it will not guarantee success
             self.app.inform.emit("%s" % _("Exclusion areas added. Checking overlap with the object geometry ..."))
+
             for el in self.exclusion_areas_storage:
                 if el["shape"].intersects(unary_union(self.solid_geometry)):
                     self.on_clear_area_click()
@@ -413,8 +496,7 @@ class ExclusionAreas(QtCore.QObject):
                         "[ERROR_NOTCL] %s" % _("Failed. Exclusion areas intersects the object geometry ..."))
                     return
 
-            self.app.inform.emit(
-                "[success] %s" % _("Exclusion areas added."))
+            self.app.inform.emit("[success] %s" % _("Exclusion areas added."))
             self.cnc_button.setStyleSheet("""
                                     QPushButton
                                     {
@@ -506,8 +588,8 @@ class ExclusionAreas(QtCore.QObject):
         self.app.dy = curr_pos[1] - float(self.cursor_pos[1])
         self.app.ui.position_label.setText("&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
                                            "<b>Y</b>: %.4f&nbsp;" % (curr_pos[0], curr_pos[1]))
-        # self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
-        #                                        "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
+        self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                               "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
 
         units = self.app.defaults["units"].lower()
         self.app.plotcanvas.text_hud.text = \
@@ -831,6 +913,50 @@ def farthest_point(origin, points_list):
     return fartherst_pt
 
 
+# def voronoi_diagram(geom, envelope, edges=False):
+#     """
+#
+#     :param geom:        a collection of Shapely Points from which to build the Voronoi diagram
+#     :type geom:          MultiPoint
+#     :param envelope:    a bounding box to constrain the diagram (Shapely Polygon)
+#     :type envelope:     Polygon
+#     :param edges:       If False, return regions as polygons. Else, return only
+#                         edges e.g. LineStrings.
+#     :type edges:        bool, False
+#     :return:
+#     :rtype:
+#     """
+#
+#     if not isinstance(geom, MultiPoint):
+#         return False
+#
+#     coords = list(envelope.exterior.coords)
+#     v_poly = voronoi_polygon(coords)
+#
+#     vp = Voronoi(v_poly)
+#
+#     points = []
+#     for pt in geom:
+#         points.append((pt.x, pt.y))
+#     vp.create_diagram(points=points, vis_steps=False, verbose=False, vis_result=False, vis_tree=False)
+#
+#     if edges is True:
+#         return vp.edges
+#     else:
+#         voronoi_polygons = []
+#         for pt in vp.points:
+#             try:
+#                 poly_coords = list(pt.get_coordinates())
+#                 new_poly_coords = []
+#                 for coord in poly_coords:
+#                     new_poly_coords.append((coord.x, coord.y))
+#
+#                 voronoi_polygons.append(Polygon(new_poly_coords))
+#             except Exception:
+#                 print(traceback.format_exc())
+#
+#         return voronoi_polygons
+
 def nearest_point(origin, points_list):
     """
     Calculate the nearest Point in a list from another Point

+ 119 - 0
appCommon/bilinear.py

@@ -0,0 +1,119 @@
+#############################################################################
+# Copyright (c) 2013 by Panagiotis Mavrogiorgos
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+#   this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+# * Neither the name(s) of the copyright holders nor the names of its
+#   contributors may be used to endorse or promote products derived from this
+#   software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AS IS AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+# EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#############################################################################
+#
+# @license: http://opensource.org/licenses/BSD-3-Clause
+
+from bisect import bisect_left
+import logging
+
+log = logging.getLogger('base')
+
+
+class BilinearInterpolation(object):
+    """
+    Bilinear interpolation with optional extrapolation.
+    Usage:
+    table = BilinearInterpolation(
+        x_index=(1, 2, 3),
+        y_index=(1, 2, 3),
+        values=((110, 120, 130),
+                (210, 220, 230),
+                (310, 320, 330)),
+        extrapolate=True)
+
+    assert table(1, 1) == 110
+    assert table(2.5, 2.5) == 275
+
+    """
+    def __init__(self, x_index, y_index, values):
+        # sanity check
+        x_length = len(x_index)
+        y_length = len(y_index)
+
+        if x_length < 2 or y_length < 2:
+            raise ValueError("Table must be at least 2x2.")
+        if y_length != len(values):
+            raise ValueError("Table must have equal number of rows to y_index.")
+        if any(x2 - x1 <= 0 for x1, x2 in zip(x_index, x_index[1:])):
+            raise ValueError("x_index must be in strictly ascending order!")
+        if any(y2 - y1 <= 0 for y1, y2 in zip(y_index, y_index[1:])):
+            raise ValueError("y_index must be in strictly ascending order!")
+
+        self.x_index = x_index
+        self.y_index = y_index
+        self.values = values
+        self.x_length = x_length
+        self.y_length = y_length
+        self.extrapolate = True
+
+        #slopes = self.slopes = []
+        #for j in range(y_length):
+            #intervals = zip(x_index, x_index[1:], values[j], values[j][1:])
+            #slopes.append([(y2 - y1) / (x2 - x1) for x1, x2, y1, y2 in intervals])
+
+    def __call__(self, x, y):
+        # local lookups
+        x_index, y_index, values = self.x_index, self.y_index, self.values
+
+        i = bisect_left(x_index, x) - 1
+        j = bisect_left(y_index, y) - 1
+
+        if self.extrapolate:
+            # fix x index
+            if i == -1:
+                x_slice = slice(None, 2)
+            elif i == self.x_length - 1:
+                x_slice = slice(-2, None)
+            else:
+                x_slice = slice(i, i + 2)
+
+            # fix y index
+            if j == -1:
+                j = 0
+                y_slice = slice(None, 2)
+            elif j == self.y_length - 1:
+                j = -2
+                y_slice = slice(-2, None)
+            else:
+                y_slice = slice(j, j + 2)
+        else:
+            if i == -1 or i == self.x_length - 1:
+                raise ValueError("Extrapolation not allowed!")
+            if j == -1 or j == self.y_length - 1:
+                raise ValueError("Extrapolation not allowed!")
+
+        # if the extrapolations is False this will fail
+        x1, x2 = x_index[x_slice]
+        y1, y2 = y_index[y_slice]
+        z11, z12 = values[j][x_slice]
+        z21, z22 = values[j + 1][x_slice]
+
+        return (z11 * (x2 - x) * (y2 - y) +
+                z21 * (x - x1) * (y2 - y) +
+                z12 * (x2 - x) * (y - y1) +
+                z22 * (x - x1) * (y - y1)) / ((x2 - x1) * (y2 - y1))

Разница между файлами не показана из-за своего большого размера
+ 740 - 274
appDatabase.py


+ 2304 - 2198
appEditors/FlatCAMExcEditor.py → appEditors/AppExcEditor.py

@@ -6,11 +6,11 @@
 # ##########################################################
 
 from PyQt5 import QtGui, QtCore, QtWidgets
-from PyQt5.QtCore import Qt, QSettings
+from PyQt5.QtCore import Qt
 
 from camlib import distance, arc, FlatCAMRTreeStorage
-from appGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, RadioSet, FCSpinner
-from appEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
+from appGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, RadioSet, FCSpinner, FCButton
+from appEditors.AppGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, AppGeoEditor
 from appParsers.ParseExcellon import Excellon
 
 from shapely.geometry import LineString, LinearRing, MultiLineString, Polygon, MultiPolygon, Point
@@ -44,6 +44,7 @@ class FCDrillAdd(FCShapeTool):
     def __init__(self, draw_app):
         DrawTool.__init__(self, draw_app)
         self.name = 'drill_add'
+        self.draw_app = draw_app
 
         self.selected_dia = None
         try:
@@ -52,8 +53,8 @@ class FCDrillAdd(FCShapeTool):
 
             # as a visual marker, select again in tooltable the actual tool that we are using
             # remember that it was deselected when clicking on canvas
-            item = self.draw_app.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
-            self.draw_app.tools_table_exc.setCurrentItem(item)
+            item = self.draw_app.e_ui.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
+            self.draw_app.e_ui.tools_table_exc.setCurrentItem(item)
         except KeyError:
             self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' % _("To add a drill first select a tool"))
             self.draw_app.select_tool("drill_select")
@@ -75,8 +76,8 @@ class FCDrillAdd(FCShapeTool):
 
         self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
 
-        # Switch notebook to Selected page
-        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
 
     def click(self, point):
         self.make()
@@ -124,7 +125,7 @@ class FCDrillAdd(FCShapeTool):
 
     def clean_up(self):
         self.draw_app.selected = []
-        self.draw_app.tools_table_exc.clearSelection()
+        self.draw_app.e_ui.tools_table_exc.clearSelection()
         self.draw_app.plot_all()
 
         try:
@@ -142,7 +143,7 @@ class FCDrillArray(FCShapeTool):
         DrawTool.__init__(self, draw_app)
         self.name = 'drill_array'
 
-        self.draw_app.array_frame.show()
+        self.draw_app.e_ui.array_frame.show()
 
         self.selected_dia = None
         self.drill_axis = 'X'
@@ -169,8 +170,8 @@ class FCDrillArray(FCShapeTool):
             self.selected_dia = self.draw_app.tool2tooldia[self.draw_app.last_tool_selected]
             # as a visual marker, select again in tooltable the actual tool that we are using
             # remember that it was deselected when clicking on canvas
-            item = self.draw_app.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
-            self.draw_app.tools_table_exc.setCurrentItem(item)
+            item = self.draw_app.e_ui.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
+            self.draw_app.e_ui.tools_table_exc.setCurrentItem(item)
         except KeyError:
             self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
                                           _("To add an Drill Array first select a tool in Tool Table"))
@@ -193,8 +194,8 @@ class FCDrillArray(FCShapeTool):
 
         self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
 
-        # Switch notebook to Selected page
-        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
 
     def click(self, point):
 
@@ -219,15 +220,15 @@ class FCDrillArray(FCShapeTool):
         self.origin = origin
 
     def utility_geometry(self, data=None, static=None):
-        self.drill_axis = self.draw_app.drill_axis_radio.get_value()
-        self.drill_direction = self.draw_app.drill_direction_radio.get_value()
-        self.drill_array = self.draw_app.array_type_combo.get_value()
+        self.drill_axis = self.draw_app.e_ui.drill_axis_radio.get_value()
+        self.drill_direction = self.draw_app.e_ui.drill_direction_radio.get_value()
+        self.drill_array = self.draw_app.e_ui.array_type_combo.get_value()
         try:
-            self.drill_array_size = int(self.draw_app.drill_array_size_entry.get_value())
+            self.drill_array_size = int(self.draw_app.e_ui.drill_array_size_entry.get_value())
             try:
-                self.drill_pitch = float(self.draw_app.drill_pitch_entry.get_value())
-                self.drill_linear_angle = float(self.draw_app.linear_angle_spinner.get_value())
-                self.drill_angle = float(self.draw_app.drill_angle_entry.get_value())
+                self.drill_pitch = float(self.draw_app.e_ui.drill_pitch_entry.get_value())
+                self.drill_linear_angle = float(self.draw_app.e_ui.linear_angle_spinner.get_value())
+                self.drill_angle = float(self.draw_app.e_ui.drill_angle_entry.get_value())
             except TypeError:
                 self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
                                               _("The value is not Float. Check for comma instead of dot separator."))
@@ -353,13 +354,13 @@ class FCDrillArray(FCShapeTool):
         self.complete = True
         self.draw_app.app.inform.emit('[success] %s' % _("Done. Drill Array added."))
         self.draw_app.in_action = False
-        self.draw_app.array_frame.hide()
+        self.draw_app.e_ui.array_frame.hide()
 
         self.draw_app.app.jump_signal.disconnect()
 
     def clean_up(self):
         self.draw_app.selected = []
-        self.draw_app.tools_table_exc.clearSelection()
+        self.draw_app.e_ui.tools_table_exc.clearSelection()
         self.draw_app.plot_all()
 
         try:
@@ -378,7 +379,7 @@ class FCSlot(FCShapeTool):
         self.name = 'slot_add'
         self.draw_app = draw_app
 
-        self.draw_app.slot_frame.show()
+        self.draw_app.e_ui.slot_frame.show()
 
         self.selected_dia = None
         try:
@@ -387,11 +388,10 @@ class FCSlot(FCShapeTool):
 
             # as a visual marker, select again in tooltable the actual tool that we are using
             # remember that it was deselected when clicking on canvas
-            item = self.draw_app.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
-            self.draw_app.tools_table_exc.setCurrentItem(item)
+            item = self.draw_app.e_ui.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
+            self.draw_app.e_ui.tools_table_exc.setCurrentItem(item)
         except KeyError:
-            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
-                                          _("To add a slot first select a tool"))
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' % _("To add a slot first select a tool"))
             self.draw_app.select_tool("drill_select")
             return
 
@@ -416,8 +416,8 @@ class FCSlot(FCShapeTool):
 
         self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
 
-        # Switch notebook to Selected page
-        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
 
     def click(self, point):
         self.make()
@@ -443,25 +443,25 @@ class FCSlot(FCShapeTool):
         self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
 
         try:
-            slot_length = float(self.draw_app.slot_length_entry.get_value())
+            slot_length = float(self.draw_app.e_ui.slot_length_entry.get_value())
         except ValueError:
             # try to convert comma to decimal point. if it's still not working error message and return
             try:
-                slot_length = float(self.draw_app.slot_length_entry.get_value().replace(',', '.'))
-                self.draw_app.slot_length_entry.set_value(slot_length)
+                slot_length = float(self.draw_app.e_ui.slot_length_entry.get_value().replace(',', '.'))
+                self.draw_app.e_ui.slot_length_entry.set_value(slot_length)
             except ValueError:
                 self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
                                               _("Value is missing or wrong format. Add it and retry."))
                 return
 
         try:
-            slot_angle = float(self.draw_app.slot_angle_spinner.get_value())
+            slot_angle = float(self.draw_app.e_ui.slot_angle_spinner.get_value())
         except ValueError:
             self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
                                           _("Value is missing or wrong format. Add it and retry."))
             return
 
-        if self.draw_app.slot_axis_radio.get_value() == 'X':
+        if self.draw_app.e_ui.slot_axis_radio.get_value() == 'X':
             self.half_width = slot_length / 2.0
             self.half_height = self.radius
         else:
@@ -502,7 +502,7 @@ class FCSlot(FCShapeTool):
                 geo.append(pt)
             geo.append(p4)
 
-            if self.draw_app.slot_axis_radio.get_value() == 'A':
+            if self.draw_app.e_ui.slot_axis_radio.get_value() == 'A':
                 return affinity.rotate(geom=Polygon(geo), angle=-slot_angle)
             else:
                 return Polygon(geo)
@@ -557,12 +557,12 @@ class FCSlot(FCShapeTool):
         self.draw_app.in_action = False
         self.complete = True
         self.draw_app.app.inform.emit('[success] %s' % _("Done. Adding Slot completed."))
-        self.draw_app.slot_frame.hide()
+        self.draw_app.e_ui.slot_frame.hide()
         self.draw_app.app.jump_signal.disconnect()
 
     def clean_up(self):
         self.draw_app.selected = []
-        self.draw_app.tools_table_exc.clearSelection()
+        self.draw_app.e_ui.tools_table_exc.clearSelection()
         self.draw_app.plot_all()
 
         try:
@@ -581,8 +581,8 @@ class FCSlotArray(FCShapeTool):
         self.name = 'slot_array'
         self.draw_app = draw_app
 
-        self.draw_app.slot_frame.show()
-        self.draw_app.slot_array_frame.show()
+        self.draw_app.e_ui.slot_frame.show()
+        self.draw_app.e_ui.slot_array_frame.show()
 
         self.selected_dia = None
         try:
@@ -590,8 +590,8 @@ class FCSlotArray(FCShapeTool):
             self.selected_dia = self.draw_app.tool2tooldia[self.draw_app.last_tool_selected]
             # as a visual marker, select again in tooltable the actual tool that we are using
             # remember that it was deselected when clicking on canvas
-            item = self.draw_app.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
-            self.draw_app.tools_table_exc.setCurrentItem(item)
+            item = self.draw_app.e_ui.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
+            self.draw_app.e_ui.tools_table_exc.setCurrentItem(item)
         except KeyError:
             self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
                                           _("To add an Slot Array first select a tool in Tool Table"))
@@ -637,8 +637,8 @@ class FCSlotArray(FCShapeTool):
 
         self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
 
-        # Switch notebook to Selected page
-        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
 
     def click(self, point):
 
@@ -663,15 +663,15 @@ class FCSlotArray(FCShapeTool):
         self.origin = origin
 
     def utility_geometry(self, data=None, static=None):
-        self.slot_axis = self.draw_app.slot_array_axis_radio.get_value()
-        self.slot_direction = self.draw_app.slot_array_direction_radio.get_value()
-        self.slot_array = self.draw_app.slot_array_type_combo.get_value()
+        self.slot_axis = self.draw_app.e_ui.slot_array_axis_radio.get_value()
+        self.slot_direction = self.draw_app.e_ui.slot_array_direction_radio.get_value()
+        self.slot_array = self.draw_app.e_ui.slot_array_type_combo.get_value()
         try:
-            self.slot_array_size = int(self.draw_app.slot_array_size_entry.get_value())
+            self.slot_array_size = int(self.draw_app.e_ui.slot_array_size_entry.get_value())
             try:
-                self.slot_pitch = float(self.draw_app.slot_array_pitch_entry.get_value())
-                self.slot_linear_angle = float(self.draw_app.slot_array_linear_angle_spinner.get_value())
-                self.slot_angle = float(self.draw_app.slot_array_angle_entry.get_value())
+                self.slot_pitch = float(self.draw_app.e_ui.slot_array_pitch_entry.get_value())
+                self.slot_linear_angle = float(self.draw_app.e_ui.slot_array_linear_angle_spinner.get_value())
+                self.slot_angle = float(self.draw_app.e_ui.slot_array_angle_entry.get_value())
             except TypeError:
                 self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
                                               _("The value is not Float. Check for comma instead of dot separator."))
@@ -731,25 +731,25 @@ class FCSlotArray(FCShapeTool):
         self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
 
         try:
-            slot_length = float(self.draw_app.slot_length_entry.get_value())
+            slot_length = float(self.draw_app.e_ui.slot_length_entry.get_value())
         except ValueError:
             # try to convert comma to decimal point. if it's still not working error message and return
             try:
-                slot_length = float(self.draw_app.slot_length_entry.get_value().replace(',', '.'))
-                self.draw_app.slot_length_entry.set_value(slot_length)
+                slot_length = float(self.draw_app.e_ui.slot_length_entry.get_value().replace(',', '.'))
+                self.draw_app.e_ui.slot_length_entry.set_value(slot_length)
             except ValueError:
                 self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
                                               _("Value is missing or wrong format. Add it and retry."))
                 return
 
         try:
-            slot_angle = float(self.draw_app.slot_angle_spinner.get_value())
+            slot_angle = float(self.draw_app.e_ui.slot_angle_spinner.get_value())
         except ValueError:
             self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
                                           _("Value is missing or wrong format. Add it and retry."))
             return
 
-        if self.draw_app.slot_axis_radio.get_value() == 'X':
+        if self.draw_app.e_ui.slot_axis_radio.get_value() == 'X':
             self.half_width = slot_length / 2.0
             self.half_height = self.radius
         else:
@@ -816,7 +816,7 @@ class FCSlotArray(FCShapeTool):
 
         # this function return one slot in the slot array and the following will rotate that one slot around it's
         # center if the radio value is "A".
-        if self.draw_app.slot_axis_radio.get_value() == 'A':
+        if self.draw_app.e_ui.slot_axis_radio.get_value() == 'A':
             return affinity.rotate(Polygon(geo), -slot_angle)
         else:
             return Polygon(geo)
@@ -881,13 +881,13 @@ class FCSlotArray(FCShapeTool):
         self.complete = True
         self.draw_app.app.inform.emit('[success] %s' % _("Done. Slot Array added."))
         self.draw_app.in_action = False
-        self.draw_app.slot_frame.hide()
-        self.draw_app.slot_array_frame.hide()
+        self.draw_app.e_ui.slot_frame.hide()
+        self.draw_app.e_ui.slot_array_frame.hide()
         self.draw_app.app.jump_signal.disconnect()
 
     def clean_up(self):
         self.draw_app.selected = []
-        self.draw_app.tools_table_exc.clearSelection()
+        self.draw_app.e_ui.tools_table_exc.clearSelection()
         self.draw_app.plot_all()
 
         try:
@@ -903,7 +903,7 @@ class FCDrillResize(FCShapeTool):
 
         self.draw_app.app.inform.emit(_("Click on the Drill(s) to resize ..."))
         self.resize_dia = None
-        self.draw_app.resize_frame.show()
+        self.draw_app.e_ui.resize_frame.show()
         self.points = None
 
         # made this a set so there are no duplicates
@@ -916,26 +916,26 @@ class FCDrillResize(FCShapeTool):
         self.draw_app.resize_btn.clicked.connect(self.make)
         self.draw_app.resdrill_entry.editingFinished.connect(self.make)
 
-        # Switch notebook to Selected page
-        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
 
     def make(self):
         self.draw_app.is_modified = True
 
         try:
-            self.draw_app.tools_table_exc.itemChanged.disconnect()
+            self.draw_app.e_ui.tools_table_exc.itemChanged.disconnect()
         except TypeError:
             pass
 
         try:
-            new_dia = self.draw_app.resdrill_entry.get_value()
+            new_dia = self.draw_app.e_ui.resdrill_entry.get_value()
         except Exception:
             self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
                                           _("Resize drill(s) failed. Please enter a diameter for resize."))
             return
 
         if new_dia not in self.draw_app.olddia_newdia:
-            self.destination_storage = FlatCAMGeoEditor.make_storage()
+            self.destination_storage = AppGeoEditor.make_storage()
             self.draw_app.storage_dict[new_dia] = self.destination_storage
 
             # self.olddia_newdia dict keeps the evidence on current tools diameters as keys and gets updated on values
@@ -944,11 +944,11 @@ class FCDrillResize(FCShapeTool):
         else:
             self.destination_storage = self.draw_app.storage_dict[new_dia]
 
-        for index in self.draw_app.tools_table_exc.selectedIndexes():
+        for index in self.draw_app.e_ui.tools_table_exc.selectedIndexes():
             row = index.row()
             # on column 1 in tool tables we hold the diameters, and we retrieve them as strings
             # therefore below we convert to float
-            dia_on_row = self.draw_app.tools_table_exc.item(row, 1).text()
+            dia_on_row = self.draw_app.e_ui.tools_table_exc.item(row, 1).text()
             self.selected_dia_set.add(float(dia_on_row))
 
         # since we add a new tool, we update also the intial state of the tool_table through it's dictionary
@@ -1106,7 +1106,7 @@ class FCDrillResize(FCShapeTool):
             self.geometry = []
 
             # we reactivate the signals after the after the tool editing
-            self.draw_app.tools_table_exc.itemChanged.connect(self.draw_app.on_tool_edit)
+            self.draw_app.e_ui.tools_table_exc.itemChanged.connect(self.draw_app.on_tool_edit)
 
             self.draw_app.app.inform.emit('[success] %s' %
                                           _("Done. Drill/Slot Resize completed."))
@@ -1117,7 +1117,7 @@ class FCDrillResize(FCShapeTool):
         # init this set() for another use perhaps
         self.selected_dia_set = set()
 
-        self.draw_app.resize_frame.hide()
+        self.draw_app.e_ui.resize_frame.hide()
         self.complete = True
 
         # MS: always return to the Select Tool
@@ -1125,7 +1125,7 @@ class FCDrillResize(FCShapeTool):
 
     def clean_up(self):
         self.draw_app.selected = []
-        self.draw_app.tools_table_exc.clearSelection()
+        self.draw_app.e_ui.tools_table_exc.clearSelection()
         self.draw_app.plot_all()
 
         try:
@@ -1154,17 +1154,17 @@ class FCDrillMove(FCShapeTool):
         self.current_storage = None
         self.geometry = []
 
-        for index in self.draw_app.tools_table_exc.selectedIndexes():
+        for index in self.draw_app.e_ui.tools_table_exc.selectedIndexes():
             row = index.row()
             # on column 1 in tool tables we hold the diameters, and we retrieve them as strings
             # therefore below we convert to float
-            dia_on_row = self.draw_app.tools_table_exc.item(row, 1).text()
+            dia_on_row = self.draw_app.e_ui.tools_table_exc.item(row, 1).text()
             self.selected_dia_list.append(float(dia_on_row))
 
         self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
 
-        # Switch notebook to Selected page
-        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
 
     def set_origin(self, origin):
         self.origin = origin
@@ -1265,7 +1265,7 @@ class FCDrillMove(FCShapeTool):
 
     def clean_up(self):
         self.draw_app.selected = []
-        self.draw_app.tools_table_exc.clearSelection()
+        self.draw_app.e_ui.tools_table_exc.clearSelection()
         self.draw_app.plot_all()
 
         try:
@@ -1320,7 +1320,7 @@ class FCDrillCopy(FCDrillMove):
 
     def clean_up(self):
         self.draw_app.selected = []
-        self.draw_app.tools_table_exc.clearSelection()
+        self.draw_app.e_ui.tools_table_exc.clearSelection()
         self.draw_app.plot_all()
 
         try:
@@ -1347,12 +1347,12 @@ class FCDrillSelect(DrawTool):
         self.sel_tools = set()
 
         # here we store all shapes that were selected so we can search for the nearest to our click location
-        self.sel_storage = FlatCAMExcEditor.make_storage()
+        self.sel_storage = AppExcEditor.make_storage()
 
-        self.exc_editor_app.resize_frame.hide()
-        self.exc_editor_app.array_frame.hide()
-        self.exc_editor_app.slot_frame.hide()
-        self.exc_editor_app.slot_array_frame.hide()
+        self.exc_editor_app.e_ui.resize_frame.hide()
+        self.exc_editor_app.e_ui.array_frame.hide()
+        self.exc_editor_app.e_ui.slot_frame.hide()
+        self.exc_editor_app.e_ui.slot_array_frame.hide()
 
     def click(self, point):
         key_modifier = QtWidgets.QApplication.keyboardModifiers()
@@ -1370,7 +1370,7 @@ class FCDrillSelect(DrawTool):
             self.exc_editor_app.selected = []
 
     def click_release(self, pos):
-        self.exc_editor_app.tools_table_exc.clearSelection()
+        self.exc_editor_app.e_ui.tools_table_exc.clearSelection()
         xmin, ymin, xmax, ymax = 0, 0, 0, 0
 
         try:
@@ -1426,12 +1426,12 @@ class FCDrillSelect(DrawTool):
 
             # select the diameter of the selected shape in the tool table
             try:
-                self.exc_editor_app.tools_table_exc.cellPressed.disconnect()
+                self.exc_editor_app.e_ui.tools_table_exc.cellPressed.disconnect()
             except (TypeError, AttributeError):
                 pass
 
             # if mod_key == self.exc_editor_app.app.defaults["global_mselect_key"]:
-            #     self.exc_editor_app.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
+            #     self.exc_editor_app.e_ui.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
             self.sel_tools.clear()
 
             for shape_s in self.exc_editor_app.selected:
@@ -1439,20 +1439,20 @@ class FCDrillSelect(DrawTool):
                     if shape_s in self.exc_editor_app.storage_dict[storage].get_objects():
                         self.sel_tools.add(storage)
 
-            self.exc_editor_app.tools_table_exc.clearSelection()
+            self.exc_editor_app.e_ui.tools_table_exc.clearSelection()
             for storage in self.sel_tools:
                 for k, v in self.exc_editor_app.tool2tooldia.items():
                     if v == storage:
-                        self.exc_editor_app.tools_table_exc.selectRow(int(k) - 1)
+                        self.exc_editor_app.e_ui.tools_table_exc.selectRow(int(k) - 1)
                         self.exc_editor_app.last_tool_selected = int(k)
                         break
 
-            # self.exc_editor_app.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+            # self.exc_editor_app.e_ui.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
 
-            self.exc_editor_app.tools_table_exc.cellPressed.connect(self.exc_editor_app.on_row_selected)
+            self.exc_editor_app.e_ui.tools_table_exc.cellPressed.connect(self.exc_editor_app.on_row_selected)
 
         # delete whatever is in selection storage, there is no longer need for those shapes
-        self.sel_storage = FlatCAMExcEditor.make_storage()
+        self.sel_storage = AppExcEditor.make_storage()
 
         return ""
 
@@ -1467,13 +1467,13 @@ class FCDrillSelect(DrawTool):
         #     # if there is no shape under our click then deselect all shapes
         #     if not over_shape_list:
         #         self.exc_editor_app.selected = []
-        #         FlatCAMExcEditor.draw_shape_idx = -1
-        #         self.exc_editor_app.tools_table_exc.clearSelection()
+        #         AppExcEditor.draw_shape_idx = -1
+        #         self.exc_editor_app.e_ui.tools_table_exc.clearSelection()
         #     else:
         #         # if there are shapes under our click then advance through the list of them, one at the time in a
         #         # circular way
-        #         FlatCAMExcEditor.draw_shape_idx = (FlatCAMExcEditor.draw_shape_idx + 1) % len(over_shape_list)
-        #         obj_to_add = over_shape_list[int(FlatCAMExcEditor.draw_shape_idx)]
+        #         AppExcEditor.draw_shape_idx = (AppExcEditor.draw_shape_idx + 1) % len(over_shape_list)
+        #         obj_to_add = over_shape_list[int(AppExcEditor.draw_shape_idx)]
         #
         #         if self.exc_editor_app.app.defaults["global_mselect_key"] == 'Shift':
         #             if self.exc_editor_app.modifiers == Qt.ShiftModifier:
@@ -1501,2625 +1501,2731 @@ class FCDrillSelect(DrawTool):
         #             if shape in self.exc_editor_app.storage_dict[storage].get_objects():
         #                 for key in self.exc_editor_app.tool2tooldia:
         #                     if self.exc_editor_app.tool2tooldia[key] == storage:
-        #                         item = self.exc_editor_app.tools_table_exc.item((key - 1), 1)
+        #                         item = self.exc_editor_app.e_ui.tools_table_exc.item((key - 1), 1)
         #                         item.setSelected(True)
-        #                         # self.exc_editor_app.tools_table_exc.selectItem(key - 1)
+        #                         # self.exc_editor_app.e_ui.tools_table_exc.selectItem(key - 1)
         #
         # except Exception as e:
         #     log.error("[ERROR] Something went bad. %s" % str(e))
         #     raise
 
 
-class FlatCAMExcEditor(QtCore.QObject):
+class AppExcEditor(QtCore.QObject):
 
     draw_shape_idx = -1
 
     def __init__(self, app):
         # assert isinstance(app, FlatCAMApp.App), "Expected the app to be a FlatCAMApp.App, got %s" % type(app)
 
-        super(FlatCAMExcEditor, self).__init__()
+        super(AppExcEditor, self).__init__()
 
         self.app = app
         self.canvas = self.app.plotcanvas
+        self.units = self.app.defaults['units'].upper()
+
+        self.dec_format = self.app.dec_format
 
         # Number of decimals used by tools in this class
         self.decimals = self.app.decimals
 
-        # ## Current application units in Upper Case
-        self.units = self.app.defaults['units'].upper()
-
-        self.exc_edit_widget = QtWidgets.QWidget()
-        # ## Box for custom widgets
-        # This gets populated in offspring implementations.
-        layout = QtWidgets.QVBoxLayout()
-        self.exc_edit_widget.setLayout(layout)
+        self.e_ui = AppExcEditorUI(app=self.app)
+        
+        # SIGNALS
+        self.e_ui.convert_slots_btn.clicked.connect(self.on_slots_conversion)
+        self.app.ui.delete_drill_btn.triggered.connect(self.on_delete_btn)
+        self.e_ui.name_entry.returnPressed.connect(self.on_name_activate)
+        self.e_ui.addtool_btn.clicked.connect(self.on_tool_add)
+        self.e_ui.addtool_entry.editingFinished.connect(self.on_tool_add)
+        self.e_ui.deltool_btn.clicked.connect(self.on_tool_delete)
+        # self.e_ui.tools_table_exc.selectionModel().currentChanged.connect(self.on_row_selected)
+        self.e_ui.tools_table_exc.cellPressed.connect(self.on_row_selected)
 
-        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets
-        # this way I can hide/show the frame
-        self.drills_frame = QtWidgets.QFrame()
-        self.drills_frame.setContentsMargins(0, 0, 0, 0)
-        layout.addWidget(self.drills_frame)
-        self.tools_box = QtWidgets.QVBoxLayout()
-        self.tools_box.setContentsMargins(0, 0, 0, 0)
-        self.drills_frame.setLayout(self.tools_box)
+        self.e_ui.array_type_combo.currentIndexChanged.connect(self.on_array_type_combo)
+        self.e_ui.slot_array_type_combo.currentIndexChanged.connect(self.on_slot_array_type_combo)
 
-        # ## Page Title box (spacing between children)
-        self.title_box = QtWidgets.QHBoxLayout()
-        self.tools_box.addLayout(self.title_box)
+        self.e_ui.drill_axis_radio.activated_custom.connect(self.on_linear_angle_radio)
+        self.e_ui.slot_axis_radio.activated_custom.connect(self.on_slot_angle_radio)
 
-        # ## Page Title icon
-        pixmap = QtGui.QPixmap(self.app.resource_location + '/flatcam_icon32.png')
-        self.icon = QtWidgets.QLabel()
-        self.icon.setPixmap(pixmap)
-        self.title_box.addWidget(self.icon, stretch=0)
+        self.e_ui.slot_array_axis_radio.activated_custom.connect(self.on_slot_array_linear_angle_radio)
 
-        # ## Title label
-        self.title_label = QtWidgets.QLabel("<font size=5><b>%s</b></font>" % _('Excellon Editor'))
-        self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
-        self.title_box.addWidget(self.title_label, stretch=1)
+        self.app.ui.exc_add_array_drill_menuitem.triggered.connect(self.exc_add_drill_array)
+        self.app.ui.exc_add_drill_menuitem.triggered.connect(self.exc_add_drill)
 
-        # ## Object name
-        self.name_box = QtWidgets.QHBoxLayout()
-        self.tools_box.addLayout(self.name_box)
-        name_label = QtWidgets.QLabel(_("Name:"))
-        self.name_box.addWidget(name_label)
-        self.name_entry = FCEntry()
-        self.name_box.addWidget(self.name_entry)
+        self.app.ui.exc_add_array_slot_menuitem.triggered.connect(self.exc_add_slot_array)
+        self.app.ui.exc_add_slot_menuitem.triggered.connect(self.exc_add_slot)
 
-        # ### Tools Drills ## ##
-        self.tools_table_label = QtWidgets.QLabel("<b>%s</b>" % _('Tools Table'))
-        self.tools_table_label.setToolTip(
-           _("Tools in this Excellon object\n"
-             "when are used for drilling.")
-        )
-        self.tools_box.addWidget(self.tools_table_label)
+        self.app.ui.exc_resize_drill_menuitem.triggered.connect(self.exc_resize_drills)
+        self.app.ui.exc_copy_drill_menuitem.triggered.connect(self.exc_copy_drills)
+        self.app.ui.exc_delete_drill_menuitem.triggered.connect(self.on_delete_btn)
 
-        self.tools_table_exc = FCTable()
-        # delegate = SpinBoxDelegate(units=self.units)
-        # self.tools_table_exc.setItemDelegateForColumn(1, delegate)
+        self.app.ui.exc_move_drill_menuitem.triggered.connect(self.exc_move_drills)
+        self.e_ui.exit_editor_button.clicked.connect(lambda: self.app.editor2object())
 
-        self.tools_box.addWidget(self.tools_table_exc)
+        self.exc_obj = None
 
-        self.tools_table_exc.setColumnCount(4)
-        self.tools_table_exc.setHorizontalHeaderLabels(['#', _('Diameter'), 'D', 'S'])
-        self.tools_table_exc.setSortingEnabled(False)
-        self.tools_table_exc.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        # ## Toolbar events and properties
+        self.tools_exc = {
+            "drill_select":     {"button": self.app.ui.select_drill_btn, "constructor": FCDrillSelect},
+            "drill_add":        {"button": self.app.ui.add_drill_btn, "constructor": FCDrillAdd},
+            "drill_array":      {"button": self.app.ui.add_drill_array_btn, "constructor": FCDrillArray},
+            "slot_add":         {"button": self.app.ui.add_slot_btn, "constructor": FCSlot},
+            "slot_array":       {"button": self.app.ui.add_slot_array_btn, "constructor": FCSlotArray},
+            "drill_resize":     {"button": self.app.ui.resize_drill_btn, "constructor": FCDrillResize},
+            "drill_copy":       {"button": self.app.ui.copy_drill_btn, "constructor": FCDrillCopy},
+            "drill_move":       {"button": self.app.ui.move_drill_btn, "constructor": FCDrillMove},
+        }
 
-        self.empty_label = QtWidgets.QLabel('')
-        self.tools_box.addWidget(self.empty_label)
+        # ## Data
+        self.active_tool = None
+        self.in_action = False
 
-        # ### Add a new Tool ## ##
-        self.addtool_label = QtWidgets.QLabel('<b>%s</b>' % _('Add/Delete Tool'))
-        self.addtool_label.setToolTip(
-            _("Add/Delete a tool to the tool list\n"
-              "for this Excellon object.")
-        )
-        self.tools_box.addWidget(self.addtool_label)
+        self.storage_dict = {}
+        self.current_storage = []
 
-        grid1 = QtWidgets.QGridLayout()
-        self.tools_box.addLayout(grid1)
-        grid1.setColumnStretch(0, 0)
-        grid1.setColumnStretch(1, 1)
+        # build the data from the Excellon point into a dictionary
+        #  {tool_dia: [geometry_in_points]}
+        self.points_edit = {}
+        self.slot_points_edit = {}
 
-        addtool_entry_lbl = QtWidgets.QLabel('%s:' % _('Tool Dia'))
-        addtool_entry_lbl.setToolTip(
-            _("Diameter for the new tool")
-        )
+        self.sorted_diameters = []
 
-        hlay = QtWidgets.QHBoxLayout()
-        self.addtool_entry = FCDoubleSpinner()
-        self.addtool_entry.set_precision(self.decimals)
-        self.addtool_entry.set_range(0.0000, 9999.9999)
+        self.new_drills = []
+        self.new_tools = {}
+        self.new_slots = []
 
-        hlay.addWidget(self.addtool_entry)
+        # dictionary to store the tool_row and diameters in Tool_table
+        # it will be updated everytime self.build_ui() is called
+        self.olddia_newdia = {}
 
-        self.addtool_btn = QtWidgets.QPushButton(_('Add Tool'))
-        self.addtool_btn.setToolTip(
-           _("Add a new tool to the tool list\n"
-             "with the diameter specified above.")
-        )
-        self.addtool_btn.setFixedWidth(80)
-        hlay.addWidget(self.addtool_btn)
+        self.tool2tooldia = {}
 
-        grid1.addWidget(addtool_entry_lbl, 0, 0)
-        grid1.addLayout(hlay, 0, 1)
+        # this will store the value for the last selected tool, for use after clicking on canvas when the selection
+        # is cleared but as a side effect also the selected tool is cleared
+        self.last_tool_selected = None
+        self.utility = []
 
-        grid2 = QtWidgets.QGridLayout()
-        self.tools_box.addLayout(grid2)
+        # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False)
+        self.launched_from_shortcuts = False
 
-        self.deltool_btn = QtWidgets.QPushButton(_('Delete Tool'))
-        self.deltool_btn.setToolTip(
-           _("Delete a tool in the tool list\n"
-             "by selecting a row in the tool table.")
-        )
-        grid2.addWidget(self.deltool_btn, 0, 1)
+        # this var will store the state of the toolbar before starting the editor
+        self.toolbar_old_state = False
 
-        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets
-        # this way I can hide/show the frame
-        self.resize_frame = QtWidgets.QFrame()
-        self.resize_frame.setContentsMargins(0, 0, 0, 0)
-        self.tools_box.addWidget(self.resize_frame)
-        self.resize_box = QtWidgets.QVBoxLayout()
-        self.resize_box.setContentsMargins(0, 0, 0, 0)
-        self.resize_frame.setLayout(self.resize_box)
+        if self.units == 'MM':
+            self.tolerance = float(self.app.defaults["global_tolerance"])
+        else:
+            self.tolerance = float(self.app.defaults["global_tolerance"]) / 20
 
-        # ### Resize a  drill ## ##
-        self.emptyresize_label = QtWidgets.QLabel('')
-        self.resize_box.addWidget(self.emptyresize_label)
+        # VisPy Visuals
+        if self.app.is_legacy is False:
+            self.shapes = self.canvas.new_shape_collection(layers=1)
+            if self.canvas.big_cursor is True:
+                self.tool_shape = self.canvas.new_shape_collection(layers=1, line_width=2)
+            else:
+                self.tool_shape = self.canvas.new_shape_collection(layers=1)
+        else:
+            from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
+            self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='shapes_exc_editor')
+            self.tool_shape = ShapeCollectionLegacy(obj=self, app=self.app, name='tool_shapes_exc_editor')
 
-        self.drillresize_label = QtWidgets.QLabel('<b>%s</b>' % _("Resize Drill(s)"))
-        self.drillresize_label.setToolTip(
-            _("Resize a drill or a selection of drills.")
-        )
-        self.resize_box.addWidget(self.drillresize_label)
+        self.app.pool_recreated.connect(self.pool_recreated)
 
-        grid3 = QtWidgets.QGridLayout()
-        self.resize_box.addLayout(grid3)
+        # Remove from scene
+        self.shapes.enabled = False
+        self.tool_shape.enabled = False
 
-        res_entry_lbl = QtWidgets.QLabel('%s:' % _('Resize Dia'))
-        res_entry_lbl.setToolTip(
-           _("Diameter to resize to.")
-        )
-        grid3.addWidget(res_entry_lbl, 0, 0)
+        # ## List of selected shapes.
+        self.selected = []
 
-        hlay2 = QtWidgets.QHBoxLayout()
-        self.resdrill_entry = FCDoubleSpinner()
-        self.resdrill_entry.set_precision(self.decimals)
-        self.resdrill_entry.set_range(0.0000, 9999.9999)
+        self.move_timer = QtCore.QTimer()
+        self.move_timer.setSingleShot(True)
 
-        hlay2.addWidget(self.resdrill_entry)
+        self.key = None  # Currently pressed key
+        self.modifiers = None
+        self.x = None  # Current mouse cursor pos
+        self.y = None
+        # Current snapped mouse pos
+        self.snap_x = None
+        self.snap_y = None
+        self.pos = None
 
-        self.resize_btn = QtWidgets.QPushButton(_('Resize'))
-        self.resize_btn.setToolTip(
-            _("Resize drill(s)")
-        )
-        self.resize_btn.setFixedWidth(80)
-        hlay2.addWidget(self.resize_btn)
-        grid3.addLayout(hlay2, 0, 1)
+        self.complete = False
 
-        self.resize_frame.hide()
+        def make_callback(thetool):
+            def f():
+                self.on_tool_select(thetool)
+            return f
 
-        # ####################################
-        # ### Add DRILL Array ################
-        # ####################################
+        for tool in self.tools_exc:
+            self.tools_exc[tool]["button"].triggered.connect(make_callback(tool))  # Events
+            self.tools_exc[tool]["button"].setCheckable(True)  # Checkable
 
-        # add a frame and inside add a vertical box layout. Inside this vbox layout I add
-        # all the add drill array  widgets
-        # this way I can hide/show the frame
-        self.array_frame = QtWidgets.QFrame()
-        self.array_frame.setContentsMargins(0, 0, 0, 0)
-        self.tools_box.addWidget(self.array_frame)
-        self.array_box = QtWidgets.QVBoxLayout()
-        self.array_box.setContentsMargins(0, 0, 0, 0)
-        self.array_frame.setLayout(self.array_box)
+        self.options = {
+            "global_gridx":     0.1,
+            "global_gridy":     0.1,
+            "snap_max":         0.05,
+            "grid_snap":        True,
+            "corner_snap":      False,
+            "grid_gap_link":    True
+        }
+        self.options.update(self.app.options)
 
-        self.emptyarray_label = QtWidgets.QLabel('')
-        self.array_box.addWidget(self.emptyarray_label)
+        for option in self.options:
+            if option in self.app.options:
+                self.options[option] = self.app.options[option]
 
-        self.drill_array_label = QtWidgets.QLabel('<b>%s</b>' % _("Add Drill Array"))
-        self.drill_array_label.setToolTip(
-            _("Add an array of drills (linear or circular array)")
-        )
-        self.array_box.addWidget(self.drill_array_label)
+        self.data_defaults = {}
 
-        self.array_type_combo = FCComboBox()
-        self.array_type_combo.setToolTip(
-           _("Select the type of drills array to create.\n"
-             "It can be Linear X(Y) or Circular")
-        )
-        self.array_type_combo.addItem(_("Linear"))
-        self.array_type_combo.addItem(_("Circular"))
+        self.rtree_exc_index = rtindex.Index()
+        # flag to show if the object was modified
+        self.is_modified = False
 
-        self.array_box.addWidget(self.array_type_combo)
+        self.edited_obj_name = ""
 
-        self.array_form = QtWidgets.QFormLayout()
-        self.array_box.addLayout(self.array_form)
+        # variable to store the total amount of drills per job
+        self.tot_drill_cnt = 0
+        self.tool_row = 0
 
-        # Set the number of drill holes in the drill array
-        self.drill_array_size_label = QtWidgets.QLabel('%s:' % _('Nr of drills'))
-        self.drill_array_size_label.setToolTip(_("Specify how many drills to be in the array."))
-        self.drill_array_size_label.setMinimumWidth(100)
+        # variable to store the total amount of slots per job
+        self.tot_slot_cnt = 0
+        self.tool_row_slots = 0
 
-        self.drill_array_size_entry = FCSpinner()
-        self.drill_array_size_entry.set_range(1, 9999)
-        self.array_form.addRow(self.drill_array_size_label, self.drill_array_size_entry)
+        self.tool_row = 0
 
-        self.array_linear_frame = QtWidgets.QFrame()
-        self.array_linear_frame.setContentsMargins(0, 0, 0, 0)
-        self.array_box.addWidget(self.array_linear_frame)
-        self.linear_box = QtWidgets.QVBoxLayout()
-        self.linear_box.setContentsMargins(0, 0, 0, 0)
-        self.array_linear_frame.setLayout(self.linear_box)
+        # 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.linear_form = QtWidgets.QFormLayout()
-        self.linear_box.addLayout(self.linear_form)
+        # def entry2option(option, entry):
+        #     self.options[option] = float(entry.text())
 
-        # Linear Drill Array direction
-        self.drill_axis_label = QtWidgets.QLabel('%s:' % _('Direction'))
-        self.drill_axis_label.setToolTip(
-            _("Direction on which the linear array is oriented:\n"
-              "- 'X' - horizontal axis \n"
-              "- 'Y' - vertical axis or \n"
-              "- 'Angle' - a custom angle for the array inclination")
-        )
-        self.drill_axis_label.setMinimumWidth(100)
+        # Event signals disconnect id holders
+        self.mp = None
+        self.mm = None
+        self.mr = None
 
-        self.drill_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
-                                          {'label': _('Y'), 'value': 'Y'},
-                                          {'label': _('Angle'), 'value': 'A'}])
-        self.linear_form.addRow(self.drill_axis_label, self.drill_axis_radio)
+        # store the status of the editor so the Delete at object level will not work until the edit is finished
+        self.editor_active = False
+        log.debug("Initialization of the Excellon Editor is finished ...")
 
-        # Linear Drill Array pitch distance
-        self.drill_pitch_label = QtWidgets.QLabel('%s:' % _('Pitch'))
-        self.drill_pitch_label.setToolTip(
-            _("Pitch = Distance between elements of the array.")
-        )
-        self.drill_pitch_label.setMinimumWidth(100)
+    def pool_recreated(self, pool):
+        self.shapes.pool = pool
+        self.tool_shape.pool = pool
 
-        self.drill_pitch_entry = FCDoubleSpinner()
-        self.drill_pitch_entry.set_precision(self.decimals)
-        self.drill_pitch_entry.set_range(0.0000, 9999.9999)
+    @staticmethod
+    def make_storage():
+        # ## Shape storage.
+        storage = FlatCAMRTreeStorage()
+        storage.get_points = DrawToolShape.get_pts
 
-        self.linear_form.addRow(self.drill_pitch_label, self.drill_pitch_entry)
+        return storage
 
-        # Linear Drill Array angle
-        self.linear_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
-        self.linear_angle_label.setToolTip(
-           _("Angle at which the linear array is placed.\n"
-             "The precision is of max 2 decimals.\n"
-             "Min value is: -360 degrees.\n"
-             "Max value is:  360.00 degrees.")
-        )
-        self.linear_angle_label.setMinimumWidth(100)
+    def set_ui(self):
+        # updated units
+        self.units = self.app.defaults['units'].upper()
 
-        self.linear_angle_spinner = FCDoubleSpinner()
-        self.linear_angle_spinner.set_precision(self.decimals)
-        self.linear_angle_spinner.setSingleStep(1.0)
-        self.linear_angle_spinner.setRange(-360.00, 360.00)
-        self.linear_form.addRow(self.linear_angle_label, self.linear_angle_spinner)
+        self.olddia_newdia.clear()
+        self.tool2tooldia.clear()
 
-        self.array_circular_frame = QtWidgets.QFrame()
-        self.array_circular_frame.setContentsMargins(0, 0, 0, 0)
-        self.array_box.addWidget(self.array_circular_frame)
-        self.circular_box = QtWidgets.QVBoxLayout()
-        self.circular_box.setContentsMargins(0, 0, 0, 0)
-        self.array_circular_frame.setLayout(self.circular_box)
+        # update the olddia_newdia dict to make sure we have an updated state of the tool_table
+        for key in self.points_edit:
+            self.olddia_newdia[key] = key
 
-        self.drill_direction_label = QtWidgets.QLabel('%s:' % _('Direction'))
-        self.drill_direction_label.setToolTip(_("Direction for circular array."
-                                                "Can be CW = clockwise or CCW = counter clockwise."))
-        self.drill_direction_label.setMinimumWidth(100)
+        for key in self.slot_points_edit:
+            if key not in self.olddia_newdia:
+                self.olddia_newdia[key] = key
 
-        self.circular_form = QtWidgets.QFormLayout()
-        self.circular_box.addLayout(self.circular_form)
+        sort_temp = []
+        for diam in self.olddia_newdia:
+            sort_temp.append(float(diam))
+        self.sorted_diameters = sorted(sort_temp)
 
-        self.drill_direction_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
-                                               {'label': _('CCW'), 'value': 'CCW'}])
-        self.circular_form.addRow(self.drill_direction_label, self.drill_direction_radio)
+        # populate self.intial_table_rows dict with the tool number as keys and tool diameters as values
+        if self.exc_obj.diameterless is False:
+            for i in range(len(self.sorted_diameters)):
+                tt_dia = self.sorted_diameters[i]
+                self.tool2tooldia[i + 1] = tt_dia
+        else:
+            # the Excellon object has diameters that are bogus information, added by the application because the
+            # Excellon file has no tool diameter information. In this case do not order the diameter in the table
+            # but use the real order found in the exc_obj.tools
+            for k, v in self.exc_obj.tools.items():
+                tool_dia = float('%.*f' % (self.decimals, v['tooldia']))
+                self.tool2tooldia[int(k)] = tool_dia
 
-        self.drill_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
-        self.drill_angle_label.setToolTip(_("Angle at which each element in circular array is placed."))
-        self.drill_angle_label.setMinimumWidth(100)
+        # Init appGUI
+        self.e_ui.addtool_entry.set_value(float(self.app.defaults['excellon_editor_newdia']))
+        self.e_ui.drill_array_size_entry.set_value(int(self.app.defaults['excellon_editor_array_size']))
+        self.e_ui.drill_axis_radio.set_value(self.app.defaults['excellon_editor_lin_dir'])
+        self.e_ui.drill_pitch_entry.set_value(float(self.app.defaults['excellon_editor_lin_pitch']))
+        self.e_ui.linear_angle_spinner.set_value(float(self.app.defaults['excellon_editor_lin_angle']))
+        self.e_ui.drill_direction_radio.set_value(self.app.defaults['excellon_editor_circ_dir'])
+        self.e_ui.drill_angle_entry.set_value(float(self.app.defaults['excellon_editor_circ_angle']))
+
+        self.e_ui.slot_length_entry.set_value(float(self.app.defaults['excellon_editor_slot_length']))
+        self.e_ui.slot_axis_radio.set_value(self.app.defaults['excellon_editor_slot_direction'])
+        self.e_ui.slot_angle_spinner.set_value(float(self.app.defaults['excellon_editor_slot_angle']))
+
+        self.e_ui.slot_array_size_entry.set_value(int(self.app.defaults['excellon_editor_slot_array_size']))
+        self.e_ui.slot_array_axis_radio.set_value(self.app.defaults['excellon_editor_slot_lin_dir'])
+        self.e_ui.slot_array_pitch_entry.set_value(float(self.app.defaults['excellon_editor_slot_lin_pitch']))
+        self.e_ui.slot_array_linear_angle_spinner.set_value(float(self.app.defaults['excellon_editor_slot_lin_angle']))
+        self.e_ui.slot_array_direction_radio.set_value(self.app.defaults['excellon_editor_slot_circ_dir'])
+        self.e_ui.slot_array_angle_entry.set_value(float(self.app.defaults['excellon_editor_slot_circ_angle']))
+
+        self.e_ui.slot_array_circular_frame.hide()
+        self.e_ui.slot_array_linear_frame.show()
 
-        self.drill_angle_entry = FCDoubleSpinner()
-        self.drill_angle_entry.set_precision(self.decimals)
-        self.drill_angle_entry.setSingleStep(1.0)
-        self.drill_angle_entry.setRange(-360.00, 360.00)
+    def build_ui(self, first_run=None):
 
-        self.circular_form.addRow(self.drill_angle_label, self.drill_angle_entry)
+        try:
+            # if connected, disconnect the signal from the slot on item_changed as it creates issues
+            self.e_ui.tools_table_exc.itemChanged.disconnect()
+        except (TypeError, AttributeError):
+            pass
 
-        self.array_circular_frame.hide()
+        try:
+            self.e_ui.tools_table_exc.cellPressed.disconnect()
+        except (TypeError, AttributeError):
+            pass
 
-        self.linear_angle_spinner.hide()
-        self.linear_angle_label.hide()
+        # updated units
+        self.units = self.app.defaults['units'].upper()
 
-        self.array_frame.hide()
+        # make a new name for the new Excellon object (the one with edited content)
+        self.edited_obj_name = self.exc_obj.options['name']
+        self.e_ui.name_entry.set_value(self.edited_obj_name)
 
-        # ######################################################
-        # ##### ADDING SLOTS ###################################
-        # ######################################################
+        sort_temp = []
 
-        # add a frame and inside add a vertical box layout. Inside this vbox layout I add
-        # all the add slot  widgets
-        # this way I can hide/show the frame
-        self.slot_frame = QtWidgets.QFrame()
-        self.slot_frame.setContentsMargins(0, 0, 0, 0)
-        self.tools_box.addWidget(self.slot_frame)
-        self.slot_box = QtWidgets.QVBoxLayout()
-        self.slot_box.setContentsMargins(0, 0, 0, 0)
-        self.slot_frame.setLayout(self.slot_box)
+        for diam in self.olddia_newdia:
+            sort_temp.append(float(diam))
+        self.sorted_diameters = sorted(sort_temp)
 
-        self.emptyarray_label = QtWidgets.QLabel('')
-        self.slot_box.addWidget(self.emptyarray_label)
+        # here, self.sorted_diameters will hold in a oblique way, the number of tools
+        n = len(self.sorted_diameters)
+        # we have (n+2) rows because there are 'n' tools, each a row, plus the last 2 rows for totals.
+        self.e_ui.tools_table_exc.setRowCount(n + 2)
 
-        self.slot_label = QtWidgets.QLabel('<b>%s</b>' % _("Slot Parameters"))
-        self.slot_label.setToolTip(
-            _("Parameters for adding a slot (hole with oval shape)\n"
-              "either single or as an part of an array.")
-        )
-        self.slot_box.addWidget(self.slot_label)
+        self.tot_drill_cnt = 0
+        self.tot_slot_cnt = 0
 
-        self.slot_form = QtWidgets.QFormLayout()
-        self.slot_box.addLayout(self.slot_form)
+        self.tool_row = 0
+        # this variable will serve as the real tool_number
+        tool_id = 0
 
-        # Slot length
-        self.slot_length_label = QtWidgets.QLabel('%s:' % _('Length'))
-        self.slot_length_label.setToolTip(
-            _("Length = The length of the slot.")
-        )
-        self.slot_length_label.setMinimumWidth(100)
+        for tool_no in self.sorted_diameters:
+            tool_id += 1
+            drill_cnt = 0  # variable to store the nr of drills per tool
+            slot_cnt = 0  # variable to store the nr of slots per tool
 
-        self.slot_length_entry = FCDoubleSpinner()
-        self.slot_length_entry.set_precision(self.decimals)
-        self.slot_length_entry.setSingleStep(0.1)
-        self.slot_length_entry.setRange(0.0000, 9999.9999)
+            # Find no of drills for the current tool
+            for tool_dia in self.points_edit:
+                if float(tool_dia) == tool_no:
+                    drill_cnt = len(self.points_edit[tool_dia])
 
-        self.slot_form.addRow(self.slot_length_label, self.slot_length_entry)
+            self.tot_drill_cnt += drill_cnt
 
-        # Slot direction
-        self.slot_axis_label = QtWidgets.QLabel('%s:' % _('Direction'))
-        self.slot_axis_label.setToolTip(
-            _("Direction on which the slot is oriented:\n"
-              "- 'X' - horizontal axis \n"
-              "- 'Y' - vertical axis or \n"
-              "- 'Angle' - a custom angle for the slot inclination")
-        )
-        self.slot_axis_label.setMinimumWidth(100)
+            # try:
+            #     # Find no of slots for the current tool
+            #     for slot in self.slot_points_edit:
+            #         if float(slot) == tool_no:
+            #             slot_cnt += 1
+            #
+            #     self.tot_slot_cnt += slot_cnt
+            # except AttributeError:
+            #     # log.debug("No slots in the Excellon file")
+            #     # Find no of slots for the current tool
+            #     for tool_dia in self.slot_points_edit:
+            #         if float(tool_dia) == tool_no:
+            #             slot_cnt = len(self.slot_points_edit[tool_dia])
+            #
+            #     self.tot_slot_cnt += slot_cnt
+
+            for tool_dia in self.slot_points_edit:
+                if float(tool_dia) == tool_no:
+                    slot_cnt = len(self.slot_points_edit[tool_dia])
 
-        self.slot_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
-                                         {'label': _('Y'), 'value': 'Y'},
-                                         {'label': _('Angle'), 'value': 'A'}])
-        self.slot_form.addRow(self.slot_axis_label, self.slot_axis_radio)
+            self.tot_slot_cnt += slot_cnt
 
-        # Slot custom angle
-        self.slot_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
-        self.slot_angle_label.setToolTip(
-           _("Angle at which the slot is placed.\n"
-             "The precision is of max 2 decimals.\n"
-             "Min value is: -360 degrees.\n"
-             "Max value is:  360.00 degrees.")
-        )
-        self.slot_angle_label.setMinimumWidth(100)
+            idd = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
+            idd.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.e_ui.tools_table_exc.setItem(self.tool_row, 0, idd)  # Tool name/id
 
-        self.slot_angle_spinner = FCDoubleSpinner()
-        self.slot_angle_spinner.set_precision(self.decimals)
-        self.slot_angle_spinner.setWrapping(True)
-        self.slot_angle_spinner.setRange(-360.00, 360.00)
-        self.slot_angle_spinner.setSingleStep(1.0)
-        self.slot_form.addRow(self.slot_angle_label, self.slot_angle_spinner)
+            # Make sure that the drill diameter when in MM is with no more than 2 decimals
+            # There are no drill bits in MM with more than 2 decimals diameter
+            # For INCH the decimals should be no more than 4. There are no drills under 10mils
+            dia = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, self.olddia_newdia[tool_no]))
 
-        self.slot_frame.hide()
+            dia.setFlags(QtCore.Qt.ItemIsEnabled)
 
-        # ######################################################
-        # ##### ADDING SLOT ARRAY  #############################
-        # ######################################################
+            drill_count = QtWidgets.QTableWidgetItem('%d' % drill_cnt)
+            drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
 
-        # add a frame and inside add a vertical box layout. Inside this vbox layout I add
-        # all the add slot  widgets
-        # this way I can hide/show the frame
-        self.slot_array_frame = QtWidgets.QFrame()
-        self.slot_array_frame.setContentsMargins(0, 0, 0, 0)
-        self.tools_box.addWidget(self.slot_array_frame)
-        self.slot_array_box = QtWidgets.QVBoxLayout()
-        self.slot_array_box.setContentsMargins(0, 0, 0, 0)
-        self.slot_array_frame.setLayout(self.slot_array_box)
+            # if the slot number is zero is better to not clutter the GUI with zero's so we print a space
+            if slot_cnt > 0:
+                slot_count = QtWidgets.QTableWidgetItem('%d' % slot_cnt)
+            else:
+                slot_count = QtWidgets.QTableWidgetItem('')
+            slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
 
-        self.emptyarray_label = QtWidgets.QLabel('')
-        self.slot_array_box.addWidget(self.emptyarray_label)
+            self.e_ui.tools_table_exc.setItem(self.tool_row, 1, dia)  # Diameter
+            self.e_ui.tools_table_exc.setItem(self.tool_row, 2, drill_count)  # Number of drills per tool
+            self.e_ui.tools_table_exc.setItem(self.tool_row, 3, slot_count)  # Number of drills per tool
 
-        self.slot_array_label = QtWidgets.QLabel('<b>%s</b>' % _("Slot Array Parameters"))
-        self.slot_array_label.setToolTip(
-            _("Parameters for the array of slots (linear or circular array)")
-        )
-        self.slot_array_box.addWidget(self.slot_array_label)
+            if first_run is True:
+                # set now the last tool selected
+                self.last_tool_selected = int(tool_id)
 
-        self.l_form = QtWidgets.QFormLayout()
-        self.slot_array_box.addLayout(self.l_form)
+            self.tool_row += 1
 
-        self.slot_array_type_combo = FCComboBox()
-        self.slot_array_type_combo.setToolTip(
-            _("Select the type of slot array to create.\n"
-              "It can be Linear X(Y) or Circular")
-        )
-        self.slot_array_type_combo.addItem(_("Linear"))
-        self.slot_array_type_combo.addItem(_("Circular"))
+        # make the diameter column editable
+        for row in range(self.tool_row):
+            self.e_ui.tools_table_exc.item(row, 1).setFlags(
+                QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.e_ui.tools_table_exc.item(row, 2).setForeground(QtGui.QColor(0, 0, 0))
+            self.e_ui.tools_table_exc.item(row, 3).setForeground(QtGui.QColor(0, 0, 0))
 
-        self.slot_array_box.addWidget(self.slot_array_type_combo)
+        # add a last row with the Total number of drills
+        # HACK: made the text on this cell '9999' such it will always be the one before last when sorting
+        # it will have to have the foreground color (font color) white
+        empty = QtWidgets.QTableWidgetItem('9998')
+        empty.setForeground(QtGui.QColor(255, 255, 255))
 
-        self.slot_array_form = QtWidgets.QFormLayout()
-        self.slot_array_box.addLayout(self.slot_array_form)
+        empty.setFlags(empty.flags() ^ QtCore.Qt.ItemIsEnabled)
+        empty_b = QtWidgets.QTableWidgetItem('')
+        empty_b.setFlags(empty_b.flags() ^ QtCore.Qt.ItemIsEnabled)
 
-        # Set the number of slot holes in the slot array
-        self.slot_array_size_label = QtWidgets.QLabel('%s:' % _('Nr of slots'))
-        self.slot_array_size_label.setToolTip(_("Specify how many slots to be in the array."))
-        self.slot_array_size_label.setMinimumWidth(100)
+        label_tot_drill_count = QtWidgets.QTableWidgetItem(_('Total Drills'))
+        tot_drill_count = QtWidgets.QTableWidgetItem('%d' % self.tot_drill_cnt)
 
-        self.slot_array_size_entry = FCSpinner()
-        self.slot_array_size_entry.set_range(0, 9999)
+        label_tot_drill_count.setFlags(label_tot_drill_count.flags() ^ QtCore.Qt.ItemIsEnabled)
+        tot_drill_count.setFlags(tot_drill_count.flags() ^ QtCore.Qt.ItemIsEnabled)
 
-        self.slot_array_form.addRow(self.slot_array_size_label, self.slot_array_size_entry)
+        self.e_ui.tools_table_exc.setItem(self.tool_row, 0, empty)
+        self.e_ui.tools_table_exc.setItem(self.tool_row, 1, label_tot_drill_count)
+        self.e_ui.tools_table_exc.setItem(self.tool_row, 2, tot_drill_count)  # Total number of drills
+        self.e_ui.tools_table_exc.setItem(self.tool_row, 3, empty_b)
 
-        self.slot_array_linear_frame = QtWidgets.QFrame()
-        self.slot_array_linear_frame.setContentsMargins(0, 0, 0, 0)
-        self.slot_array_box.addWidget(self.slot_array_linear_frame)
-        self.slot_array_linear_box = QtWidgets.QVBoxLayout()
-        self.slot_array_linear_box.setContentsMargins(0, 0, 0, 0)
-        self.slot_array_linear_frame.setLayout(self.slot_array_linear_box)
+        font = QtGui.QFont()
+        font.setBold(True)
+        font.setWeight(75)
 
-        self.slot_array_linear_form = QtWidgets.QFormLayout()
-        self.slot_array_linear_box.addLayout(self.slot_array_linear_form)
+        for k in [1, 2]:
+            self.e_ui.tools_table_exc.item(self.tool_row, k).setForeground(QtGui.QColor(127, 0, 255))
+            self.e_ui.tools_table_exc.item(self.tool_row, k).setFont(font)
 
-        # Linear Slot Array direction
-        self.slot_array_axis_label = QtWidgets.QLabel('%s:' % _('Direction'))
-        self.slot_array_axis_label.setToolTip(
-            _("Direction on which the linear array is oriented:\n"
-              "- 'X' - horizontal axis \n"
-              "- 'Y' - vertical axis or \n"
-              "- 'Angle' - a custom angle for the array inclination")
-        )
-        self.slot_array_axis_label.setMinimumWidth(100)
+        self.tool_row += 1
 
-        self.slot_array_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
-                                               {'label': _('Y'), 'value': 'Y'},
-                                               {'label': _('Angle'), 'value': 'A'}])
-        self.slot_array_linear_form.addRow(self.slot_array_axis_label, self.slot_array_axis_radio)
+        # add a last row with the Total number of slots
+        # HACK: made the text on this cell '9999' such it will always be the last when sorting
+        # it will have to have the foreground color (font color) white
+        empty_2 = QtWidgets.QTableWidgetItem('9999')
+        empty_2.setForeground(QtGui.QColor(255, 255, 255))
 
-        # Linear Slot Array pitch distance
-        self.slot_array_pitch_label = QtWidgets.QLabel('%s:' % _('Pitch'))
-        self.slot_array_pitch_label.setToolTip(
-            _("Pitch = Distance between elements of the array.")
-        )
-        self.slot_array_pitch_label.setMinimumWidth(100)
+        empty_2.setFlags(empty_2.flags() ^ QtCore.Qt.ItemIsEnabled)
 
-        self.slot_array_pitch_entry = FCDoubleSpinner()
-        self.slot_array_pitch_entry.set_precision(self.decimals)
-        self.slot_array_pitch_entry.setSingleStep(0.1)
-        self.slot_array_pitch_entry.setRange(0.0000, 9999.9999)
+        empty_3 = QtWidgets.QTableWidgetItem('')
+        empty_3.setFlags(empty_3.flags() ^ QtCore.Qt.ItemIsEnabled)
 
-        self.slot_array_linear_form.addRow(self.slot_array_pitch_label, self.slot_array_pitch_entry)
+        label_tot_slot_count = QtWidgets.QTableWidgetItem(_('Total Slots'))
+        tot_slot_count = QtWidgets.QTableWidgetItem('%d' % self.tot_slot_cnt)
+        label_tot_slot_count.setFlags(label_tot_slot_count.flags() ^ QtCore.Qt.ItemIsEnabled)
+        tot_slot_count.setFlags(tot_slot_count.flags() ^ QtCore.Qt.ItemIsEnabled)
 
-        # Linear Slot Array angle
-        self.slot_array_linear_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
-        self.slot_array_linear_angle_label.setToolTip(
-            _("Angle at which the linear array is placed.\n"
-              "The precision is of max 2 decimals.\n"
-              "Min value is: -360 degrees.\n"
-              "Max value is:  360.00 degrees.")
-        )
-        self.slot_array_linear_angle_label.setMinimumWidth(100)
+        self.e_ui.tools_table_exc.setItem(self.tool_row, 0, empty_2)
+        self.e_ui.tools_table_exc.setItem(self.tool_row, 1, label_tot_slot_count)
+        self.e_ui.tools_table_exc.setItem(self.tool_row, 2, empty_3)
+        self.e_ui.tools_table_exc.setItem(self.tool_row, 3, tot_slot_count)  # Total number of slots
 
-        self.slot_array_linear_angle_spinner = FCDoubleSpinner()
-        self.slot_array_linear_angle_spinner.set_precision(self.decimals)
-        self.slot_array_linear_angle_spinner.setSingleStep(1.0)
-        self.slot_array_linear_angle_spinner.setRange(-360.00, 360.00)
-        self.slot_array_linear_form.addRow(self.slot_array_linear_angle_label, self.slot_array_linear_angle_spinner)
+        for kl in [1, 2, 3]:
+            self.e_ui.tools_table_exc.item(self.tool_row, kl).setFont(font)
+            self.e_ui.tools_table_exc.item(self.tool_row, kl).setForeground(QtGui.QColor(0, 70, 255))
 
-        self.slot_array_circular_frame = QtWidgets.QFrame()
-        self.slot_array_circular_frame.setContentsMargins(0, 0, 0, 0)
-        self.slot_array_box.addWidget(self.slot_array_circular_frame)
-        self.slot_array_circular_box = QtWidgets.QVBoxLayout()
-        self.slot_array_circular_box.setContentsMargins(0, 0, 0, 0)
-        self.slot_array_circular_frame.setLayout(self.slot_array_circular_box)
+        # all the tools are selected by default
+        self.e_ui.tools_table_exc.selectColumn(0)
+        #
+        self.e_ui.tools_table_exc.resizeColumnsToContents()
+        self.e_ui.tools_table_exc.resizeRowsToContents()
 
-        self.slot_array_direction_label = QtWidgets.QLabel('%s:' % _('Direction'))
-        self.slot_array_direction_label.setToolTip(_("Direction for circular array."
-                                                     "Can be CW = clockwise or CCW = counter clockwise."))
-        self.slot_array_direction_label.setMinimumWidth(100)
+        vertical_header = self.e_ui.tools_table_exc.verticalHeader()
+        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+        vertical_header.hide()
+        self.e_ui.tools_table_exc.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
 
-        self.slot_array_circular_form = QtWidgets.QFormLayout()
-        self.slot_array_circular_box.addLayout(self.slot_array_circular_form)
+        horizontal_header = self.e_ui.tools_table_exc.horizontalHeader()
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+        # horizontal_header.setStretchLastSection(True)
 
-        self.slot_array_direction_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
-                                                    {'label': _('CCW'), 'value': 'CCW'}])
-        self.slot_array_circular_form.addRow(self.slot_array_direction_label, self.slot_array_direction_radio)
+        # self.e_ui.tools_table_exc.setSortingEnabled(True)
+        # sort by tool diameter
+        self.e_ui.tools_table_exc.sortItems(1)
 
-        self.slot_array_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
-        self.slot_array_angle_label.setToolTip(_("Angle at which each element in circular array is placed."))
-        self.slot_array_angle_label.setMinimumWidth(100)
+        # After sorting, to display also the number of drills in the right row we need to update self.initial_rows dict
+        # with the new order. Of course the last 2 rows in the tool table are just for display therefore we don't
+        # use them
+        self.tool2tooldia.clear()
+        for row in range(self.e_ui.tools_table_exc.rowCount() - 2):
+            tool = int(self.e_ui.tools_table_exc.item(row, 0).text())
+            diameter = float(self.e_ui.tools_table_exc.item(row, 1).text())
+            self.tool2tooldia[tool] = diameter
 
-        self.slot_array_angle_entry = FCDoubleSpinner()
-        self.slot_array_angle_entry.set_precision(self.decimals)
-        self.slot_array_angle_entry.setSingleStep(1)
-        self.slot_array_angle_entry.setRange(-360.00, 360.00)
+        self.e_ui.tools_table_exc.setMinimumHeight(self.e_ui.tools_table_exc.getHeight())
+        self.e_ui.tools_table_exc.setMaximumHeight(self.e_ui.tools_table_exc.getHeight())
 
-        self.slot_array_circular_form.addRow(self.slot_array_angle_label, self.slot_array_angle_entry)
+        # make sure no rows are selected so the user have to click the correct row, meaning selecting the correct tool
+        self.e_ui.tools_table_exc.clearSelection()
 
-        self.slot_array_linear_angle_spinner.hide()
-        self.slot_array_linear_angle_label.hide()
+        # Remove anything else in the GUI Selected Tab
+        self.app.ui.selected_scroll_area.takeWidget()
+        # Put ourselves in the GUI Properties Tab
+        self.app.ui.selected_scroll_area.setWidget(self.e_ui.exc_edit_widget)
+        # Switch notebook to Properties page
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
 
-        self.slot_array_frame.hide()
+        # we reactivate the signals after the after the tool adding as we don't need to see the tool been populated
+        self.e_ui.tools_table_exc.itemChanged.connect(self.on_tool_edit)
+        self.e_ui.tools_table_exc.cellPressed.connect(self.on_row_selected)
 
-        self.tools_box.addStretch()
+    def on_tool_add(self, tooldia=None):
+        self.is_modified = True
+        if tooldia:
+            tool_dia = tooldia
+        else:
+            try:
+                tool_dia = float(self.e_ui.addtool_entry.get_value())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    tool_dia = float(self.e_ui.addtool_entry.get_value().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
+                    return
 
-        # ## Toolbar events and properties
-        self.tools_exc = {
-            "drill_select": {"button": self.app.ui.select_drill_btn, "constructor": FCDrillSelect},
-            "drill_add": {"button": self.app.ui.add_drill_btn, "constructor": FCDrillAdd},
-            "drill_array": {"button": self.app.ui.add_drill_array_btn, "constructor": FCDrillArray},
-            "slot_add": {"button": self.app.ui.add_slot_btn, "constructor": FCSlot},
-            "slot_array": {"button": self.app.ui.add_slot_array_btn, "constructor": FCSlotArray},
-            "drill_resize": {"button": self.app.ui.resize_drill_btn, "constructor": FCDrillResize},
-            "drill_copy": {"button": self.app.ui.copy_drill_btn, "constructor": FCDrillCopy},
-            "drill_move": {"button": self.app.ui.move_drill_btn, "constructor": FCDrillMove},
-        }
+        if tool_dia not in self.olddia_newdia:
+            storage_elem = AppGeoEditor.make_storage()
+            self.storage_dict[tool_dia] = storage_elem
 
-        # ## Data
-        self.active_tool = None
+            # self.olddia_newdia dict keeps the evidence on current tools diameters as keys and gets updated on values
+            # each time a tool diameter is edited or added
+            self.olddia_newdia[tool_dia] = tool_dia
+        else:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Tool already in the original or actual tool list.\n" 
+                                                          "Save and reedit Excellon if you need to add this tool. "))
+            return
 
-        self.in_action = False
+        # since we add a new tool, we update also the initial state of the tool_table through it's dictionary
+        # we add a new entry in the tool2tooldia dict
+        self.tool2tooldia[len(self.olddia_newdia)] = tool_dia
 
-        self.storage_dict = {}
+        self.app.inform.emit('[success] %s: %s %s' % (_("Added new tool with dia"), str(tool_dia), str(self.units)))
 
-        self.current_storage = []
+        self.build_ui()
 
-        # build the data from the Excellon point into a dictionary
-        #  {tool_dia: [geometry_in_points]}
-        self.points_edit = {}
-        self.slot_points_edit = {}
+        # make a quick sort through the tool2tooldia dict so we find which row to select
+        row_to_be_selected = None
+        for key in sorted(self.tool2tooldia):
+            if self.tool2tooldia[key] == tool_dia:
+                row_to_be_selected = int(key) - 1
+                self.last_tool_selected = int(key)
+                break
+        try:
+            self.e_ui.tools_table_exc.selectRow(row_to_be_selected)
+        except TypeError as e:
+            log.debug("AppExcEditor.on_tool_add() --> %s" % str(e))
 
-        self.sorted_diameters = []
+    def on_tool_delete(self, dia=None):
+        self.is_modified = True
+        deleted_tool_dia_list = []
 
-        self.new_drills = []
-        self.new_tools = {}
-        self.new_slots = []
+        try:
+            if dia is None or dia is False:
+                # deleted_tool_dia = float(self.e_ui.tools_table_exc.item(self.e_ui.tools_table_exc.currentRow(), 1).text())
+                for index in self.e_ui.tools_table_exc.selectionModel().selectedRows():
+                    row = index.row()
+                    deleted_tool_dia_list.append(float(self.e_ui.tools_table_exc.item(row, 1).text()))
+            else:
+                if isinstance(dia, list):
+                    for dd in dia:
+                        deleted_tool_dia_list.append(float('%.*f' % (self.decimals, dd)))
+                else:
+                    deleted_tool_dia_list.append(float('%.*f' % (self.decimals, dia)))
+        except Exception:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Select a tool in Tool Table"))
+            return
 
-        # dictionary to store the tool_row and diameters in Tool_table
-        # it will be updated everytime self.build_ui() is called
-        self.olddia_newdia = {}
+        for deleted_tool_dia in deleted_tool_dia_list:
 
-        self.tool2tooldia = {}
+            # delete the storage used for that tool
+            storage_elem = AppGeoEditor.make_storage()
+            self.storage_dict[deleted_tool_dia] = storage_elem
+            self.storage_dict.pop(deleted_tool_dia, None)
 
-        # this will store the value for the last selected tool, for use after clicking on canvas when the selection
-        # is cleared but as a side effect also the selected tool is cleared
-        self.last_tool_selected = None
-        self.utility = []
+            # I've added this flag_del variable because dictionary don't like
+            # having keys deleted while iterating through them
+            flag_del = []
+            # self.points_edit.pop(deleted_tool_dia, None)
+            for deleted_tool in self.tool2tooldia:
+                if self.tool2tooldia[deleted_tool] == deleted_tool_dia:
+                    flag_del.append(deleted_tool)
 
-        # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False)
-        self.launched_from_shortcuts = False
+            if flag_del:
+                for tool_to_be_deleted in flag_del:
+                    # delete the tool
+                    self.tool2tooldia.pop(tool_to_be_deleted, None)
 
-        # this var will store the state of the toolbar before starting the editor
-        self.toolbar_old_state = False
+                    # delete also the drills from points_edit dict just in case we add the tool again,
+                    # we don't want to show the number of drills from before was deleter
+                    self.points_edit[deleted_tool_dia] = []
 
-        if self.units == 'MM':
-            self.tolerance = float(self.app.defaults["global_tolerance"])
-        else:
-            self.tolerance = float(self.app.defaults["global_tolerance"]) / 20
+            self.olddia_newdia.pop(deleted_tool_dia, None)
 
-        self.app.ui.delete_drill_btn.triggered.connect(self.on_delete_btn)
-        self.name_entry.returnPressed.connect(self.on_name_activate)
-        self.addtool_btn.clicked.connect(self.on_tool_add)
-        self.addtool_entry.editingFinished.connect(self.on_tool_add)
-        self.deltool_btn.clicked.connect(self.on_tool_delete)
-        # self.tools_table_exc.selectionModel().currentChanged.connect(self.on_row_selected)
-        self.tools_table_exc.cellPressed.connect(self.on_row_selected)
+            self.app.inform.emit('[success] %s: %s %s' %
+                                 (_("Deleted tool with diameter"), str(deleted_tool_dia), str(self.units)))
 
-        self.array_type_combo.currentIndexChanged.connect(self.on_array_type_combo)
-        self.slot_array_type_combo.currentIndexChanged.connect(self.on_slot_array_type_combo)
+        self.replot()
+        # self.app.inform.emit("Could not delete selected tool")
 
-        self.drill_axis_radio.activated_custom.connect(self.on_linear_angle_radio)
-        self.slot_axis_radio.activated_custom.connect(self.on_slot_angle_radio)
+        self.build_ui()
 
-        self.slot_array_axis_radio.activated_custom.connect(self.on_slot_array_linear_angle_radio)
+    def on_tool_edit(self, item_changed):
+        # if connected, disconnect the signal from the slot on item_changed as it creates issues
+        try:
+            self.e_ui.tools_table_exc.itemChanged.disconnect()
+        except TypeError:
+            pass
 
-        self.app.ui.exc_add_array_drill_menuitem.triggered.connect(self.exc_add_drill_array)
-        self.app.ui.exc_add_drill_menuitem.triggered.connect(self.exc_add_drill)
+        try:
+            self.e_ui.tools_table_exc.cellPressed.disconnect()
+        except TypeError:
+            pass
+        # self.e_ui.tools_table_exc.selectionModel().currentChanged.disconnect()
 
-        self.app.ui.exc_add_array_slot_menuitem.triggered.connect(self.exc_add_slot_array)
-        self.app.ui.exc_add_slot_menuitem.triggered.connect(self.exc_add_slot)
+        self.is_modified = True
+        # new_dia = None
 
-        self.app.ui.exc_resize_drill_menuitem.triggered.connect(self.exc_resize_drills)
-        self.app.ui.exc_copy_drill_menuitem.triggered.connect(self.exc_copy_drills)
-        self.app.ui.exc_delete_drill_menuitem.triggered.connect(self.on_delete_btn)
+        try:
+            new_dia = float(self.e_ui.tools_table_exc.currentItem().text())
+        except ValueError as e:
+            log.debug("AppExcEditor.on_tool_edit() --> %s" % str(e))
+            return
 
-        self.app.ui.exc_move_drill_menuitem.triggered.connect(self.exc_move_drills)
+        row_of_item_changed = self.e_ui.tools_table_exc.currentRow()
+        # rows start with 0, tools start with 1 so we adjust the value by 1
+        key_in_tool2tooldia = row_of_item_changed + 1
+        old_dia = self.tool2tooldia[key_in_tool2tooldia]
 
-        self.exc_obj = None
+        # SOURCE storage
+        source_storage = self.storage_dict[old_dia]
 
-        # VisPy Visuals
-        if self.app.is_legacy is False:
-            self.shapes = self.app.plotcanvas.new_shape_collection(layers=1)
-            if self.app.plotcanvas.big_cursor is True:
-                self.tool_shape = self.app.plotcanvas.new_shape_collection(layers=1, line_width=2)
-            else:
-                self.tool_shape = self.app.plotcanvas.new_shape_collection(layers=1)
+        # DESTINATION storage
+        # tool diameter is not used so we create a new tool with the desired diameter
+        if new_dia not in self.olddia_newdia:
+            destination_storage = AppGeoEditor.make_storage()
+            self.storage_dict[new_dia] = destination_storage
+
+            # self.olddia_newdia dict keeps the evidence on current tools diameters as keys and gets updated on values
+            # each time a tool diameter is edited or added
+            self.olddia_newdia[new_dia] = new_dia
         else:
-            from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
-            self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='shapes_exc_editor')
-            self.tool_shape = ShapeCollectionLegacy(obj=self, app=self.app, name='tool_shapes_exc_editor')
+            # tool diameter is already in use so we move the drills from the prior tool to the new tool
+            destination_storage = self.storage_dict[new_dia]
 
-        self.app.pool_recreated.connect(self.pool_recreated)
+        # since we add a new tool, we update also the intial state of the tool_table through it's dictionary
+        # we add a new entry in the tool2tooldia dict
+        self.tool2tooldia[len(self.olddia_newdia)] = new_dia
 
-        # Remove from scene
-        self.shapes.enabled = False
-        self.tool_shape.enabled = False
+        # CHANGE the elements geometry according to the new diameter
+        factor = new_dia / old_dia
+        new_geo = Polygon()
+        for shape_exc in source_storage.get_objects():
+            geo_list = []
+            if isinstance(shape_exc.geo, MultiLineString):
+                for subgeo in shape_exc.geo:
+                    geo_list.append(affinity.scale(subgeo, xfact=factor, yfact=factor, origin='center'))
+                new_geo = MultiLineString(geo_list)
+            elif isinstance(shape_exc.geo, Polygon):
+                # I don't have any info regarding the angle of the slot geometry, nor how thick it is or
+                # how long it is given the angle. So I will have to make an approximation because
+                # we need to conserve the slot length, we only resize the diameter for the tool
+                # Therefore scaling won't work and buffering will not work either.
 
-        # ## List of selected shapes.
-        self.selected = []
+                # First we get the Linestring that is one that the original slot is built around with the
+                # tool having the diameter sel_dia
+                poly = shape_exc.geo
+                xmin, ymin, xmax, ymax = poly.bounds
+                # a line that is certain to be bigger than our slot because it's the diagonal
+                # of it's bounding box
+                poly_diagonal = LineString([(xmin, ymin), (xmax, ymax)])
+                poly_centroid = poly.centroid
+                # center of the slot geometry
+                poly_center = (poly_centroid.x, poly_centroid.y)
 
-        self.move_timer = QtCore.QTimer()
-        self.move_timer.setSingleShot(True)
+                # make a list of intersections with the rotated line
+                list_of_cuttings = []
+                for angle in range(0, 359, 1):
+                    rot_poly_diagonal = affinity.rotate(poly_diagonal, angle=angle, origin=poly_center)
+                    cut_line = rot_poly_diagonal.intersection(poly)
+                    cut_line_len = cut_line.length
+                    list_of_cuttings.append(
+                        (cut_line_len, cut_line)
+                    )
+                # find the cut_line with the maximum length which is the LineString for which the start
+                # and stop point are the start and stop point of the slot as in the Gerber file
+                cut_line_with_max_length = max(list_of_cuttings, key=lambda i: i[0])[1]
+                # find the coordinates of this line
+                cut_line_with_max_length_coords = list(cut_line_with_max_length.coords)
+                # extract the first and last point of the line and build some buffered polygon circles
+                # around them
+                start_pt = Point(cut_line_with_max_length_coords[0])
+                stop_pt = Point(cut_line_with_max_length_coords[1])
+                start_cut_geo = start_pt.buffer(new_dia / 2)
+                stop_cut_geo = stop_pt.buffer(new_dia / 2)
 
-        self.key = None  # Currently pressed key
-        self.modifiers = None
-        self.x = None  # Current mouse cursor pos
-        self.y = None
-        # Current snapped mouse pos
-        self.snap_x = None
-        self.snap_y = None
-        self.pos = None
+                # and we cut the above circle polygons from our line and get in this way a line around
+                # which we can build the new slot by buffering with the new tool diameter
+                new_line = cut_line_with_max_length.difference(start_cut_geo)
+                new_line = new_line.difference(stop_cut_geo)
 
-        self.complete = False
+                # create the geometry for the resized slot by buffering with half of the
+                # new diameter value: new_dia
+                new_geo = new_line.buffer(new_dia / 2)
 
-        def make_callback(thetool):
-            def f():
-                self.on_tool_select(thetool)
-            return f
+            try:
+                self.points_edit.pop(old_dia, None)
+            except KeyError:
+                pass
+            try:
+                self.slot_points_edit.pop(old_dia, None)
+            except KeyError:
+                pass
 
-        for tool in self.tools_exc:
-            self.tools_exc[tool]["button"].triggered.connect(make_callback(tool))  # Events
-            self.tools_exc[tool]["button"].setCheckable(True)  # Checkable
+            # add bogus drill/slots points (for total count of drills/slots)
+            # for drills
+            if isinstance(shape_exc.geo, MultiLineString):
+                if new_dia not in self.points_edit:
+                    self.points_edit[new_dia] = [(0, 0)]
+                else:
+                    self.points_edit[new_dia].append((0, 0))
 
-        self.options = {
-            "global_gridx": 0.1,
-            "global_gridy": 0.1,
-            "snap_max": 0.05,
-            "grid_snap": True,
-            "corner_snap": False,
-            "grid_gap_link": True
-        }
-        self.options.update(self.app.options)
+            # for slots
+            if isinstance(shape_exc.geo, Polygon):
+                if new_dia not in self.slot_points_edit:
+                    self.slot_points_edit[new_dia] = [(0, 0)]
+                else:
+                    self.slot_points_edit[new_dia].append((0, 0))
 
-        for option in self.options:
-            if option in self.app.options:
-                self.options[option] = self.app.options[option]
+            self.add_exc_shape(shape=DrawToolShape(new_geo), storage=destination_storage)
 
-        self.data_defaults = {
-            "plot": self.app.defaults["excellon_plot"],
-            "solid": self.app.defaults["excellon_solid"],
-
-            "operation": self.app.defaults["excellon_operation"],
-            "milling_type": self.app.defaults["excellon_milling_type"],
-
-            "milling_dia": self.app.defaults["excellon_milling_dia"],
-
-            "cutz": self.app.defaults["excellon_cutz"],
-            "multidepth": self.app.defaults["excellon_multidepth"],
-            "depthperpass": self.app.defaults["excellon_depthperpass"],
-            "travelz": self.app.defaults["excellon_travelz"],
-            "feedrate": self.app.defaults["geometry_feedrate"],
-            "feedrate_z": self.app.defaults["excellon_feedrate_z"],
-            "feedrate_rapid": self.app.defaults["excellon_feedrate_rapid"],
-            "tooldia": self.app.defaults["excellon_tooldia"],
-            "slot_tooldia": self.app.defaults["excellon_slot_tooldia"],
-            "toolchange": self.app.defaults["excellon_toolchange"],
-            "toolchangez": self.app.defaults["excellon_toolchangez"],
-            "toolchangexy": self.app.defaults["excellon_toolchangexy"],
-            "extracut": self.app.defaults["geometry_extracut"],
-            "extracut_length": self.app.defaults["geometry_extracut_length"],
-            "endz": self.app.defaults["excellon_endz"],
-            "endxy": self.app.defaults["excellon_endxy"],
-            "startz": self.app.defaults["excellon_startz"],
-            "offset": self.app.defaults["excellon_offset"],
-            "spindlespeed": self.app.defaults["excellon_spindlespeed"],
-            "dwell": self.app.defaults["excellon_dwell"],
-            "dwelltime": self.app.defaults["excellon_dwelltime"],
-            "ppname_e": self.app.defaults["excellon_ppname_e"],
-            "ppname_g": self.app.defaults["geometry_ppname_g"],
-            "z_pdepth": self.app.defaults["excellon_z_pdepth"],
-            "feedrate_probe": self.app.defaults["excellon_feedrate_probe"],
-            "optimization_type": self.app.defaults["excellon_optimization_type"]
-        }
+        # update the UI and the CANVAS
+        self.build_ui()
+        self.replot()
 
-        self.rtree_exc_index = rtindex.Index()
-        # flag to show if the object was modified
-        self.is_modified = False
+        # delete the old tool
+        self.on_tool_delete(dia=old_dia)
 
-        self.edited_obj_name = ""
+        # we reactivate the signals after the after the tool editing
+        self.e_ui.tools_table_exc.itemChanged.connect(self.on_tool_edit)
+        self.e_ui.tools_table_exc.cellPressed.connect(self.on_row_selected)
 
-        # variable to store the total amount of drills per job
-        self.tot_drill_cnt = 0
-        self.tool_row = 0
+        self.app.inform.emit('[success] %s' % _("Done. Tool edit completed."))
 
-        # variable to store the total amount of slots per job
-        self.tot_slot_cnt = 0
-        self.tool_row_slots = 0
+        # self.e_ui.tools_table_exc.selectionModel().currentChanged.connect(self.on_row_selected)
 
-        self.tool_row = 0
+    def on_name_activate(self):
+        self.edited_obj_name = self.e_ui.name_entry.get_value()
 
-        # store the status of the editor so the Delete at object level will not work until the edit is finished
-        self.editor_active = False
+    def activate(self):
+        # adjust the status of the menu entries related to the editor
+        self.app.ui.menueditedit.setDisabled(True)
+        self.app.ui.menueditok.setDisabled(False)
+        # adjust the visibility of some of the canvas context menu
+        self.app.ui.popmenu_edit.setVisible(False)
+        self.app.ui.popmenu_save.setVisible(True)
 
-        # def entry2option(option, entry):
-        #     self.options[option] = float(entry.text())
+        self.connect_canvas_event_handlers()
 
-        # Event signals disconnect id holders
-        self.mp = None
-        self.mm = None
-        self.mr = None
+        # initialize working objects
+        self.storage_dict = {}
+        self.current_storage = []
+        self.points_edit = {}
+        self.sorted_diameters = []
+        self.new_drills = []
+        self.new_tools = {}
+        self.new_slots = []
 
-        # store the status of the editor so the Delete at object level will not work until the edit is finished
-        self.editor_active = False
-        log.debug("Initialization of the Excellon Editor is finished ...")
+        self.olddia_newdia = {}
 
-    def pool_recreated(self, pool):
-        self.shapes.pool = pool
-        self.tool_shape.pool = pool
+        self.shapes.enabled = True
+        self.tool_shape.enabled = True
+        # self.app.app_cursor.enabled = True
 
-    @staticmethod
-    def make_storage():
-        # ## Shape storage.
-        storage = FlatCAMRTreeStorage()
-        storage.get_points = DrawToolShape.get_pts
+        self.app.ui.corner_snap_btn.setVisible(True)
+        self.app.ui.snap_magnet.setVisible(True)
 
-        return storage
+        self.app.ui.exc_editor_menu.setDisabled(False)
+        self.app.ui.exc_editor_menu.menuAction().setVisible(True)
 
-    def set_ui(self):
-        # updated units
-        self.units = self.app.defaults['units'].upper()
+        self.app.ui.update_obj_btn.setEnabled(True)
+        self.app.ui.e_editor_cmenu.setEnabled(True)
 
-        self.olddia_newdia.clear()
-        self.tool2tooldia.clear()
+        self.app.ui.exc_edit_toolbar.setDisabled(False)
+        self.app.ui.exc_edit_toolbar.setVisible(True)
+        # self.app.ui.grid_toolbar.setDisabled(False)
 
-        # build the self.points_edit dict {dimaters: [point_list]}
-        for drill in self.exc_obj.drills:
-            if drill['tool'] in self.exc_obj.tools:
-                tool_dia = float('%.*f' % (self.decimals, self.exc_obj.tools[drill['tool']]['C']))
+        # start with GRID toolbar activated
+        if self.app.ui.grid_snap_btn.isChecked() is False:
+            self.app.ui.grid_snap_btn.trigger()
 
-                try:
-                    self.points_edit[tool_dia].append(drill['point'])
-                except KeyError:
-                    self.points_edit[tool_dia] = [drill['point']]
+        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)
 
-        # build the self.slot_points_edit dict {dimaters: {"start": Point, "stop": Point}}
-        for slot in self.exc_obj.slots:
-            if slot['tool'] in self.exc_obj.tools:
-                tool_dia = float('%.*f' % (self.decimals, self.exc_obj.tools[slot['tool']]['C']))
+        # Tell the App that the editor is active
+        self.editor_active = True
 
-                try:
-                    self.slot_points_edit[tool_dia].append({
-                        "start": slot["start"],
-                        "stop": slot["stop"]
-                    })
-                except KeyError:
-                    self.slot_points_edit[tool_dia] = [{
-                        "start": slot["start"],
-                        "stop": slot["stop"]
-                    }]
+        # show the UI
+        self.e_ui.drills_frame.show()
 
-        # update the olddia_newdia dict to make sure we have an updated state of the tool_table
-        for key in self.points_edit:
-            self.olddia_newdia[key] = key
+    def deactivate(self):
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
 
-        for key in self.slot_points_edit:
-            if key not in self.olddia_newdia:
-                self.olddia_newdia[key] = key
+        # adjust the status of the menu entries related to the editor
+        self.app.ui.menueditedit.setDisabled(False)
+        self.app.ui.menueditok.setDisabled(True)
+        # adjust the visibility of some of the canvas context menu
+        self.app.ui.popmenu_edit.setVisible(True)
+        self.app.ui.popmenu_save.setVisible(False)
 
-        sort_temp = []
-        for diam in self.olddia_newdia:
-            sort_temp.append(float(diam))
-        self.sorted_diameters = sorted(sort_temp)
+        self.disconnect_canvas_event_handlers()
+        self.clear()
+        self.app.ui.exc_edit_toolbar.setDisabled(True)
 
-        # populate self.intial_table_rows dict with the tool number as keys and tool diameters as values
-        if self.exc_obj.diameterless is False:
-            for i in range(len(self.sorted_diameters)):
-                tt_dia = self.sorted_diameters[i]
-                self.tool2tooldia[i + 1] = tt_dia
-        else:
-            # the Excellon object has diameters that are bogus information, added by the application because the
-            # Excellon file has no tool diameter information. In this case do not order the diameter in the table
-            # but use the real order found in the exc_obj.tools
-            for k, v in self.exc_obj.tools.items():
-                tool_dia = float('%.*f' % (self.decimals, v['C']))
-                self.tool2tooldia[int(k)] = tool_dia
+        self.app.ui.corner_snap_btn.setVisible(False)
+        self.app.ui.snap_magnet.setVisible(False)
 
-        # Init appGUI
-        self.addtool_entry.set_value(float(self.app.defaults['excellon_editor_newdia']))
-        self.drill_array_size_entry.set_value(int(self.app.defaults['excellon_editor_array_size']))
-        self.drill_axis_radio.set_value(self.app.defaults['excellon_editor_lin_dir'])
-        self.drill_pitch_entry.set_value(float(self.app.defaults['excellon_editor_lin_pitch']))
-        self.linear_angle_spinner.set_value(float(self.app.defaults['excellon_editor_lin_angle']))
-        self.drill_direction_radio.set_value(self.app.defaults['excellon_editor_circ_dir'])
-        self.drill_angle_entry.set_value(float(self.app.defaults['excellon_editor_circ_angle']))
-
-        self.slot_length_entry.set_value(float(self.app.defaults['excellon_editor_slot_length']))
-        self.slot_axis_radio.set_value(self.app.defaults['excellon_editor_slot_direction'])
-        self.slot_angle_spinner.set_value(float(self.app.defaults['excellon_editor_slot_angle']))
-
-        self.slot_array_size_entry.set_value(int(self.app.defaults['excellon_editor_slot_array_size']))
-        self.slot_array_axis_radio.set_value(self.app.defaults['excellon_editor_slot_lin_dir'])
-        self.slot_array_pitch_entry.set_value(float(self.app.defaults['excellon_editor_slot_lin_pitch']))
-        self.slot_array_linear_angle_spinner.set_value(float(self.app.defaults['excellon_editor_slot_lin_angle']))
-        self.slot_array_direction_radio.set_value(self.app.defaults['excellon_editor_slot_circ_dir'])
-        self.slot_array_angle_entry.set_value(float(self.app.defaults['excellon_editor_slot_circ_angle']))
-
-        self.slot_array_circular_frame.hide()
-        self.slot_array_linear_frame.show()
+        # set the Editor Toolbar visibility to what was before entering in the Editor
+        self.app.ui.exc_edit_toolbar.setVisible(False) if self.toolbar_old_state is False \
+            else self.app.ui.exc_edit_toolbar.setVisible(True)
 
-    def build_ui(self, first_run=None):
+        # Disable visuals
+        self.shapes.enabled = False
+        self.tool_shape.enabled = False
+        # self.app.app_cursor.enabled = False
 
-        try:
-            # if connected, disconnect the signal from the slot on item_changed as it creates issues
-            self.tools_table_exc.itemChanged.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
-        try:
-            self.tools_table_exc.cellPressed.disconnect()
-        except (TypeError, AttributeError):
-            pass
+        # Tell the app that the editor is no longer active
+        self.editor_active = False
 
-        # updated units
-        self.units = self.app.defaults['units'].upper()
+        self.app.ui.exc_editor_menu.setDisabled(True)
+        self.app.ui.exc_editor_menu.menuAction().setVisible(False)
 
-        # make a new name for the new Excellon object (the one with edited content)
-        self.edited_obj_name = self.exc_obj.options['name']
-        self.name_entry.set_value(self.edited_obj_name)
+        self.app.ui.update_obj_btn.setEnabled(False)
 
-        sort_temp = []
+        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)
 
-        for diam in self.olddia_newdia:
-            sort_temp.append(float(diam))
-        self.sorted_diameters = sorted(sort_temp)
+        # Show original geometry
+        if self.exc_obj:
+            self.exc_obj.visible = True
 
-        # here, self.sorted_diameters will hold in a oblique way, the number of tools
-        n = len(self.sorted_diameters)
-        # we have (n+2) rows because there are 'n' tools, each a row, plus the last 2 rows for totals.
-        self.tools_table_exc.setRowCount(n + 2)
+        # hide the UI
+        self.e_ui.drills_frame.hide()
 
-        self.tot_drill_cnt = 0
-        self.tot_slot_cnt = 0
+    def connect_canvas_event_handlers(self):
+        # ## Canvas events
 
-        self.tool_row = 0
-        # this variable will serve as the real tool_number
-        tool_id = 0
+        # first connect to new, then disconnect the old handlers
+        # don't ask why but if there is nothing connected I've seen issues
+        self.mp = self.canvas.graph_event_connect('mouse_press', self.on_canvas_click)
+        self.mm = self.canvas.graph_event_connect('mouse_move', self.on_canvas_move)
+        self.mr = self.canvas.graph_event_connect('mouse_release', self.on_exc_click_release)
 
-        for tool_no in self.sorted_diameters:
-            tool_id += 1
-            drill_cnt = 0  # variable to store the nr of drills per tool
-            slot_cnt = 0  # variable to store the nr of slots per tool
+        # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp
+        # but those from AppGeoEditor
+        if self.app.is_legacy is False:
+            self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
+            self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
+            self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+            self.app.plotcanvas.graph_event_disconnect('mouse_double_click', self.app.on_mouse_double_click_over_plot)
+        else:
+            self.app.plotcanvas.graph_event_disconnect(self.app.mp)
+            self.app.plotcanvas.graph_event_disconnect(self.app.mm)
+            self.app.plotcanvas.graph_event_disconnect(self.app.mr)
+            self.app.plotcanvas.graph_event_disconnect(self.app.mdc)
 
-            # Find no of drills for the current tool
-            for tool_dia in self.points_edit:
-                if float(tool_dia) == tool_no:
-                    drill_cnt = len(self.points_edit[tool_dia])
+        self.app.collection.view.clicked.disconnect()
 
-            self.tot_drill_cnt += drill_cnt
+        self.app.ui.popmenu_copy.triggered.disconnect()
+        self.app.ui.popmenu_delete.triggered.disconnect()
+        self.app.ui.popmenu_move.triggered.disconnect()
 
-            try:
-                # Find no of slots for the current tool
-                for slot in self.slot_points_edit:
-                    if slot['tool'] == tool_no:
-                        slot_cnt += 1
+        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)
 
-                self.tot_slot_cnt += slot_cnt
-            except AttributeError:
-                # log.debug("No slots in the Excellon file")
-                # Find no of slots for the current tool
-                for tool_dia in self.slot_points_edit:
-                    if float(tool_dia) == tool_no:
-                        slot_cnt = len(self.slot_points_edit[tool_dia])
+        # Excellon Editor
+        self.app.ui.drill.triggered.connect(self.exc_add_drill)
+        self.app.ui.drill_array.triggered.connect(self.exc_add_drill_array)
 
-                self.tot_slot_cnt += slot_cnt
+    def disconnect_canvas_event_handlers(self):
+        # we restore the key and mouse control to FlatCAMApp method
+        # first connect to new, then disconnect the old handlers
+        # don't ask why but if there is nothing connected I've seen issues
+        self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
+        self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot)
+        self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                              self.app.on_mouse_click_release_over_plot)
+        self.app.mdc = self.app.plotcanvas.graph_event_connect('mouse_double_click',
+                                                               self.app.on_mouse_double_click_over_plot)
+        self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
 
-            idd = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
-            idd.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.tools_table_exc.setItem(self.tool_row, 0, idd)  # Tool name/id
+        if self.app.is_legacy is False:
+            self.canvas.graph_event_disconnect('mouse_press', self.on_canvas_click)
+            self.canvas.graph_event_disconnect('mouse_move', self.on_canvas_move)
+            self.canvas.graph_event_disconnect('mouse_release', self.on_exc_click_release)
+        else:
+            self.canvas.graph_event_disconnect(self.mp)
+            self.canvas.graph_event_disconnect(self.mm)
+            self.canvas.graph_event_disconnect(self.mr)
 
-            # Make sure that the drill diameter when in MM is with no more than 2 decimals
-            # There are no drill bits in MM with more than 2 decimals diameter
-            # For INCH the decimals should be no more than 4. There are no drills under 10mils
-            dia = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, self.olddia_newdia[tool_no]))
+        try:
+            self.app.ui.popmenu_copy.triggered.disconnect(self.exc_copy_drills)
+        except (TypeError, AttributeError):
+            pass
 
-            dia.setFlags(QtCore.Qt.ItemIsEnabled)
+        try:
+            self.app.ui.popmenu_delete.triggered.disconnect(self.on_delete_btn)
+        except (TypeError, AttributeError):
+            pass
 
-            drill_count = QtWidgets.QTableWidgetItem('%d' % drill_cnt)
-            drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
+        try:
+            self.app.ui.popmenu_move.triggered.disconnect(self.exc_move_drills)
+        except (TypeError, AttributeError):
+            pass
 
-            # if the slot number is zero is better to not clutter the GUI with zero's so we print a space
-            if slot_cnt > 0:
-                slot_count = QtWidgets.QTableWidgetItem('%d' % slot_cnt)
-            else:
-                slot_count = QtWidgets.QTableWidgetItem('')
-            slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
+        self.app.ui.popmenu_copy.triggered.connect(self.app.on_copy_command)
+        self.app.ui.popmenu_delete.triggered.connect(self.app.on_delete)
+        self.app.ui.popmenu_move.triggered.connect(self.app.obj_move)
 
-            self.tools_table_exc.setItem(self.tool_row, 1, dia)  # Diameter
-            self.tools_table_exc.setItem(self.tool_row, 2, drill_count)  # Number of drills per tool
-            self.tools_table_exc.setItem(self.tool_row, 3, slot_count)  # Number of drills per tool
+        # Excellon Editor
+        try:
+            self.app.ui.drill.triggered.disconnect(self.exc_add_drill)
+        except (TypeError, AttributeError):
+            pass
 
-            if first_run is True:
-                # set now the last tool selected
-                self.last_tool_selected = int(tool_id)
+        try:
+            self.app.ui.drill_array.triggered.disconnect(self.exc_add_drill_array)
+        except (TypeError, AttributeError):
+            pass
 
-            self.tool_row += 1
+        try:
+            self.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
 
-        # make the diameter column editable
-        for row in range(self.tool_row):
-            self.tools_table_exc.item(row, 1).setFlags(
-                QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.tools_table_exc.item(row, 2).setForeground(QtGui.QColor(0, 0, 0))
-            self.tools_table_exc.item(row, 3).setForeground(QtGui.QColor(0, 0, 0))
+    def clear(self):
+        self.active_tool = None
+        # self.shape_buffer = []
+        self.selected = []
 
-        # add a last row with the Total number of drills
-        # HACK: made the text on this cell '9999' such it will always be the one before last when sorting
-        # it will have to have the foreground color (font color) white
-        empty = QtWidgets.QTableWidgetItem('9998')
-        empty.setForeground(QtGui.QColor(255, 255, 255))
+        self.points_edit = {}
+        self.new_tools = {}
+        self.new_drills = []
 
-        empty.setFlags(empty.flags() ^ QtCore.Qt.ItemIsEnabled)
-        empty_b = QtWidgets.QTableWidgetItem('')
-        empty_b.setFlags(empty_b.flags() ^ QtCore.Qt.ItemIsEnabled)
+        # self.storage_dict = {}
 
-        label_tot_drill_count = QtWidgets.QTableWidgetItem(_('Total Drills'))
-        tot_drill_count = QtWidgets.QTableWidgetItem('%d' % self.tot_drill_cnt)
+        self.shapes.clear(update=True)
+        self.tool_shape.clear(update=True)
 
-        label_tot_drill_count.setFlags(label_tot_drill_count.flags() ^ QtCore.Qt.ItemIsEnabled)
-        tot_drill_count.setFlags(tot_drill_count.flags() ^ QtCore.Qt.ItemIsEnabled)
+        # self.storage = AppExcEditor.make_storage()
+        self.replot()
 
-        self.tools_table_exc.setItem(self.tool_row, 0, empty)
-        self.tools_table_exc.setItem(self.tool_row, 1, label_tot_drill_count)
-        self.tools_table_exc.setItem(self.tool_row, 2, tot_drill_count)  # Total number of drills
-        self.tools_table_exc.setItem(self.tool_row, 3, empty_b)
+    def edit_fcexcellon(self, exc_obj):
+        """
+        Imports the geometry from the given FlatCAM Excellon object
+        into the editor.
 
-        font = QtGui.QFont()
-        font.setBold(True)
-        font.setWeight(75)
+        :param exc_obj: ExcellonObject object
+        :return: None
+        """
 
-        for k in [1, 2]:
-            self.tools_table_exc.item(self.tool_row, k).setForeground(QtGui.QColor(127, 0, 255))
-            self.tools_table_exc.item(self.tool_row, k).setFont(font)
+        self.deactivate()
+        self.activate()
 
-        self.tool_row += 1
+        # Hide original geometry
+        self.exc_obj = exc_obj
+        exc_obj.visible = False
 
-        # add a last row with the Total number of slots
-        # HACK: made the text on this cell '9999' such it will always be the last when sorting
-        # it will have to have the foreground color (font color) white
-        empty_2 = QtWidgets.QTableWidgetItem('9999')
-        empty_2.setForeground(QtGui.QColor(255, 255, 255))
+        if self.exc_obj:
+            outname = self.exc_obj.options['name']
+        else:
+            outname = ''
 
-        empty_2.setFlags(empty_2.flags() ^ QtCore.Qt.ItemIsEnabled)
+        self.data_defaults = {
+            "name":                         outname + '_drill',
+            "plot":                         self.app.defaults["excellon_plot"],
+            "solid":                        self.app.defaults["excellon_solid"],
+            "multicolored":                 self.app.defaults["excellon_multicolored"],
+            "merge_fuse_tools":             self.app.defaults["excellon_merge_fuse_tools"],
+            "format_upper_in":              self.app.defaults["excellon_format_upper_in"],
+            "format_lower_in":              self.app.defaults["excellon_format_lower_in"],
+            "format_upper_mm":              self.app.defaults["excellon_format_upper_mm"],
+            "lower_mm":                     self.app.defaults["excellon_format_lower_mm"],
+            "zeros":                        self.app.defaults["excellon_zeros"],
+
+            "tools_drill_tool_order":       self.app.defaults["tools_drill_tool_order"],
+            "tools_drill_cutz":             self.app.defaults["tools_drill_cutz"],
+            "tools_drill_multidepth":       self.app.defaults["tools_drill_multidepth"],
+            "tools_drill_depthperpass":     self.app.defaults["tools_drill_depthperpass"],
+            "tools_drill_travelz":          self.app.defaults["tools_drill_travelz"],
+
+            "tools_drill_feedrate_z":       self.app.defaults["tools_drill_feedrate_z"],
+            "tools_drill_feedrate_rapid":   self.app.defaults["tools_drill_feedrate_rapid"],
+
+            "tools_drill_toolchange":       self.app.defaults["tools_drill_toolchange"],
+            "tools_drill_toolchangez":      self.app.defaults["tools_drill_toolchangez"],
+            "tools_drill_toolchangexy":     self.app.defaults["tools_drill_toolchangexy"],
+
+            # Drill Slots
+            "tools_drill_drill_slots":      self.app.defaults["tools_drill_drill_slots"],
+            "tools_drill_drill_overlap":    self.app.defaults["tools_drill_drill_overlap"],
+            "tools_drill_last_drill":       self.app.defaults["tools_drill_last_drill"],
+
+            "tools_drill_endz":             self.app.defaults["tools_drill_endz"],
+            "tools_drill_endxy":            self.app.defaults["tools_drill_endxy"],
+            "tools_drill_startz":           self.app.defaults["tools_drill_startz"],
+            "tools_drill_offset":           self.app.defaults["tools_drill_offset"],
+            "tools_drill_spindlespeed":     self.app.defaults["tools_drill_spindlespeed"],
+            "tools_drill_dwell":            self.app.defaults["tools_drill_dwell"],
+            "tools_drill_dwelltime":        self.app.defaults["tools_drill_dwelltime"],
+            "tools_drill_ppname_e":         self.app.defaults["tools_drill_ppname_e"],
+            "tools_drill_z_pdepth":         self.app.defaults["tools_drill_z_pdepth"],
+            "tools_drill_feedrate_probe":   self.app.defaults["tools_drill_feedrate_probe"],
+            "tools_drill_spindledir":       self.app.defaults["tools_drill_spindledir"],
+            "tools_drill_f_plunge":         self.app.defaults["tools_drill_f_plunge"],
+            "tools_drill_f_retract":        self.app.defaults["tools_drill_f_retract"],
+
+            "tools_drill_area_exclusion":   self.app.defaults["tools_drill_area_exclusion"],
+            "tools_drill_area_shape":       self.app.defaults["tools_drill_area_shape"],
+            "tools_drill_area_strategy":    self.app.defaults["tools_drill_area_strategy"],
+            "tools_drill_area_overz":       self.app.defaults["tools_drill_area_overz"],
+        }
 
-        empty_3 = QtWidgets.QTableWidgetItem('')
-        empty_3.setFlags(empty_3.flags() ^ QtCore.Qt.ItemIsEnabled)
+        # fill in self.default_data values from self.options
+        for opt_key, opt_val in self.app.options.items():
+            if opt_key.find('excellon_') == 0:
+                self.data_defaults[opt_key] = deepcopy(opt_val)
 
-        label_tot_slot_count = QtWidgets.QTableWidgetItem(_('Total Slots'))
-        tot_slot_count = QtWidgets.QTableWidgetItem('%d' % self.tot_slot_cnt)
-        label_tot_slot_count.setFlags(label_tot_slot_count.flags() ^ QtCore.Qt.ItemIsEnabled)
-        tot_slot_count.setFlags(tot_slot_count.flags() ^ QtCore.Qt.ItemIsEnabled)
+        self.points_edit = {}
+        # build the self.points_edit dict {dimaters: [point_list]}
+        for tool, tool_dict in self.exc_obj.tools.items():
+            tool_dia = self.dec_format(self.exc_obj.tools[tool]['tooldia'])
 
-        self.tools_table_exc.setItem(self.tool_row, 0, empty_2)
-        self.tools_table_exc.setItem(self.tool_row, 1, label_tot_slot_count)
-        self.tools_table_exc.setItem(self.tool_row, 2, empty_3)
-        self.tools_table_exc.setItem(self.tool_row, 3, tot_slot_count)  # Total number of slots
+            if 'drills' in tool_dict and tool_dict['drills']:
+                for drill in tool_dict['drills']:
+                    try:
+                        self.points_edit[tool_dia].append(drill)
+                    except KeyError:
+                        self.points_edit[tool_dia] = [drill]
 
-        for kl in [1, 2, 3]:
-            self.tools_table_exc.item(self.tool_row, kl).setFont(font)
-            self.tools_table_exc.item(self.tool_row, kl).setForeground(QtGui.QColor(0, 70, 255))
+        self.slot_points_edit = {}
+        # build the self.slot_points_edit dict {dimaters: {"start": Point, "stop": Point}}
+        for tool, tool_dict in self.exc_obj.tools.items():
+            tool_dia = float('%.*f' % (self.decimals, self.exc_obj.tools[tool]['tooldia']))
 
-        # all the tools are selected by default
-        self.tools_table_exc.selectColumn(0)
-        #
-        self.tools_table_exc.resizeColumnsToContents()
-        self.tools_table_exc.resizeRowsToContents()
+            if 'slots' in tool_dict and tool_dict['slots']:
+                for slot in tool_dict['slots']:
+                    try:
+                        self.slot_points_edit[tool_dia].append({
+                            "start": slot[0],
+                            "stop": slot[1]
+                        })
+                    except KeyError:
+                        self.slot_points_edit[tool_dia] = [{
+                            "start": slot[0],
+                            "stop": slot[1]
+                        }]
 
-        vertical_header = self.tools_table_exc.verticalHeader()
-        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
-        vertical_header.hide()
-        self.tools_table_exc.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+        # Set selection tolerance
+        # DrawToolShape.tolerance = fc_excellon.drawing_tolerance * 10
 
-        horizontal_header = self.tools_table_exc.horizontalHeader()
-        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
-        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
-        # horizontal_header.setStretchLastSection(True)
+        self.select_tool("drill_select")
 
-        # self.tools_table_exc.setSortingEnabled(True)
-        # sort by tool diameter
-        self.tools_table_exc.sortItems(1)
+        # reset the tool table
+        self.e_ui.tools_table_exc.clear()
+        self.e_ui.tools_table_exc.setHorizontalHeaderLabels(['#', _('Diameter'), 'D', 'S'])
+        self.last_tool_selected = None
 
-        # After sorting, to display also the number of drills in the right row we need to update self.initial_rows dict
-        # with the new order. Of course the last 2 rows in the tool table are just for display therefore we don't
-        # use them
-        self.tool2tooldia.clear()
-        for row in range(self.tools_table_exc.rowCount() - 2):
-            tool = int(self.tools_table_exc.item(row, 0).text())
-            diameter = float(self.tools_table_exc.item(row, 1).text())
-            self.tool2tooldia[tool] = diameter
+        self.set_ui()
 
-        self.tools_table_exc.setMinimumHeight(self.tools_table_exc.getHeight())
-        self.tools_table_exc.setMaximumHeight(self.tools_table_exc.getHeight())
+        # now that we have data, create the appGUI interface and add it to the Tool Tab
+        self.build_ui(first_run=True)
 
-        # make sure no rows are selected so the user have to click the correct row, meaning selecting the correct tool
-        self.tools_table_exc.clearSelection()
+        # we activate this after the initial build as we don't need to see the tool been populated
+        self.e_ui.tools_table_exc.itemChanged.connect(self.on_tool_edit)
 
-        # Remove anything else in the GUI Selected Tab
-        self.app.ui.selected_scroll_area.takeWidget()
-        # Put ourself in the GUI Selected Tab
-        self.app.ui.selected_scroll_area.setWidget(self.exc_edit_widget)
-        # Switch notebook to Selected page
-        self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+        # build the geometry for each tool-diameter, each drill will be represented by a '+' symbol
+        # and then add it to the storage elements (each storage elements is a member of a list
+        for tool_dia in self.points_edit:
+            storage_elem = AppGeoEditor.make_storage()
+            for point in self.points_edit[tool_dia]:
+                # make a '+' sign, the line length is the tool diameter
+                start_hor_line = ((point.x - (tool_dia / 2)), point.y)
+                stop_hor_line = ((point.x + (tool_dia / 2)), point.y)
+                start_vert_line = (point.x, (point.y - (tool_dia / 2)))
+                stop_vert_line = (point.x, (point.y + (tool_dia / 2)))
+                shape_geo = MultiLineString([(start_hor_line, stop_hor_line), (start_vert_line, stop_vert_line)])
+                if shape_geo is not None:
+                    self.add_exc_shape(DrawToolShape(shape_geo), storage_elem)
+            self.storage_dict[tool_dia] = storage_elem
 
-        # we reactivate the signals after the after the tool adding as we don't need to see the tool been populated
-        self.tools_table_exc.itemChanged.connect(self.on_tool_edit)
-        self.tools_table_exc.cellPressed.connect(self.on_row_selected)
+        # slots
+        for tool_dia in self.slot_points_edit:
+            buf_value = float(tool_dia) / 2
+            for elem_dict in self.slot_points_edit[tool_dia]:
 
-    def on_tool_add(self, tooldia=None):
-        self.is_modified = True
-        if tooldia:
-            tool_dia = tooldia
-        else:
-            try:
-                tool_dia = float(self.addtool_entry.get_value())
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    tool_dia = float(self.addtool_entry.get_value().replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                         _("Wrong value format entered, use a number."))
-                    return
-
-        if tool_dia not in self.olddia_newdia:
-            storage_elem = FlatCAMGeoEditor.make_storage()
-            self.storage_dict[tool_dia] = storage_elem
-
-            # self.olddia_newdia dict keeps the evidence on current tools diameters as keys and gets updated on values
-            # each time a tool diameter is edited or added
-            self.olddia_newdia[tool_dia] = tool_dia
-        else:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Tool already in the original or actual tool list.\n" 
-                                                          "Save and reedit Excellon if you need to add this tool. "))
-            return
-
-        # since we add a new tool, we update also the initial state of the tool_table through it's dictionary
-        # we add a new entry in the tool2tooldia dict
-        self.tool2tooldia[len(self.olddia_newdia)] = tool_dia
+                line_geo = LineString([elem_dict['start'], elem_dict['stop']])
+                shape_geo = line_geo.buffer(buf_value)
 
-        self.app.inform.emit('[success] %s: %s %s' %
-                             (_("Added new tool with dia"), str(tool_dia), str(self.units)))
+                if tool_dia not in self.storage_dict:
+                    storage_elem = AppGeoEditor.make_storage()
+                    self.storage_dict[tool_dia] = storage_elem
 
-        self.build_ui()
+                if shape_geo is not None:
+                    self.add_exc_shape(DrawToolShape(shape_geo), self.storage_dict[tool_dia])
 
-        # make a quick sort through the tool2tooldia dict so we find which row to select
-        row_to_be_selected = None
-        for key in sorted(self.tool2tooldia):
-            if self.tool2tooldia[key] == tool_dia:
-                row_to_be_selected = int(key) - 1
-                self.last_tool_selected = int(key)
-                break
-        try:
-            self.tools_table_exc.selectRow(row_to_be_selected)
-        except TypeError as e:
-            log.debug("FlatCAMExcEditor.on_tool_add() --> %s" % str(e))
+        self.replot()
 
-    def on_tool_delete(self, dia=None):
-        self.is_modified = True
-        deleted_tool_dia_list = []
+        # add a first tool in the Tool Table but only if the Excellon Object is empty
+        if not self.tool2tooldia:
+            self.on_tool_add(self.dec_format(float(self.app.defaults['excellon_editor_newdia'])))
 
-        try:
-            if dia is None or dia is False:
-                # deleted_tool_dia = float(self.tools_table_exc.item(self.tools_table_exc.currentRow(), 1).text())
-                for index in self.tools_table_exc.selectionModel().selectedRows():
-                    row = index.row()
-                    deleted_tool_dia_list.append(float(self.tools_table_exc.item(row, 1).text()))
-            else:
-                if isinstance(dia, list):
-                    for dd in dia:
-                        deleted_tool_dia_list.append(float('%.*f' % (self.decimals, dd)))
-                else:
-                    deleted_tool_dia_list.append(float('%.*f' % (self.decimals, dia)))
-        except Exception:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Select a tool in Tool Table"))
-            return
+    def update_fcexcellon(self, exc_obj):
+        """
+        Create a new Excellon object that contain the edited content of the source Excellon object
 
-        for deleted_tool_dia in deleted_tool_dia_list:
+        :param exc_obj: ExcellonObject
+        :return: None
+        """
 
-            # delete the storage used for that tool
-            storage_elem = FlatCAMGeoEditor.make_storage()
-            self.storage_dict[deleted_tool_dia] = storage_elem
-            self.storage_dict.pop(deleted_tool_dia, None)
+        # this dictionary will contain tooldia's as keys and a list of coordinates tuple as values
+        # the values of this dict are coordinates of the holes (drills)
+        edited_points = {}
 
-            # I've added this flag_del variable because dictionary don't like
-            # having keys deleted while iterating through them
-            flag_del = []
-            # self.points_edit.pop(deleted_tool_dia, None)
-            for deleted_tool in self.tool2tooldia:
-                if self.tool2tooldia[deleted_tool] == deleted_tool_dia:
-                    flag_del.append(deleted_tool)
+        """
+         - this dictionary will contain tooldia's as keys and a list of another dicts as values
+         - the dict element of the list has the structure
+         ================  ====================================
+        Key               Value
+        ================  ====================================
+        start             (Shapely.Point) Start point of the slot
+        stop              (Shapely.Point) Stop point of the slot
+        ================  ====================================
+        """
+        edited_slot_points = {}
 
-            if flag_del:
-                for tool_to_be_deleted in flag_del:
-                    # delete the tool
-                    self.tool2tooldia.pop(tool_to_be_deleted, None)
+        for storage_tooldia in self.storage_dict:
+            for x in self.storage_dict[storage_tooldia].get_objects():
+                if isinstance(x.geo, MultiLineString):
+                    # all x.geo in self.storage_dict[storage] are MultiLinestring objects for drills
+                    # each MultiLineString is made out of Linestrings
+                    # select first Linestring object in the current MultiLineString
+                    first_linestring = x.geo[0]
+                    # get it's coordinates
+                    first_linestring_coords = first_linestring.coords
+                    x_coord = first_linestring_coords[0][0] + (float(first_linestring.length / 2))
+                    y_coord = first_linestring_coords[0][1]
 
-                    # delete also the drills from points_edit dict just in case we add the tool again,
-                    # we don't want to show the number of drills from before was deleter
-                    self.points_edit[deleted_tool_dia] = []
+                    # create a tuple with the coordinates (x, y) and add it to the list that is the value of the
+                    # edited_points dictionary
+                    point = (x_coord, y_coord)
+                    if storage_tooldia not in edited_points:
+                        edited_points[storage_tooldia] = [point]
+                    else:
+                        edited_points[storage_tooldia].append(point)
+                elif isinstance(x.geo, Polygon):
+                    # create a tuple with the points (start, stop) and add it to the list that is the value of the
+                    # edited_points dictionary
 
-            self.olddia_newdia.pop(deleted_tool_dia, None)
+                    # first determine the start and stop coordinates for the slot knowing the geometry and the tool
+                    # diameter
+                    radius = float(storage_tooldia) / 2
+                    radius = radius - 0.0000001
 
-            self.app.inform.emit('[success] %s: %s %s' %
-                                 (_("Deleted tool with diameter"), str(deleted_tool_dia), str(self.units)))
+                    poly = x.geo
+                    poly = poly.buffer(-radius)
 
-        self.replot()
-        # self.app.inform.emit("Could not delete selected tool")
+                    if not poly.is_valid or poly.is_empty:
+                        # print("Polygon not valid: %s" % str(poly.wkt))
+                        continue
 
-        self.build_ui()
+                    xmin, ymin, xmax, ymax = poly.bounds
+                    line_one = LineString([(xmin, ymin), (xmax, ymax)]).intersection(poly).length
+                    line_two = LineString([(xmin, ymax), (xmax, ymin)]).intersection(poly).length
 
-    def on_tool_edit(self, item_changed):
-        # if connected, disconnect the signal from the slot on item_changed as it creates issues
-        try:
-            self.tools_table_exc.itemChanged.disconnect()
-        except TypeError:
-            pass
+                    if line_one < line_two:
+                        point_elem = {
+                            "start": (xmin, ymax),
+                            "stop": (xmax, ymin)
+                        }
+                    else:
+                        point_elem = {
+                            "start": (xmin, ymin),
+                            "stop": (xmax, ymax)
+                        }
 
-        try:
-            self.tools_table_exc.cellPressed.disconnect()
-        except TypeError:
-            pass
-        # self.tools_table_exc.selectionModel().currentChanged.disconnect()
+                    if storage_tooldia not in edited_slot_points:
+                        edited_slot_points[storage_tooldia] = [point_elem]
+                    else:
+                        edited_slot_points[storage_tooldia].append(point_elem)
 
-        self.is_modified = True
-        # new_dia = None
+        # recreate the drills and tools to be added to the new Excellon edited object
+        # first, we look in the tool table if one of the tool diameters was changed then
+        # append that a tuple formed by (old_dia, edited_dia) to a list
+        changed_key = set()
+        for initial_dia in self.olddia_newdia:
+            edited_dia = self.olddia_newdia[initial_dia]
+            if edited_dia != initial_dia:
+                # for drills
+                for old_dia in edited_points:
+                    if old_dia == initial_dia:
+                        changed_key.add((old_dia, edited_dia))
+                # for slots
+                for old_dia in edited_slot_points:
+                    if old_dia == initial_dia:
+                        changed_key.add((old_dia, edited_dia))
+            # if the initial_dia is not in edited_points it means it is a new tool with no drill points
+            # (and we have to add it)
+            # because in case we have drill points it will have to be already added in edited_points
+            # if initial_dia not in edited_points.keys():
+            #     edited_points[initial_dia] = []
 
-        try:
-            new_dia = float(self.tools_table_exc.currentItem().text())
-        except ValueError as e:
-            log.debug("FlatCAMExcEditor.on_tool_edit() --> %s" % str(e))
-            return
+        for el in changed_key:
+            edited_points[el[1]] = edited_points.pop(el[0])
+            edited_slot_points[el[1]] = edited_slot_points.pop(el[0])
 
-        row_of_item_changed = self.tools_table_exc.currentRow()
-        # rows start with 0, tools start with 1 so we adjust the value by 1
-        key_in_tool2tooldia = row_of_item_changed + 1
-        old_dia = self.tool2tooldia[key_in_tool2tooldia]
+        # Let's sort the edited_points dictionary by keys (diameters) and store the result in a zipped list
+        # ordered_edited_points is a ordered list of tuples;
+        # element[0] of the tuple is the diameter and
+        # element[1] of the tuple is a list of coordinates (a tuple themselves)
+        ordered_edited_points = sorted(zip(edited_points.keys(), edited_points.values()))
 
-        # SOURCE storage
-        source_storage = self.storage_dict[old_dia]
+        current_tool = 0
+        for tool_dia in ordered_edited_points:
+            current_tool += 1
 
-        # DESTINATION storage
-        # tool diameter is not used so we create a new tool with the desired diameter
-        if new_dia not in self.olddia_newdia:
-            destination_storage = FlatCAMGeoEditor.make_storage()
-            self.storage_dict[new_dia] = destination_storage
+            # create the self.tools for the new Excellon object (the one with edited content)
+            if current_tool not in self.new_tools:
+                self.new_tools[current_tool] = {}
+            self.new_tools[current_tool]['tooldia'] = float(tool_dia[0])
 
-            # self.olddia_newdia dict keeps the evidence on current tools diameters as keys and gets updated on values
-            # each time a tool diameter is edited or added
-            self.olddia_newdia[new_dia] = new_dia
-        else:
-            # tool diameter is already in use so we move the drills from the prior tool to the new tool
-            destination_storage = self.storage_dict[new_dia]
+            # add in self.tools the 'solid_geometry' key, the value (a list) is populated below
+            self.new_tools[current_tool]['solid_geometry'] = []
 
-        # since we add a new tool, we update also the intial state of the tool_table through it's dictionary
-        # we add a new entry in the tool2tooldia dict
-        self.tool2tooldia[len(self.olddia_newdia)] = new_dia
+            # create the self.drills for the new Excellon object (the one with edited content)
+            for point in tool_dia[1]:
+                try:
+                    self.new_tools[current_tool]['drills'].append(Point(point))
+                except KeyError:
+                    self.new_tools[current_tool]['drills'] = [Point(point)]
 
-        # CHANGE the elements geometry according to the new diameter
-        factor = new_dia / old_dia
-        new_geo = Polygon()
-        for shape_exc in source_storage.get_objects():
-            geo_list = []
-            if isinstance(shape_exc.geo, MultiLineString):
-                for subgeo in shape_exc.geo:
-                    geo_list.append(affinity.scale(subgeo, xfact=factor, yfact=factor, origin='center'))
-                new_geo = MultiLineString(geo_list)
-            elif isinstance(shape_exc.geo, Polygon):
-                # I don't have any info regarding the angle of the slot geometry, nor how thick it is or
-                # how long it is given the angle. So I will have to make an approximation because
-                # we need to conserve the slot length, we only resize the diameter for the tool
-                # Therefore scaling won't work and buffering will not work either.
+                # repopulate the 'solid_geometry' for each tool
+                poly = Point(point).buffer(float(tool_dia[0]) / 2.0, int(int(exc_obj.geo_steps_per_circle) / 4))
+                self.new_tools[current_tool]['solid_geometry'].append(poly)
 
-                # First we get the Linestring that is one that the original slot is built around with the
-                # tool having the diameter sel_dia
-                poly = shape_exc.geo
-                xmin, ymin, xmax, ymax = poly.bounds
-                # a line that is certain to be bigger than our slot because it's the diagonal
-                # of it's bounding box
-                poly_diagonal = LineString([(xmin, ymin), (xmax, ymax)])
-                poly_centroid = poly.centroid
-                # center of the slot geometry
-                poly_center = (poly_centroid.x, poly_centroid.y)
+        ordered_edited_slot_points = sorted(zip(edited_slot_points.keys(), edited_slot_points.values()))
+        for tool_dia in ordered_edited_slot_points:
 
-                # make a list of intersections with the rotated line
-                list_of_cuttings = []
-                for angle in range(0, 359, 1):
-                    rot_poly_diagonal = affinity.rotate(poly_diagonal, angle=angle, origin=poly_center)
-                    cut_line = rot_poly_diagonal.intersection(poly)
-                    cut_line_len = cut_line.length
-                    list_of_cuttings.append(
-                        (cut_line_len, cut_line)
-                    )
-                # find the cut_line with the maximum length which is the LineString for which the start
-                # and stop point are the start and stop point of the slot as in the Gerber file
-                cut_line_with_max_length = max(list_of_cuttings, key=lambda i: i[0])[1]
-                # find the coordinates of this line
-                cut_line_with_max_length_coords = list(cut_line_with_max_length.coords)
-                # extract the first and last point of the line and build some buffered polygon circles
-                # around them
-                start_pt = Point(cut_line_with_max_length_coords[0])
-                stop_pt = Point(cut_line_with_max_length_coords[1])
-                start_cut_geo = start_pt.buffer(new_dia / 2)
-                stop_cut_geo = stop_pt.buffer(new_dia / 2)
+            tool_exist_flag = False
+            for tool in self.new_tools:
+                if tool_dia[0] == self.new_tools[tool]["tooldia"]:
+                    current_tool = tool
+                    tool_exist_flag = True
+                    break
 
-                # and we cut the above circle polygons from our line and get in this way a line around
-                # which we can build the new slot by buffering with the new tool diameter
-                new_line = cut_line_with_max_length.difference(start_cut_geo)
-                new_line = new_line.difference(stop_cut_geo)
+            if tool_exist_flag is False:
+                current_tool += 1
 
-                # create the geometry for the resized slot by buffering with half of the
-                # new diameter value: new_dia
-                new_geo = new_line.buffer(new_dia / 2)
+                # create the self.tools for the new Excellon object (the one with edited content)
+                if current_tool not in self.new_tools:
+                    self.new_tools[current_tool] = {}
+                self.new_tools[current_tool]['tooldia'] = float(tool_dia[0])
 
-            try:
-                self.points_edit.pop(old_dia, None)
-            except KeyError:
-                pass
-            try:
-                self.slot_points_edit.pop(old_dia, None)
-            except KeyError:
-                pass
+                # add in self.tools the 'solid_geometry' key, the value (a list) is populated below
+                self.new_tools[current_tool]['solid_geometry'] = []
 
-            # add bogus drill/slots points (for total count of drills/slots)
-            # for drills
-            if isinstance(shape_exc.geo, MultiLineString):
-                if new_dia not in self.points_edit:
-                    self.points_edit[new_dia] = [(0, 0)]
-                else:
-                    self.points_edit[new_dia].append((0, 0))
+            # create the self.slots for the new Excellon object (the one with edited content)
+            for coord_dict in tool_dia[1]:
+                slot = (
+                    Point(coord_dict['start']),
+                    Point(coord_dict['stop'])
+                )
+                try:
+                    self.new_tools[current_tool]['slots'].append(slot)
+                except KeyError:
+                    self.new_tools[current_tool]['slots'] = [slot]
 
-            # for slots
-            if isinstance(shape_exc.geo, Polygon):
-                if new_dia not in self.slot_points_edit:
-                    self.slot_points_edit[new_dia] = [(0, 0)]
-                else:
-                    self.slot_points_edit[new_dia].append((0, 0))
+                # repopulate the 'solid_geometry' for each tool
+                poly = LineString([coord_dict['start'], coord_dict['stop']]).buffer(
+                    float(tool_dia[0]) / 2.0, int(int(exc_obj.geo_steps_per_circle) / 4)
+                )
+                self.new_tools[current_tool]['solid_geometry'].append(poly)
 
-            self.add_exc_shape(shape=DrawToolShape(new_geo), storage=destination_storage)
+        if self.is_modified is True:
+            if "_edit" in self.edited_obj_name:
+                try:
+                    idd = int(self.edited_obj_name[-1]) + 1
+                    self.edited_obj_name = self.edited_obj_name[:-1] + str(idd)
+                except ValueError:
+                    self.edited_obj_name += "_1"
+            else:
+                self.edited_obj_name += "_edit"
 
-        # update the UI and the CANVAS
-        self.build_ui()
-        self.replot()
+        self.app.worker_task.emit({'fcn': self.new_edited_excellon,
+                                   'params': [self.edited_obj_name,
+                                              self.new_drills,
+                                              self.new_slots,
+                                              self.new_tools]})
 
-        # delete the old tool
-        self.on_tool_delete(dia=old_dia)
+        return self.edited_obj_name
 
-        # we reactivate the signals after the after the tool editing
-        self.tools_table_exc.itemChanged.connect(self.on_tool_edit)
-        self.tools_table_exc.cellPressed.connect(self.on_row_selected)
+    @staticmethod
+    def update_options(obj):
+        try:
+            if not obj.options:
+                obj.options = {}
+                obj.options['xmin'] = 0
+                obj.options['ymin'] = 0
+                obj.options['xmax'] = 0
+                obj.options['ymax'] = 0
+                return True
+            else:
+                return False
+        except AttributeError:
+            obj.options = {}
+            return True
 
-        self.app.inform.emit('[success] %s' %
-                             _("Done. Tool edit completed."))
+    def new_edited_excellon(self, outname, n_drills, n_slots, n_tools):
+        """
+        Creates a new Excellon object for the edited Excellon. Thread-safe.
 
-        # self.tools_table_exc.selectionModel().currentChanged.connect(self.on_row_selected)
+        :param outname:     Name of the resulting object. None causes the
+                            name to be that of the file.
+        :type outname:      str
 
-    def on_name_activate(self):
-        self.edited_obj_name = self.name_entry.get_value()
+        :param n_drills:    The new Drills storage
+        :param n_slots:     The new Slots storage
+        :param n_tools:     The new Tools storage
+        :return:            None
+        """
 
-    def activate(self):
-        # adjust the status of the menu entries related to the editor
-        self.app.ui.menueditedit.setDisabled(True)
-        self.app.ui.menueditok.setDisabled(False)
-        # adjust the visibility of some of the canvas context menu
-        self.app.ui.popmenu_edit.setVisible(False)
-        self.app.ui.popmenu_save.setVisible(True)
+        self.app.log.debug("Update the Excellon object with edited content. Source is %s" %
+                           self.exc_obj.options['name'])
 
-        self.connect_canvas_event_handlers()
+        new_drills = n_drills
+        new_slots = n_slots
+        new_tools = n_tools
 
-        # initialize working objects
-        self.storage_dict = {}
-        self.current_storage = []
-        self.points_edit = {}
-        self.sorted_diameters = []
-        self.new_drills = []
-        self.new_tools = {}
-        self.new_slots = []
+        # How the object should be initialized
+        def obj_init(excellon_obj, app_obj):
 
-        self.olddia_newdia = {}
+            excellon_obj.drills = deepcopy(new_drills)
+            excellon_obj.tools = deepcopy(new_tools)
+            excellon_obj.slots = deepcopy(new_slots)
 
-        self.shapes.enabled = True
-        self.tool_shape.enabled = True
-        # self.app.app_cursor.enabled = True
+            excellon_obj.options['name'] = outname
 
-        self.app.ui.corner_snap_btn.setVisible(True)
-        self.app.ui.snap_magnet.setVisible(True)
+            # add a 'data' dict for each tool with the default values
+            for tool in excellon_obj.tools:
+                excellon_obj.tools[tool]['data'] = {}
+                excellon_obj.tools[tool]['data'].update(deepcopy(self.data_defaults))
 
-        self.app.ui.exc_editor_menu.setDisabled(False)
-        self.app.ui.exc_editor_menu.menuAction().setVisible(True)
+            try:
+                excellon_obj.create_geometry()
+            except KeyError:
+                self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                     _("There are no Tools definitions in the file. Aborting Excellon creation.")
+                                     )
+            except Exception:
+                msg = '[ERROR] %s' % \
+                      _("An internal error has ocurred. See Shell.\n")
+                msg += traceback.format_exc()
+                app_obj.inform.emit(msg)
+                return
 
-        self.app.ui.update_obj_btn.setEnabled(True)
-        self.app.ui.e_editor_cmenu.setEnabled(True)
+        with self.app.proc_container.new(_("Creating Excellon.")):
 
-        self.app.ui.exc_edit_toolbar.setDisabled(False)
-        self.app.ui.exc_edit_toolbar.setVisible(True)
-        # self.app.ui.status_toolbar.setDisabled(False)
+            try:
+                edited_obj = self.app.app_obj.new_object("excellon", outname, obj_init)
+                edited_obj.source_file = self.app.export_excellon(obj_name=edited_obj.options['name'],
+                                                                  local_use=edited_obj,
+                                                                  filename=None,
+                                                                  use_thread=False)
+            except Exception as e:
+                self.deactivate()
+                log.error("Error on Edited object creation: %s" % str(e))
+                return
 
-        # start with GRID toolbar activated
-        if self.app.ui.grid_snap_btn.isChecked() is False:
-            self.app.ui.grid_snap_btn.trigger()
+            self.deactivate()
+            self.app.inform.emit('[success] %s' % _("Excellon editing finished."))
 
-        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)
+    def on_tool_select(self, tool):
+        """
+        Behavior of the toolbar. Tool initialization.
 
-        # Tell the App that the editor is active
-        self.editor_active = True
+        :rtype : None
+        """
+        current_tool = tool
 
-        # show the UI
-        self.drills_frame.show()
+        self.app.log.debug("on_tool_select('%s')" % tool)
 
-    def deactivate(self):
-        try:
-            QtGui.QGuiApplication.restoreOverrideCursor()
-        except Exception:
-            pass
+        if self.last_tool_selected is None and current_tool != 'drill_select':
+            # self.draw_app.select_tool('drill_select')
+            self.complete = True
+            current_tool = 'drill_select'
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. There is no Tool/Drill selected"))
 
-        # adjust the status of the menu entries related to the editor
-        self.app.ui.menueditedit.setDisabled(False)
-        self.app.ui.menueditok.setDisabled(True)
-        # adjust the visibility of some of the canvas context menu
-        self.app.ui.popmenu_edit.setVisible(True)
-        self.app.ui.popmenu_save.setVisible(False)
+        # This is to make the group behave as radio group
+        if current_tool in self.tools_exc:
+            if self.tools_exc[current_tool]["button"].isChecked():
+                self.app.log.debug("%s is checked." % current_tool)
+                for t in self.tools_exc:
+                    if t != current_tool:
+                        self.tools_exc[t]["button"].setChecked(False)
 
-        self.disconnect_canvas_event_handlers()
-        self.clear()
-        self.app.ui.exc_edit_toolbar.setDisabled(True)
+                # this is where the Editor toolbar classes (button's) are instantiated
+                self.active_tool = self.tools_exc[current_tool]["constructor"](self)
+                # self.app.inform.emit(self.active_tool.start_msg)
+            else:
+                self.app.log.debug("%s is NOT checked." % current_tool)
+                for t in self.tools_exc:
+                    self.tools_exc[t]["button"].setChecked(False)
 
-        self.app.ui.corner_snap_btn.setVisible(False)
-        self.app.ui.snap_magnet.setVisible(False)
+                self.select_tool('drill_select')
+                self.active_tool = FCDrillSelect(self)
 
-        # set the Editor Toolbar visibility to what was before entering in the Editor
-        self.app.ui.exc_edit_toolbar.setVisible(False) if self.toolbar_old_state is False \
-            else self.app.ui.exc_edit_toolbar.setVisible(True)
+    def on_row_selected(self, row, col):
+        if col == 0:
+            key_modifier = QtWidgets.QApplication.keyboardModifiers()
+            if self.app.defaults["global_mselect_key"] == 'Control':
+                modifier_to_use = Qt.ControlModifier
+            else:
+                modifier_to_use = Qt.ShiftModifier
 
-        # Disable visuals
-        self.shapes.enabled = False
-        self.tool_shape.enabled = False
-        # self.app.app_cursor.enabled = False
+            if key_modifier == modifier_to_use:
+                pass
+            else:
+                self.selected = []
+
+            try:
+                selected_dia = self.tool2tooldia[self.e_ui.tools_table_exc.currentRow() + 1]
+                self.last_tool_selected = int(self.e_ui.tools_table_exc.currentRow()) + 1
+                for obj in self.storage_dict[selected_dia].get_objects():
+                    self.selected.append(obj)
+            except Exception as e:
+                self.app.log.debug(str(e))
+
+            self.replot()
+
+    def on_canvas_click(self, event):
+        """
+        event.x and .y have canvas coordinates
+        event.xdata and .ydata have plot coordinates
+
+        :param event:       Event object dispatched by VisPy
+        :return:            None
+        """
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            # event_is_dragging = event.is_dragging
+            # right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            # event_is_dragging = self.app.plotcanvas.is_dragging
+            # right_button = 3
 
-        # Tell the app that the editor is no longer active
-        self.editor_active = False
+        self.pos = self.canvas.translate_coords(event_pos)
 
-        self.app.ui.exc_editor_menu.setDisabled(True)
-        self.app.ui.exc_editor_menu.menuAction().setVisible(False)
+        if self.app.grid_status():
+            self.pos = self.app.geo_editor.snap(self.pos[0], self.pos[1])
+        else:
+            self.pos = (self.pos[0], self.pos[1])
 
-        self.app.ui.update_obj_btn.setEnabled(False)
+        if event.button == 1:
+            self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                                   "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
 
-        self.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)
+            # Selection with left mouse button
+            if self.active_tool is not None and event.button == 1:
+                # Dispatch event to active_tool
+                # msg = self.active_tool.click(self.app.geo_editor.snap(event.xdata, event.ydata))
+                self.active_tool.click(self.app.geo_editor.snap(self.pos[0], self.pos[1]))
 
-        # Show original geometry
-        if self.exc_obj:
-            self.exc_obj.visible = True
+                # If it is a shape generating tool
+                if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete:
+                    if self.current_storage is not None:
+                        self.on_exc_shape_complete(self.current_storage)
+                        self.build_ui()
 
-        # hide the UI
-        self.drills_frame.hide()
+                    # MS: always return to the Select Tool if modifier key is not pressed
+                    # else return to the current tool
+                    key_modifier = QtWidgets.QApplication.keyboardModifiers()
+                    if self.app.defaults["global_mselect_key"] == 'Control':
+                        modifier_to_use = Qt.ControlModifier
+                    else:
+                        modifier_to_use = Qt.ShiftModifier
 
-    def connect_canvas_event_handlers(self):
-        # ## Canvas events
+                    # if modifier key is pressed then we add to the selected list the current shape but if it's already
+                    # in the selected list, we removed it. Therefore first click selects, second deselects.
+                    if key_modifier == modifier_to_use:
+                        self.select_tool(self.active_tool.name)
+                    else:
+                        # return to Select tool but not for FCDrillAdd or FCSlot
+                        if isinstance(self.active_tool, FCDrillAdd) or isinstance(self.active_tool, FCSlot):
+                            self.select_tool(self.active_tool.name)
+                        else:
+                            self.select_tool("drill_select")
+                        return
 
-        # first connect to new, then disconnect the old handlers
-        # don't ask why but if there is nothing connected I've seen issues
-        self.mp = self.canvas.graph_event_connect('mouse_press', self.on_canvas_click)
-        self.mm = self.canvas.graph_event_connect('mouse_move', self.on_canvas_move)
-        self.mr = self.canvas.graph_event_connect('mouse_release', self.on_exc_click_release)
+                if isinstance(self.active_tool, FCDrillSelect):
+                    # self.app.log.debug("Replotting after click.")
+                    self.replot()
+            else:
+                self.app.log.debug("No active tool to respond to click!")
 
-        # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp
-        # but those from FlatCAMGeoEditor
-        if self.app.is_legacy is False:
-            self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
-            self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
-            self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
-            self.app.plotcanvas.graph_event_disconnect('mouse_double_click', self.app.on_mouse_double_click_over_plot)
+    def on_exc_shape_complete(self, storage):
+        self.app.log.debug("on_shape_complete()")
+
+        # Add shape
+        if type(storage) is list:
+            for item_storage in storage:
+                self.add_exc_shape(self.active_tool.geometry, item_storage)
         else:
-            self.app.plotcanvas.graph_event_disconnect(self.app.mp)
-            self.app.plotcanvas.graph_event_disconnect(self.app.mm)
-            self.app.plotcanvas.graph_event_disconnect(self.app.mr)
-            self.app.plotcanvas.graph_event_disconnect(self.app.mdc)
+            self.add_exc_shape(self.active_tool.geometry, storage)
 
-        self.app.collection.view.clicked.disconnect()
+        # Remove any utility shapes
+        self.delete_utility_geometry()
+        self.tool_shape.clear(update=True)
 
-        self.app.ui.popmenu_copy.triggered.disconnect()
-        self.app.ui.popmenu_delete.triggered.disconnect()
-        self.app.ui.popmenu_move.triggered.disconnect()
+        # Replot and reset tool.
+        self.replot()
+        # self.active_tool = type(self.active_tool)(self)
 
-        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)
+    def add_exc_shape(self, shape, storage):
+        """
+        Adds a shape to a specified shape storage.
 
-        # Excellon Editor
-        self.app.ui.drill.triggered.connect(self.exc_add_drill)
-        self.app.ui.drill_array.triggered.connect(self.exc_add_drill_array)
+        :param shape:       Shape to be added.
+        :type shape:        DrawToolShape
+        :param storage:     object where to store the shapes
+        :return:            None
+        """
+        # List of DrawToolShape?
+        if isinstance(shape, list):
+            for subshape in shape:
+                self.add_exc_shape(subshape, storage)
+            return
 
-    def disconnect_canvas_event_handlers(self):
-        # we restore the key and mouse control to FlatCAMApp method
-        # first connect to new, then disconnect the old handlers
-        # don't ask why but if there is nothing connected I've seen issues
-        self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
-        self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot)
-        self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
-                                                              self.app.on_mouse_click_release_over_plot)
-        self.app.mdc = self.app.plotcanvas.graph_event_connect('mouse_double_click',
-                                                               self.app.on_mouse_double_click_over_plot)
-        self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
+        assert isinstance(shape, DrawToolShape), \
+            "Expected a DrawToolShape, got %s" % str(type(shape))
 
-        if self.app.is_legacy is False:
-            self.canvas.graph_event_disconnect('mouse_press', self.on_canvas_click)
-            self.canvas.graph_event_disconnect('mouse_move', self.on_canvas_move)
-            self.canvas.graph_event_disconnect('mouse_release', self.on_exc_click_release)
+        assert shape.geo is not None, \
+            "Shape object has empty geometry (None)"
+
+        assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or not isinstance(shape.geo, list), \
+            "Shape objects has empty geometry ([])"
+
+        if isinstance(shape, DrawToolUtilityShape):
+            self.utility.append(shape)
         else:
-            self.canvas.graph_event_disconnect(self.mp)
-            self.canvas.graph_event_disconnect(self.mm)
-            self.canvas.graph_event_disconnect(self.mr)
+            storage.insert(shape)  # TODO: Check performance
 
-        try:
-            self.app.ui.popmenu_copy.triggered.disconnect(self.exc_copy_drills)
-        except (TypeError, AttributeError):
-            pass
+    def add_shape(self, shape):
+        """
+        Adds a shape to the shape storage.
 
-        try:
-            self.app.ui.popmenu_delete.triggered.disconnect(self.on_delete_btn)
-        except (TypeError, AttributeError):
-            pass
+        :param shape:       Shape to be added.
+        :type shape:        DrawToolShape
+        :return:            None
+        """
 
-        try:
-            self.app.ui.popmenu_move.triggered.disconnect(self.exc_move_drills)
-        except (TypeError, AttributeError):
-            pass
+        # List of DrawToolShape?
+        if isinstance(shape, list):
+            for subshape in shape:
+                self.add_shape(subshape)
+            return
 
-        self.app.ui.popmenu_copy.triggered.connect(self.app.on_copy_command)
-        self.app.ui.popmenu_delete.triggered.connect(self.app.on_delete)
-        self.app.ui.popmenu_move.triggered.connect(self.app.obj_move)
+        assert isinstance(shape, DrawToolShape), \
+            "Expected a DrawToolShape, got %s" % type(shape)
 
-        # Excellon Editor
-        try:
-            self.app.ui.drill.triggered.disconnect(self.exc_add_drill)
-        except (TypeError, AttributeError):
-            pass
+        assert shape.geo is not None, \
+            "Shape object has empty geometry (None)"
 
-        try:
-            self.app.ui.drill_array.triggered.disconnect(self.exc_add_drill_array)
-        except (TypeError, AttributeError):
-            pass
+        assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or not isinstance(shape.geo, list), \
+            "Shape objects has empty geometry ([])"
 
-        try:
-            self.app.jump_signal.disconnect()
-        except (TypeError, AttributeError):
-            pass
+        if isinstance(shape, DrawToolUtilityShape):
+            self.utility.append(shape)
+        # else:
+        #     self.storage.insert(shape)
 
-    def clear(self):
-        self.active_tool = None
-        # self.shape_buffer = []
-        self.selected = []
+    def on_exc_click_release(self, event):
+        """
+        Handler of the "mouse_release" event.
+        It will pop-up the context menu on right mouse click unless there was a panning move (decided in the
+        "mouse_move" event handler) and only if the current tool is the Select tool.
+        It will 'close' a Editor tool if it is the case.
 
-        self.points_edit = {}
-        self.new_tools = {}
-        self.new_drills = []
+        :param event:       Event object dispatched by VisPy SceneCavas
+        :return:            None
+        """
 
-        # self.storage_dict = {}
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            # event_is_dragging = event.is_dragging
+            right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            # event_is_dragging = self.app.plotcanvas.is_dragging
+            right_button = 3
 
-        self.shapes.clear(update=True)
-        self.tool_shape.clear(update=True)
+        pos_canvas = self.canvas.translate_coords(event_pos)
 
-        # self.storage = FlatCAMExcEditor.make_storage()
-        self.replot()
+        if self.app.grid_status():
+            pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+        else:
+            pos = (pos_canvas[0], pos_canvas[1])
 
-    def edit_fcexcellon(self, exc_obj):
-        """
-        Imports the geometry from the given FlatCAM Excellon object
-        into the editor.
+        # if the released mouse button was RMB then test if it was a panning motion or not, if not it was a context
+        # canvas menu
+        try:
+            if event.button == right_button:  # right click
+                if self.app.ui.popMenu.mouse_is_panning is False:
+                    try:
+                        QtGui.QGuiApplication.restoreOverrideCursor()
+                    except Exception:
+                        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] %s' % _("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] %s' % _("Done."))
+                            self.select_tool('drill_select')
 
-        :param exc_obj: ExcellonObject object
-        :return: None
-        """
+                        self.app.cursor = QtGui.QCursor()
+                        self.app.populate_cmenu_grids()
+                        self.app.ui.popMenu.popup(self.app.cursor.pos())
+
+        except Exception as e:
+            log.warning("AppExcEditor.on_exc_click_release() RMB click --> Error: %s" % str(e))
+            raise
 
-        assert isinstance(exc_obj, Excellon), \
-            "Expected an Excellon Object, got %s" % type(exc_obj)
+        # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
+        # selection and then select a type of selection ("enclosing" or "touching")
+        try:
+            if event.button == 1:  # left click
+                if self.app.selection_type is not None:
+                    self.draw_selection_area_handler(self.pos, pos, self.app.selection_type)
+                    self.app.selection_type = None
 
-        self.deactivate()
-        self.activate()
+                elif isinstance(self.active_tool, FCDrillSelect):
+                    self.active_tool.click_release((self.pos[0], self.pos[1]))
 
-        # Hide original geometry
-        self.exc_obj = exc_obj
-        exc_obj.visible = False
+                    # if there are selected objects then plot them
+                    if self.selected:
+                        self.replot()
+        except Exception as e:
+            log.warning("AppExcEditor.on_exc_click_release() LMB click --> Error: %s" % str(e))
+            raise
 
-        self.points_edit = {}
-        self.slot_points_edit = {}
+    def draw_selection_area_handler(self, start, end, sel_type):
+        """
+        This function is called whenever we have a left mouse click release and only we have a left mouse click drag,
+        be it from left to right or from right to left. The direction of the drag is decided in the "mouse_move"
+        event handler.
+        Pressing a modifier key (eg. Ctrl, Shift or Alt) will change the behavior of the selection.
 
-        # Set selection tolerance
-        # DrawToolShape.tolerance = fc_excellon.drawing_tolerance * 10
+        Depending on which tool belongs the selected shapes, the corresponding rows in the Tools Table are selected or
+        deselected.
 
-        self.select_tool("drill_select")
+        :param start:       mouse position when the selection LMB click was done
+        :param end:         mouse position when the left mouse button is released
+        :param sel_type:    if True it's a left to right selection (enclosure), if False it's a 'touch' selection
+        :return:
+        """
 
-        # reset the tool table
-        self.tools_table_exc.clear()
-        self.tools_table_exc.setHorizontalHeaderLabels(['#', _('Diameter'), 'D', 'S'])
-        self.last_tool_selected = None
+        start_pos = (start[0], start[1])
+        end_pos = (end[0], end[1])
+        poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
+        modifiers = None
 
-        self.set_ui()
+        # delete the selection shape that was just drawn, we no longer need it
+        self.app.delete_selection_shape()
 
-        # now that we hava data, create the appGUI interface and add it to the Tool Tab
-        self.build_ui(first_run=True)
+        # detect if a modifier key was pressed while the left mouse button was released
+        self.modifiers = QtWidgets.QApplication.keyboardModifiers()
+        if self.modifiers == QtCore.Qt.ShiftModifier:
+            modifiers = 'Shift'
+        elif self.modifiers == QtCore.Qt.ControlModifier:
+            modifiers = 'Control'
 
-        # we activate this after the initial build as we don't need to see the tool been populated
-        self.tools_table_exc.itemChanged.connect(self.on_tool_edit)
+        if modifiers == self.app.defaults["global_mselect_key"]:
+            for storage in self.storage_dict:
+                for obj in self.storage_dict[storage].get_objects():
+                    if (sel_type is True and poly_selection.contains(obj.geo)) or \
+                            (sel_type is False and poly_selection.intersects(obj.geo)):
 
-        # build the geometry for each tool-diameter, each drill will be represented by a '+' symbol
-        # and then add it to the storage elements (each storage elements is a member of a list
-        for tool_dia in self.points_edit:
-            storage_elem = FlatCAMGeoEditor.make_storage()
-            for point in self.points_edit[tool_dia]:
-                # make a '+' sign, the line length is the tool diameter
-                start_hor_line = ((point.x - (tool_dia / 2)), point.y)
-                stop_hor_line = ((point.x + (tool_dia / 2)), point.y)
-                start_vert_line = (point.x, (point.y - (tool_dia / 2)))
-                stop_vert_line = (point.x, (point.y + (tool_dia / 2)))
-                shape_geo = MultiLineString([(start_hor_line, stop_hor_line), (start_vert_line, stop_vert_line)])
-                if shape_geo is not None:
-                    self.add_exc_shape(DrawToolShape(shape_geo), storage_elem)
-            self.storage_dict[tool_dia] = storage_elem
+                        if obj in self.selected:
+                            # remove the shape object from the selected shapes storage
+                            self.selected.remove(obj)
+                        else:
+                            # add the shape object to the selected shapes storage
+                            self.selected.append(obj)
+        else:
+            # clear the selection shapes storage
+            self.selected = []
+            # then add to the selection shapes storage the shapes that are included (touched) by the selection rectangle
+            for storage in self.storage_dict:
+                for obj in self.storage_dict[storage].get_objects():
+                    if (sel_type is True and poly_selection.contains(obj.geo)) or \
+                            (sel_type is False and poly_selection.intersects(obj.geo)):
+                        self.selected.append(obj)
 
-        # slots
-        for tool_dia in self.slot_points_edit:
-            buf_value = float(tool_dia) / 2
-            for elem_dict in self.slot_points_edit[tool_dia]:
+        try:
+            self.e_ui.tools_table_exc.cellPressed.disconnect()
+        except Exception:
+            pass
 
-                line_geo = LineString([elem_dict['start'], elem_dict['stop']])
-                shape_geo = line_geo.buffer(buf_value)
+        # first deselect all rows (tools) in the Tools Table
+        self.e_ui.tools_table_exc.clearSelection()
+        # and select the rows (tools) in the tool table according to the diameter(s) of the selected shape(s)
+        self.e_ui.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
+        for storage in self.storage_dict:
+            for shape_s in self.selected:
+                if shape_s in self.storage_dict[storage].get_objects():
+                    for key_tool_nr in self.tool2tooldia:
+                        if self.tool2tooldia[key_tool_nr] == storage:
+                            row_to_sel = key_tool_nr - 1
+                            # item = self.e_ui.tools_table_exc.item(row_to_sel, 1)
+                            # self.e_ui.tools_table_exc.setCurrentItem(item)
+                            # item.setSelected(True)
 
-                if tool_dia not in self.storage_dict:
-                    storage_elem = FlatCAMGeoEditor.make_storage()
-                    self.storage_dict[tool_dia] = storage_elem
+                            # if the row to be selected is not already in the selected rows then select it
+                            # otherwise don't do it as it seems that we have a toggle effect
+                            if row_to_sel not in set(index.row() for index in self.e_ui.tools_table_exc.selectedIndexes()):
+                                self.e_ui.tools_table_exc.selectRow(row_to_sel)
+                            self.last_tool_selected = int(key_tool_nr)
 
-                if shape_geo is not None:
-                    self.add_exc_shape(DrawToolShape(shape_geo), self.storage_dict[tool_dia])
+        self.e_ui.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
 
+        self.e_ui.tools_table_exc.cellPressed.connect(self.on_row_selected)
         self.replot()
 
-        # add a first tool in the Tool Table but only if the Excellon Object is empty
-        if not self.tool2tooldia:
-            self.on_tool_add(tooldia=float('%.*f' % (self.decimals,
-                                                     float(self.app.defaults['excellon_editor_newdia']))))
-
-    def update_fcexcellon(self, exc_obj):
+    def on_canvas_move(self, event):
         """
-        Create a new Excellon object that contain the edited content of the source Excellon object
+        Called on 'mouse_move' event.
+        It updates the mouse cursor if the grid snapping is ON.
+        It decide if we have a mouse drag and if it is done with the right mouse click. Then it passes this info to a
+        class object which is used in the "mouse_release" handler to decide if to pop-up the context menu or not.
+        It draws utility_geometry for the Editor tools.
+        Update the position labels from status bar.
+        Decide if we have a right to left or a left to right mouse drag with left mouse button and call a function
+        that will draw a selection shape on canvas.
 
-        :param exc_obj: ExcellonObject
-        :return: None
+        event.pos have canvas screen coordinates
+
+        :param event:       Event object dispatched by VisPy SceneCavas
+        :return:            None
         """
 
-        # this dictionary will contain tooldia's as keys and a list of coordinates tuple as values
-        # the values of this dict are coordinates of the holes (drills)
-        edited_points = {}
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            event_is_dragging = event.is_dragging
+            right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            event_is_dragging = self.app.plotcanvas.is_dragging
+            right_button = 3
 
-        """
-         - this dictionary will contain tooldia's as keys and a list of another dicts as values
-         - the dict element of the list has the structure
-         ================  ====================================
-        Key               Value
-        ================  ====================================
-        start             (Shapely.Point) Start point of the slot
-        stop              (Shapely.Point) Stop point of the slot
-        ================  ====================================
-        """
-        edited_slot_points = {}
+        pos = self.canvas.translate_coords(event_pos)
+        event.xdata, event.ydata = pos[0], pos[1]
 
-        for storage_tooldia in self.storage_dict:
-            for x in self.storage_dict[storage_tooldia].get_objects():
-                if isinstance(x.geo, MultiLineString):
-                    # all x.geo in self.storage_dict[storage] are MultiLinestring objects for drills
-                    # each MultiLineString is made out of Linestrings
-                    # select first Linestring object in the current MultiLineString
-                    first_linestring = x.geo[0]
-                    # get it's coordinates
-                    first_linestring_coords = first_linestring.coords
-                    x_coord = first_linestring_coords[0][0] + (float(first_linestring.length / 2))
-                    y_coord = first_linestring_coords[0][1]
+        self.x = event.xdata
+        self.y = event.ydata
 
-                    # create a tuple with the coordinates (x, y) and add it to the list that is the value of the
-                    # edited_points dictionary
-                    point = (x_coord, y_coord)
-                    if storage_tooldia not in edited_points:
-                        edited_points[storage_tooldia] = [point]
-                    else:
-                        edited_points[storage_tooldia].append(point)
-                elif isinstance(x.geo, Polygon):
-                    # create a tuple with the points (start, stop) and add it to the list that is the value of the
-                    # edited_points dictionary
+        self.app.ui.popMenu.mouse_is_panning = False
 
-                    # first determine the start and stop coordinates for the slot knowing the geometry and the tool
-                    # diameter
-                    radius = float(storage_tooldia) / 2
-                    radius = radius - 0.0000001
+        # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
+        if event.button == right_button and event_is_dragging == 1:
+            self.app.ui.popMenu.mouse_is_panning = True
+            return
 
-                    poly = x.geo
-                    poly = poly.buffer(-radius)
+        try:
+            x = float(event.xdata)
+            y = float(event.ydata)
+        except TypeError:
+            return
 
-                    xmin, ymin, xmax, ymax = poly.bounds
-                    line_one = LineString([(xmin, ymin), (xmax, ymax)]).intersection(poly).length
-                    line_two = LineString([(xmin, ymax), (xmax, ymin)]).intersection(poly).length
+        if self.active_tool is None:
+            return
 
-                    if line_one < line_two:
-                        point_elem = {
-                            "start": (xmin, ymax),
-                            "stop": (xmax, ymin)
-                        }
-                    else:
-                        point_elem = {
-                            "start": (xmin, ymin),
-                            "stop": (xmax, ymax)
-                        }
+        # ## Snap coordinates
+        if self.app.grid_status():
+            x, y = self.app.geo_editor.snap(x, y)
 
-                    if storage_tooldia not in edited_slot_points:
-                        edited_slot_points[storage_tooldia] = [point_elem]
-                    else:
-                        edited_slot_points[storage_tooldia].append(point_elem)
+            # Update cursor
+            self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color=self.app.cursor_color_3D,
+                                         edge_width=self.app.defaults["global_cursor_width"],
+                                         size=self.app.defaults["global_cursor_size"])
 
-        # recreate the drills and tools to be added to the new Excellon edited object
-        # first, we look in the tool table if one of the tool diameters was changed then
-        # append that a tuple formed by (old_dia, edited_dia) to a list
-        changed_key = set()
-        for initial_dia in self.olddia_newdia:
-            edited_dia = self.olddia_newdia[initial_dia]
-            if edited_dia != initial_dia:
-                # for drills
-                for old_dia in edited_points:
-                    if old_dia == initial_dia:
-                        changed_key.add((old_dia, edited_dia))
-                # for slots
-                for old_dia in edited_slot_points:
-                    if old_dia == initial_dia:
-                        changed_key.add((old_dia, edited_dia))
-            # if the initial_dia is not in edited_points it means it is a new tool with no drill points
-            # (and we have to add it)
-            # because in case we have drill points it will have to be already added in edited_points
-            # if initial_dia not in edited_points.keys():
-            #     edited_points[initial_dia] = []
+        self.snap_x = x
+        self.snap_y = y
 
-        for el in changed_key:
-            edited_points[el[1]] = edited_points.pop(el[0])
-            edited_slot_points[el[1]] = edited_slot_points.pop(el[0])
+        if self.pos is None:
+            self.pos = (0, 0)
+        self.app.dx = x - self.pos[0]
+        self.app.dy = y - self.pos[1]
 
-        # Let's sort the edited_points dictionary by keys (diameters) and store the result in a zipped list
-        # ordered_edited_points is a ordered list of tuples;
-        # element[0] of the tuple is the diameter and
-        # element[1] of the tuple is a list of coordinates (a tuple themselves)
-        ordered_edited_points = sorted(zip(edited_points.keys(), edited_points.values()))
+        # # update the position label in the infobar since the APP mouse event handlers are disconnected
+        self.app.ui.position_label.setText("&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
+                                           "<b>Y</b>: %.4f&nbsp;" % (x, y))
+        # # update the reference position label in the infobar since the APP mouse event handlers are disconnected
+        self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                               "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
+
+        units = self.app.defaults["units"].lower()
+        self.app.plotcanvas.text_hud.text = \
+            'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX:  \t{:<.4f} [{:s}]\nY:  \t{:<.4f} [{:s}]'.format(
+                self.app.dx, units, self.app.dy, units, x, units, y, units)
+
+        # ## Utility geometry (animated)
+        self.update_utility_geometry(data=(x, y))
 
-        current_tool = 0
-        for tool_dia in ordered_edited_points:
-            current_tool += 1
+        # ## Selection area on canvas section # ##
+        if event_is_dragging == 1 and event.button == 1:
+            # I make an exception for FCDrillAdd and FCDrillArray because clicking and dragging while making regions
+            # can create strange issues. Also for FCSlot and FCSlotArray
+            if isinstance(self.active_tool, FCDrillAdd) or isinstance(self.active_tool, FCDrillArray) or \
+                    isinstance(self.active_tool, FCSlot) or isinstance(self.active_tool, FCSlotArray):
+                self.app.selection_type = None
+            else:
+                dx = pos[0] - self.pos[0]
+                self.app.delete_selection_shape()
+                if dx < 0:
+                    self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y),
+                                                         color=self.app.defaults["global_alt_sel_line"],
+                                                         face_color=self.app.defaults['global_alt_sel_fill'])
+                    self.app.selection_type = False
+                else:
+                    self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y))
+                    self.app.selection_type = True
+        else:
+            self.app.selection_type = None
 
-            # create the self.tools for the new Excellon object (the one with edited content)
-            name = str(current_tool)
-            spec = {"C": float(tool_dia[0])}
-            self.new_tools[name] = spec
+        # Update cursor
+        self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color=self.app.cursor_color_3D,
+                                     edge_width=self.app.defaults["global_cursor_width"],
+                                     size=self.app.defaults["global_cursor_size"])
 
-            # add in self.tools the 'solid_geometry' key, the value (a list) is populated below
-            self.new_tools[name]['solid_geometry'] = []
+    def update_utility_geometry(self, data):
+        # ### Utility geometry (animated) ###
+        geo = self.active_tool.utility_geometry(data=data)
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
+            # Remove any previous utility shape
+            self.tool_shape.clear(update=True)
+            self.draw_utility_geometry(geo=geo)
 
-            # create the self.drills for the new Excellon object (the one with edited content)
-            for point in tool_dia[1]:
-                self.new_drills.append(
-                    {
-                        'point': Point(point),
-                        'tool': str(current_tool)
-                    }
-                )
-                # repopulate the 'solid_geometry' for each tool
-                poly = Point(point).buffer(float(tool_dia[0]) / 2.0, int(int(exc_obj.geo_steps_per_circle) / 4))
-                self.new_tools[name]['solid_geometry'].append(poly)
+    def on_canvas_key_release(self, event):
+        self.key = None
 
-        ordered_edited_slot_points = sorted(zip(edited_slot_points.keys(), edited_slot_points.values()))
-        for tool_dia in ordered_edited_slot_points:
+    def draw_utility_geometry(self, geo):
+        # Add the new utility shape
+        try:
+            # this case is for the Font Parse
+            for el in list(geo.geo):
+                if type(el) == MultiPolygon:
+                    for poly in el:
+                        self.tool_shape.add(
+                            shape=poly,
+                            color=(self.app.defaults["global_draw_color"] + '80'),
+                            update=False,
+                            layer=0,
+                            tolerance=None
+                        )
+                elif type(el) == MultiLineString:
+                    for linestring in el:
+                        self.tool_shape.add(
+                            shape=linestring,
+                            color=(self.app.defaults["global_draw_color"] + '80'),
+                            update=False,
+                            layer=0,
+                            tolerance=None
+                        )
+                else:
+                    self.tool_shape.add(
+                        shape=el,
+                        color=(self.app.defaults["global_draw_color"] + '80'),
+                        update=False,
+                        layer=0,
+                        tolerance=None
+                    )
+        except TypeError:
+            self.tool_shape.add(
+                shape=geo.geo, color=(self.app.defaults["global_draw_color"] + '80'),
+                update=False, layer=0, tolerance=None)
+        self.tool_shape.redraw()
 
-            tool_exist_flag = False
-            for tool in self.new_tools:
-                if tool_dia[0] == self.new_tools[tool]["C"]:
-                    current_tool = tool
-                    tool_exist_flag = True
-                    break
+    def replot(self):
+        self.plot_all()
 
-            if tool_exist_flag is False:
-                current_tool += 1
-                # create the self.tools for the new Excellon object (the one with edited content)
-                name = str(current_tool)
-                spec = {"C": float(tool_dia[0])}
-                self.new_tools[name] = spec
+    def plot_all(self):
+        """
+        Plots all shapes in the editor.
 
-                # add in self.tools the 'solid_geometry' key, the value (a list) is populated below
-                self.new_tools[name]['solid_geometry'] = []
+        :return:    None
+        :rtype:     None
+        """
 
-            # create the self.slots for the new Excellon object (the one with edited content)
-            for coord_dict in tool_dia[1]:
-                self.new_slots.append(
-                    {
-                        'start': Point(coord_dict['start']),
-                        'stop': Point(coord_dict['stop']),
-                        'tool': str(current_tool)
-                    }
-                )
-                # repopulate the 'solid_geometry' for each tool
-                poly = LineString([coord_dict['start'], coord_dict['stop']]).buffer(
-                    float(tool_dia[0]) / 2.0, int(int(exc_obj.geo_steps_per_circle) / 4)
-                )
-                self.new_tools[str(current_tool)]['solid_geometry'].append(poly)
+        self.shapes.clear(update=True)
 
-        if self.is_modified is True:
-            if "_edit" in self.edited_obj_name:
-                try:
-                    idd = int(self.edited_obj_name[-1]) + 1
-                    self.edited_obj_name = self.edited_obj_name[:-1] + str(idd)
-                except ValueError:
-                    self.edited_obj_name += "_1"
-            else:
-                self.edited_obj_name += "_edit"
+        for storage in self.storage_dict:
+            for shape_plus in self.storage_dict[storage].get_objects():
+                if shape_plus.geo is None:
+                    continue
 
-        self.app.worker_task.emit({'fcn': self.new_edited_excellon,
-                                   'params': [self.edited_obj_name,
-                                              self.new_drills,
-                                              self.new_slots,
-                                              self.new_tools]})
+                if shape_plus in self.selected:
+                    self.plot_shape(geometry=shape_plus.geo, color=self.app.defaults['global_sel_draw_color'] + 'FF',
+                                    linewidth=2)
+                    continue
+                self.plot_shape(geometry=shape_plus.geo, color=self.app.defaults['global_draw_color'] + 'FF')
 
-        return self.edited_obj_name
+        for shape_form in self.utility:
+            self.plot_shape(geometry=shape_form.geo, linewidth=1)
+            continue
 
-    @staticmethod
-    def update_options(obj):
-        try:
-            if not obj.options:
-                obj.options = {}
-                obj.options['xmin'] = 0
-                obj.options['ymin'] = 0
-                obj.options['xmax'] = 0
-                obj.options['ymax'] = 0
-                return True
-            else:
-                return False
-        except AttributeError:
-            obj.options = {}
-            return True
+        self.shapes.redraw()
 
-    def new_edited_excellon(self, outname, n_drills, n_slots, n_tools):
+    def plot_shape(self, geometry=None, color='0x000000FF', linewidth=1):
         """
-        Creates a new Excellon object for the edited Excellon. Thread-safe.
-
-        :param outname:     Name of the resulting object. None causes the
-                            name to be that of the file.
-        :type outname:      str
+        Plots a geometric object or list of objects without rendering. Plotted objects
+        are returned as a list. This allows for efficient/animated rendering.
 
-        :param n_drills:    The new Drills storage
-        :param n_slots:     The new Slots storage
-        :param n_tools:     The new Tools storage
-        :return:            None
+        :param geometry:    Geometry to be plotted (Any Shapely.geom kind or list of such)
+        :param color:       Shape color
+        :param linewidth:   Width of lines in # of pixels.
+        :return:            List of plotted elements.
         """
+        plot_elements = []
 
-        self.app.log.debug("Update the Excellon object with edited content. Source is %s" %
-                           self.exc_obj.options['name'])
+        if geometry is None:
+            geometry = self.active_tool.geometry
 
-        new_drills = n_drills
-        new_slots = n_slots
-        new_tools = n_tools
+        try:
+            for geo in geometry:
+                plot_elements += self.plot_shape(geometry=geo, color=color, linewidth=linewidth)
 
-        # How the object should be initialized
-        def obj_init(excellon_obj, app_obj):
+        # ## Non-iterable
+        except TypeError:
+            # ## DrawToolShape
+            if isinstance(geometry, DrawToolShape):
+                plot_elements += self.plot_shape(geometry=geometry.geo, color=color, linewidth=linewidth)
 
-            excellon_obj.drills = deepcopy(new_drills)
-            excellon_obj.tools = deepcopy(new_tools)
-            excellon_obj.slots = deepcopy(new_slots)
+            # ## Polygon: Descend into exterior and each interior.
+            if type(geometry) == Polygon:
+                plot_elements += self.plot_shape(geometry=geometry.exterior, color=color, linewidth=linewidth)
+                plot_elements += self.plot_shape(geometry=geometry.interiors, color=color, linewidth=linewidth)
 
-            excellon_obj.options['name'] = outname
+            if type(geometry) == LineString or type(geometry) == LinearRing:
+                plot_elements.append(self.shapes.add(shape=geometry, color=color, layer=0, tolerance=self.tolerance))
 
-            # add a 'data' dict for each tool with the default values
-            for tool in excellon_obj.tools:
-                excellon_obj.tools[tool]['data'] = {}
-                excellon_obj.tools[tool]['data'].update(deepcopy(self.data_defaults))
+            if type(geometry) == Point:
+                pass
 
-            try:
-                excellon_obj.create_geometry()
-            except KeyError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("There are no Tools definitions in the file. Aborting Excellon creation.")
-                                     )
-            except Exception:
-                msg = '[ERROR] %s' % \
-                      _("An internal error has ocurred. See Shell.\n")
-                msg += traceback.format_exc()
-                app_obj.inform.emit(msg)
-                return
+        return plot_elements
 
-        with self.app.proc_container.new(_("Creating Excellon.")):
+    def on_shape_complete(self):
+        # Add shape
+        self.add_shape(self.active_tool.geometry)
 
-            try:
-                edited_obj = self.app.app_obj.new_object("excellon", outname, obj_init)
-                edited_obj.source_file = self.app.export_excellon(obj_name=edited_obj.options['name'],
-                                                                  local_use=edited_obj,
-                                                                  filename=None,
-                                                                  use_thread=False)
-            except Exception as e:
-                self.deactivate()
-                log.error("Error on Edited object creation: %s" % str(e))
-                return
+        # Remove any utility shapes
+        self.delete_utility_geometry()
+        self.tool_shape.clear(update=True)
 
-            self.deactivate()
-            self.app.inform.emit('[success] %s' % _("Excellon editing finished."))
+        # Replot and reset tool.
+        self.replot()
+        # self.active_tool = type(self.active_tool)(self)
 
-    def on_tool_select(self, tool):
+    def get_selected(self):
         """
-        Behavior of the toolbar. Tool initialization.
+        Returns list of shapes that are selected in the editor.
 
-        :rtype : None
+        :return: List of shapes.
         """
-        current_tool = tool
-
-        self.app.log.debug("on_tool_select('%s')" % tool)
-
-        if self.last_tool_selected is None and current_tool != 'drill_select':
-            # self.draw_app.select_tool('drill_select')
-            self.complete = True
-            current_tool = 'drill_select'
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("Cancelled. There is no Tool/Drill selected"))
+        return self.selected
 
-        # This is to make the group behave as radio group
-        if current_tool in self.tools_exc:
-            if self.tools_exc[current_tool]["button"].isChecked():
-                self.app.log.debug("%s is checked." % current_tool)
-                for t in self.tools_exc:
-                    if t != current_tool:
-                        self.tools_exc[t]["button"].setChecked(False)
+    def delete_selected(self):
+        temp_ref = [s for s in self.selected]
+        for shape_sel in temp_ref:
+            self.delete_shape(shape_sel)
 
-                # this is where the Editor toolbar classes (button's) are instantiated
-                self.active_tool = self.tools_exc[current_tool]["constructor"](self)
-                # self.app.inform.emit(self.active_tool.start_msg)
-            else:
-                self.app.log.debug("%s is NOT checked." % current_tool)
-                for t in self.tools_exc:
-                    self.tools_exc[t]["button"].setChecked(False)
+        self.selected = []
+        self.build_ui()
+        self.app.inform.emit('[success] %s' % _("Done. Drill(s) deleted."))
 
-                self.select_tool('drill_select')
-                self.active_tool = FCDrillSelect(self)
+    def delete_shape(self, del_shape):
+        self.is_modified = True
 
-    def on_row_selected(self, row, col):
-        if col == 0:
-            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 del_shape in self.utility:
+            self.utility.remove(del_shape)
+            return
 
-            if key_modifier == modifier_to_use:
-                pass
-            else:
-                self.selected = []
+        for storage in self.storage_dict:
+            # try:
+            #     self.storage_dict[storage].remove(shape)
+            # except:
+            #     pass
+            if del_shape in self.storage_dict[storage].get_objects():
+                if isinstance(del_shape.geo, MultiLineString):
+                    self.storage_dict[storage].remove(del_shape)
+                    # a hack to make the tool_table display less drills per diameter
+                    # self.points_edit it's only useful first time when we load the data into the storage
+                    # but is still used as referecen when building tool_table in self.build_ui()
+                    # the number of drills displayed in column 2 is just a len(self.points_edit) therefore
+                    # deleting self.points_edit elements (doesn't matter who but just the number)
+                    # solved the display issue.
+                    del self.points_edit[storage][0]
+                else:
+                    self.storage_dict[storage].remove(del_shape)
+                    del self.slot_points_edit[storage][0]
 
-            try:
-                selected_dia = self.tool2tooldia[self.tools_table_exc.currentRow() + 1]
-                self.last_tool_selected = int(self.tools_table_exc.currentRow()) + 1
-                for obj in self.storage_dict[selected_dia].get_objects():
-                    self.selected.append(obj)
-            except Exception as e:
-                self.app.log.debug(str(e))
+        if del_shape in self.selected:
+            self.selected.remove(del_shape)
 
-            self.replot()
+    def delete_utility_geometry(self):
+        for_deletion = [util_shape for util_shape in self.utility]
+        for util_shape in for_deletion:
+            self.delete_shape(util_shape)
 
-    # def toolbar_tool_toggle(self, key):
-    #     self.options[key] = self.sender().isChecked()
-    #     if self.options[key] is True:
-    #         return 1
-    #     else:
-    #         return 0
+        self.tool_shape.clear(update=True)
+        self.tool_shape.redraw()
 
-    def on_canvas_click(self, event):
+    def on_delete_btn(self):
+        self.delete_selected()
+        self.replot()
+
+    def select_tool(self, toolname):
         """
-        event.x and .y have canvas coordinates
-        event.xdata and .ydata have plot coordinates
+        Selects a drawing tool. Impacts the object and appGUI.
 
-        :param event: Event object dispatched by VisPy
-        :return: None
+        :param toolname:    Name of the tool.
+        :return:            None
         """
-        if self.app.is_legacy is False:
-            event_pos = event.pos
-            # event_is_dragging = event.is_dragging
-            # right_button = 2
+        self.tools_exc[toolname]["button"].setChecked(True)
+        self.on_tool_select(toolname)
+
+    def set_selected(self, sel_shape):
+
+        # Remove and add to the end.
+        if sel_shape in self.selected:
+            self.selected.remove(sel_shape)
+
+        self.selected.append(sel_shape)
+
+    def set_unselected(self, unsel_shape):
+        if unsel_shape in self.selected:
+            self.selected.remove(unsel_shape)
+
+    def on_array_type_combo(self):
+        if self.e_ui.array_type_combo.currentIndex() == 0:
+            self.e_ui.array_circular_frame.hide()
+            self.e_ui.array_linear_frame.show()
         else:
-            event_pos = (event.xdata, event.ydata)
-            # event_is_dragging = self.app.plotcanvas.is_dragging
-            # right_button = 3
+            self.delete_utility_geometry()
+            self.e_ui.array_circular_frame.show()
+            self.e_ui.array_linear_frame.hide()
+            self.app.inform.emit(_("Click on the circular array Center position"))
 
-        self.pos = self.canvas.translate_coords(event_pos)
+    def on_slot_array_type_combo(self):
+        if self.e_ui.slot_array_type_combo.currentIndex() == 0:
+            self.e_ui.slot_array_circular_frame.hide()
+            self.e_ui.slot_array_linear_frame.show()
+        else:
+            self.delete_utility_geometry()
+            self.e_ui.slot_array_circular_frame.show()
+            self.e_ui.slot_array_linear_frame.hide()
+            self.app.inform.emit(_("Click on the circular array Center position"))
 
-        if self.app.grid_status():
-            self.pos = self.app.geo_editor.snap(self.pos[0], self.pos[1])
+    def on_linear_angle_radio(self):
+        val = self.e_ui.drill_axis_radio.get_value()
+        if val == 'A':
+            self.e_ui.linear_angle_spinner.show()
+            self.e_ui.linear_angle_label.show()
         else:
-            self.pos = (self.pos[0], self.pos[1])
+            self.e_ui.linear_angle_spinner.hide()
+            self.e_ui.linear_angle_label.hide()
 
-        if event.button == 1:
-            # self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
-            #                                        "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
+    def on_slot_array_linear_angle_radio(self):
+        val = self.e_ui.slot_array_axis_radio.get_value()
+        if val == 'A':
+            self.e_ui.slot_array_linear_angle_spinner.show()
+            self.e_ui.slot_array_linear_angle_label.show()
+        else:
+            self.e_ui.slot_array_linear_angle_spinner.hide()
+            self.e_ui.slot_array_linear_angle_label.hide()
 
-            # Selection with left mouse button
-            if self.active_tool is not None and event.button == 1:
-                # Dispatch event to active_tool
-                # msg = self.active_tool.click(self.app.geo_editor.snap(event.xdata, event.ydata))
-                self.active_tool.click(self.app.geo_editor.snap(self.pos[0], self.pos[1]))
+    def on_slot_angle_radio(self):
+        val = self.e_ui.slot_axis_radio.get_value()
+        if val == 'A':
+            self.e_ui.slot_angle_spinner.show()
+            self.e_ui.slot_angle_label.show()
+        else:
+            self.e_ui.slot_angle_spinner.hide()
+            self.e_ui.slot_angle_label.hide()
 
-                # If it is a shape generating tool
-                if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete:
-                    if self.current_storage is not None:
-                        self.on_exc_shape_complete(self.current_storage)
-                        self.build_ui()
+    def exc_add_drill(self):
+        self.select_tool('drill_add')
+        return
 
-                    # MS: always return to the Select Tool if modifier key is not pressed
-                    # else return to the current tool
-                    key_modifier = QtWidgets.QApplication.keyboardModifiers()
-                    if self.app.defaults["global_mselect_key"] == 'Control':
-                        modifier_to_use = Qt.ControlModifier
-                    else:
-                        modifier_to_use = Qt.ShiftModifier
+    def exc_add_drill_array(self):
+        self.select_tool('drill_array')
+        return
 
-                    # if modifier key is pressed then we add to the selected list the current shape but if it's already
-                    # in the selected list, we removed it. Therefore first click selects, second deselects.
-                    if key_modifier == modifier_to_use:
-                        self.select_tool(self.active_tool.name)
-                    else:
-                        # return to Select tool but not for FCDrillAdd or FCSlot
-                        if isinstance(self.active_tool, FCDrillAdd) or isinstance(self.active_tool, FCSlot):
-                            self.select_tool(self.active_tool.name)
-                        else:
-                            self.select_tool("drill_select")
-                        return
+    def exc_add_slot(self):
+        self.select_tool('slot_add')
+        return
 
-                if isinstance(self.active_tool, FCDrillSelect):
-                    # self.app.log.debug("Replotting after click.")
-                    self.replot()
-            else:
-                self.app.log.debug("No active tool to respond to click!")
+    def exc_add_slot_array(self):
+        self.select_tool('slot_array')
+        return
 
-    def on_exc_shape_complete(self, storage):
-        self.app.log.debug("on_shape_complete()")
+    def exc_resize_drills(self):
+        self.select_tool('drill_resize')
+        return
 
-        # Add shape
-        if type(storage) is list:
-            for item_storage in storage:
-                self.add_exc_shape(self.active_tool.geometry, item_storage)
-        else:
-            self.add_exc_shape(self.active_tool.geometry, storage)
+    def exc_copy_drills(self):
+        self.select_tool('drill_copy')
+        return
 
-        # Remove any utility shapes
-        self.delete_utility_geometry()
-        self.tool_shape.clear(update=True)
+    def exc_move_drills(self):
+        self.select_tool('drill_move')
+        return
 
-        # Replot and reset tool.
+    def on_slots_conversion(self):
+        # selected rows
+        selected_rows = set()
+        for it in self.e_ui.tools_table_exc.selectedItems():
+            selected_rows.add(it.row())
+
+        # convert a Polygon (slot) to a MultiLineString (drill)
+        def convert_slot2drill(geo_elem, tool_dia):
+            point = geo_elem.centroid
+            start_hor_line = ((point.x - (tool_dia / 2)), point.y)
+            stop_hor_line = ((point.x + (tool_dia / 2)), point.y)
+            start_vert_line = (point.x, (point.y - (tool_dia / 2)))
+            stop_vert_line = (point.x, (point.y + (tool_dia / 2)))
+            return MultiLineString([(start_hor_line, stop_hor_line), (start_vert_line, stop_vert_line)])
+
+        # temporary new storage: a dist with keys the tool diameter and values Rtree storage
+        new_storage_dict = {}
+
+        for row in selected_rows:
+            table_tooldia = self.dec_format(float(self.e_ui.tools_table_exc.item(row, 1).text()))
+            for dict_dia, geo_dict in self.storage_dict.items():
+                if self.dec_format(float(dict_dia)) == table_tooldia:
+                    storage_elem = AppGeoEditor.make_storage()
+                    for shape in geo_dict.get_objects():
+                        if isinstance(shape.geo, MultiLineString):
+                            # it's a drill just add it as it is to storage
+                            self.add_exc_shape(shape, storage_elem)
+                        if isinstance(shape.geo, Polygon):
+                            # it's a slot, convert drill to slot and then add it to storage
+                            new_shape = convert_slot2drill(shape.geo, table_tooldia)
+                            self.add_exc_shape(DrawToolShape(new_shape), storage_elem)
+
+                    new_storage_dict[table_tooldia] = storage_elem
+
+        self.storage_dict.update(new_storage_dict)
         self.replot()
-        # self.active_tool = type(self.active_tool)(self)
 
-    def add_exc_shape(self, shape, storage):
-        """
-        Adds a shape to a specified shape storage.
 
-        :param shape: Shape to be added.
-        :type shape: DrawToolShape
-        :param storage: object where to store the shapes
-        :return: None
-        """
-        # List of DrawToolShape?
-        if isinstance(shape, list):
-            for subshape in shape:
-                self.add_exc_shape(subshape, storage)
-            return
+class AppExcEditorUI:
+    def __init__(self, app):
+        self.app = app
 
-        assert isinstance(shape, DrawToolShape), \
-            "Expected a DrawToolShape, got %s" % str(type(shape))
+        # Number of decimals used by tools in this class
+        self.decimals = self.app.decimals
 
-        assert shape.geo is not None, \
-            "Shape object has empty geometry (None)"
+        # ## Current application units in Upper Case
+        self.units = self.app.defaults['units'].upper()
 
-        assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or not isinstance(shape.geo, list), \
-            "Shape objects has empty geometry ([])"
+        self.exc_edit_widget = QtWidgets.QWidget()
+        # ## Box for custom widgets
+        # This gets populated in offspring implementations.
+        layout = QtWidgets.QVBoxLayout()
+        self.exc_edit_widget.setLayout(layout)
 
-        if isinstance(shape, DrawToolUtilityShape):
-            self.utility.append(shape)
-        else:
-            storage.insert(shape)  # TODO: Check performance
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets
+        # this way I can hide/show the frame
+        self.drills_frame = QtWidgets.QFrame()
+        self.drills_frame.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self.drills_frame)
+        self.tools_box = QtWidgets.QVBoxLayout()
+        self.tools_box.setContentsMargins(0, 0, 0, 0)
+        self.drills_frame.setLayout(self.tools_box)
 
-    def add_shape(self, shape):
-        """
-        Adds a shape to the shape storage.
+        # ## Page Title box (spacing between children)
+        self.title_box = QtWidgets.QHBoxLayout()
+        self.tools_box.addLayout(self.title_box)
 
-        :param shape: Shape to be added.
-        :type shape: DrawToolShape
-        :return: None
-        """
+        # ## Page Title icon
+        pixmap = QtGui.QPixmap(self.app.resource_location + '/flatcam_icon32.png')
+        self.icon = QtWidgets.QLabel()
+        self.icon.setPixmap(pixmap)
+        self.title_box.addWidget(self.icon, stretch=0)
 
-        # List of DrawToolShape?
-        if isinstance(shape, list):
-            for subshape in shape:
-                self.add_shape(subshape)
-            return
+        # ## Title label
+        self.title_label = QtWidgets.QLabel("<font size=5><b>%s</b></font>" % _('Excellon Editor'))
+        self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.title_box.addWidget(self.title_label, stretch=1)
 
-        assert isinstance(shape, DrawToolShape), \
-            "Expected a DrawToolShape, got %s" % type(shape)
+        # ## Object name
+        self.name_box = QtWidgets.QHBoxLayout()
+        self.tools_box.addLayout(self.name_box)
+        name_label = QtWidgets.QLabel(_("Name:"))
+        self.name_box.addWidget(name_label)
+        self.name_entry = FCEntry()
+        self.name_box.addWidget(self.name_entry)
 
-        assert shape.geo is not None, \
-            "Shape object has empty geometry (None)"
+        # ### Tools Drills ## ##
+        self.tools_table_label = QtWidgets.QLabel("<b>%s</b>" % _('Tools Table'))
+        self.tools_table_label.setToolTip(
+            _("Tools in this Excellon object\n"
+              "when are used for drilling.")
+        )
+        self.tools_box.addWidget(self.tools_table_label)
 
-        assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or not isinstance(shape.geo, list), \
-            "Shape objects has empty geometry ([])"
+        self.tools_table_exc = FCTable()
+        # delegate = SpinBoxDelegate(units=self.units)
+        # self.e_ui.tools_table_exc.setItemDelegateForColumn(1, delegate)
 
-        if isinstance(shape, DrawToolUtilityShape):
-            self.utility.append(shape)
-        # else:
-        #     self.storage.insert(shape)
+        self.tools_box.addWidget(self.tools_table_exc)
 
-    def on_exc_click_release(self, event):
-        """
-        Handler of the "mouse_release" event.
-        It will pop-up the context menu on right mouse click unless there was a panning move (decided in the
-        "mouse_move" event handler) and only if the current tool is the Select tool.
-        It will 'close' a Editor tool if it is the case.
+        self.tools_table_exc.setColumnCount(4)
+        self.tools_table_exc.setHorizontalHeaderLabels(['#', _('Diameter'), 'D', 'S'])
+        self.tools_table_exc.setSortingEnabled(False)
+        self.tools_table_exc.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
 
-        :param event: Event object dispatched by VisPy SceneCavas
-        :return: None
-        """
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.tools_box.addWidget(separator_line)
 
-        if self.app.is_legacy is False:
-            event_pos = event.pos
-            # event_is_dragging = event.is_dragging
-            right_button = 2
-        else:
-            event_pos = (event.xdata, event.ydata)
-            # event_is_dragging = self.app.plotcanvas.is_dragging
-            right_button = 3
+        self.convert_slots_btn = FCButton('%s' % _("Convert Slots"))
+        self.convert_slots_btn.setToolTip(
+            _("Convert the slots in the selected tools to drills.")
+        )
+        self.tools_box.addWidget(self.convert_slots_btn)
 
-        pos_canvas = self.canvas.translate_coords(event_pos)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.tools_box.addWidget(separator_line)
 
-        if self.app.grid_status():
-            pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
-        else:
-            pos = (pos_canvas[0], pos_canvas[1])
+        # ### Add a new Tool ## ##
+        self.addtool_label = QtWidgets.QLabel('<b>%s</b>' % _('Add/Delete Tool'))
+        self.addtool_label.setToolTip(
+            _("Add/Delete a tool to the tool list\n"
+              "for this Excellon object.")
+        )
+        self.tools_box.addWidget(self.addtool_label)
 
-        # if the released mouse button was RMB then test if it was a panning motion or not, if not it was a context
-        # canvas menu
-        try:
-            if event.button == right_button:  # right click
-                if self.app.ui.popMenu.mouse_is_panning is False:
-                    try:
-                        QtGui.QGuiApplication.restoreOverrideCursor()
-                    except Exception:
-                        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] %s' %
-                                             _("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] %s' %
-                                                 _("Done."))
-                            self.select_tool('drill_select')
+        grid1 = QtWidgets.QGridLayout()
+        self.tools_box.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
 
-                        self.app.cursor = QtGui.QCursor()
-                        self.app.populate_cmenu_grids()
-                        self.app.ui.popMenu.popup(self.app.cursor.pos())
+        addtool_entry_lbl = QtWidgets.QLabel('%s:' % _('Tool Dia'))
+        addtool_entry_lbl.setToolTip(
+            _("Diameter for the new tool")
+        )
 
-        except Exception as e:
-            log.warning("FlatCAMExcEditor.on_exc_click_release() RMB click --> Error: %s" % str(e))
-            raise
+        hlay = QtWidgets.QHBoxLayout()
+        self.addtool_entry = FCDoubleSpinner()
+        self.addtool_entry.set_precision(self.decimals)
+        self.addtool_entry.set_range(0.0000, 9999.9999)
 
-        # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
-        # selection and then select a type of selection ("enclosing" or "touching")
-        try:
-            if event.button == 1:  # left click
-                if self.app.selection_type is not None:
-                    self.draw_selection_area_handler(self.pos, pos, self.app.selection_type)
-                    self.app.selection_type = None
+        hlay.addWidget(self.addtool_entry)
 
-                elif isinstance(self.active_tool, FCDrillSelect):
-                    self.active_tool.click_release((self.pos[0], self.pos[1]))
+        self.addtool_btn = QtWidgets.QPushButton(_('Add Tool'))
+        self.addtool_btn.setToolTip(
+            _("Add a new tool to the tool list\n"
+              "with the diameter specified above.")
+        )
+        self.addtool_btn.setFixedWidth(80)
+        hlay.addWidget(self.addtool_btn)
 
-                    # if there are selected objects then plot them
-                    if self.selected:
-                        self.replot()
-        except Exception as e:
-            log.warning("FlatCAMExcEditor.on_exc_click_release() LMB click --> Error: %s" % str(e))
-            raise
+        grid1.addWidget(addtool_entry_lbl, 0, 0)
+        grid1.addLayout(hlay, 0, 1)
 
-    def draw_selection_area_handler(self, start, end, sel_type):
-        """
-        This function is called whenever we have a left mouse click release and only we have a left mouse click drag,
-        be it from left to right or from right to left. The direction of the drag is decided in the "mouse_move"
-        event handler.
-        Pressing a modifier key (eg. Ctrl, Shift or Alt) will change the behavior of the selection.
+        grid2 = QtWidgets.QGridLayout()
+        self.tools_box.addLayout(grid2)
 
-        Depending on which tool belongs the selected shapes, the corresponding rows in the Tools Table are selected or
-        deselected.
+        self.deltool_btn = QtWidgets.QPushButton(_('Delete Tool'))
+        self.deltool_btn.setToolTip(
+            _("Delete a tool in the tool list\n"
+              "by selecting a row in the tool table.")
+        )
+        grid2.addWidget(self.deltool_btn, 0, 1)
 
-        :param start: mouse position when the selection LMB click was done
-        :param end: mouse position when the left mouse button is released
-        :param sel_type: if True it's a left to right selection (enclosure), if False it's a 'touch' selection
-        :return:
-        """
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets
+        # this way I can hide/show the frame
+        self.resize_frame = QtWidgets.QFrame()
+        self.resize_frame.setContentsMargins(0, 0, 0, 0)
+        self.tools_box.addWidget(self.resize_frame)
+        self.resize_box = QtWidgets.QVBoxLayout()
+        self.resize_box.setContentsMargins(0, 0, 0, 0)
+        self.resize_frame.setLayout(self.resize_box)
 
-        start_pos = (start[0], start[1])
-        end_pos = (end[0], end[1])
-        poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
-        modifiers = None
+        # ### Resize a  drill ## ##
+        self.emptyresize_label = QtWidgets.QLabel('')
+        self.resize_box.addWidget(self.emptyresize_label)
 
-        # delete the selection shape that was just drawn, we no longer need it
-        self.app.delete_selection_shape()
+        self.drillresize_label = QtWidgets.QLabel('<b>%s</b>' % _("Resize Drill(s)"))
+        self.drillresize_label.setToolTip(
+            _("Resize a drill or a selection of drills.")
+        )
+        self.resize_box.addWidget(self.drillresize_label)
 
-        # detect if a modifier key was pressed while the left mouse button was released
-        self.modifiers = QtWidgets.QApplication.keyboardModifiers()
-        if self.modifiers == QtCore.Qt.ShiftModifier:
-            modifiers = 'Shift'
-        elif self.modifiers == QtCore.Qt.ControlModifier:
-            modifiers = 'Control'
+        grid3 = QtWidgets.QGridLayout()
+        self.resize_box.addLayout(grid3)
 
-        if modifiers == self.app.defaults["global_mselect_key"]:
-            for storage in self.storage_dict:
-                for obj in self.storage_dict[storage].get_objects():
-                    if (sel_type is True and poly_selection.contains(obj.geo)) or \
-                            (sel_type is False and poly_selection.intersects(obj.geo)):
+        res_entry_lbl = QtWidgets.QLabel('%s:' % _('Resize Dia'))
+        res_entry_lbl.setToolTip(
+            _("Diameter to resize to.")
+        )
+        grid3.addWidget(res_entry_lbl, 0, 0)
 
-                        if obj in self.selected:
-                            # remove the shape object from the selected shapes storage
-                            self.selected.remove(obj)
-                        else:
-                            # add the shape object to the selected shapes storage
-                            self.selected.append(obj)
-        else:
-            # clear the selection shapes storage
-            self.selected = []
-            # then add to the selection shapes storage the shapes that are included (touched) by the selection rectangle
-            for storage in self.storage_dict:
-                for obj in self.storage_dict[storage].get_objects():
-                    if (sel_type is True and poly_selection.contains(obj.geo)) or \
-                            (sel_type is False and poly_selection.intersects(obj.geo)):
-                        self.selected.append(obj)
+        hlay2 = QtWidgets.QHBoxLayout()
+        self.resdrill_entry = FCDoubleSpinner()
+        self.resdrill_entry.set_precision(self.decimals)
+        self.resdrill_entry.set_range(0.0000, 9999.9999)
 
-        try:
-            self.tools_table_exc.cellPressed.disconnect()
-        except Exception:
-            pass
+        hlay2.addWidget(self.resdrill_entry)
 
-        # first deselect all rows (tools) in the Tools Table
-        self.tools_table_exc.clearSelection()
-        # and select the rows (tools) in the tool table according to the diameter(s) of the selected shape(s)
-        self.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
-        for storage in self.storage_dict:
-            for shape_s in self.selected:
-                if shape_s in self.storage_dict[storage].get_objects():
-                    for key_tool_nr in self.tool2tooldia:
-                        if self.tool2tooldia[key_tool_nr] == storage:
-                            row_to_sel = key_tool_nr - 1
-                            # item = self.tools_table_exc.item(row_to_sel, 1)
-                            # self.tools_table_exc.setCurrentItem(item)
-                            # item.setSelected(True)
+        self.resize_btn = QtWidgets.QPushButton(_('Resize'))
+        self.resize_btn.setToolTip(
+            _("Resize drill(s)")
+        )
+        self.resize_btn.setFixedWidth(80)
+        hlay2.addWidget(self.resize_btn)
+        grid3.addLayout(hlay2, 0, 1)
 
-                            # if the row to be selected is not already in the selected rows then select it
-                            # otherwise don't do it as it seems that we have a toggle effect
-                            if row_to_sel not in set(index.row() for index in self.tools_table_exc.selectedIndexes()):
-                                self.tools_table_exc.selectRow(row_to_sel)
-                            self.last_tool_selected = int(key_tool_nr)
+        self.resize_frame.hide()
 
-        self.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+        # ####################################
+        # ### Add DRILL Array ################
+        # ####################################
 
-        self.tools_table_exc.cellPressed.connect(self.on_row_selected)
-        self.replot()
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add
+        # all the add drill array  widgets
+        # this way I can hide/show the frame
+        self.array_frame = QtWidgets.QFrame()
+        self.array_frame.setContentsMargins(0, 0, 0, 0)
+        self.tools_box.addWidget(self.array_frame)
+        self.array_box = QtWidgets.QVBoxLayout()
+        self.array_box.setContentsMargins(0, 0, 0, 0)
+        self.array_frame.setLayout(self.array_box)
 
-    def on_canvas_move(self, event):
-        """
-        Called on 'mouse_move' event.
-        It updates the mouse cursor if the grid snapping is ON.
-        It decide if we have a mouse drag and if it is done with the right mouse click. Then it passes this info to a
-        class object which is used in the "mouse_release" handler to decide if to pop-up the context menu or not.
-        It draws utility_geometry for the Editor tools.
-        Update the position labels from status bar.
-        Decide if we have a right to left or a left to right mouse drag with left mouse button and call a function
-        that will draw a selection shape on canvas.
+        self.emptyarray_label = QtWidgets.QLabel('')
+        self.array_box.addWidget(self.emptyarray_label)
 
-        event.pos have canvas screen coordinates
+        self.drill_array_label = QtWidgets.QLabel('<b>%s</b>' % _("Add Drill Array"))
+        self.drill_array_label.setToolTip(
+            _("Add an array of drills (linear or circular array)")
+        )
+        self.array_box.addWidget(self.drill_array_label)
 
-        :param event: Event object dispatched by VisPy SceneCavas
-        :return: None
-        """
+        self.array_type_combo = FCComboBox()
+        self.array_type_combo.setToolTip(
+            _("Select the type of drills array to create.\n"
+              "It can be Linear X(Y) or Circular")
+        )
+        self.array_type_combo.addItem(_("Linear"))
+        self.array_type_combo.addItem(_("Circular"))
 
-        if self.app.is_legacy is False:
-            event_pos = event.pos
-            event_is_dragging = event.is_dragging
-            right_button = 2
-        else:
-            event_pos = (event.xdata, event.ydata)
-            event_is_dragging = self.app.plotcanvas.is_dragging
-            right_button = 3
+        self.array_box.addWidget(self.array_type_combo)
 
-        pos = self.canvas.translate_coords(event_pos)
-        event.xdata, event.ydata = pos[0], pos[1]
+        self.array_form = QtWidgets.QFormLayout()
+        self.array_box.addLayout(self.array_form)
 
-        self.x = event.xdata
-        self.y = event.ydata
+        # Set the number of drill holes in the drill array
+        self.drill_array_size_label = QtWidgets.QLabel('%s:' % _('Nr of drills'))
+        self.drill_array_size_label.setToolTip(_("Specify how many drills to be in the array."))
+        self.drill_array_size_label.setMinimumWidth(100)
 
-        self.app.ui.popMenu.mouse_is_panning = False
+        self.drill_array_size_entry = FCSpinner()
+        self.drill_array_size_entry.set_range(1, 9999)
+        self.array_form.addRow(self.drill_array_size_label, self.drill_array_size_entry)
 
-        # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
-        if event.button == right_button and event_is_dragging == 1:
-            self.app.ui.popMenu.mouse_is_panning = True
-            return
+        self.array_linear_frame = QtWidgets.QFrame()
+        self.array_linear_frame.setContentsMargins(0, 0, 0, 0)
+        self.array_box.addWidget(self.array_linear_frame)
+        self.linear_box = QtWidgets.QVBoxLayout()
+        self.linear_box.setContentsMargins(0, 0, 0, 0)
+        self.array_linear_frame.setLayout(self.linear_box)
 
-        try:
-            x = float(event.xdata)
-            y = float(event.ydata)
-        except TypeError:
-            return
+        self.linear_form = QtWidgets.QFormLayout()
+        self.linear_box.addLayout(self.linear_form)
 
-        if self.active_tool is None:
-            return
+        # Linear Drill Array direction
+        self.drill_axis_label = QtWidgets.QLabel('%s:' % _('Direction'))
+        self.drill_axis_label.setToolTip(
+            _("Direction on which the linear array is oriented:\n"
+              "- 'X' - horizontal axis \n"
+              "- 'Y' - vertical axis or \n"
+              "- 'Angle' - a custom angle for the array inclination")
+        )
+        self.drill_axis_label.setMinimumWidth(100)
 
-        # ## Snap coordinates
-        if self.app.grid_status():
-            x, y = self.app.geo_editor.snap(x, y)
+        self.drill_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
+                                          {'label': _('Y'), 'value': 'Y'},
+                                          {'label': _('Angle'), 'value': 'A'}])
+        self.linear_form.addRow(self.drill_axis_label, self.drill_axis_radio)
 
-            # Update cursor
-            self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color=self.app.cursor_color_3D,
-                                         edge_width=self.app.defaults["global_cursor_width"],
-                                         size=self.app.defaults["global_cursor_size"])
+        # Linear Drill Array pitch distance
+        self.drill_pitch_label = QtWidgets.QLabel('%s:' % _('Pitch'))
+        self.drill_pitch_label.setToolTip(
+            _("Pitch = Distance between elements of the array.")
+        )
+        self.drill_pitch_label.setMinimumWidth(100)
 
-        self.snap_x = x
-        self.snap_y = y
+        self.drill_pitch_entry = FCDoubleSpinner()
+        self.drill_pitch_entry.set_precision(self.decimals)
+        self.drill_pitch_entry.set_range(0.0000, 9999.9999)
 
-        if self.pos is None:
-            self.pos = (0, 0)
-        self.app.dx = x - self.pos[0]
-        self.app.dy = y - self.pos[1]
+        self.linear_form.addRow(self.drill_pitch_label, self.drill_pitch_entry)
 
-        # # update the position label in the infobar since the APP mouse event handlers are disconnected
-        self.app.ui.position_label.setText("&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
-                                           "<b>Y</b>: %.4f&nbsp;" % (x, y))
-        # # update the reference position label in the infobar since the APP mouse event handlers are disconnected
-        # self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
-        #                                        "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
+        # Linear Drill Array angle
+        self.linear_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
+        self.linear_angle_label.setToolTip(
+            _("Angle at which the linear array is placed.\n"
+              "The precision is of max 2 decimals.\n"
+              "Min value is: -360 degrees.\n"
+              "Max value is:  360.00 degrees.")
+        )
+        self.linear_angle_label.setMinimumWidth(100)
 
-        units = self.app.defaults["units"].lower()
-        self.app.plotcanvas.text_hud.text = \
-            'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX:  \t{:<.4f} [{:s}]\nY:  \t{:<.4f} [{:s}]'.format(
-                self.app.dx, units, self.app.dy, units, x, units, y, units)
+        self.linear_angle_spinner = FCDoubleSpinner()
+        self.linear_angle_spinner.set_precision(self.decimals)
+        self.linear_angle_spinner.setSingleStep(1.0)
+        self.linear_angle_spinner.setRange(-360.00, 360.00)
+        self.linear_form.addRow(self.linear_angle_label, self.linear_angle_spinner)
 
-        # ## Utility geometry (animated)
-        self.update_utility_geometry(data=(x, y))
+        self.array_circular_frame = QtWidgets.QFrame()
+        self.array_circular_frame.setContentsMargins(0, 0, 0, 0)
+        self.array_box.addWidget(self.array_circular_frame)
+        self.circular_box = QtWidgets.QVBoxLayout()
+        self.circular_box.setContentsMargins(0, 0, 0, 0)
+        self.array_circular_frame.setLayout(self.circular_box)
 
-        # ## Selection area on canvas section # ##
-        if event_is_dragging == 1 and event.button == 1:
-            # I make an exception for FCDrillAdd and FCDrillArray because clicking and dragging while making regions
-            # can create strange issues. Also for FCSlot and FCSlotArray
-            if isinstance(self.active_tool, FCDrillAdd) or isinstance(self.active_tool, FCDrillArray) or \
-                    isinstance(self.active_tool, FCSlot) or isinstance(self.active_tool, FCSlotArray):
-                self.app.selection_type = None
-            else:
-                dx = pos[0] - self.pos[0]
-                self.app.delete_selection_shape()
-                if dx < 0:
-                    self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y),
-                                                         color=self.app.defaults["global_alt_sel_line"],
-                                                         face_color=self.app.defaults['global_alt_sel_fill'])
-                    self.app.selection_type = False
-                else:
-                    self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y))
-                    self.app.selection_type = True
-        else:
-            self.app.selection_type = None
+        self.drill_direction_label = QtWidgets.QLabel('%s:' % _('Direction'))
+        self.drill_direction_label.setToolTip(_("Direction for circular array."
+                                                "Can be CW = clockwise or CCW = counter clockwise."))
+        self.drill_direction_label.setMinimumWidth(100)
 
-        # Update cursor
-        self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color=self.app.cursor_color_3D,
-                                     edge_width=self.app.defaults["global_cursor_width"],
-                                     size=self.app.defaults["global_cursor_size"])
+        self.circular_form = QtWidgets.QFormLayout()
+        self.circular_box.addLayout(self.circular_form)
 
-    def update_utility_geometry(self, data):
-        # ### Utility geometry (animated) ###
-        geo = self.active_tool.utility_geometry(data=data)
-        if isinstance(geo, DrawToolShape) and geo.geo is not None:
-            # Remove any previous utility shape
-            self.tool_shape.clear(update=True)
-            self.draw_utility_geometry(geo=geo)
+        self.drill_direction_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
+                                               {'label': _('CCW'), 'value': 'CCW'}])
+        self.circular_form.addRow(self.drill_direction_label, self.drill_direction_radio)
 
-    def on_canvas_key_release(self, event):
-        self.key = None
+        self.drill_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
+        self.drill_angle_label.setToolTip(_("Angle at which each element in circular array is placed."))
+        self.drill_angle_label.setMinimumWidth(100)
 
-    def draw_utility_geometry(self, geo):
-        # Add the new utility shape
-        try:
-            # this case is for the Font Parse
-            for el in list(geo.geo):
-                if type(el) == MultiPolygon:
-                    for poly in el:
-                        self.tool_shape.add(
-                            shape=poly,
-                            color=(self.app.defaults["global_draw_color"] + '80'),
-                            update=False,
-                            layer=0,
-                            tolerance=None
-                        )
-                elif type(el) == MultiLineString:
-                    for linestring in el:
-                        self.tool_shape.add(
-                            shape=linestring,
-                            color=(self.app.defaults["global_draw_color"] + '80'),
-                            update=False,
-                            layer=0,
-                            tolerance=None
-                        )
-                else:
-                    self.tool_shape.add(
-                        shape=el,
-                        color=(self.app.defaults["global_draw_color"] + '80'),
-                        update=False,
-                        layer=0,
-                        tolerance=None
-                    )
-        except TypeError:
-            self.tool_shape.add(
-                shape=geo.geo, color=(self.app.defaults["global_draw_color"] + '80'),
-                update=False, layer=0, tolerance=None)
-        self.tool_shape.redraw()
+        self.drill_angle_entry = FCDoubleSpinner()
+        self.drill_angle_entry.set_precision(self.decimals)
+        self.drill_angle_entry.setSingleStep(1.0)
+        self.drill_angle_entry.setRange(-360.00, 360.00)
 
-    def replot(self):
-        self.plot_all()
+        self.circular_form.addRow(self.drill_angle_label, self.drill_angle_entry)
 
-    def plot_all(self):
-        """
-        Plots all shapes in the editor.
+        self.array_circular_frame.hide()
 
-        :return: None
-        :rtype: None
-        """
-        # self.app.log.debug("plot_all()")
-        self.shapes.clear(update=True)
+        self.linear_angle_spinner.hide()
+        self.linear_angle_label.hide()
 
-        for storage in self.storage_dict:
-            for shape_plus in self.storage_dict[storage].get_objects():
-                if shape_plus.geo is None:
-                    continue
+        self.array_frame.hide()
 
-                if shape_plus in self.selected:
-                    self.plot_shape(geometry=shape_plus.geo, color=self.app.defaults['global_sel_draw_color'] + 'FF',
-                                    linewidth=2)
-                    continue
-                self.plot_shape(geometry=shape_plus.geo, color=self.app.defaults['global_draw_color'] + 'FF')
+        # ######################################################
+        # ##### ADDING SLOTS ###################################
+        # ######################################################
 
-        # for shape in self.storage.get_objects():
-        #     if shape.geo is None:  # TODO: This shouldn't have happened
-        #         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'])
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add
+        # all the add slot  widgets
+        # this way I can hide/show the frame
+        self.slot_frame = QtWidgets.QFrame()
+        self.slot_frame.setContentsMargins(0, 0, 0, 0)
+        self.tools_box.addWidget(self.slot_frame)
+        self.slot_box = QtWidgets.QVBoxLayout()
+        self.slot_box.setContentsMargins(0, 0, 0, 0)
+        self.slot_frame.setLayout(self.slot_box)
 
-        for shape_form in self.utility:
-            self.plot_shape(geometry=shape_form.geo, linewidth=1)
-            continue
+        self.emptyarray_label = QtWidgets.QLabel('')
+        self.slot_box.addWidget(self.emptyarray_label)
 
-        self.shapes.redraw()
+        self.slot_label = QtWidgets.QLabel('<b>%s</b>' % _("Slot Parameters"))
+        self.slot_label.setToolTip(
+            _("Parameters for adding a slot (hole with oval shape)\n"
+              "either single or as an part of an array.")
+        )
+        self.slot_box.addWidget(self.slot_label)
 
-    def plot_shape(self, geometry=None, color='0x000000FF', linewidth=1):
-        """
-        Plots a geometric object or list of objects without rendering. Plotted objects
-        are returned as a list. This allows for efficient/animated rendering.
+        self.slot_form = QtWidgets.QFormLayout()
+        self.slot_box.addLayout(self.slot_form)
 
-        :param geometry: Geometry to be plotted (Any Shapely.geom kind or list of such)
-        :param color: Shape color
-        :param linewidth: Width of lines in # of pixels.
-        :return: List of plotted elements.
-        """
-        plot_elements = []
+        # Slot length
+        self.slot_length_label = QtWidgets.QLabel('%s:' % _('Length'))
+        self.slot_length_label.setToolTip(
+            _("Length = The length of the slot.")
+        )
+        self.slot_length_label.setMinimumWidth(100)
 
-        if geometry is None:
-            geometry = self.active_tool.geometry
+        self.slot_length_entry = FCDoubleSpinner()
+        self.slot_length_entry.set_precision(self.decimals)
+        self.slot_length_entry.setSingleStep(0.1)
+        self.slot_length_entry.setRange(0.0000, 9999.9999)
 
-        try:
-            for geo in geometry:
-                plot_elements += self.plot_shape(geometry=geo, color=color, linewidth=linewidth)
+        self.slot_form.addRow(self.slot_length_label, self.slot_length_entry)
 
-        # ## Non-iterable
-        except TypeError:
-            # ## DrawToolShape
-            if isinstance(geometry, DrawToolShape):
-                plot_elements += self.plot_shape(geometry=geometry.geo, color=color, linewidth=linewidth)
+        # Slot direction
+        self.slot_axis_label = QtWidgets.QLabel('%s:' % _('Direction'))
+        self.slot_axis_label.setToolTip(
+            _("Direction on which the slot is oriented:\n"
+              "- 'X' - horizontal axis \n"
+              "- 'Y' - vertical axis or \n"
+              "- 'Angle' - a custom angle for the slot inclination")
+        )
+        self.slot_axis_label.setMinimumWidth(100)
 
-            # ## Polygon: Descend into exterior and each interior.
-            if type(geometry) == Polygon:
-                plot_elements += self.plot_shape(geometry=geometry.exterior, color=color, linewidth=linewidth)
-                plot_elements += self.plot_shape(geometry=geometry.interiors, color=color, linewidth=linewidth)
+        self.slot_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
+                                         {'label': _('Y'), 'value': 'Y'},
+                                         {'label': _('Angle'), 'value': 'A'}])
+        self.slot_form.addRow(self.slot_axis_label, self.slot_axis_radio)
 
-            if type(geometry) == LineString or type(geometry) == LinearRing:
-                plot_elements.append(self.shapes.add(shape=geometry, color=color, layer=0, tolerance=self.tolerance))
+        # Slot custom angle
+        self.slot_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
+        self.slot_angle_label.setToolTip(
+            _("Angle at which the slot is placed.\n"
+              "The precision is of max 2 decimals.\n"
+              "Min value is: -360 degrees.\n"
+              "Max value is:  360.00 degrees.")
+        )
+        self.slot_angle_label.setMinimumWidth(100)
 
-            if type(geometry) == Point:
-                pass
+        self.slot_angle_spinner = FCDoubleSpinner()
+        self.slot_angle_spinner.set_precision(self.decimals)
+        self.slot_angle_spinner.setWrapping(True)
+        self.slot_angle_spinner.setRange(-360.00, 360.00)
+        self.slot_angle_spinner.setSingleStep(1.0)
+        self.slot_form.addRow(self.slot_angle_label, self.slot_angle_spinner)
 
-        return plot_elements
+        self.slot_frame.hide()
 
-    def on_shape_complete(self):
-        self.app.log.debug("on_shape_complete()")
+        # ######################################################
+        # ##### ADDING SLOT ARRAY  #############################
+        # ######################################################
 
-        # Add shape
-        self.add_shape(self.active_tool.geometry)
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add
+        # all the add slot  widgets
+        # this way I can hide/show the frame
+        self.slot_array_frame = QtWidgets.QFrame()
+        self.slot_array_frame.setContentsMargins(0, 0, 0, 0)
+        self.tools_box.addWidget(self.slot_array_frame)
+        self.slot_array_box = QtWidgets.QVBoxLayout()
+        self.slot_array_box.setContentsMargins(0, 0, 0, 0)
+        self.slot_array_frame.setLayout(self.slot_array_box)
 
-        # Remove any utility shapes
-        self.delete_utility_geometry()
-        self.tool_shape.clear(update=True)
+        self.emptyarray_label = QtWidgets.QLabel('')
+        self.slot_array_box.addWidget(self.emptyarray_label)
 
-        # Replot and reset tool.
-        self.replot()
-        # self.active_tool = type(self.active_tool)(self)
+        self.slot_array_label = QtWidgets.QLabel('<b>%s</b>' % _("Slot Array Parameters"))
+        self.slot_array_label.setToolTip(
+            _("Parameters for the array of slots (linear or circular array)")
+        )
+        self.slot_array_box.addWidget(self.slot_array_label)
 
-    def get_selected(self):
-        """
-        Returns list of shapes that are selected in the editor.
+        self.l_form = QtWidgets.QFormLayout()
+        self.slot_array_box.addLayout(self.l_form)
 
-        :return: List of shapes.
-        """
-        # return [shape for shape in self.shape_buffer if shape["selected"]]
-        return self.selected
+        self.slot_array_type_combo = FCComboBox()
+        self.slot_array_type_combo.setToolTip(
+            _("Select the type of slot array to create.\n"
+              "It can be Linear X(Y) or Circular")
+        )
+        self.slot_array_type_combo.addItem(_("Linear"))
+        self.slot_array_type_combo.addItem(_("Circular"))
 
-    def delete_selected(self):
-        temp_ref = [s for s in self.selected]
-        for shape_sel in temp_ref:
-            self.delete_shape(shape_sel)
+        self.slot_array_box.addWidget(self.slot_array_type_combo)
 
-        self.selected = []
-        self.build_ui()
-        self.app.inform.emit('[success] %s' %
-                             _("Done. Drill(s) deleted."))
+        self.slot_array_form = QtWidgets.QFormLayout()
+        self.slot_array_box.addLayout(self.slot_array_form)
 
-    def delete_shape(self, del_shape):
-        self.is_modified = True
+        # Set the number of slot holes in the slot array
+        self.slot_array_size_label = QtWidgets.QLabel('%s:' % _('Nr of slots'))
+        self.slot_array_size_label.setToolTip(_("Specify how many slots to be in the array."))
+        self.slot_array_size_label.setMinimumWidth(100)
 
-        if del_shape in self.utility:
-            self.utility.remove(del_shape)
-            return
+        self.slot_array_size_entry = FCSpinner()
+        self.slot_array_size_entry.set_range(0, 9999)
 
-        for storage in self.storage_dict:
-            # try:
-            #     self.storage_dict[storage].remove(shape)
-            # except:
-            #     pass
-            if del_shape in self.storage_dict[storage].get_objects():
-                if isinstance(del_shape.geo, MultiLineString):
-                    self.storage_dict[storage].remove(del_shape)
-                    # a hack to make the tool_table display less drills per diameter
-                    # self.points_edit it's only useful first time when we load the data into the storage
-                    # but is still used as referecen when building tool_table in self.build_ui()
-                    # the number of drills displayed in column 2 is just a len(self.points_edit) therefore
-                    # deleting self.points_edit elements (doesn't matter who but just the number)
-                    # solved the display issue.
-                    del self.points_edit[storage][0]
-                else:
-                    self.storage_dict[storage].remove(del_shape)
-                    del self.slot_points_edit[storage][0]
+        self.slot_array_form.addRow(self.slot_array_size_label, self.slot_array_size_entry)
 
-        if del_shape in self.selected:
-            self.selected.remove(del_shape)
+        self.slot_array_linear_frame = QtWidgets.QFrame()
+        self.slot_array_linear_frame.setContentsMargins(0, 0, 0, 0)
+        self.slot_array_box.addWidget(self.slot_array_linear_frame)
+        self.slot_array_linear_box = QtWidgets.QVBoxLayout()
+        self.slot_array_linear_box.setContentsMargins(0, 0, 0, 0)
+        self.slot_array_linear_frame.setLayout(self.slot_array_linear_box)
 
-    def delete_utility_geometry(self):
-        for_deletion = [util_shape for util_shape in self.utility]
-        for util_shape in for_deletion:
-            self.delete_shape(util_shape)
+        self.slot_array_linear_form = QtWidgets.QFormLayout()
+        self.slot_array_linear_box.addLayout(self.slot_array_linear_form)
 
-        self.tool_shape.clear(update=True)
-        self.tool_shape.redraw()
+        # Linear Slot Array direction
+        self.slot_array_axis_label = QtWidgets.QLabel('%s:' % _('Direction'))
+        self.slot_array_axis_label.setToolTip(
+            _("Direction on which the linear array is oriented:\n"
+              "- 'X' - horizontal axis \n"
+              "- 'Y' - vertical axis or \n"
+              "- 'Angle' - a custom angle for the array inclination")
+        )
+        self.slot_array_axis_label.setMinimumWidth(100)
 
-    def on_delete_btn(self):
-        self.delete_selected()
-        self.replot()
+        self.slot_array_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
+                                               {'label': _('Y'), 'value': 'Y'},
+                                               {'label': _('Angle'), 'value': 'A'}])
+        self.slot_array_linear_form.addRow(self.slot_array_axis_label, self.slot_array_axis_radio)
 
-    def select_tool(self, toolname):
-        """
-        Selects a drawing tool. Impacts the object and appGUI.
+        # Linear Slot Array pitch distance
+        self.slot_array_pitch_label = QtWidgets.QLabel('%s:' % _('Pitch'))
+        self.slot_array_pitch_label.setToolTip(
+            _("Pitch = Distance between elements of the array.")
+        )
+        self.slot_array_pitch_label.setMinimumWidth(100)
 
-        :param toolname: Name of the tool.
-        :return: None
-        """
-        self.tools_exc[toolname]["button"].setChecked(True)
-        self.on_tool_select(toolname)
+        self.slot_array_pitch_entry = FCDoubleSpinner()
+        self.slot_array_pitch_entry.set_precision(self.decimals)
+        self.slot_array_pitch_entry.setSingleStep(0.1)
+        self.slot_array_pitch_entry.setRange(0.0000, 9999.9999)
 
-    def set_selected(self, sel_shape):
+        self.slot_array_linear_form.addRow(self.slot_array_pitch_label, self.slot_array_pitch_entry)
 
-        # Remove and add to the end.
-        if sel_shape in self.selected:
-            self.selected.remove(sel_shape)
+        # Linear Slot Array angle
+        self.slot_array_linear_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
+        self.slot_array_linear_angle_label.setToolTip(
+            _("Angle at which the linear array is placed.\n"
+              "The precision is of max 2 decimals.\n"
+              "Min value is: -360 degrees.\n"
+              "Max value is:  360.00 degrees.")
+        )
+        self.slot_array_linear_angle_label.setMinimumWidth(100)
 
-        self.selected.append(sel_shape)
+        self.slot_array_linear_angle_spinner = FCDoubleSpinner()
+        self.slot_array_linear_angle_spinner.set_precision(self.decimals)
+        self.slot_array_linear_angle_spinner.setSingleStep(1.0)
+        self.slot_array_linear_angle_spinner.setRange(-360.00, 360.00)
+        self.slot_array_linear_form.addRow(self.slot_array_linear_angle_label, self.slot_array_linear_angle_spinner)
 
-    def set_unselected(self, unsel_shape):
-        if unsel_shape in self.selected:
-            self.selected.remove(unsel_shape)
+        self.slot_array_circular_frame = QtWidgets.QFrame()
+        self.slot_array_circular_frame.setContentsMargins(0, 0, 0, 0)
+        self.slot_array_box.addWidget(self.slot_array_circular_frame)
+        self.slot_array_circular_box = QtWidgets.QVBoxLayout()
+        self.slot_array_circular_box.setContentsMargins(0, 0, 0, 0)
+        self.slot_array_circular_frame.setLayout(self.slot_array_circular_box)
 
-    def on_array_type_combo(self):
-        if self.array_type_combo.currentIndex() == 0:
-            self.array_circular_frame.hide()
-            self.array_linear_frame.show()
-        else:
-            self.delete_utility_geometry()
-            self.array_circular_frame.show()
-            self.array_linear_frame.hide()
-            self.app.inform.emit(_("Click on the circular array Center position"))
+        self.slot_array_direction_label = QtWidgets.QLabel('%s:' % _('Direction'))
+        self.slot_array_direction_label.setToolTip(_("Direction for circular array."
+                                                     "Can be CW = clockwise or CCW = counter clockwise."))
+        self.slot_array_direction_label.setMinimumWidth(100)
 
-    def on_slot_array_type_combo(self):
-        if self.slot_array_type_combo.currentIndex() == 0:
-            self.slot_array_circular_frame.hide()
-            self.slot_array_linear_frame.show()
-        else:
-            self.delete_utility_geometry()
-            self.slot_array_circular_frame.show()
-            self.slot_array_linear_frame.hide()
-            self.app.inform.emit(_("Click on the circular array Center position"))
+        self.slot_array_circular_form = QtWidgets.QFormLayout()
+        self.slot_array_circular_box.addLayout(self.slot_array_circular_form)
 
-    def on_linear_angle_radio(self):
-        val = self.drill_axis_radio.get_value()
-        if val == 'A':
-            self.linear_angle_spinner.show()
-            self.linear_angle_label.show()
-        else:
-            self.linear_angle_spinner.hide()
-            self.linear_angle_label.hide()
+        self.slot_array_direction_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
+                                                    {'label': _('CCW'), 'value': 'CCW'}])
+        self.slot_array_circular_form.addRow(self.slot_array_direction_label, self.slot_array_direction_radio)
 
-    def on_slot_array_linear_angle_radio(self):
-        val = self.slot_array_axis_radio.get_value()
-        if val == 'A':
-            self.slot_array_linear_angle_spinner.show()
-            self.slot_array_linear_angle_label.show()
-        else:
-            self.slot_array_linear_angle_spinner.hide()
-            self.slot_array_linear_angle_label.hide()
+        self.slot_array_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
+        self.slot_array_angle_label.setToolTip(_("Angle at which each element in circular array is placed."))
+        self.slot_array_angle_label.setMinimumWidth(100)
 
-    def on_slot_angle_radio(self):
-        val = self.slot_axis_radio.get_value()
-        if val == 'A':
-            self.slot_angle_spinner.show()
-            self.slot_angle_label.show()
-        else:
-            self.slot_angle_spinner.hide()
-            self.slot_angle_label.hide()
+        self.slot_array_angle_entry = FCDoubleSpinner()
+        self.slot_array_angle_entry.set_precision(self.decimals)
+        self.slot_array_angle_entry.setSingleStep(1)
+        self.slot_array_angle_entry.setRange(-360.00, 360.00)
 
-    def exc_add_drill(self):
-        self.select_tool('drill_add')
-        return
+        self.slot_array_circular_form.addRow(self.slot_array_angle_label, self.slot_array_angle_entry)
 
-    def exc_add_drill_array(self):
-        self.select_tool('drill_array')
-        return
+        self.slot_array_linear_angle_spinner.hide()
+        self.slot_array_linear_angle_label.hide()
 
-    def exc_add_slot(self):
-        self.select_tool('slot_add')
-        return
+        self.slot_array_frame.hide()
 
-    def exc_add_slot_array(self):
-        self.select_tool('slot_array')
-        return
+        self.tools_box.addStretch()
 
-    def exc_resize_drills(self):
-        self.select_tool('drill_resize')
-        return
+        layout.addStretch()
 
-    def exc_copy_drills(self):
-        self.select_tool('drill_copy')
-        return
+        # Editor
+        self.exit_editor_button = QtWidgets.QPushButton(_('Exit Editor'))
+        self.exit_editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/power16.png'))
+        self.exit_editor_button.setToolTip(
+            _("Exit from Editor.")
+        )
+        self.exit_editor_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        layout.addWidget(self.exit_editor_button)
+        # ############################ FINSIHED GUI ###################################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
 
-    def exc_move_drills(self):
-        self.select_tool('drill_move')
-        return
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
 
 
 def get_shapely_list_bounds(geometry_list):

+ 129 - 107
appEditors/FlatCAMGeoEditor.py → appEditors/AppGeoEditor.py

@@ -399,7 +399,7 @@ class TextInputTool(AppTool):
 
     def hide_tool(self):
         self.text_tool_frame.hide()
-        self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
         # self.app.ui.splitter.setSizes([0, 1])
         self.app.ui.notebook.setTabText(2, _("Tool"))
 
@@ -557,8 +557,8 @@ class PaintOptionsTool(AppTool):
         else:
             self.paintoverlap_entry.set_value(0.0)
 
-        if self.app.defaults["tools_paintmargin"]:
-            self.paintmargin_entry.set_value(self.app.defaults["tools_paintmargin"])
+        if self.app.defaults["tools_paintoffset"]:
+            self.paintmargin_entry.set_value(self.app.defaults["tools_paintoffset"])
         else:
             self.paintmargin_entry.set_value(0.0)
 
@@ -1023,7 +1023,7 @@ class TransformEditorTool(AppTool):
         if toggle:
             try:
                 if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                    self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+                    self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
                 else:
                     self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
             except AttributeError:
@@ -2523,12 +2523,12 @@ class FCSelect(DrawTool):
             # it will not work for 3rd method of click selection
             if not over_shape_list:
                 self.draw_app.selected = []
-                FlatCAMGeoEditor.draw_shape_idx = -1
+                AppGeoEditor.draw_shape_idx = -1
             else:
                 # if there are shapes under our click then advance through the list of them, one at the time in a
                 # circular way
-                FlatCAMGeoEditor.draw_shape_idx = (FlatCAMGeoEditor.draw_shape_idx + 1) % len(over_shape_list)
-                obj_to_add = over_shape_list[int(FlatCAMGeoEditor.draw_shape_idx)]
+                AppGeoEditor.draw_shape_idx = (AppGeoEditor.draw_shape_idx + 1) % len(over_shape_list)
+                obj_to_add = over_shape_list[int(AppGeoEditor.draw_shape_idx)]
 
                 key_modifier = QtWidgets.QApplication.keyboardModifiers()
 
@@ -2550,7 +2550,7 @@ class FCSelect(DrawTool):
                     self.draw_app.selected = []
                     self.draw_app.selected.append(obj_to_add)
         except Exception as e:
-            log.error("[ERROR] FlatCAMGeoEditor.FCSelect.click_release() -> Something went bad. %s" % str(e))
+            log.error("[ERROR] AppGeoEditor.FCSelect.click_release() -> Something went bad. %s" % str(e))
 
         # if selection is done on canvas update the Tree in Selected Tab with the selection
         try:
@@ -2788,9 +2788,9 @@ class FCMove(FCShapeTool):
             else:
                 # if there are shapes under our click then advance through the list of them, one at the time in a
                 # circular way
-                self.draw_app.draw_shape_idx = (FlatCAMGeoEditor.draw_shape_idx + 1) % len(over_shape_list)
+                self.draw_app.draw_shape_idx = (AppGeoEditor.draw_shape_idx + 1) % len(over_shape_list)
                 try:
-                    obj_to_add = over_shape_list[int(FlatCAMGeoEditor.draw_shape_idx)]
+                    obj_to_add = over_shape_list[int(AppGeoEditor.draw_shape_idx)]
                 except IndexError:
                     return
 
@@ -3099,8 +3099,8 @@ class FCEraser(FCShapeTool):
         self.geometry = []
         self.storage = self.draw_app.storage
 
-        # Switch notebook to Selected page
-        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
         self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
 
     def set_origin(self, origin):
@@ -3230,7 +3230,7 @@ class FCTransform(FCShapeTool):
 # ###############################################
 # ################ Main Application #############
 # ###############################################
-class FlatCAMGeoEditor(QtCore.QObject):
+class AppGeoEditor(QtCore.QObject):
 
     # will emit the name of the object that was just selected
     item_selected = QtCore.pyqtSignal(str)
@@ -3243,7 +3243,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # assert isinstance(app, FlatCAMApp.App), \
         #     "Expected the app to be a FlatCAMApp.App, got %s" % type(app)
 
-        super(FlatCAMGeoEditor, self).__init__()
+        super(AppGeoEditor, self).__init__()
 
         self.app = app
         self.canvas = app.plotcanvas
@@ -3295,6 +3295,24 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         self.geo_parent = self.tw.invisibleRootItem()
 
+        layout.addStretch()
+
+        # Editor
+        self.exit_editor_button = QtWidgets.QPushButton(_('Exit Editor'))
+        self.exit_editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/power16.png'))
+        self.exit_editor_button.setToolTip(
+            _("Exit from Editor.")
+        )
+        self.exit_editor_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        layout.addWidget(self.exit_editor_button)
+
+        self.exit_editor_button.clicked.connect(lambda: self.app.editor2object())
+
         # ## Toolbar events and properties
         self.tools = {
             "select": {"button": self.app.ui.geo_select_btn, "constructor": FCSelect},
@@ -3316,7 +3334,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # # ## Data
         self.active_tool = None
 
-        self.storage = FlatCAMGeoEditor.make_storage()
+        self.storage = AppGeoEditor.make_storage()
         self.utility = []
 
         # VisPy visuals
@@ -3415,7 +3433,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
                 self.options[opt] = float(text_value)
             except Exception as e:
                 entry.set_value(self.app.defaults[opt])
-                log.debug("FlatCAMGeoEditor.__init__().entry2option() --> %s" % str(e))
+                log.debug("AppGeoEditor.__init__().entry2option() --> %s" % str(e))
                 return
 
         def grid_changed(goption, gentry):
@@ -3511,14 +3529,14 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         # Remove anything else in the GUI Selected Tab
         self.app.ui.selected_scroll_area.takeWidget()
-        # Put ourselves in the appGUI Selected Tab
+        # Put ourselves in the appGUI Properties Tab
         self.app.ui.selected_scroll_area.setWidget(self.geo_edit_widget)
-        # Switch notebook to Selected page
-        self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+        # Switch notebook to Properties page
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
 
     def build_ui(self):
         """
-        Build the appGUI in the Selected Tab for this editor
+        Build the appGUI in the Properties Tab for this editor
 
         :return:
         """
@@ -3588,7 +3606,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.connect_canvas_event_handlers()
 
         # initialize working objects
-        self.storage = FlatCAMGeoEditor.make_storage()
+        self.storage = AppGeoEditor.make_storage()
         self.utility = []
         self.selected = []
 
@@ -3615,8 +3633,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app.ui.popmenu_properties.setVisible(False)
         self.app.ui.g_editor_cmenu.menuAction().setVisible(True)
 
-        # prevent the user to change anything in the Selected Tab while the Geo Editor is active
-        # sel_tab_widget_list = self.app.ui.selected_tab.findChildren(QtWidgets.QWidget)
+        # prevent the user to change anything in the Properties Tab while the Geo Editor is active
+        # sel_tab_widget_list = self.app.ui.properties_tab.findChildren(QtWidgets.QWidget)
         # for w in sel_tab_widget_list:
         #     w.setEnabled(False)
 
@@ -3697,11 +3715,11 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         # try:
         #     # re-enable all the widgets in the Selected Tab that were disabled after entering in Edit Geometry Mode
-        #     sel_tab_widget_list = self.app.ui.selected_tab.findChildren(QtWidgets.QWidget)
+        #     sel_tab_widget_list = self.app.ui.properties_tab.findChildren(QtWidgets.QWidget)
         #     for w in sel_tab_widget_list:
         #         w.setEnabled(True)
         # except Exception as e:
-        #     log.debug("FlatCAMGeoEditor.deactivate() --> %s" % str(e))
+        #     log.debug("AppGeoEditor.deactivate() --> %s" % str(e))
 
         # Show original geometry
         if self.fcgeometry:
@@ -3727,7 +3745,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         if self.app.is_legacy is False:
             # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp
-            # but those from FlatCAMGeoEditor
+            # but those from AppGeoEditor
             self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
             self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
             self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
@@ -3958,80 +3976,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.shapes.clear(update=True)
         self.tool_shape.clear(update=True)
 
-        # self.storage = FlatCAMGeoEditor.make_storage()
+        # self.storage = AppGeoEditor.make_storage()
         self.replot()
 
-    def edit_fcgeometry(self, fcgeometry, multigeo_tool=None):
-        """
-        Imports the geometry from the given FlatCAM Geometry object
-        into the editor.
-
-        :param fcgeometry:      GeometryObject
-        :param multigeo_tool:   A tool for the case of the edited geometry being of type 'multigeo'
-        :return:                None
-        """
-        assert isinstance(fcgeometry, Geometry), "Expected a Geometry, got %s" % type(fcgeometry)
-
-        self.deactivate()
-        self.activate()
-
-        self.set_ui()
-
-        # Hide original geometry
-        self.fcgeometry = fcgeometry
-        fcgeometry.visible = False
-
-        # Set selection tolerance
-        DrawToolShape.tolerance = fcgeometry.drawing_tolerance * 10
-
-        self.select_tool("select")
-
-        if self.app.defaults['geometry_spindledir'] == 'CW':
-            if self.app.defaults['geometry_editor_milling_type'] == 'cl':
-                milling_type = 1    # CCW motion = climb milling (spindle is rotating CW)
-            else:
-                milling_type = -1   # CW motion = conventional milling (spindle is rotating CW)
-        else:
-            if self.app.defaults['geometry_editor_milling_type'] == 'cl':
-                milling_type = -1    # CCW motion = climb milling (spindle is rotating CCW)
-            else:
-                milling_type = 1   # CW motion = conventional milling (spindle is rotating CCW)
-
-        # Link shapes into editor.
-        if multigeo_tool:
-            self.multigeo_tool = multigeo_tool
-            geo_to_edit = self.flatten(geometry=fcgeometry.tools[self.multigeo_tool]['solid_geometry'],
-                                       orient_val=milling_type)
-            self.app.inform.emit(
-                '[WARNING_NOTCL] %s: %s %s: %s' % (
-                    _("Editing MultiGeo Geometry, tool"),
-                    str(self.multigeo_tool),
-                    _("with diameter"),
-                    str(fcgeometry.tools[self.multigeo_tool]['tooldia'])
-                )
-            )
-        else:
-            geo_to_edit = self.flatten(geometry=fcgeometry.solid_geometry, orient_val=milling_type)
-
-        for shape in geo_to_edit:
-            if shape is not None:
-                if type(shape) == Polygon:
-                    self.add_shape(DrawToolShape(shape.exterior))
-                    for inter in shape.interiors:
-                        self.add_shape(DrawToolShape(inter))
-                else:
-                    self.add_shape(DrawToolShape(shape))
-
-        self.replot()
-
-        # updated units
-        self.units = self.app.defaults['units'].upper()
-        self.decimals = self.app.decimals
-
-        # start with GRID toolbar activated
-        if self.app.ui.grid_snap_btn.isChecked() is False:
-            self.app.ui.grid_snap_btn.trigger()
-
     def on_buffer_tool(self):
         buff_tool = BufferSelectionTool(self.app, self)
         buff_tool.run()
@@ -4105,8 +4052,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.pos = (self.pos[0], self.pos[1])
 
         if event.button == 1:
-            # self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
-            #                                        "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
+            self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                                   "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
 
             modifiers = QtWidgets.QApplication.keyboardModifiers()
             # If the SHIFT key is pressed when LMB is clicked then the coordinates are copied to clipboard
@@ -4203,8 +4150,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
                                            "<b>Y</b>: %.4f&nbsp;" % (x, y))
         #
         # # update the reference position label in the infobar since the APP mouse event handlers are disconnected
-        # self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
-        #                                        "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
+        self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                               "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
 
         units = self.app.defaults["units"].lower()
         self.app.plotcanvas.text_hud.text = \
@@ -4431,8 +4378,12 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.on_tool_select('move')
 
     def on_move_click(self):
+        try:
+            x, y = self.snap(self.x, self.y)
+        except TypeError:
+            return
         self.on_move()
-        self.active_tool.set_origin(self.snap(self.x, self.y))
+        self.active_tool.set_origin((x, y))
 
     def on_copy_click(self):
         if not self.selected:
@@ -4567,10 +4518,10 @@ class FlatCAMGeoEditor(QtCore.QObject):
                 elif isinstance(geom, LineString) and geom is not None:
                     geom = LineString(geom.coords[::-1])
                 else:
-                    log.debug("FlatCAMGeoEditor.on_shape_complete() Error --> Unexpected Geometry %s" %
+                    log.debug("AppGeoEditor.on_shape_complete() Error --> Unexpected Geometry %s" %
                               type(geom))
             except Exception as e:
-                log.debug("FlatCAMGeoEditor.on_shape_complete() Error --> %s" % str(e))
+                log.debug("AppGeoEditor.on_shape_complete() Error --> %s" % str(e))
                 return 'fail'
 
         shape_list = []
@@ -4682,6 +4633,77 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         return snap_x, snap_y
 
+    def edit_fcgeometry(self, fcgeometry, multigeo_tool=None):
+        """
+        Imports the geometry from the given FlatCAM Geometry object
+        into the editor.
+
+        :param fcgeometry:      GeometryObject
+        :param multigeo_tool:   A tool for the case of the edited geometry being of type 'multigeo'
+        :return:                None
+        """
+        assert isinstance(fcgeometry, Geometry), "Expected a Geometry, got %s" % type(fcgeometry)
+
+        self.deactivate()
+        self.activate()
+
+        self.set_ui()
+
+        # Hide original geometry
+        self.fcgeometry = fcgeometry
+        fcgeometry.visible = False
+
+        # Set selection tolerance
+        DrawToolShape.tolerance = fcgeometry.drawing_tolerance * 10
+
+        self.select_tool("select")
+
+        if self.app.defaults['geometry_spindledir'] == 'CW':
+            if self.app.defaults['geometry_editor_milling_type'] == 'cl':
+                milling_type = 1    # CCW motion = climb milling (spindle is rotating CW)
+            else:
+                milling_type = -1   # CW motion = conventional milling (spindle is rotating CW)
+        else:
+            if self.app.defaults['geometry_editor_milling_type'] == 'cl':
+                milling_type = -1    # CCW motion = climb milling (spindle is rotating CCW)
+            else:
+                milling_type = 1   # CW motion = conventional milling (spindle is rotating CCW)
+
+        # Link shapes into editor.
+        if multigeo_tool:
+            self.multigeo_tool = multigeo_tool
+            geo_to_edit = self.flatten(geometry=fcgeometry.tools[self.multigeo_tool]['solid_geometry'],
+                                       orient_val=milling_type)
+            self.app.inform.emit(
+                '[WARNING_NOTCL] %s: %s %s: %s' % (
+                    _("Editing MultiGeo Geometry, tool"),
+                    str(self.multigeo_tool),
+                    _("with diameter"),
+                    str(fcgeometry.tools[self.multigeo_tool]['tooldia'])
+                )
+            )
+        else:
+            geo_to_edit = self.flatten(geometry=fcgeometry.solid_geometry, orient_val=milling_type)
+
+        for shape in geo_to_edit:
+            if shape is not None:
+                if type(shape) == Polygon:
+                    self.add_shape(DrawToolShape(shape.exterior))
+                    for inter in shape.interiors:
+                        self.add_shape(DrawToolShape(inter))
+                else:
+                    self.add_shape(DrawToolShape(shape))
+
+        self.replot()
+
+        # updated units
+        self.units = self.app.defaults['units'].upper()
+        self.decimals = self.app.decimals
+
+        # start with GRID toolbar activated
+        if self.app.ui.grid_snap_btn.isChecked() is False:
+            self.app.ui.grid_snap_btn.trigger()
+
     def update_fcgeometry(self, fcgeometry):
         """
         Transfers the geometry tool shape buffer to the selected geometry
@@ -4757,7 +4779,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         try:
             results = geo_shapes[0].geo
         except Exception as e:
-            log.debug("FlatCAMGeoEditor.intersection() --> %s" % str(e))
+            log.debug("AppGeoEditor.intersection() --> %s" % str(e))
             self.app.inform.emit('[WARNING_NOTCL] %s' %
                                  _("A selection of at least 2 geo items is required to do Intersection."))
             self.select_tool('select')
@@ -4792,7 +4814,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         try:
             intersector = geo_shapes[0].geo
         except Exception as e:
-            log.debug("FlatCAMGeoEditor.intersection() --> %s" % str(e))
+            log.debug("AppGeoEditor.intersection() --> %s" % str(e))
             self.app.inform.emit('[WARNING_NOTCL] %s' %
                                  _("A selection of at least 2 geo items is required to do Intersection."))
             self.select_tool('select')

+ 74 - 57
appEditors/FlatCAMGrbEditor.py → appEditors/AppGerberEditor.py

@@ -242,8 +242,8 @@ class FCPad(FCShapeTool):
 
         self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
 
-        # Switch notebook to Selected page
-        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
 
         self.start_msg = _("Click to place ...")
 
@@ -472,8 +472,8 @@ class FCPadArray(FCShapeTool):
 
         self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
 
-        # Switch notebook to Selected page
-        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
 
     def click(self, point):
 
@@ -885,7 +885,7 @@ class FCRegion(FCShapeTool):
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
         except Exception as e:
-            log.debug("FlatCAMGrbEditor.FCRegion --> %s" % str(e))
+            log.debug("AppGerberEditor.FCRegion --> %s" % str(e))
 
         self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -991,7 +991,7 @@ class FCRegion(FCShapeTool):
                                                                               join_style=1)
                     return DrawToolUtilityShape(new_geo_el)
                 except Exception as e:
-                    log.debug("FlatCAMGrbEditor.FCRegion.utility_geometry() --> %s" % str(e))
+                    log.debug("AppGerberEditor.FCRegion.utility_geometry() --> %s" % str(e))
             else:
                 new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val,
                                                                      resolution=int(self.steps_per_circle / 4))
@@ -1182,7 +1182,7 @@ class FCTrack(FCShapeTool):
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
         except Exception as e:
-            log.debug("FlatCAMGrbEditor.FCTrack.__init__() --> %s" % str(e))
+            log.debug("AppGerberEditor.FCTrack.__init__() --> %s" % str(e))
 
         self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location +
                                                   '/aero_path%s.png' % self.draw_app.bend_mode))
@@ -1329,7 +1329,7 @@ class FCTrack(FCShapeTool):
             try:
                 QtGui.QGuiApplication.restoreOverrideCursor()
             except Exception as e:
-                log.debug("FlatCAMGrbEditor.FCTrack.on_key() --> %s" % str(e))
+                log.debug("AppGerberEditor.FCTrack.on_key() --> %s" % str(e))
 
             if self.draw_app.bend_mode == 1:
                 self.draw_app.bend_mode = 2
@@ -1368,7 +1368,7 @@ class FCTrack(FCShapeTool):
             try:
                 QtGui.QGuiApplication.restoreOverrideCursor()
             except Exception as e:
-                log.debug("FlatCAMGrbEditor.FCTrack.on_key() --> %s" % str(e))
+                log.debug("AppGerberEditor.FCTrack.on_key() --> %s" % str(e))
 
             if self.draw_app.bend_mode == 1:
                 self.draw_app.bend_mode = 5
@@ -1477,7 +1477,7 @@ class FCDisc(FCShapeTool):
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
         except Exception as e:
-            log.debug("FlatCAMGrbEditor.FCDisc --> %s" % str(e))
+            log.debug("AppGerberEditor.FCDisc --> %s" % str(e))
 
         self.draw_app.current_storage = self.storage_obj
 
@@ -1514,7 +1514,7 @@ class FCSemiDisc(FCShapeTool):
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
         except Exception as e:
-            log.debug("FlatCAMGrbEditor.FCSemiDisc --> %s" % str(e))
+            log.debug("AppGerberEditor.FCSemiDisc --> %s" % str(e))
 
         self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_semidisc.png'))
         QtGui.QGuiApplication.setOverrideCursor(self.cursor)
@@ -1954,8 +1954,8 @@ class FCApertureMove(FCShapeTool):
             aperture_on_row = self.draw_app.apertures_table.item(row, 1).text()
             self.selected_apertures.append(aperture_on_row)
 
-        # Switch notebook to Selected page
-        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
 
         self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
 
@@ -2170,8 +2170,8 @@ class FCEraser(FCShapeTool):
             aperture_on_row = self.draw_app.apertures_table.item(row, 1).text()
             self.selected_apertures.append(aperture_on_row)
 
-        # Switch notebook to Selected page
-        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
 
         self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
 
@@ -2201,7 +2201,7 @@ class FCEraser(FCShapeTool):
             try:
                 self.draw_app.apertures_table.cellPressed.disconnect()
             except Exception as e:
-                log.debug("FlatCAMGrbEditor.FCEraser.click_release() --> %s" % str(e))
+                log.debug("AppGerberEditor.FCEraser.click_release() --> %s" % str(e))
 
             self.draw_app.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
             for aper in sel_aperture:
@@ -2325,7 +2325,7 @@ class FCApertureSelect(DrawTool):
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
         except Exception as e:
-            log.debug("FlatCAMGrbEditor.FCApertureSelect --> %s" % str(e))
+            log.debug("AppGerberEditor.FCApertureSelect --> %s" % str(e))
 
     def set_origin(self, origin):
         self.origin = origin
@@ -2381,7 +2381,7 @@ class FCApertureSelect(DrawTool):
         try:
             self.grb_editor_app.apertures_table.cellPressed.disconnect()
         except Exception as e:
-            log.debug("FlatCAMGrbEditor.FCApertureSelect.click_release() --> %s" % str(e))
+            log.debug("AppGerberEditor.FCApertureSelect.click_release() --> %s" % str(e))
 
         for shape_s in self.grb_editor_app.selected:
             for storage in self.grb_editor_app.storage_dict:
@@ -2424,7 +2424,7 @@ class FCTransform(FCShapeTool):
         self.draw_app.plot_all()
 
 
-class FlatCAMGrbEditor(QtCore.QObject):
+class AppGerberEditor(QtCore.QObject):
 
     draw_shape_idx = -1
     # plot_finished = QtCore.pyqtSignal()
@@ -2434,7 +2434,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # assert isinstance(app, FlatCAMApp.App), \
         #     "Expected the app to be a FlatCAMApp.App, got %s" % type(app)
 
-        super(FlatCAMGrbEditor, self).__init__()
+        super(AppGerberEditor, self).__init__()
 
         self.app = app
         self.canvas = self.app.plotcanvas
@@ -2891,9 +2891,26 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.linear_angle_label.hide()
 
         self.array_frame.hide()
-
         self.custom_box.addStretch()
 
+        layout.addStretch()
+
+        # Editor
+        self.exit_editor_button = QtWidgets.QPushButton(_('Exit Editor'))
+        self.exit_editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/power16.png'))
+        self.exit_editor_button.setToolTip(
+            _("Exit from Editor.")
+        )
+        self.exit_editor_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        layout.addWidget(self.exit_editor_button)
+
+        self.exit_editor_button.clicked.connect(lambda: self.app.editor2object())
+
         # Toolbar events and properties
         self.tools_gerber = {
             "select": {"button": self.app.ui.grb_select_btn,
@@ -3305,12 +3322,12 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # make sure no rows are selected so the user have to click the correct row, meaning selecting the correct tool
         self.apertures_table.clearSelection()
 
-        # Remove anything else in the GUI Selected Tab
+        # Remove anything else in the GUI Properties Tab
         self.app.ui.selected_scroll_area.takeWidget()
-        # Put ourselves in the GUI Selected Tab
+        # Put ourselves in the GUI Properties Tab
         self.app.ui.selected_scroll_area.setWidget(self.grb_edit_widget)
-        # Switch notebook to Selected page
-        self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+        # Switch notebook to Properties page
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
 
         # we reactivate the signals after the after the tool adding as we don't need to see the tool been populated
         self.apertures_table.itemChanged.connect(self.on_tool_edit)
@@ -3369,7 +3386,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                         self.apsize_entry.set_value(size_val)
 
                     except Exception as e:
-                        log.error("FlatCAMGrbEditor.on_aperture_add() --> the R or O aperture dims has to be in a "
+                        log.error("AppGerberEditor.on_aperture_add() --> the R or O aperture dims has to be in a "
                                   "tuple format (x,y)\nError: %s" % str(e))
                         self.app.inform.emit('[WARNING_NOTCL] %s' %
                                              _("Aperture dimensions value is missing or wrong format. "
@@ -3497,7 +3514,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             try:
                 val_edited = int(self.apertures_table.currentItem().text())
             except ValueError as e:
-                log.debug("FlatCAMGrbEditor.on_tool_edit() --> %s" % str(e))
+                log.debug("AppGerberEditor.on_tool_edit() --> %s" % str(e))
                 # self.apertures_table.setCurrentItem(None)
                 # we reactivate the signals after the after the tool editing
                 self.apertures_table.itemChanged.connect(self.on_tool_edit)
@@ -3507,7 +3524,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             try:
                 val_edited = float(self.apertures_table.currentItem().text())
             except ValueError as e:
-                log.debug("FlatCAMGrbEditor.on_tool_edit() --> %s" % str(e))
+                log.debug("AppGerberEditor.on_tool_edit() --> %s" % str(e))
                 # self.apertures_table.setCurrentItem(None)
                 # we reactivate the signals after the after the tool editing
                 self.apertures_table.itemChanged.connect(self.on_tool_edit)
@@ -3519,7 +3536,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                     float(x.strip()) for x in self.apertures_table.currentItem().text().split(",") if x != ''
                 ]
             except ValueError as e:
-                log.debug("FlatCAMGrbEditor.on_tool_edit() --> %s" % str(e))
+                log.debug("AppGerberEditor.on_tool_edit() --> %s" % str(e))
                 # we reactivate the signals after the after the tool editing
                 self.apertures_table.itemChanged.connect(self.on_tool_edit)
                 return
@@ -3703,7 +3720,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         self.app.ui.grb_edit_toolbar.setDisabled(False)
         self.app.ui.grb_edit_toolbar.setVisible(True)
-        # self.app.ui.status_toolbar.setDisabled(False)
+        # self.app.ui.grid_toolbar.setDisabled(False)
 
         # start with GRID toolbar activated
         if self.app.ui.grid_snap_btn.isChecked() is False:
@@ -3725,7 +3742,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
         except Exception as e:
-            log.debug("FlatCAMGrbEditor.deactivate_grb_editor() --> %s" % str(e))
+            log.debug("AppGerberEditor.deactivate_grb_editor() --> %s" % str(e))
 
         self.clear()
 
@@ -3778,7 +3795,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # Canvas events
 
         # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp
-        # but those from FlatCAMGeoEditor
+        # but those from AppGeoEditor
 
         # first connect to new, then disconnect the old handlers
         # don't ask why but if there is nothing connected I've seen issues
@@ -3980,7 +3997,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             # we activate this after the initial build as we don't need to see the tool been populated
             self.apertures_table.itemChanged.connect(self.on_tool_edit)
         except Exception as e:
-            log.debug("FlatCAMGrbEditor.edit_fcgerber() --> %s" % str(e))
+            log.debug("AppGerberEditor.edit_fcgerber() --> %s" % str(e))
 
         # apply the conversion factor on the obj.apertures
         conv_apertures = deepcopy(self.gerber_obj.apertures)
@@ -4020,7 +4037,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         #                 else:
         #                     self.storage_dict[aperture_id][k] = self.gerber_obj.apertures[aperture_id][k]
         #             except Exception as e:
-        #                 log.debug("FlatCAMGrbEditor.edit_fcgerber().job_thread() --> %s" % str(e))
+        #                 log.debug("AppGerberEditor.edit_fcgerber().job_thread() --> %s" % str(e))
         #
         #         # Check promises and clear if exists
         #         while True:
@@ -4150,7 +4167,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                             )
                     except Exception as ee:
                         log.debug(
-                            "FlatCAMGrbEditor.edit_fcgerber.worker_job() Adding processes to pool --> %s" % str(ee))
+                            "AppGerberEditor.edit_fcgerber.worker_job() Adding processes to pool --> %s" % str(ee))
                         traceback.print_exc()
 
                     output = []
@@ -4186,7 +4203,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 else:
                     storage_dict[k] = aperture_dict[k]
             except Exception as e:
-                log.debug("FlatCAMGrbEditor.edit_fcgerber().job_thread() --> %s" % str(e))
+                log.debug("AppGerberEditor.edit_fcgerber().job_thread() --> %s" % str(e))
 
         return [aperture_id, storage_dict]
 
@@ -4215,7 +4232,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         try:
             self.plot_thread.stop()
         except Exception as e:
-            log.debug("FlatCAMGrbEditor.update_fcgerber() --> %s" % str(e))
+            log.debug("AppGerberEditor.update_fcgerber() --> %s" % str(e))
 
         if "_edit" in self.edited_obj_name:
             try:
@@ -4522,8 +4539,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
             self.pos = (self.pos[0], self.pos[1])
 
         if event.button == 1:
-            # self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
-            #                                        "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
+            self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                                   "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
 
             # Selection with left mouse button
             if self.active_tool is not None:
@@ -4598,7 +4615,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                         try:
                             QtGui.QGuiApplication.restoreOverrideCursor()
                         except Exception as e:
-                            log.debug("FlatCAMGrbEditor.on_grb_click_release() --> %s" % str(e))
+                            log.debug("AppGerberEditor.on_grb_click_release() --> %s" % str(e))
 
                         if self.active_tool.complete is False and not isinstance(self.active_tool, FCApertureSelect):
                             self.active_tool.complete = True
@@ -4640,7 +4657,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                                     else:
                                         self.select_tool("select")
         except Exception as e:
-            log.warning("FlatCAMGrbEditor.on_grb_click_release() RMB click --> Error: %s" % str(e))
+            log.warning("AppGerberEditor.on_grb_click_release() RMB click --> Error: %s" % str(e))
             raise
 
         # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
@@ -4658,7 +4675,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                     if self.selected:
                         self.plot_all()
         except Exception as e:
-            log.warning("FlatCAMGrbEditor.on_grb_click_release() LMB click --> Error: %s" % str(e))
+            log.warning("AppGerberEditor.on_grb_click_release() LMB click --> Error: %s" % str(e))
             raise
 
     def draw_selection_area_handler(self, start_pos, end_pos, sel_type):
@@ -4694,7 +4711,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         try:
             self.apertures_table.cellPressed.disconnect()
         except Exception as e:
-            log.debug("FlatCAMGrbEditor.draw_selection_Area_handler() --> %s" % str(e))
+            log.debug("AppGerberEditor.draw_selection_Area_handler() --> %s" % str(e))
         # select the aperture code of the selected geometry, in the tool table
         self.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
         for aper in sel_aperture:
@@ -4770,10 +4787,10 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # # update the position label in the infobar since the APP mouse event handlers are disconnected
         self.app.ui.position_label.setText("&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
                                            "<b>Y</b>: %.4f&nbsp;" % (x, y))
-        #
-        # # update the reference position label in the infobar since the APP mouse event handlers are disconnected
-        # self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
-        #                                        "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
+
+        # update the reference position label in the infobar since the APP mouse event handlers are disconnected
+        self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                               "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
 
         units = self.app.defaults["units"].lower()
         self.app.plotcanvas.text_hud.text = \
@@ -4901,7 +4918,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
     #
     #     # self.plot_thread = threading.Thread(target=lambda: self.check_plot_finished(check_period))
     #     # self.plot_thread.start()
-    #     log.debug("FlatCAMGrbEditor --> Delayed Plot started.")
+    #     log.debug("AppGerberEditor --> Delayed Plot started.")
     #     self.plot_thread = QtCore.QTimer()
     #     self.plot_thread.setInterval(check_period)
     #     self.plot_finished.connect(self.setup_ui_after_delayed_plot)
@@ -4919,7 +4936,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
     #         if not self.grb_plot_promises:
     #             self.plot_thread.stop()
     #             self.plot_finished.emit()
-    #             log.debug("FlatCAMGrbEditor --> delayed_plot finished")
+    #             log.debug("AppGerberEditor --> delayed_plot finished")
     #     except Exception as e:
     #         traceback.print_exc()
     #
@@ -4941,7 +4958,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         :return:        None
         """
-        log.debug("FlatCAMGrbEditor.on_zoom_fit()")
+        log.debug("AppGerberEditor.on_zoom_fit()")
 
         # calculate all the geometry in the edited Gerber object
         edit_geo = []
@@ -5095,7 +5112,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
     def on_buffer(self):
         buff_value = 0.01
-        log.debug("FlatCAMGrbEditor.on_buffer()")
+        log.debug("AppGerberEditor.on_buffer()")
 
         try:
             buff_value = float(self.buffer_distance_entry.get_value())
@@ -5145,7 +5162,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 self.storage_dict[apcode]['geometry'] = []
                 self.storage_dict[apcode]['geometry'] = temp_storage
             except Exception as e:
-                log.debug("FlatCAMGrbEditor.buffer() --> %s" % str(e))
+                log.debug("AppGerberEditor.buffer() --> %s" % str(e))
                 self.app.inform.emit('[ERROR_NOTCL] %s\n%s' % (_("Failed."), str(traceback.print_exc())))
                 return
 
@@ -5154,7 +5171,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
     def on_scale(self):
         scale_factor = 1.0
-        log.debug("FlatCAMGrbEditor.on_scale()")
+        log.debug("AppGerberEditor.on_scale()")
 
         try:
             scale_factor = float(self.scale_factor_entry.get_value())
@@ -5209,7 +5226,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 self.storage_dict[apcode]['geometry'] = temp_storage
 
             except Exception as e:
-                log.debug("FlatCAMGrbEditor.on_scale() --> %s" % str(e))
+                log.debug("AppGerberEditor.on_scale() --> %s" % str(e))
 
         self.plot_all()
         self.app.inform.emit('[success] %s' %
@@ -5287,8 +5304,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
             if tool_name == 'markarea' or tool_name == 'all':
                 self.ma_tool_frame.hide()
         except Exception as e:
-            log.debug("FlatCAMGrbEditor.hide_tool() --> %s" % str(e))
-        self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+            log.debug("AppGerberEditor.hide_tool() --> %s" % str(e))
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
 
 
 class TransformEditorTool(AppTool):
@@ -5716,7 +5733,7 @@ class TransformEditorTool(AppTool):
         if toggle:
             try:
                 if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                    self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+                    self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
                 else:
                     self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
             except AttributeError:

+ 54 - 14
appEditors/FlatCAMTextEditor.py → appEditors/AppTextEditor.py

@@ -5,7 +5,7 @@
 # MIT Licence                                              #
 # ##########################################################
 
-from appGUI.GUIElements import FCFileSaveDialog, FCEntry, FCTextAreaExtended, FCTextAreaLineNumber
+from appGUI.GUIElements import FCFileSaveDialog, FCEntry, FCTextAreaExtended, FCTextAreaLineNumber, FCButton
 from PyQt5 import QtPrintSupport, QtWidgets, QtCore, QtGui
 
 from reportlab.platypus import SimpleDocTemplate, Paragraph
@@ -23,13 +23,14 @@ if '_' not in builtins.__dict__:
     _ = gettext.gettext
 
 
-class TextEditor(QtWidgets.QWidget):
+class AppTextEditor(QtWidgets.QWidget):
 
     def __init__(self, app, text=None, plain_text=None, parent=None):
         super().__init__(parent=parent)
 
         self.app = app
         self.plain_text = plain_text
+        self.callback = lambda x: None
 
         self.setSizePolicy(
             QtWidgets.QSizePolicy.MinimumExpanding,
@@ -71,21 +72,26 @@ class TextEditor(QtWidgets.QWidget):
         if text:
             self.code_editor.setPlainText(text)
 
-        self.buttonPreview = QtWidgets.QPushButton(_('Print Preview'))
+        self.buttonPreview = FCButton(_('Print Preview'))
+        self.buttonPreview.setIcon(QtGui.QIcon(self.app.resource_location + '/preview32.png'))
         self.buttonPreview.setToolTip(_("Open a OS standard Preview Print window."))
         self.buttonPreview.setMinimumWidth(100)
 
-        self.buttonPrint = QtWidgets.QPushButton(_('Print Code'))
+        self.buttonPrint = FCButton(_('Print Code'))
+        self.buttonPrint.setIcon(QtGui.QIcon(self.app.resource_location + '/printer32.png'))
         self.buttonPrint.setToolTip(_("Open a OS standard Print window."))
+        self.buttonPrint.setMinimumWidth(100)
 
-        self.buttonFind = QtWidgets.QPushButton(_('Find in Code'))
+        self.buttonFind = FCButton(_('Find in Code'))
+        self.buttonFind.setIcon(QtGui.QIcon(self.app.resource_location + '/find32.png'))
         self.buttonFind.setToolTip(_("Will search and highlight in yellow the string in the Find box."))
         self.buttonFind.setMinimumWidth(100)
 
         self.entryFind = FCEntry()
         self.entryFind.setToolTip(_("Find box. Enter here the strings to be searched in the text."))
 
-        self.buttonReplace = QtWidgets.QPushButton(_('Replace With'))
+        self.buttonReplace = FCButton(_('Replace With'))
+        self.buttonReplace.setIcon(QtGui.QIcon(self.app.resource_location + '/replace32.png'))
         self.buttonReplace.setToolTip(_("Will replace the string from the Find box with the one in the Replace box."))
         self.buttonReplace.setMinimumWidth(100)
 
@@ -96,18 +102,29 @@ class TextEditor(QtWidgets.QWidget):
         self.sel_all_cb.setToolTip(_("When checked it will replace all instances in the 'Find' box\n"
                                      "with the text in the 'Replace' box.."))
 
-        self.button_copy_all = QtWidgets.QPushButton(_('Copy All'))
+        self.button_copy_all = FCButton(_('Copy All'))
+        self.button_copy_all.setIcon(QtGui.QIcon(self.app.resource_location + '/copy_file32.png'))
         self.button_copy_all.setToolTip(_("Will copy all the text in the Code Editor to the clipboard."))
         self.button_copy_all.setMinimumWidth(100)
 
-        self.buttonOpen = QtWidgets.QPushButton(_('Open Code'))
+        self.button_update_code = QtWidgets.QToolButton()
+        self.button_update_code.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+        self.button_update_code.setToolTip(_("Save changes internally."))
+        self.button_update_code.hide()
+
+        self.buttonOpen = FCButton(_('Open Code'))
+        self.buttonOpen.setIcon(QtGui.QIcon(self.app.resource_location + '/folder32_bis.png'))
         self.buttonOpen.setToolTip(_("Will open a text file in the editor."))
+        self.buttonOpen.setMinimumWidth(100)
 
-        self.buttonSave = QtWidgets.QPushButton(_('Save Code'))
+        self.buttonSave = FCButton(_('Save Code'))
+        self.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
         self.buttonSave.setToolTip(_("Will save the text in the editor into a file."))
+        self.buttonSave.setMinimumWidth(100)
 
-        self.buttonRun = QtWidgets.QPushButton(_('Run Code'))
+        self.buttonRun = FCButton(_('Run Code'))
         self.buttonRun.setToolTip(_("Will run the TCL commands found in the text file, one by one."))
+        self.buttonRun.setMinimumWidth(100)
 
         self.buttonRun.hide()
 
@@ -119,6 +136,7 @@ class TextEditor(QtWidgets.QWidget):
         editor_hlay_1.addWidget(self.entryReplace)
         editor_hlay_1.addWidget(self.sel_all_cb)
         editor_hlay_1.addWidget(self.button_copy_all)
+        editor_hlay_1.addWidget(self.button_update_code)
         self.work_editor_layout.addLayout(editor_hlay_1, 1, 0, 1, 5)
 
         editor_hlay_2 = QtWidgets.QHBoxLayout()
@@ -151,6 +169,9 @@ class TextEditor(QtWidgets.QWidget):
 
         self.code_edited = ''
 
+    def set_callback(self, callback):
+        self.callback = callback
+
     def handlePrint(self):
         self.app.defaults.report_usage("handlePrint()")
 
@@ -169,7 +190,26 @@ class TextEditor(QtWidgets.QWidget):
         # enable = not self.ui.code_editor.document().isEmpty()
         # self.ui.buttonPrint.setEnabled(enable)
         # self.ui.buttonPreview.setEnabled(enable)
-        pass
+
+        self.buttonSave.setStyleSheet("QPushButton {color: red;}")
+        self.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as_red.png'))
+
+    def load_text(self, text, move_to_start=False, move_to_end=False, clear_text=True, as_html=False):
+        self.code_editor.textChanged.disconnect()
+        if clear_text:
+            # first clear previous text in text editor (if any)
+            self.code_editor.clear()
+
+        self.code_editor.setReadOnly(False)
+        if as_html is False:
+            self.code_editor.setPlainText(text)
+        else:
+            self.code_editor.setHtml(text)
+        if move_to_start:
+            self.code_editor.moveCursor(QtGui.QTextCursor.Start)
+        elif move_to_end:
+            self.code_editor.moveCursor(QtGui.QTextCursor.End)
+        self.code_editor.textChanged.connect(self.handleTextChanged)
 
     def handleOpen(self, filt=None):
         self.app.defaults.report_usage("handleOpen()")
@@ -268,6 +308,8 @@ class TextEditor(QtWidgets.QWidget):
                     with open(filename, 'w') as f:
                         for line in my_gcode:
                             f.write(line)
+                self.buttonSave.setStyleSheet("")
+                self.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
             except FileNotFoundError:
                 self.app.inform.emit('[WARNING] %s' % _("No such file or directory"))
                 return
@@ -287,7 +329,6 @@ class TextEditor(QtWidgets.QWidget):
             callback()
 
     def handleFindGCode(self):
-        self.app.defaults.report_usage("handleFindGCode()")
 
         flags = QtGui.QTextDocument.FindCaseSensitively
         text_to_be_found = self.entryFind.get_value()
@@ -298,7 +339,6 @@ class TextEditor(QtWidgets.QWidget):
             r = self.code_editor.find(str(text_to_be_found), flags)
 
     def handleReplaceGCode(self):
-        self.app.defaults.report_usage("handleReplaceGCode()")
 
         old = self.entryFind.get_value()
         new = self.entryReplace.get_value()
@@ -331,7 +371,7 @@ class TextEditor(QtWidgets.QWidget):
     def handleCopyAll(self):
         text = self.code_editor.toPlainText()
         self.app.clipboard.setText(text)
-        self.app.inform.emit(_("Code Editor content copied to clipboard ..."))
+        self.app.inform.emit(_("Content copied to clipboard ..."))
 
     # def closeEvent(self, QCloseEvent):
     #     super().closeEvent(QCloseEvent)

+ 788 - 0
appEditors/appGCodeEditor.py

@@ -0,0 +1,788 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 07/22/2020                                         #
+# MIT Licence                                              #
+# ##########################################################
+
+from appEditors.AppTextEditor import AppTextEditor
+from appObjects.FlatCAMCNCJob import CNCJobObject
+from appGUI.GUIElements import FCTextArea, FCEntry, FCButton, FCTable
+from PyQt5 import QtWidgets, QtCore, QtGui
+
+# from io import StringIO
+
+import logging
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class AppGCodeEditor(QtCore.QObject):
+
+    def __init__(self, app, parent=None):
+        super().__init__(parent=parent)
+
+        self.app = app
+        self.decimals = self.app.decimals
+        self.plain_text = ''
+        self.callback = lambda x: None
+
+        self.ui = AppGCodeEditorUI(app=self.app)
+
+        self.edited_obj_name = ""
+        self.edit_area = None
+
+        self.gcode_obj = None
+        self.code_edited = ''
+
+        # store the status of the editor so the Delete at object level will not work until the edit is finished
+        self.editor_active = False
+        log.debug("Initialization of the GCode Editor is finished ...")
+
+    def set_ui(self):
+        """
+
+        :return:
+        :rtype:
+        """
+
+        self.decimals = self.app.decimals
+
+        # #############################################################################################################
+        # ############# ADD a new TAB in the PLot Tab Area
+        # #############################################################################################################
+        self.ui.gcode_editor_tab = AppTextEditor(app=self.app, plain_text=True)
+        self.edit_area = self.ui.gcode_editor_tab.code_editor
+
+        # add the tab if it was closed
+        self.app.ui.plot_tab_area.addTab(self.ui.gcode_editor_tab, '%s' % _("Code Editor"))
+        self.ui.gcode_editor_tab.setObjectName('gcode_editor_tab')
+        # protect the tab that was just added
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.widget(idx).objectName() == self.ui.gcode_editor_tab.objectName():
+                self.app.ui.plot_tab_area.protectTab(idx)
+
+        # delete the absolute and relative position and messages in the infobar
+        self.app.ui.position_label.setText("")
+        self.app.ui.rel_position_label.setText("")
+
+        self.ui.gcode_editor_tab.code_editor.completer_enable = False
+        self.ui.gcode_editor_tab.buttonRun.hide()
+
+        # Switch plot_area to CNCJob tab
+        self.app.ui.plot_tab_area.setCurrentWidget(self.ui.gcode_editor_tab)
+
+        self.ui.gcode_editor_tab.t_frame.hide()
+
+        self.ui.gcode_editor_tab.t_frame.show()
+        self.app.proc_container.view.set_idle()
+        # #############################################################################################################
+        # #############################################################################################################
+
+        self.ui.append_text.set_value(self.app.defaults["cncjob_append"])
+        self.ui.prepend_text.set_value(self.app.defaults["cncjob_prepend"])
+
+        # Remove anything else in the GUI Properties Tab
+        self.app.ui.selected_scroll_area.takeWidget()
+        # Put ourselves in the GUI Properties Tab
+        self.app.ui.selected_scroll_area.setWidget(self.ui.edit_widget)
+        # Switch notebook to Properties page
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
+
+        # make a new name for the new Excellon object (the one with edited content)
+        self.edited_obj_name = self.gcode_obj.options['name']
+        self.ui.name_entry.set_value(self.edited_obj_name)
+
+        self.activate()
+
+        # #################################################################################
+        # ################### SIGNALS #####################################################
+        # #################################################################################
+        self.ui.name_entry.returnPressed.connect(self.on_name_activate)
+        self.ui.update_gcode_button.clicked.connect(self.insert_gcode)
+        self.ui.exit_editor_button.clicked.connect(lambda: self.app.editor2object())
+
+    def build_ui(self):
+        """
+
+        :return:
+        :rtype:
+        """
+
+        self.ui_disconnect()
+
+        # if the FlatCAM object is Excellon don't build the CNC Tools Table but hide it
+        self.ui.cnc_tools_table.hide()
+        if self.gcode_obj.cnc_tools:
+            self.ui.cnc_tools_table.show()
+            self.build_cnc_tools_table()
+
+        self.ui.exc_cnc_tools_table.hide()
+        if self.gcode_obj.exc_cnc_tools:
+            self.ui.exc_cnc_tools_table.show()
+            self.build_excellon_cnc_tools()
+
+        self.ui_connect()
+
+    def build_cnc_tools_table(self):
+        tool_idx = 0
+        row_no = 0
+
+        n = len(self.gcode_obj.cnc_tools) + 3
+        self.ui.cnc_tools_table.setRowCount(n)
+
+        # add the All Gcode selection
+        allgcode_item = QtWidgets.QTableWidgetItem('%s' % _("All GCode"))
+        allgcode_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        self.ui.cnc_tools_table.setItem(row_no, 1, allgcode_item)
+        row_no += 1
+
+        # add the Header Gcode selection
+        header_item = QtWidgets.QTableWidgetItem('%s' % _("Header GCode"))
+        header_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        self.ui.cnc_tools_table.setItem(row_no, 1, header_item)
+        row_no += 1
+
+        # add the Start Gcode selection
+        start_item = QtWidgets.QTableWidgetItem('%s' % _("Start GCode"))
+        start_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        self.ui.cnc_tools_table.setItem(row_no, 1, start_item)
+
+        for dia_key, dia_value in self.gcode_obj.cnc_tools.items():
+
+            tool_idx += 1
+            row_no += 1
+
+            t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
+            # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.cnc_tools_table.setItem(row_no, 0, t_id)  # Tool name/id
+
+            dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(dia_value['tooldia'])))
+
+            offset_txt = list(str(dia_value['offset']))
+            offset_txt[0] = offset_txt[0].upper()
+            offset_item = QtWidgets.QTableWidgetItem(''.join(offset_txt))
+            type_item = QtWidgets.QTableWidgetItem(str(dia_value['type']))
+            tool_type_item = QtWidgets.QTableWidgetItem(str(dia_value['tool_type']))
+
+            t_id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            dia_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            offset_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            type_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            tool_type_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+            self.ui.cnc_tools_table.setItem(row_no, 1, dia_item)  # Diameter
+            self.ui.cnc_tools_table.setItem(row_no, 2, offset_item)  # Offset
+            self.ui.cnc_tools_table.setItem(row_no, 3, type_item)  # Toolpath Type
+            self.ui.cnc_tools_table.setItem(row_no, 4, tool_type_item)  # Tool Type
+
+            tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_key))
+            # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
+            self.ui.cnc_tools_table.setItem(row_no, 5, tool_uid_item)  # Tool unique ID)
+
+        self.ui.cnc_tools_table.resizeColumnsToContents()
+        self.ui.cnc_tools_table.resizeRowsToContents()
+
+        vertical_header = self.ui.cnc_tools_table.verticalHeader()
+        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+        vertical_header.hide()
+        self.ui.cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.ui.cnc_tools_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(4, 40)
+
+        # horizontal_header.setStretchLastSection(True)
+        self.ui.cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        self.ui.cnc_tools_table.setColumnWidth(0, 20)
+        self.ui.cnc_tools_table.setColumnWidth(4, 40)
+        self.ui.cnc_tools_table.setColumnWidth(6, 17)
+
+        # self.ui.geo_tools_table.setSortingEnabled(True)
+
+        self.ui.cnc_tools_table.setMinimumHeight(self.ui.cnc_tools_table.getHeight())
+        self.ui.cnc_tools_table.setMaximumHeight(self.ui.cnc_tools_table.getHeight())
+
+    def build_excellon_cnc_tools(self):
+        """
+
+        :return:
+        :rtype:
+        """
+
+        tool_idx = 0
+        row_no = 0
+
+        n = len(self.gcode_obj.exc_cnc_tools) + 3
+        self.ui.exc_cnc_tools_table.setRowCount(n)
+
+        # add the All Gcode selection
+        allgcode_item = QtWidgets.QTableWidgetItem('%s' % _("All GCode"))
+        allgcode_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        self.ui.exc_cnc_tools_table.setItem(row_no, 1, allgcode_item)
+        row_no += 1
+
+        # add the Header Gcode selection
+        header_item = QtWidgets.QTableWidgetItem('%s' % _("Header GCode"))
+        header_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        self.ui.exc_cnc_tools_table.setItem(row_no, 1, header_item)
+        row_no += 1
+
+        # add the Start Gcode selection
+        start_item = QtWidgets.QTableWidgetItem('%s' % _("Start GCode"))
+        start_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        self.ui.exc_cnc_tools_table.setItem(row_no, 1, start_item)
+
+        for tooldia_key, dia_value in self.gcode_obj.exc_cnc_tools.items():
+
+            tool_idx += 1
+            row_no += 1
+
+            t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
+            dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(tooldia_key)))
+            nr_drills_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_drills']))
+            nr_slots_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_slots']))
+            cutz_item = QtWidgets.QTableWidgetItem('%.*f' % (
+                self.decimals, float(dia_value['offset']) + float(dia_value['data']['tools_drill_cutz'])))
+
+            t_id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            dia_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            nr_drills_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            nr_slots_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            cutz_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+            self.ui.exc_cnc_tools_table.setItem(row_no, 0, t_id)  # Tool name/id
+            self.ui.exc_cnc_tools_table.setItem(row_no, 1, dia_item)  # Diameter
+            self.ui.exc_cnc_tools_table.setItem(row_no, 2, nr_drills_item)  # Nr of drills
+            self.ui.exc_cnc_tools_table.setItem(row_no, 3, nr_slots_item)  # Nr of slots
+
+            tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_value['tool']))
+            # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
+            self.ui.exc_cnc_tools_table.setItem(row_no, 4, tool_uid_item)  # Tool unique ID)
+            self.ui.exc_cnc_tools_table.setItem(row_no, 5, cutz_item)
+
+        self.ui.exc_cnc_tools_table.resizeColumnsToContents()
+        self.ui.exc_cnc_tools_table.resizeRowsToContents()
+
+        vertical_header = self.ui.exc_cnc_tools_table.verticalHeader()
+        vertical_header.hide()
+        self.ui.exc_cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.ui.exc_cnc_tools_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.ResizeToContents)
+
+        # horizontal_header.setStretchLastSection(True)
+        self.ui.exc_cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        self.ui.exc_cnc_tools_table.setColumnWidth(0, 20)
+        self.ui.exc_cnc_tools_table.setColumnWidth(6, 17)
+
+        self.ui.exc_cnc_tools_table.setMinimumHeight(self.ui.exc_cnc_tools_table.getHeight())
+        self.ui.exc_cnc_tools_table.setMaximumHeight(self.ui.exc_cnc_tools_table.getHeight())
+
+    def ui_connect(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        # rows selected
+        if self.gcode_obj.cnc_tools:
+            self.ui.cnc_tools_table.clicked.connect(self.on_row_selection_change)
+            self.ui.cnc_tools_table.horizontalHeader().sectionClicked.connect(self.on_toggle_all_rows)
+        if self.gcode_obj.exc_cnc_tools:
+            self.ui.exc_cnc_tools_table.clicked.connect(self.on_row_selection_change)
+            self.ui.exc_cnc_tools_table.horizontalHeader().sectionClicked.connect(self.on_toggle_all_rows)
+
+    def ui_disconnect(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        # rows selected
+        if self.gcode_obj.cnc_tools:
+            try:
+                self.ui.cnc_tools_table.clicked.disconnect(self.on_row_selection_change)
+            except (TypeError, AttributeError):
+                pass
+            try:
+                self.ui.cnc_tools_table.horizontalHeader().sectionClicked.disconnect(self.on_toggle_all_rows)
+            except (TypeError, AttributeError):
+                pass
+
+        if self.gcode_obj.exc_cnc_tools:
+            try:
+                self.ui.exc_cnc_tools_table.clicked.disconnect(self.on_row_selection_change)
+            except (TypeError, AttributeError):
+                pass
+            try:
+                self.ui.exc_cnc_tools_table.horizontalHeader().sectionClicked.disconnect(self.on_toggle_all_rows)
+            except (TypeError, AttributeError):
+                pass
+
+    def on_row_selection_change(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        flags = QtGui.QTextDocument.FindCaseSensitively
+        self.edit_area.moveCursor(QtGui.QTextCursor.Start)
+
+        if self.gcode_obj.cnc_tools:
+            t_table = self.ui.cnc_tools_table
+        elif self.gcode_obj.exc_cnc_tools:
+            t_table = self.ui.exc_cnc_tools_table
+        else:
+            return
+
+        sel_model = t_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        if 0 in sel_rows:
+            self.edit_area.selectAll()
+            return
+
+        if 1 in sel_rows:
+            text_to_be_found = self.gcode_obj.gc_header
+            text_list = [x for x in text_to_be_found.split("\n") if x != '']
+
+            self.edit_area.find(str(text_list[0]), flags)
+            my_text_cursor = self.edit_area.textCursor()
+            start_sel = my_text_cursor.selectionStart()
+
+            end_sel = 0
+            while True:
+                f = self.edit_area.find(str(text_list[-1]), flags)
+                if f is False:
+                    break
+                my_text_cursor = self.edit_area.textCursor()
+                end_sel = my_text_cursor.selectionEnd()
+
+            my_text_cursor.setPosition(start_sel)
+            my_text_cursor.setPosition(end_sel, QtGui.QTextCursor.KeepAnchor)
+            self.edit_area.setTextCursor(my_text_cursor)
+
+        if 2 in sel_rows:
+            text_to_be_found = self.gcode_obj.gc_start
+            text_list = [x for x in text_to_be_found.split("\n") if x != '']
+
+            self.edit_area.find(str(text_list[0]), flags)
+            my_text_cursor = self.edit_area.textCursor()
+            start_sel = my_text_cursor.selectionStart()
+
+            end_sel = 0
+            while True:
+                f = self.edit_area.find(str(text_list[-1]), flags)
+                if f is False:
+                    break
+                my_text_cursor = self.edit_area.textCursor()
+                end_sel = my_text_cursor.selectionEnd()
+
+            my_text_cursor.setPosition(start_sel)
+            my_text_cursor.setPosition(end_sel, QtGui.QTextCursor.KeepAnchor)
+            self.edit_area.setTextCursor(my_text_cursor)
+
+        sel_list = []
+        for row in sel_rows:
+            # those are special rows treated before so we except them
+            if row not in [0, 1, 2]:
+                tool_no = int(t_table.item(row, 0).text())
+
+                text_to_be_found = None
+                if self.gcode_obj.cnc_tools:
+                    text_to_be_found = self.gcode_obj.cnc_tools[tool_no]['gcode']
+                elif self.gcode_obj.exc_cnc_tools:
+                    tool_dia = self.app.dec_format(float(t_table.item(row, 1).text()), dec=self.decimals)
+                    for tool_d in self.gcode_obj.exc_cnc_tools:
+                        if self.app.dec_format(tool_d, dec=self.decimals) == tool_dia:
+                            text_to_be_found = self.gcode_obj.exc_cnc_tools[tool_d]['gcode']
+                    if text_to_be_found is None:
+                        continue
+                else:
+                    continue
+
+                text_list = [x for x in text_to_be_found.split("\n") if x != '']
+
+                # self.edit_area.find(str(text_list[0]), flags)
+                # my_text_cursor = self.edit_area.textCursor()
+                # start_sel = my_text_cursor.selectionStart()
+
+                # first I search for the tool
+                found_tool = self.edit_area.find('T%d' % tool_no, flags)
+                if found_tool is False:
+                    continue
+
+                # once the tool found then I set the text Cursor position to the tool Tx position
+                my_text_cursor = self.edit_area.textCursor()
+                tool_pos = my_text_cursor.selectionStart()
+                my_text_cursor.setPosition(tool_pos)
+
+                # I search for the first finding of the first line in the Tool GCode
+                f = self.edit_area.find(str(text_list[0]), flags)
+                if f is False:
+                    continue
+
+                # once found I set the text Cursor position here
+                my_text_cursor = self.edit_area.textCursor()
+                start_sel = my_text_cursor.selectionStart()
+
+                # I search for the next find of M6 (which belong to the next tool
+                m6 = self.edit_area.find('M6', flags)
+                if m6 is False:
+                    # this mean that we are in the last tool, we take all to the end
+                    self.edit_area.moveCursor(QtGui.QTextCursor.End)
+                    my_text_cursor = self.edit_area.textCursor()
+                    end_sel = my_text_cursor.selectionEnd()
+                else:
+                    pos_list = []
+                    end_sel = 0
+
+                    my_text_cursor = self.edit_area.textCursor()
+                    m6_pos = my_text_cursor.selectionEnd()
+
+                    # move cursor back to the start of the tool gcode so the find method will work on the tool gcode
+                    t_curs = self.edit_area.textCursor()
+                    t_curs.setPosition(start_sel)
+                    self.edit_area.setTextCursor(t_curs)
+
+                    # search for all findings of the last line in the tool gcode
+                    # yet, we may find in multiple locations or in the gcode that belong to other tools
+                    while True:
+                        f = self.edit_area.find(str(text_list[-1]), flags)
+                        if f is False:
+                            break
+                        my_text_cursor = self.edit_area.textCursor()
+                        pos_list.append(my_text_cursor.selectionEnd())
+
+                    # now we find a position that is less than the m6_pos but also the closest (maximum)
+                    belong_to_tool_list = []
+                    for last_line_pos in pos_list:
+                        if last_line_pos < m6_pos:
+                            belong_to_tool_list.append(last_line_pos)
+                    if belong_to_tool_list:
+                        end_sel = max(belong_to_tool_list)
+                    else:
+                        # this mean that we are in the last tool, we take all to the end
+                        self.edit_area.moveCursor(QtGui.QTextCursor.End)
+                        my_text_cursor = self.edit_area.textCursor()
+                        end_sel = my_text_cursor.selectionEnd()
+
+                my_text_cursor.setPosition(start_sel)
+                my_text_cursor.setPosition(end_sel, QtGui.QTextCursor.KeepAnchor)
+                self.edit_area.setTextCursor(my_text_cursor)
+
+                tool_selection = QtWidgets.QTextEdit.ExtraSelection()
+                tool_selection.cursor = self.edit_area.textCursor()
+                tool_selection.format.setFontUnderline(True)
+                sel_list.append(tool_selection)
+
+        self.edit_area.setExtraSelections(sel_list)
+
+    def on_toggle_all_rows(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        if self.gcode_obj.cnc_tools:
+            t_table = self.ui.cnc_tools_table
+        elif self.gcode_obj.exc_cnc_tools:
+            t_table = self.ui.exc_cnc_tools_table
+        else:
+            return
+
+        sel_model = t_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        if len(sel_rows) == t_table.rowCount():
+            t_table.clearSelection()
+            my_text_cursor = self.edit_area.textCursor()
+            my_text_cursor.clearSelection()
+        else:
+            t_table.selectAll()
+
+    def handleTextChanged(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        # enable = not self.ui.code_editor.document().isEmpty()
+        # self.ui.buttonPrint.setEnabled(enable)
+        # self.ui.buttonPreview.setEnabled(enable)
+
+        self.buttonSave.setStyleSheet("QPushButton {color: red;}")
+        self.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as_red.png'))
+
+    def insert_gcode(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        pass
+
+    def edit_fcgcode(self, cnc_obj):
+        """
+
+        :param cnc_obj:
+        :type cnc_obj:
+        :return:
+        :rtype:
+        """
+        assert isinstance(cnc_obj, CNCJobObject)
+        self.gcode_obj = cnc_obj
+
+        gcode_text = self.gcode_obj.source_file
+
+        self.set_ui()
+        self.build_ui()
+
+        # then append the text from GCode to the text editor
+        self.ui.gcode_editor_tab.load_text(gcode_text, move_to_start=True, clear_text=True)
+        self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor'))
+
+    def update_fcgcode(self, edited_obj):
+        """
+
+        :return:
+        :rtype:
+        """
+        my_gcode = self.ui.gcode_editor_tab.code_editor.toPlainText()
+        self.gcode_obj.source_file = my_gcode
+        self.deactivate()
+
+        self.ui.gcode_editor_tab.buttonSave.setStyleSheet("")
+        self.ui.gcode_editor_tab.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+
+    def on_open_gcode(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        _filter_ = "G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
+                   "All Files (*.*)"
+
+        path, _f = QtWidgets.QFileDialog.getOpenFileName(
+            caption=_('Open file'), directory=self.app.get_last_folder(), filter=_filter_)
+
+        if path:
+            file = QtCore.QFile(path)
+            if file.open(QtCore.QIODevice.ReadOnly):
+                stream = QtCore.QTextStream(file)
+                self.code_edited = stream.readAll()
+                self.ui.gcode_editor_tab.load_text(self.code_edited, move_to_start=True, clear_text=True)
+                file.close()
+
+    def activate(self):
+        self.editor_active = True
+        self.app.call_source = 'gcode_editor'
+
+    def deactivate(self):
+        self.editor_active = False
+        self.app.call_source = 'app'
+
+    def on_name_activate(self):
+        self.edited_obj_name = self.ui.name_entry.get_value()
+
+
+class AppGCodeEditorUI:
+    def __init__(self, app):
+        self.app = app
+
+        # Number of decimals used by tools in this class
+        self.decimals = self.app.decimals
+
+        # ## Current application units in Upper Case
+        self.units = self.app.defaults['units'].upper()
+
+        # self.setSizePolicy(
+        #     QtWidgets.QSizePolicy.MinimumExpanding,
+        #     QtWidgets.QSizePolicy.MinimumExpanding
+        # )
+
+        self.gcode_editor_tab = None
+
+        self.edit_widget = QtWidgets.QWidget()
+        # ## Box for custom widgets
+        # This gets populated in offspring implementations.
+        layout = QtWidgets.QVBoxLayout()
+        self.edit_widget.setLayout(layout)
+
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets
+        # this way I can hide/show the frame
+        self.edit_frame = QtWidgets.QFrame()
+        self.edit_frame.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self.edit_frame)
+        self.edit_box = QtWidgets.QVBoxLayout()
+        self.edit_box.setContentsMargins(0, 0, 0, 0)
+        self.edit_frame.setLayout(self.edit_box)
+
+        # ## Page Title box (spacing between children)
+        self.title_box = QtWidgets.QHBoxLayout()
+        self.edit_box.addLayout(self.title_box)
+
+        # ## Page Title icon
+        pixmap = QtGui.QPixmap(self.app.resource_location + '/flatcam_icon32.png')
+        self.icon = QtWidgets.QLabel()
+        self.icon.setPixmap(pixmap)
+        self.title_box.addWidget(self.icon, stretch=0)
+
+        # ## Title label
+        self.title_label = QtWidgets.QLabel("<font size=5><b>%s</b></font>" % _('GCode Editor'))
+        self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.title_box.addWidget(self.title_label, stretch=1)
+
+        # ## Object name
+        self.name_box = QtWidgets.QHBoxLayout()
+        self.edit_box.addLayout(self.name_box)
+        name_label = QtWidgets.QLabel(_("Name:"))
+        self.name_box.addWidget(name_label)
+        self.name_entry = FCEntry()
+        self.name_box.addWidget(self.name_entry)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.edit_box.addWidget(separator_line)
+
+        # CNC Tools Table when made out of Geometry
+        self.cnc_tools_table = FCTable()
+        self.cnc_tools_table.setSortingEnabled(False)
+        self.cnc_tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        self.edit_box.addWidget(self.cnc_tools_table)
+
+        self.cnc_tools_table.setColumnCount(6)
+        self.cnc_tools_table.setColumnWidth(0, 20)
+        self.cnc_tools_table.setHorizontalHeaderLabels(['#', _('Dia'), _('Offset'), _('Type'), _('TT'), ''])
+        self.cnc_tools_table.setColumnHidden(5, True)
+
+        # CNC Tools Table when made out of Excellon
+        self.exc_cnc_tools_table = FCTable()
+        self.exc_cnc_tools_table.setSortingEnabled(False)
+        self.exc_cnc_tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        self.edit_box.addWidget(self.exc_cnc_tools_table)
+
+        self.exc_cnc_tools_table.setColumnCount(6)
+        self.exc_cnc_tools_table.setColumnWidth(0, 20)
+        self.exc_cnc_tools_table.setHorizontalHeaderLabels(['#', _('Dia'), _('Drills'), _('Slots'), '', _("Cut Z")])
+        self.exc_cnc_tools_table.setColumnHidden(4, True)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.edit_box.addWidget(separator_line)
+
+        # Prepend text to GCode
+        prependlabel = QtWidgets.QLabel('%s 1:' % _('CNC Code Snippet'))
+        prependlabel.setToolTip(
+            _("Code snippet defined in Preferences.")
+        )
+        self.edit_box.addWidget(prependlabel)
+
+        self.prepend_text = FCTextArea()
+        self.prepend_text.setPlaceholderText(
+            _("Type here any G-Code commands you would\n"
+              "like to insert at the cursor location.")
+        )
+        self.edit_box.addWidget(self.prepend_text)
+
+        # Insert Button
+        self.update_gcode_button = FCButton(_('Insert Code'))
+        # self.update_gcode_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+        self.update_gcode_button.setToolTip(
+            _("Insert the code above at the cursor location.")
+        )
+        self.edit_box.addWidget(self.update_gcode_button)
+
+        # Append text to GCode
+        appendlabel = QtWidgets.QLabel('%s 2:' % _('CNC Code Snippet'))
+        appendlabel.setToolTip(
+            _("Code snippet defined in Preferences.")
+        )
+        self.edit_box.addWidget(appendlabel)
+
+        self.append_text = FCTextArea()
+        self.append_text.setPlaceholderText(
+            _("Type here any G-Code commands you would\n"
+              "like to insert at the cursor location.")
+        )
+        self.edit_box.addWidget(self.append_text)
+
+        # Insert Button
+        self.update_gcode_sec_button = FCButton(_('Insert Code'))
+        # self.update_gcode_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+        self.update_gcode_sec_button.setToolTip(
+            _("Insert the code above at the cursor location.")
+        )
+        self.edit_box.addWidget(self.update_gcode_sec_button)
+
+        layout.addStretch()
+
+        # Editor
+        self.exit_editor_button = FCButton(_('Exit Editor'))
+        self.exit_editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/power16.png'))
+        self.exit_editor_button.setToolTip(
+            _("Exit from Editor.")
+        )
+        self.exit_editor_button.setStyleSheet("""
+                                          QPushButton
+                                          {
+                                              font-weight: bold;
+                                          }
+                                          """)
+        layout.addWidget(self.exit_editor_button)
+        # ############################ FINSIHED GUI ##################################################################
+        # #############################################################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 561 - 64
appGUI/GUIElements.py

@@ -12,7 +12,7 @@
 # ##########################################################
 
 from PyQt5 import QtGui, QtCore, QtWidgets
-from PyQt5.QtCore import Qt, pyqtSlot, QSettings
+from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QSettings
 from PyQt5.QtWidgets import QTextEdit, QCompleter, QAction
 from PyQt5.QtGui import QKeySequence, QTextCursor
 
@@ -99,6 +99,11 @@ class RadioSet(QtWidgets.QWidget):
                 return
         log.error("Value given is not part of this RadioSet: %s" % str(val))
 
+    def setOptionsDisabled(self, options: list, val: bool) -> None:
+        for option in self.choices:
+            if option['label'] in options:
+                option['radio'].setDisabled(val)
+
 
 # class RadioGroupChoice(QtWidgets.QWidget):
 #     def __init__(self, label_1, label_2, to_check, hide_list, show_list, parent=None):
@@ -281,7 +286,7 @@ class LengthEntry(QtWidgets.QLineEdit):
         # Unit conversion table OUTPUT-INPUT
         self.scales = {
             'IN': {'IN': 1.0,
-                   'MM': 1/25.4},
+                   'MM': 1 / 25.4},
             'MM': {'IN': 25.4,
                    'MM': 1.0}
         }
@@ -659,10 +664,11 @@ class EvalEntry2(QtWidgets.QLineEdit):
         return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
 
 
-class NumericalEvalEntry(EvalEntry):
+class NumericalEvalEntry(FCEntry):
     """
     Will evaluate the input and return a value. Accepts only float numbers and formulas using the operators: /,*,+,-,%
     """
+
     def __init__(self, border_color=None):
         super().__init__(border_color=border_color)
 
@@ -682,17 +688,28 @@ class NumericalEvalEntry(EvalEntry):
         return evaled
 
 
-class NumericalEvalTupleEntry(FCEntry):
+class NumericalEvalTupleEntry(EvalEntry):
     """
     Will return a text value. Accepts only float numbers and formulas using the operators: /,*,+,-,%
     """
+
     def __init__(self, border_color=None):
         super().__init__(border_color=border_color)
 
-        regex = QtCore.QRegExp("[0-9\/\*\+\-\%\.\s\,]*")
+        regex = QtCore.QRegExp("[0-9\/\*\+\-\%\.\s\,\[\]\(\)]*")
         validator = QtGui.QRegExpValidator(regex, self)
         self.setValidator(validator)
 
+    def get_value(self):
+        raw = str(self.text()).strip(' ')
+        try:
+            evaled = eval(raw)
+        except Exception as e:
+            if raw != '':
+                log.error("Could not evaluate val: %s, error: %s" % (str(raw), str(e)))
+            return None
+        return evaled
+
 
 class FCColorEntry(QtWidgets.QFrame):
 
@@ -797,7 +814,6 @@ class FCSliderWithSpinner(QtWidgets.QFrame):
 
 
 class FCSpinner(QtWidgets.QSpinBox):
-
     returnPressed = QtCore.pyqtSignal()
     confirmation_signal = QtCore.pyqtSignal(bool, float, float)
 
@@ -918,8 +934,123 @@ class FCSpinner(QtWidgets.QSpinBox):
     #     return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
 
 
-class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
+class FCDoubleSlider(QtWidgets.QSlider):
+    # frome here: https://stackoverflow.com/questions/42820380/use-float-for-qslider
+
+    # create our our signal that we can connect to if necessary
+    doubleValueChanged = pyqtSignal(float)
+
+    def __init__(self, decimals=3, orientation='horizontal', *args, **kargs):
+        if orientation == 'horizontal':
+            super(FCDoubleSlider, self).__init__(QtCore.Qt.Horizontal, *args, **kargs)
+        else:
+            super(FCDoubleSlider, self).__init__(QtCore.Qt.Vertical, *args, **kargs)
+
+        self._multi = 10 ** decimals
+
+        self.valueChanged.connect(self.emitDoubleValueChanged)
+
+    def emitDoubleValueChanged(self):
+        value = float(super(FCDoubleSlider, self).value()) / self._multi
+        self.doubleValueChanged.emit(value)
+
+    def value(self):
+        return float(super(FCDoubleSlider, self).value()) / self._multi
+
+    def get_value(self):
+        return self.value()
+
+    def setMinimum(self, value):
+        return super(FCDoubleSlider, self).setMinimum(value * self._multi)
+
+    def setMaximum(self, value):
+        return super(FCDoubleSlider, self).setMaximum(value * self._multi)
+
+    def setSingleStep(self, value):
+        return super(FCDoubleSlider, self).setSingleStep(value * self._multi)
+
+    def singleStep(self):
+        return float(super(FCDoubleSlider, self).singleStep()) / self._multi
+
+    def set_value(self, value):
+        super(FCDoubleSlider, self).setValue(int(value * self._multi))
+
+    def set_precision(self, decimals):
+        self._multi = 10 ** decimals
+
+    def set_range(self, min, max):
+        self.blockSignals(True)
+        self.setRange(min * self._multi, max * self._multi)
+        self.blockSignals(False)
+
+
+class FCSliderWithDoubleSpinner(QtWidgets.QFrame):
+
+    def __init__(self, min=0, max=9999.9999, step=1, precision=4, orientation='horizontal', **kwargs):
+        super().__init__(**kwargs)
+
+        self.slider = FCDoubleSlider(orientation=orientation)
+        self.slider.setMinimum(min)
+        self.slider.setMaximum(max)
+        self.slider.setSingleStep(step)
+        self.slider.set_range(min, max)
+
+        self.spinner = FCDoubleSpinner()
+        self.spinner.set_range(min, max)
+        self.spinner.set_precision(precision)
+
+        self.spinner.set_step(step)
+        self.spinner.setMinimumWidth(70)
+
+        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
+        self.spinner.setSizePolicy(sizePolicy)
+
+        self.layout = QtWidgets.QHBoxLayout()
+        self.layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.layout.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.slider)
+        self.layout.addWidget(self.spinner)
+        self.setLayout(self.layout)
+
+        self.slider.doubleValueChanged.connect(self._on_slider)
+        self.spinner.valueChanged.connect(self._on_spinner)
+
+        self.valueChanged = self.spinner.valueChanged
+
+    def set_precision(self, prec):
+        self.spinner.set_precision(prec)
+
+    def setSingleStep(self, step):
+        self.spinner.set_step(step)
+
+    def set_range(self, min, max):
+        self.spinner.set_range(min, max)
+        self.slider.set_range(min, max)
+
+    def set_minimum(self, min):
+        self.slider.setMinimum(min)
+        self.spinner.setMinimum(min)
+
+    def set_maximum(self, max):
+        self.slider.setMaximum(max)
+        self.spinner.setMaximum(max)
+
+    def get_value(self) -> float:
+        return self.spinner.get_value()
+
+    def set_value(self, value: float):
+        self.spinner.set_value(value)
+
+    def _on_spinner(self):
+        spinner_value = self.spinner.value()
+        self.slider.set_value(spinner_value)
+
+    def _on_slider(self):
+        slider_value = self.slider.value()
+        self.spinner.set_value(slider_value)
+
 
+class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
     returnPressed = QtCore.pyqtSignal()
     confirmation_signal = QtCore.pyqtSignal(bool, float, float)
 
@@ -1051,6 +1182,11 @@ class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
 
         self.setRange(min_val, max_val)
 
+    def set_step(self, p_int):
+        self.blockSignals(True)
+        self.setSingleStep(p_int)
+        self.blockSignals(False)
+
     # def sizeHint(self):
     #     default_hint_size = super(FCDoubleSpinner, self).sizeHint()
     #     return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
@@ -1552,8 +1688,13 @@ class FCInputDialog(QtWidgets.QInputDialog):
 
 
 class FCButton(QtWidgets.QPushButton):
-    def __init__(self, parent=None):
-        super(FCButton, self).__init__(parent)
+    def __init__(self, text=None, checkable=None, click_callback=None, parent=None):
+        super(FCButton, self).__init__(text, parent)
+        if not checkable is None:
+            self.setCheckable(checkable)
+
+        if not click_callback is None:
+            self.clicked.connect(click_callback)
 
     def get_value(self):
         return self.isChecked()
@@ -1563,18 +1704,28 @@ class FCButton(QtWidgets.QPushButton):
 
 
 class FCLabel(QtWidgets.QLabel):
-
     clicked = QtCore.pyqtSignal(bool)
+    right_clicked = QtCore.pyqtSignal(bool)
+    middle_clicked = QtCore.pyqtSignal(bool)
 
     def __init__(self, parent=None):
         super(FCLabel, self).__init__(parent)
 
         # for the usage of this label as a clickable label, to know that current state
         self.clicked_state = False
+        self.middle_clicked_state = False
+        self.right_clicked_state = False
 
     def mousePressEvent(self, event):
-        self.clicked_state = not self.clicked_state
-        self.clicked.emit(self.clicked_state)
+        if event.button() == Qt.LeftButton:
+            self.clicked_state = not self.clicked_state
+            self.clicked.emit(self.clicked_state)
+        elif event.button() == Qt.RightButton:
+            self.right_clicked_state = not self.right_clicked_state
+            self.right_clicked.emit(True)
+        elif event.button() == Qt.MiddleButton:
+            self.middle_clicked_state = not self.middle_clicked_state
+            self.middle_clicked.emit(True)
 
     def get_value(self):
         return self.text()
@@ -1876,7 +2027,7 @@ class FCDetachableTab(QtWidgets.QTabWidget):
                     if str(tab_name) == str(self.tabText(index)):
                         self.protectTab(index)
 
-        # Make this tab the current tab
+            # Make this tab the current tab
             if index > -1:
                 self.setCurrentIndex(insert_index) if self.use_old_index else self.setCurrentIndex(index)
 
@@ -1959,7 +2110,6 @@ class FCDetachableTab(QtWidgets.QTabWidget):
                 # area to the side of the QTabBar) or there are not tabs
                 # currently attached...
                 if tabDropPos.y() < self.tabBar.height() or self.count() == 0:
-
                     # Close the detached tab and allow it to re-attach
                     # automatically
                     self.detachedTabs[name].close()
@@ -2280,6 +2430,7 @@ class VerticalScrollArea(QtWidgets.QScrollArea):
     scroll area that also expands horizontally to accommodate
     its contents.
     """
+
     def __init__(self, parent=None):
         QtWidgets.QScrollArea.__init__(self, parent=parent)
         self.setWidgetResizable(True)
@@ -2395,8 +2546,7 @@ class OptionalHideInputSection:
 
 
 class FCTable(QtWidgets.QTableWidget):
-
-    drag_drop_sig = QtCore.pyqtSignal()
+    drag_drop_sig = QtCore.pyqtSignal(object, int)
     lost_focus = QtCore.pyqtSignal()
 
     def __init__(self, drag_drop=False, protected_rows=None, parent=None):
@@ -2407,8 +2557,8 @@ class FCTable(QtWidgets.QTableWidget):
                          palette.color(QtGui.QPalette.Active, QtGui.QPalette.Highlight))
 
         # make inactive rows text some color as active; may be useful in the future
-        # palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.HighlightedText,
-        #                  palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
+        palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.HighlightedText,
+                         palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
         self.setPalette(palette)
 
         if drag_drop:
@@ -2431,6 +2581,7 @@ class FCTable(QtWidgets.QTableWidget):
                 self.rows_not_for_drag_and_drop = [protected_rows]
 
         self.rows_to_move = []
+        self.rows_dragged = None
 
     def sizeHint(self):
         default_hint_size = super(FCTable, self).sizeHint()
@@ -2455,10 +2606,12 @@ class FCTable(QtWidgets.QTableWidget):
 
     # if user is clicking an blank area inside the QTableWidget it will deselect currently selected rows
     def mousePressEvent(self, event):
-        if not self.itemAt(event.pos()):
+        clicked_item = self.itemAt(event.pos())
+        if not clicked_item:
             self.clearSelection()
             self.clearFocus()
         else:
+            self.rows_dragged = [it.row() for it in self.selectedItems()]
             QtWidgets.QTableWidget.mousePressEvent(self, event)
 
     def focusOutEvent(self, event):
@@ -2545,57 +2698,160 @@ class FCTable(QtWidgets.QTableWidget):
     #     return rect.contains(pos, True) and not (
     #                 int(self.model().flags(index)) & Qt.ItemIsDropEnabled) and pos.y() >= rect.center().y()
 
-    def dropEvent(self, event):
-        """
-        From here: https://stackoverflow.com/questions/26227885/drag-and-drop-rows-within-qtablewidget
-        :param event:
-        :return:
-        """
-        if event.source() == self:
-            rows = set([mi.row() for mi in self.selectedIndexes()])
-
-            # if one of the selected rows for drag and drop is within the protected list, return
-            for r in rows:
-                if r in self.rows_not_for_drag_and_drop:
-                    return
+    def dragEnterEvent(self, e: QtGui.QDragEnterEvent) -> None:
+        if e.source() == self:
+            self.blockSignals(True)
+            e.accept()
+        else:
+            e.ignore()
+
+    # def dropEvent(self, event):
+    #     """
+    #     From here: https://stackoverflow.com/questions/26227885/drag-and-drop-rows-within-qtablewidget
+    #     :param event:
+    #     :return:
+    #     """
+    #     if event.source() == self:
+    #         event.acceptProposedAction()
+    #
+    #         # create a set of the selected rows that are dragged to another position
+    #         rows = set([mi.row() for mi in self.selectedIndexes()])
+    #         # if one of the selected rows for drag and drop is within the protected list, return
+    #         for r in rows:
+    #             if r in self.rows_not_for_drag_and_drop:
+    #                 return
+    #
+    #         drop_index = self.indexAt(event.pos())
+    #         # row where we drop the selected rows
+    #         targetRow = drop_index.row()
+    #
+    #         # drop_indicator = self.dropIndicatorPosition()
+    #         # if targetRow != -1:
+    #         #     if drop_indicator == QtWidgets.QAbstractItemView.AboveItem:
+    #         #         print("above")
+    #         #     elif drop_indicator == QtWidgets.QAbstractItemView.BelowItem:
+    #         #         print("below")
+    #         #     elif drop_indicator == QtWidgets.QAbstractItemView.OnItem:
+    #         #         print("on")
+    #         #     elif drop_indicator == QtWidgets.QAbstractItemView.OnViewport:
+    #         #         print("on viewport")
+    #
+    #         # if we drop on one row from the already dragged rows
+    #         rows.discard(targetRow)
+    #         rows = sorted(rows)
+    #         if not rows:
+    #             return
+    #         if targetRow == -1:
+    #             targetRow = self.rowCount()
+    #
+    #         # insert empty rows at the index of the targetRow
+    #         for _ in range(len(rows)):
+    #             self.insertRow(targetRow)
+    #
+    #         rowMapping = {}  # Src row to target row.
+    #         for idx, row in enumerate(rows):
+    #             if row < targetRow:
+    #                 rowMapping[row] = targetRow + idx
+    #             else:
+    #                 rowMapping[row + len(rows)] = targetRow + idx
+    #
+    #         colCount = self.columnCount()
+    #         for srcRow, tgtRow in sorted(rowMapping.items()):
+    #             for col in range(0, colCount):
+    #                 new_item = self.item(srcRow, col)
+    #                 if new_item is None:
+    #                     new_item = self.cellWidget(srcRow, col)
+    #
+    #                 if isinstance(new_item, QtWidgets.QTableWidgetItem):
+    #                     new_item = self.takeItem(srcRow, col)
+    #                     self.setItem(tgtRow, col, new_item)
+    #                 else:
+    #                     self.setCellWidget(tgtRow, col, new_item)
+    #
+    #         for row in reversed(sorted(rowMapping.keys())):
+    #             self.removeRow(row)
+    #
+    #         self.blockSignals(False)
+    #         self.drag_drop_sig.emit(int(self.row_dragged), int(targetRow))
+    #     else:
+    #         event.ignore()
 
-            targetRow = self.indexAt(event.pos()).row()
-            rows.discard(targetRow)
-            rows = sorted(rows)
+    def dropEvent(self, event: QtGui.QDropEvent):
+        if not event.isAccepted() and event.source() == self:
+            drop_row = self.drop_on(event)
 
-            if not rows:
-                return
-            if targetRow == -1:
-                targetRow = self.rowCount()
+            rows = sorted(set(item.row() for item in self.selectedItems()))
 
-            for _ in range(len(rows)):
-                self.insertRow(targetRow)
+            rows_to_move = []
+            for row_index in rows:
+                temp_lst = []
+                for column_index in range(self.columnCount()):
+                    col_data = self.item(row_index, column_index)
 
-            rowMapping = {}  # Src row to target row.
-            for idx, row in enumerate(rows):
-                if row < targetRow:
-                    rowMapping[row] = targetRow + idx
-                else:
-                    rowMapping[row + len(rows)] = targetRow + idx
-
-            colCount = self.columnCount()
-            for srcRow, tgtRow in sorted(rowMapping.items()):
-                for col in range(0, colCount):
-                    new_item = self.item(srcRow, col)
-                    if new_item is None:
-                        new_item = self.cellWidget(srcRow, col)
-                    if isinstance(new_item, QtWidgets.QTableWidgetItem):
-                        new_item = self.takeItem(srcRow, col)
-                        self.setItem(tgtRow, col, new_item)
+                    if isinstance(col_data, QtWidgets.QTableWidgetItem):
+                        table_item = QtWidgets.QTableWidgetItem(col_data)
                     else:
-                        self.setCellWidget(tgtRow, col, new_item)
-
-            for row in reversed(sorted(rowMapping.keys())):
-                self.removeRow(row)
+                        old_item = self.cellWidget(row_index, column_index)
+                        if isinstance(old_item, QtWidgets.QComboBox):
+                            table_item = FCComboBox()
+                            items = [old_item.itemText(i) for i in range(old_item.count())]
+                            table_item.addItems(items)
+                            table_item.setCurrentIndex(old_item.currentIndex())
+                        elif isinstance(old_item, QtWidgets.QCheckBox):
+                            table_item = FCCheckBox()
+                            table_item.setChecked(old_item.isChecked())
+                            table_item.setText(old_item.text())
+                        else:
+                            table_item = None
+
+                    temp_lst.append(table_item)
+                rows_to_move.append(temp_lst)
+
+            for row_index in reversed(rows):
+                self.removeRow(row_index)
+                if row_index < drop_row:
+                    drop_row -= 1
+
+            for row_index, data in enumerate(rows_to_move):
+                row_index += drop_row
+                self.insertRow(row_index)
+                for column_index, column_data in enumerate(data):
+                    if column_data is None:
+                        continue
+
+                    if isinstance(column_data, QtWidgets.QTableWidgetItem):
+                        self.setItem(row_index, column_index, column_data)
+                    else:
+                        self.setCellWidget(row_index, column_index, column_data)
             event.accept()
-            self.drag_drop_sig.emit()
+            for row_index in range(len(rows_to_move)):
+                self.item(drop_row + row_index, 0).setSelected(True)
+                self.item(drop_row + row_index, 1).setSelected(True)
 
-            return
+            self.blockSignals(False)
+            self.drag_drop_sig.emit(self.rows_dragged, int(drop_row))
+
+        self.blockSignals(False)
+        self.resizeRowsToContents()
+        super().dropEvent(event)
+
+    def drop_on(self, event):
+        index = self.indexAt(event.pos())
+        if not index.isValid():
+            return self.rowCount()
+
+        return index.row() + 1 if self.is_below(event.pos(), index) else index.row()
+
+    def is_below(self, pos, index):
+        rect = self.visualRect(index)
+        margin = 2
+        if pos.y() - rect.top() < margin:
+            return False
+        elif rect.bottom() - pos.y() < margin:
+            return True
+        # noinspection PyTypeChecker
+        return rect.contains(pos, True) and not (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) and \
+               pos.y() >= rect.center().y()
 
 
 class SpinBoxDelegate(QtWidgets.QItemDelegate):
@@ -2763,7 +3019,7 @@ class _BrowserTextEdit(QTextEdit):
             save_action.triggered.connect(lambda: self.save_log(app=self.app))
 
         clear_action = QAction(_("Clear"), self)
-        clear_action.setShortcut(QKeySequence(Qt.Key_Delete))   # it's not working, the shortcut
+        clear_action.setShortcut(QKeySequence(Qt.Key_Delete))  # it's not working, the shortcut
         self.menu.addAction(clear_action)
         clear_action.triggered.connect(self.clear)
 
@@ -3123,6 +3379,247 @@ class FCDock(QtWidgets.QDockWidget):
         super().show()
 
 
+class FCJog(QtWidgets.QFrame):
+
+    def __init__(self, app, *args, **kwargs):
+        super(FCJog, self).__init__(*args, **kwargs)
+
+        self.app = app
+        self.setFrameShape(QtWidgets.QFrame.Box)
+        self.setLineWidth(1)
+
+        # JOG axes
+        grbl_jog_grid = QtWidgets.QGridLayout()
+        grbl_jog_grid.setAlignment(QtCore.Qt.AlignCenter)
+        grbl_jog_grid.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)
+        grbl_jog_grid.setContentsMargins(2, 4, 2, 4)
+
+        self.setLayout(grbl_jog_grid)
+
+        # JOG Y Up
+        self.jog_up_button = QtWidgets.QToolButton()
+        self.jog_up_button.setIcon(QtGui.QIcon(self.app.resource_location + '/up-arrow32.png'))
+        self.jog_up_button.setToolTip(
+            _("Jog the Y axis.")
+        )
+        grbl_jog_grid.addWidget(self.jog_up_button, 2, 1)
+
+        # Origin
+        self.jog_origin_button = QtWidgets.QToolButton()
+        self.jog_origin_button.setIcon(QtGui.QIcon(self.app.resource_location + '/origin2_32.png'))
+        self.jog_origin_button.setToolTip(
+            _("Move to Origin.")
+        )
+
+        grbl_jog_grid.addWidget(self.jog_origin_button, 3, 1)
+
+        # JOG Y Down
+        self.jog_down_button = QtWidgets.QToolButton()
+        self.jog_down_button.setIcon(QtGui.QIcon(self.app.resource_location + '/down-arrow32.png'))
+        self.jog_down_button.setToolTip(
+            _("Jog the Y axis.")
+        )
+        grbl_jog_grid.addWidget(self.jog_down_button, 4, 1)
+
+        # JOG X Left
+        self.jog_left_button = QtWidgets.QToolButton()
+        self.jog_left_button.setIcon(QtGui.QIcon(self.app.resource_location + '/left_arrow32.png'))
+        self.jog_left_button.setToolTip(
+            _("Jog the X axis.")
+        )
+        grbl_jog_grid.addWidget(self.jog_left_button, 3, 0)
+
+        # JOG X Right
+        self.jog_right_button = QtWidgets.QToolButton()
+        self.jog_right_button.setIcon(QtGui.QIcon(self.app.resource_location + '/right_arrow32.png'))
+        self.jog_right_button.setToolTip(
+            _("Jog the X axis.")
+        )
+        grbl_jog_grid.addWidget(self.jog_right_button, 3, 2)
+
+        # JOG Z Up
+        self.jog_z_up_button = QtWidgets.QToolButton()
+        self.jog_z_up_button.setIcon(QtGui.QIcon(self.app.resource_location + '/up-arrow32.png'))
+        self.jog_z_up_button.setText('Z')
+        self.jog_z_up_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
+        self.jog_z_up_button.setToolTip(
+            _("Jog the Z axis.")
+        )
+        grbl_jog_grid.addWidget(self.jog_z_up_button, 2, 3)
+
+        # JOG Z Down
+        self.jog_z_down_button = QtWidgets.QToolButton()
+        self.jog_z_down_button.setIcon(QtGui.QIcon(self.app.resource_location + '/down-arrow32.png'))
+        self.jog_z_down_button.setText('Z')
+        self.jog_z_down_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
+        self.jog_z_down_button.setToolTip(
+            _("Jog the Z axis.")
+        )
+        grbl_jog_grid.addWidget(self.jog_z_down_button, 4, 3)
+
+
+class FCZeroAxes(QtWidgets.QFrame):
+
+    def __init__(self, app, *args, **kwargs):
+        super(FCZeroAxes, self).__init__(*args, **kwargs)
+        self.app = app
+
+        self.setFrameShape(QtWidgets.QFrame.Box)
+        self.setLineWidth(1)
+
+        # Zero the axes
+        grbl_zero_grid = QtWidgets.QGridLayout()
+        grbl_zero_grid.setContentsMargins(2, 4, 2, 4)
+        grbl_zero_grid.setColumnStretch(0, 0)
+        grbl_zero_grid.setColumnStretch(1, 0)
+        # grbl_zero_grid.setRowStretch(4, 1)
+        self.setLayout(grbl_zero_grid)
+
+        # Zero X axis
+        self.grbl_zerox_button = QtWidgets.QToolButton()
+        self.grbl_zerox_button.setText(_("X"))
+        self.grbl_zerox_button.setToolTip(
+            _("Zero the CNC X axes at current position.")
+        )
+        grbl_zero_grid.addWidget(self.grbl_zerox_button, 1, 0)
+        # Zero Y axis
+        self.grbl_zeroy_button = QtWidgets.QToolButton()
+        self.grbl_zeroy_button.setText(_("Y"))
+
+        self.grbl_zeroy_button.setToolTip(
+            _("Zero the CNC Y axes at current position.")
+        )
+        grbl_zero_grid.addWidget(self.grbl_zeroy_button, 2, 0)
+        # Zero Z axis
+        self.grbl_zeroz_button = QtWidgets.QToolButton()
+        self.grbl_zeroz_button.setText(_("Z"))
+
+        self.grbl_zeroz_button.setToolTip(
+            _("Zero the CNC Z axes at current position.")
+        )
+        grbl_zero_grid.addWidget(self.grbl_zeroz_button, 3, 0)
+        self.grbl_homing_button = QtWidgets.QToolButton()
+        self.grbl_homing_button.setText(_("Do Home"))
+        self.grbl_homing_button.setToolTip(
+            _("Perform a homing cycle on all axis."))
+        grbl_zero_grid.addWidget(self.grbl_homing_button, 4, 0, 1, 2)
+        # Zeroo all axes
+        self.grbl_zero_all_button = QtWidgets.QToolButton()
+        self.grbl_zero_all_button.setText(_("All"))
+        self.grbl_zero_all_button.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+
+        self.grbl_zero_all_button.setToolTip(
+            _("Zero all CNC axes at current position.")
+        )
+        grbl_zero_grid.addWidget(self.grbl_zero_all_button, 1, 1, 3, 1)
+
+
+class RotatedToolButton(QtWidgets.QToolButton):
+    def __init__(self, orientation="east", *args, **kwargs):
+        super(RotatedToolButton, self).__init__(*args, **kwargs)
+        self.orientation = orientation
+
+    def paintEvent(self, event):
+        painter = QtWidgets.QStylePainter(self)
+        if self.orientation == "east":
+            painter.rotate(270)
+            painter.translate(-1 * self.height(), 0)
+        if self.orientation == "west":
+            painter.rotate(90)
+            painter.translate(0, -1 * self.width())
+        painter.drawControl(QtWidgets.QStyle.CE_PushButton, self.getSyleOptions())
+
+    def minimumSizeHint(self):
+        size = super(RotatedToolButton, self).minimumSizeHint()
+        size.transpose()
+        return size
+
+    def sizeHint(self):
+        size = super(RotatedToolButton, self).sizeHint()
+        size.transpose()
+        return size
+
+    def getSyleOptions(self):
+        options = QtWidgets.QStyleOptionButton()
+        options.initFrom(self)
+        size = options.rect.size()
+        size.transpose()
+        options.rect.setSize(size)
+        options.features = QtWidgets.QStyleOptionButton.None_
+        # if self.isFlat():
+        #     options.features |= QtWidgets.QStyleOptionButton.Flat
+        if self.menu():
+            options.features |= QtWidgets.QStyleOptionButton.HasMenu
+        # if self.autoDefault() or self.isDefault():
+        #     options.features |= QtWidgets.QStyleOptionButton.AutoDefaultButton
+        # if self.isDefault():
+        #     options.features |= QtWidgets.QStyleOptionButton.DefaultButton
+        if self.isDown() or (self.menu() and self.menu().isVisible()):
+            options.state |= QtWidgets.QStyle.State_Sunken
+        if self.isChecked():
+            options.state |= QtWidgets.QStyle.State_On
+        # if not self.isFlat() and not self.isDown():
+        #     options.state |= QtWidgets.QStyle.State_Raised
+
+        options.text = self.text()
+        options.icon = self.icon()
+        options.iconSize = self.iconSize()
+        return options
+
+
+class RotatedButton(QtWidgets.QPushButton):
+    def __init__(self, orientation="west", *args, **kwargs):
+        super(RotatedButton, self).__init__(*args, **kwargs)
+        self.orientation = orientation
+
+    def paintEvent(self, event):
+        painter = QtWidgets.QStylePainter(self)
+        if self.orientation == "east":
+            painter.rotate(270)
+            painter.translate(-1 * self.height(), 0)
+        if self.orientation == "west":
+            painter.rotate(90)
+            painter.translate(0, -1 * self.width())
+        painter.drawControl(QtWidgets.QStyle.CE_PushButton, self.getSyleOptions())
+
+    def minimumSizeHint(self):
+        size = super(RotatedButton, self).minimumSizeHint()
+        size.transpose()
+        return size
+
+    def sizeHint(self):
+        size = super(RotatedButton, self).sizeHint()
+        size.transpose()
+        return size
+
+    def getSyleOptions(self):
+        options = QtWidgets.QStyleOptionButton()
+        options.initFrom(self)
+        size = options.rect.size()
+        size.transpose()
+        options.rect.setSize(size)
+        options.features = QtWidgets.QStyleOptionButton.None_
+        if self.isFlat():
+            options.features |= QtWidgets.QStyleOptionButton.Flat
+        if self.menu():
+            options.features |= QtWidgets.QStyleOptionButton.HasMenu
+        if self.autoDefault() or self.isDefault():
+            options.features |= QtWidgets.QStyleOptionButton.AutoDefaultButton
+        if self.isDefault():
+            options.features |= QtWidgets.QStyleOptionButton.DefaultButton
+        if self.isDown() or (self.menu() and self.menu().isVisible()):
+            options.state |= QtWidgets.QStyle.State_Sunken
+        if self.isChecked():
+            options.state |= QtWidgets.QStyle.State_On
+        if not self.isFlat() and not self.isDown():
+            options.state |= QtWidgets.QStyle.State_Raised
+
+        options.text = self.text()
+        options.icon = self.icon()
+        options.iconSize = self.iconSize()
+        return options
+
+
 class FlatCAMActivityView(QtWidgets.QWidget):
     """
     This class create and control the activity icon displayed in the App status bar

+ 226 - 70
appGUI/MainGUI.py

@@ -19,7 +19,7 @@ from appGUI.preferences.excellon.ExcellonPreferencesUI import ExcellonPreference
 from appGUI.preferences.general.GeneralPreferencesUI import GeneralPreferencesUI
 from appGUI.preferences.geometry.GeometryPreferencesUI import GeometryPreferencesUI
 from appGUI.preferences.gerber.GerberPreferencesUI import GerberPreferencesUI
-from appEditors.FlatCAMGeoEditor import FCShapeTool
+from appEditors.AppGeoEditor import FCShapeTool
 from matplotlib.backend_bases import KeyEvent as mpl_key_event
 
 import webbrowser
@@ -326,31 +326,12 @@ class MainGUI(QtWidgets.QMainWindow):
         self.menueditedit.setDisabled(False)
         self.menueditok.setDisabled(True)
 
+        # ############################ EDIT -> CONVERSION ######################################################
         # Separator
         self.menuedit.addSeparator()
         self.menuedit_convert = self.menuedit.addMenu(
             QtGui.QIcon(self.app.resource_location + '/convert24.png'), _('Conversion'))
-        self.menuedit_convertjoin = self.menuedit_convert.addAction(
-            QtGui.QIcon(self.app.resource_location + '/join16.png'), _('&Join Geo/Gerber/Exc -> Geo'))
-        self.menuedit_convertjoin.setToolTip(
-            _("Merge a selection of objects, which can be of type:\n"
-              "- Gerber\n"
-              "- Excellon\n"
-              "- Geometry\n"
-              "into a new combo Geometry object.")
-        )
-        self.menuedit_convertjoinexc = self.menuedit_convert.addAction(
-            QtGui.QIcon(self.app.resource_location + '/join16.png'), _('Join Excellon(s) -> Excellon'))
-        self.menuedit_convertjoinexc.setToolTip(
-            _("Merge a selection of Excellon objects into a new combo Excellon object.")
-        )
-        self.menuedit_convertjoingrb = self.menuedit_convert.addAction(
-            QtGui.QIcon(self.app.resource_location + '/join16.png'), _('Join Gerber(s) -> Gerber'))
-        self.menuedit_convertjoingrb.setToolTip(
-            _("Merge a selection of Gerber objects into a new combo Gerber object.")
-        )
-        # Separator
-        self.menuedit_convert.addSeparator()
+
         self.menuedit_convert_sg2mg = self.menuedit_convert.addAction(
             QtGui.QIcon(self.app.resource_location + '/convert24.png'), _('Convert Single to MultiGeo'))
         self.menuedit_convert_sg2mg.setToolTip(
@@ -371,8 +352,36 @@ class MainGUI(QtWidgets.QMainWindow):
         self.menueditconvert_any2gerber = self.menuedit_convert.addAction(
             QtGui.QIcon(self.app.resource_location + '/copy_geo.png'),
             _('Convert Any to Gerber'))
+        self.menueditconvert_any2excellon = self.menuedit_convert.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy_geo.png'),
+            _('Convert Any to Excellon'))
         self.menuedit_convert.setToolTipsVisible(True)
 
+        # ############################ EDIT -> JOIN        ######################################################
+        self.menuedit_join = self.menuedit.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/join16.png'), _('Join Objects'))
+        self.menuedit_join2geo = self.menuedit_join.addAction(
+            QtGui.QIcon(self.app.resource_location + '/join16.png'), _('&Join Geo/Gerber/Exc -> Geo'))
+        self.menuedit_join2geo.setToolTip(
+            _("Merge a selection of objects, which can be of type:\n"
+              "- Gerber\n"
+              "- Excellon\n"
+              "- Geometry\n"
+              "into a new combo Geometry object.")
+        )
+        self.menuedit_join_exc2exc = self.menuedit_join.addAction(
+            QtGui.QIcon(self.app.resource_location + '/join16.png'), _('Join Excellon(s) -> Excellon'))
+        self.menuedit_join_exc2exc.setToolTip(
+            _("Merge a selection of Excellon objects into a new combo Excellon object.")
+        )
+        self.menuedit_join_grb2grb = self.menuedit_join.addAction(
+            QtGui.QIcon(self.app.resource_location + '/join16.png'), _('Join Gerber(s) -> Gerber'))
+        self.menuedit_join_grb2grb.setToolTip(
+            _("Merge a selection of Gerber objects into a new combo Gerber object.")
+        )
+        self.menuedit_join.setToolTipsVisible(True)
+
+        # ############################ EDIT -> COPY        ######################################################
         # Separator
         self.menuedit.addSeparator()
         self.menueditcopyobject = self.menuedit.addAction(
@@ -536,7 +545,7 @@ class MainGUI(QtWidgets.QMainWindow):
         self.menuhelp.addSeparator()
 
         self.menuhelp_readme = self.menuhelp.addAction(
-            QtGui.QIcon(self.app.resource_location + '/warning.png'), _('ReadMe?'))
+            QtGui.QIcon(self.app.resource_location + '/warning.png'), _('How To'))
 
         self.menuhelp_about = self.menuhelp.addAction(
             QtGui.QIcon(self.app.resource_location + '/about32.png'), _('About FlatCAM'))
@@ -814,9 +823,23 @@ class MainGUI(QtWidgets.QMainWindow):
         self.grb_edit_toolbar.setObjectName('GrbEditor_TB')
         self.addToolBar(self.grb_edit_toolbar)
 
-        self.status_toolbar = QtWidgets.QToolBar(_('Grid Toolbar'))
-        self.status_toolbar.setObjectName('Snap_TB')
-        # self.addToolBar(self.status_toolbar)
+        # ### INFOBAR TOOLBARS ###################################################
+        self.delta_coords_toolbar = QtWidgets.QToolBar(_('Delta Coordinates Toolbar'))
+        self.delta_coords_toolbar.setObjectName('Delta_Coords_TB')
+
+        self.coords_toolbar = QtWidgets.QToolBar(_('Coordinates Toolbar'))
+        self.coords_toolbar.setObjectName('Coords_TB')
+
+        self.grid_toolbar = QtWidgets.QToolBar(_('Grid Toolbar'))
+        self.grid_toolbar.setObjectName('Snap_TB')
+        self.grid_toolbar.setStyleSheet(
+            """
+            QToolBar { padding: 0; }
+            QToolBar QToolButton { padding: -2; margin: -2; }
+            """
+        )
+
+        self.status_toolbar = QtWidgets.QToolBar(_('Status Toolbar'))
         self.status_toolbar.setStyleSheet(
             """
             QToolBar { padding: 0; }
@@ -912,6 +935,8 @@ class MainGUI(QtWidgets.QMainWindow):
             QtGui.QIcon(self.app.resource_location + '/paint20_1.png'), _("Paint Tool"))
         self.isolation_btn = self.toolbartools.addAction(
             QtGui.QIcon(self.app.resource_location + '/iso_16.png'), _("Isolation Tool"))
+        self.drill_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/drilling_tool32.png'), _("Drilling Tool"))
         self.toolbartools.addSeparator()
 
         self.panelize_btn = self.toolbartools.addAction(
@@ -1073,30 +1098,46 @@ class MainGUI(QtWidgets.QMainWindow):
             QtGui.QIcon(self.app.resource_location + '/move32.png'), _("Move"))
 
         # ########################################################################
-        # ########################## Snap Toolbar# ###############################
+        # ########################## GRID Toolbar# ###############################
         # ########################################################################
 
         # Snap GRID toolbar is always active to facilitate usage of measurements done on GRID
-        self.grid_snap_btn = self.status_toolbar.addAction(
+        self.grid_snap_btn = self.grid_toolbar.addAction(
             QtGui.QIcon(self.app.resource_location + '/grid32.png'), _('Snap to grid'))
         self.grid_gap_x_entry = FCEntry2()
         self.grid_gap_x_entry.setMaximumWidth(70)
         self.grid_gap_x_entry.setToolTip(_("Grid X snapping distance"))
-        self.status_toolbar.addWidget(self.grid_gap_x_entry)
+        self.grid_toolbar.addWidget(self.grid_gap_x_entry)
 
-        self.status_toolbar.addWidget(QtWidgets.QLabel(" "))
+        self.grid_toolbar.addWidget(QtWidgets.QLabel(" "))
         self.grid_gap_link_cb = FCCheckBox()
         self.grid_gap_link_cb.setToolTip(_("When active, value on Grid_X\n"
                                            "is copied to the Grid_Y value."))
-        self.status_toolbar.addWidget(self.grid_gap_link_cb)
-        self.status_toolbar.addWidget(QtWidgets.QLabel(" "))
+        self.grid_toolbar.addWidget(self.grid_gap_link_cb)
+        self.grid_toolbar.addWidget(QtWidgets.QLabel(" "))
 
         self.grid_gap_y_entry = FCEntry2()
         self.grid_gap_y_entry.setMaximumWidth(70)
         self.grid_gap_y_entry.setToolTip(_("Grid Y snapping distance"))
-        self.status_toolbar.addWidget(self.grid_gap_y_entry)
-        self.status_toolbar.addWidget(QtWidgets.QLabel(" "))
+        self.grid_toolbar.addWidget(self.grid_gap_y_entry)
+        self.grid_toolbar.addWidget(QtWidgets.QLabel(" "))
+
+        self.ois_grid = OptionalInputSection(self.grid_gap_link_cb, [self.grid_gap_y_entry], logic=False)
+
+        self.corner_snap_btn = self.grid_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/corner32.png'), _('Snap to corner'))
+
+        self.snap_max_dist_entry = FCEntry()
+        self.snap_max_dist_entry.setMaximumWidth(70)
+        self.snap_max_dist_entry.setToolTip(_("Max. magnet distance"))
+        self.snap_magnet = self.grid_toolbar.addWidget(self.snap_max_dist_entry)
 
+        self.corner_snap_btn.setVisible(False)
+        self.snap_magnet.setVisible(False)
+
+        # ########################################################################
+        # ########################## Status Toolbar ##############################
+        # ########################################################################
         self.axis_status_label = FCLabel()
         self.axis_status_label.setToolTip(_("Toggle the display of axis on canvas"))
         self.axis_status_label.setPixmap(QtGui.QPixmap(self.app.resource_location + '/axis16.png'))
@@ -1129,18 +1170,23 @@ class MainGUI(QtWidgets.QMainWindow):
         self.status_toolbar.addWidget(self.wplace_label)
         self.status_toolbar.addWidget(QtWidgets.QLabel(" "))
 
-        self.ois_grid = OptionalInputSection(self.grid_gap_link_cb, [self.grid_gap_y_entry], logic=False)
-
-        self.corner_snap_btn = self.status_toolbar.addAction(
-            QtGui.QIcon(self.app.resource_location + '/corner32.png'), _('Snap to corner'))
-
-        self.snap_max_dist_entry = FCEntry()
-        self.snap_max_dist_entry.setMaximumWidth(70)
-        self.snap_max_dist_entry.setToolTip(_("Max. magnet distance"))
-        self.snap_magnet = self.status_toolbar.addWidget(self.snap_max_dist_entry)
+        # #######################################################################
+        # ####################### Delta Coordinates TOOLBAR #####################
+        # #######################################################################
+        self.rel_position_label = QtWidgets.QLabel(
+            "<b>Dx</b>: 0.0000&nbsp;&nbsp;   <b>Dy</b>: 0.0000&nbsp;&nbsp;&nbsp;&nbsp;")
+        self.rel_position_label.setMinimumWidth(110)
+        self.rel_position_label.setToolTip(_("Relative measurement.\nReference is last click position"))
+        self.delta_coords_toolbar.addWidget(self.rel_position_label)
 
-        self.corner_snap_btn.setVisible(False)
-        self.snap_magnet.setVisible(False)
+        # #######################################################################
+        # ####################### Coordinates TOOLBAR ###########################
+        # #######################################################################
+        self.position_label = QtWidgets.QLabel("&nbsp;<b>X</b>: 0.0000&nbsp;&nbsp;   <b>Y</b>: 0.0000&nbsp;")
+        self.position_label.setMinimumWidth(110)
+        self.position_label.setToolTip(_("Absolute measurement.\n"
+                                         "Reference is (X=0, Y= 0) position"))
+        self.coords_toolbar.addWidget(self.position_label)
 
         # #######################################################################
         # ####################### TCL Shell DOCK ################################
@@ -1178,16 +1224,16 @@ class MainGUI(QtWidgets.QMainWindow):
         # ########################################################################
         # ########################## SELECTED Tab # ##############################
         # ########################################################################
-        self.selected_tab = QtWidgets.QWidget()
-        # self.selected_tab.setMinimumWidth(270)
-        self.selected_tab.setObjectName("selected_tab")
-        self.selected_tab_layout = QtWidgets.QVBoxLayout(self.selected_tab)
-        self.selected_tab_layout.setContentsMargins(2, 2, 2, 2)
+        self.properties_tab = QtWidgets.QWidget()
+        # self.properties_tab.setMinimumWidth(270)
+        self.properties_tab.setObjectName("properties_tab")
+        self.properties_tab_layout = QtWidgets.QVBoxLayout(self.properties_tab)
+        self.properties_tab_layout.setContentsMargins(2, 2, 2, 2)
 
         self.selected_scroll_area = VerticalScrollArea()
         # self.selected_scroll_area.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
-        self.selected_tab_layout.addWidget(self.selected_scroll_area)
-        self.notebook.addTab(self.selected_tab, _("Selected"))
+        self.properties_tab_layout.addWidget(self.selected_scroll_area)
+        self.notebook.addTab(self.properties_tab, _("Properties"))
 
         # ########################################################################
         # ########################## TOOL Tab # ##################################
@@ -1339,8 +1385,8 @@ class MainGUI(QtWidgets.QMainWindow):
         self.pref_tab_bottom_layout_1.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
         self.pref_tab_bottom_layout.addLayout(self.pref_tab_bottom_layout_1)
 
-        self.pref_defaults_button = QtWidgets.QPushButton()
-        self.pref_defaults_button.setText(_("Restore Defaults"))
+        self.pref_defaults_button = FCButton(_("Restore Defaults"))
+        self.pref_defaults_button.setIcon(QtGui.QIcon(self.app.resource_location + '/restore32.png'))
         self.pref_defaults_button.setMinimumWidth(130)
         self.pref_defaults_button.setToolTip(
             _("Restore the entire set of default values\n"
@@ -1349,6 +1395,7 @@ class MainGUI(QtWidgets.QMainWindow):
 
         self.pref_open_button = QtWidgets.QPushButton()
         self.pref_open_button.setText(_("Open Pref Folder"))
+        self.pref_open_button.setIcon(QtGui.QIcon(self.app.resource_location + '/pref.png'))
         self.pref_open_button.setMinimumWidth(130)
         self.pref_open_button.setToolTip(
             _("Open the folder where FlatCAM save the preferences files."))
@@ -1356,6 +1403,7 @@ class MainGUI(QtWidgets.QMainWindow):
 
         # Clear Settings
         self.clear_btn = FCButton('%s' % _('Clear GUI Settings'))
+        self.clear_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/trash32.png'))
         self.clear_btn.setMinimumWidth(130)
 
         self.clear_btn.setToolTip(
@@ -1370,6 +1418,7 @@ class MainGUI(QtWidgets.QMainWindow):
         self.pref_tab_bottom_layout.addLayout(self.pref_tab_bottom_layout_2)
 
         self.pref_apply_button = FCButton()
+        self.pref_apply_button.setIcon(QtGui.QIcon(self.app.resource_location + '/apply32.png'))
         self.pref_apply_button.setText(_("Apply"))
         self.pref_apply_button.setMinimumWidth(130)
         self.pref_apply_button.setToolTip(
@@ -1377,6 +1426,7 @@ class MainGUI(QtWidgets.QMainWindow):
         self.pref_tab_bottom_layout_2.addWidget(self.pref_apply_button)
 
         self.pref_save_button = QtWidgets.QPushButton()
+        self.pref_save_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
         self.pref_save_button.setText(_("Save"))
         self.pref_save_button.setMinimumWidth(130)
         self.pref_save_button.setToolTip(
@@ -1544,26 +1594,29 @@ class MainGUI(QtWidgets.QMainWindow):
         self.fcinfo = FlatCAMInfoBar(app=self.app)
         self.infobar.addWidget(self.fcinfo, stretch=1)
 
-        # self.rel_position_label = QtWidgets.QLabel(
-        # "<b>Dx</b>: 0.0000&nbsp;&nbsp;   <b>Dy</b>: 0.0000&nbsp;&nbsp;&nbsp;&nbsp;")
-        # self.rel_position_label.setMinimumWidth(110)
-        # self.rel_position_label.setToolTip(_("Relative measurement.\nReference is last click position"))
-        # self.infobar.addWidget(self.rel_position_label)
-        #
-        self.position_label = QtWidgets.QLabel("&nbsp;<b>X</b>: 0.0000&nbsp;&nbsp;   <b>Y</b>: 0.0000&nbsp;")
-        self.position_label.setMinimumWidth(110)
-        self.position_label.setToolTip(_("Absolute measurement.\n"
-                                         "Reference is (X=0, Y= 0) position"))
-        self.infobar.addWidget(self.position_label)
+        self.infobar.addWidget(self.delta_coords_toolbar)
+        self.delta_coords_toolbar.setVisible(self.app.defaults["global_delta_coords_show"])
+
+        self.infobar.addWidget(self.coords_toolbar)
+        self.coords_toolbar.setVisible(self.app.defaults["global_coords_show"])
+
+        self.grid_toolbar.setMaximumHeight(24)
+        self.infobar.addWidget(self.grid_toolbar)
+        self.grid_toolbar.setVisible(self.app.defaults["global_grid_show"])
 
         self.status_toolbar.setMaximumHeight(24)
         self.infobar.addWidget(self.status_toolbar)
+        self.status_toolbar.setVisible(self.app.defaults["global_status_show"])
 
         self.units_label = QtWidgets.QLabel("[mm]")
         self.units_label.setToolTip(_("Application units"))
         self.units_label.setMargin(2)
         self.infobar.addWidget(self.units_label)
 
+        # this used to be done in the APP.__init__()
+        self.activity_view = FlatCAMActivityView(app=self.app)
+        self.infobar.addWidget(self.activity_view)
+
         # disabled
         # self.progress_bar = QtWidgets.QProgressBar()
         # self.progress_bar.setMinimum(0)
@@ -1707,6 +1760,18 @@ class MainGUI(QtWidgets.QMainWindow):
 
         self.shell_dock.visibilityChanged.connect(self.on_shelldock_toggled)
 
+        # Notebook and Plot Tab Area signals
+        # make the right click on the notebook tab and plot tab area tab raise a menu
+        self.notebook.tabBar.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
+        self.plot_tab_area.tabBar.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
+        self.on_tab_setup_context_menu()
+        # activate initial state
+        self.on_detachable_tab_rmb_click(self.app.defaults["global_tabs_detachable"])
+
+        # status bar activation/deactivation
+        self.infobar.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
+        self.build_infobar_context_menu()
+
     def save_geometry(self, x, y, width, height, notebook_width):
         """
         Will save the application geometry and positions in the defaults dicitionary to be restored at the next
@@ -1782,18 +1847,90 @@ class MainGUI(QtWidgets.QMainWindow):
             self.grb_edit_toolbar.setVisible(False)
 
         # if tb & 128:
-        #     self.ui.status_toolbar.setVisible(True)
+        #     self.ui.grid_toolbar.setVisible(True)
         # else:
-        #     self.ui.status_toolbar.setVisible(False)
+        #     self.ui.grid_toolbar.setVisible(False)
 
-        # Grid Toolbar is always active now
-        self.status_toolbar.setVisible(True)
+        # Grid Toolbar is controlled by its own setting
 
         if tb & 256:
             self.toolbarshell.setVisible(True)
         else:
             self.toolbarshell.setVisible(False)
 
+    def on_tab_setup_context_menu(self):
+        initial_checked = self.app.defaults["global_tabs_detachable"]
+        action_name = str(_("Detachable Tabs"))
+        action = QtWidgets.QAction(self)
+        action.setCheckable(True)
+        action.setText(action_name)
+        action.setChecked(initial_checked)
+
+        self.notebook.tabBar.addAction(action)
+        self.plot_tab_area.tabBar.addAction(action)
+
+        try:
+            action.triggered.disconnect()
+        except TypeError:
+            pass
+        action.triggered.connect(self.on_detachable_tab_rmb_click)
+
+    def on_detachable_tab_rmb_click(self, checked):
+        self.notebook.set_detachable(val=checked)
+        self.app.defaults["global_tabs_detachable"] = checked
+
+        self.plot_tab_area.set_detachable(val=checked)
+        self.app.defaults["global_tabs_detachable"] = checked
+
+    def build_infobar_context_menu(self):
+        delta_coords_action_name = str(_("Delta Coordinates Toolbar"))
+        delta_coords_action = QtWidgets.QAction(self)
+        delta_coords_action.setCheckable(True)
+        delta_coords_action.setText(delta_coords_action_name)
+        delta_coords_action.setChecked(self.app.defaults["global_delta_coords_show"])
+        self.infobar.addAction(delta_coords_action)
+        delta_coords_action.triggered.connect(self.toggle_delta_coords)
+
+        coords_action_name = str(_("Coordinates Toolbar"))
+        coords_action = QtWidgets.QAction(self)
+        coords_action.setCheckable(True)
+        coords_action.setText(coords_action_name)
+        coords_action.setChecked(self.app.defaults["global_coords_show"])
+        self.infobar.addAction(coords_action)
+        coords_action.triggered.connect(self.toggle_coords)
+
+        grid_action_name = str(_("Grid Toolbar"))
+        grid_action = QtWidgets.QAction(self)
+        grid_action.setCheckable(True)
+        grid_action.setText(grid_action_name)
+        grid_action.setChecked(self.app.defaults["global_grid_show"])
+        self.infobar.addAction(grid_action)
+        grid_action.triggered.connect(self.toggle_gridbar)
+
+        status_action_name = str(_("Status Toolbar"))
+        status_action = QtWidgets.QAction(self)
+        status_action.setCheckable(True)
+        status_action.setText(status_action_name)
+        status_action.setChecked(self.app.defaults["global_status_show"])
+        self.infobar.addAction(status_action)
+        status_action.triggered.connect(self.toggle_statusbar)
+
+    def toggle_coords(self, checked):
+        self.app.defaults["global_coords_show"] = checked
+        self.coords_toolbar.setVisible(checked)
+
+    def toggle_delta_coords(self, checked):
+        self.app.defaults["global_delta_coords_show"] = checked
+        self.delta_coords_toolbar.setVisible(checked)
+
+    def toggle_gridbar(self, checked):
+        self.app.defaults["global_grid_show"] = checked
+        self.grid_toolbar.setVisible(checked)
+
+    def toggle_statusbar(self, checked):
+        self.app.defaults["global_status_show"] = checked
+        self.status_toolbar.setVisible(checked)
+
     def eventFilter(self, obj, event):
         """
         Filter the ToolTips display based on a Preferences setting
@@ -1953,6 +2090,8 @@ class MainGUI(QtWidgets.QMainWindow):
             QtGui.QIcon(self.app.resource_location + '/paint20_1.png'), _("Paint Tool"))
         self.isolation_btn = self.toolbartools.addAction(
             QtGui.QIcon(self.app.resource_location + '/iso_16.png'), _("Isolation Tool"))
+        self.drill_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/drilling_tool32.png'), _("Drilling Tool"))
         self.toolbartools.addSeparator()
 
         self.panelize_btn = self.toolbartools.addAction(
@@ -3375,6 +3514,22 @@ class MainGUI(QtWidgets.QMainWindow):
                         else:
                             self.app.inform.emit('[WARNING_NOTCL] %s' % _("Adding Tool cancelled ..."))
                         return
+        elif self.app.call_source == 'gcode_editor':
+            # CTRL
+            if modifiers == QtCore.Qt.ControlModifier:
+                # save (update) the current geometry and return to the App
+                if key == QtCore.Qt.Key_S or key == 'S':
+                    self.app.editor2object()
+                    return
+            # SHIFT
+            elif modifiers == QtCore.Qt.ShiftModifier:
+                pass
+            # ALT
+            elif modifiers == QtCore.Qt.AltModifier:
+                pass
+            # NO MODIFIER
+            elif modifiers == QtCore.Qt.NoModifier:
+                pass
         elif self.app.call_source == 'measurement':
             if modifiers == QtCore.Qt.ControlModifier:
                 pass
@@ -3613,7 +3768,8 @@ class MainGUI(QtWidgets.QMainWindow):
             # hide all Toolbars
             for tb in self.findChildren(QtWidgets.QToolBar):
                 tb.setVisible(False)
-            self.status_toolbar.setVisible(True)  # This Toolbar is always visible so restore it
+
+            self.grid_toolbar.setVisible(self.app.defaults["global_grid_show"])
 
             self.splitter.setSizes([0, 1])
             self.toggle_fscreen = True

Разница между файлами не показана из-за своего большого размера
+ 180 - 686
appGUI/ObjectUI.py


+ 30 - 12
appGUI/PlotCanvasLegacy.py

@@ -1272,6 +1272,7 @@ class ShapeCollectionLegacy:
                     'face_color': self._face_color,
                     'linewidth': line_width,
                     'alpha': self._alpha,
+                    'visible': self._visible,
                     'shape': sh
                 })
 
@@ -1285,6 +1286,7 @@ class ShapeCollectionLegacy:
                 'face_color': self._face_color,
                 'linewidth': line_width,
                 'alpha': self._alpha,
+                'visible': self._visible,
                 'shape': shape
             })
 
@@ -1336,19 +1338,20 @@ class ShapeCollectionLegacy:
         except AttributeError:
             obj_type = 'utility'
 
-        if self._visible:
-            # if we don't use this then when adding each new shape, the old ones will be added again, too
-            if obj_type == 'utility':
-                self.axes.patches.clear()
+        # if we don't use this then when adding each new shape, the old ones will be added again, too
+        # if obj_type == 'utility':
+        #     self.axes.patches.clear()
+        self.axes.patches.clear()
 
-            for element in local_shapes:
+        for element in local_shapes:
+            if local_shapes[element]['visible'] is True:
                 if obj_type == 'excellon':
                     # Plot excellon (All polygons?)
                     if self.obj.options["solid"] and isinstance(local_shapes[element]['shape'], Polygon):
                         try:
                             patch = PolygonPatch(local_shapes[element]['shape'],
-                                                 facecolor="#C40000",
-                                                 edgecolor="#750000",
+                                                 facecolor=local_shapes[element]['face_color'],
+                                                 edgecolor=local_shapes[element]['color'],
                                                  alpha=local_shapes[element]['alpha'],
                                                  zorder=3,
                                                  linewidth=local_shapes[element]['linewidth']
@@ -1358,11 +1361,15 @@ class ShapeCollectionLegacy:
                             log.debug("ShapeCollectionLegacy.redraw() excellon poly --> %s" % str(e))
                     else:
                         try:
-                            x, y = local_shapes[element]['shape'].exterior.coords.xy
-                            self.axes.plot(x, y, 'r-', linewidth=local_shapes[element]['linewidth'])
-                            for ints in local_shapes[element]['shape'].interiors:
-                                x, y = ints.coords.xy
-                                self.axes.plot(x, y, 'o-', linewidth=local_shapes[element]['linewidth'])
+                            if isinstance(local_shapes[element]['shape'], Polygon):
+                                x, y = local_shapes[element]['shape'].exterior.coords.xy
+                                self.axes.plot(x, y, 'r-', linewidth=local_shapes[element]['linewidth'])
+                                for ints in local_shapes[element]['shape'].interiors:
+                                    x, y = ints.coords.xy
+                                    self.axes.plot(x, y, 'o-', linewidth=local_shapes[element]['linewidth'])
+                            elif isinstance(local_shapes[element]['shape'], LinearRing):
+                                x, y = local_shapes[element]['shape'].coords.xy
+                                self.axes.plot(x, y, 'r-', linewidth=local_shapes[element]['linewidth'])
                         except Exception as e:
                             log.debug("ShapeCollectionLegacy.redraw() excellon no poly --> %s" % str(e))
                 elif obj_type == 'geometry':
@@ -1546,6 +1553,17 @@ class ShapeCollectionLegacy:
                 self.redraw()
         self._visible = value
 
+    def update_visibility(self, state, indexes=None):
+        if indexes:
+            for i in indexes:
+                if i in self._shapes:
+                    self._shapes[i]['visible'] = state
+        else:
+            for i in self._shapes:
+                self._shapes[i]['visible'] = state
+
+        self.redraw()
+
     @property
     def enabled(self):
         return self._visible

+ 35 - 3
appGUI/VisPyVisuals.py

@@ -145,7 +145,15 @@ class ShapeGroup(object):
         :param kwargs: keyword arguments
             Arguments for ShapeCollection.add function
         """
-        self._indexes.append(self._collection.add(**kwargs))
+        key = self._collection.add(**kwargs)
+        self._indexes.append(key)
+        return key
+
+    def remove(self, idx, update=False):
+        self._indexes.remove(idx)
+        self._collection.remove(idx, False)
+        if update:
+            self._collection.redraw([])             # Skip waiting results
 
     def clear(self, update=False):
         """
@@ -190,6 +198,17 @@ class ShapeGroup(object):
 
         self._collection.redraw([])
 
+    def update_visibility(self, state, indexes=None):
+        if indexes:
+            for i in indexes:
+                if i in self._indexes:
+                    self._collection.data[i]['visible'] = state
+        else:
+            for i in self._indexes:
+                self._collection.data[i]['visible'] = state
+
+        self._collection.redraw([])
+
 
 class ShapeCollectionVisual(CompoundVisual):
 
@@ -320,6 +339,19 @@ class ShapeCollectionVisual(CompoundVisual):
         if update:
             self.__update()
 
+    def update_visibility(self, state:bool, indexes=None) -> None:
+        # Lock sub-visuals updates
+        self.update_lock.acquire(True)
+        if indexes is None:
+            for k, data in list(self.data.items()):
+                self.data[k]['visible'] = state
+        else:
+            for k, data in list(self.data.items()):
+                if k in indexes:
+                    self.data[k]['visible'] = state
+
+        self.update_lock.release()
+
     def update_color(self, new_mesh_color=None, new_line_color=None, indexes=None):
         if new_mesh_color is None and new_line_color is None:
             return
@@ -527,7 +559,7 @@ class ShapeCollectionVisual(CompoundVisual):
 
         self.results_lock.release()
 
-        if update_colors is None:
+        if update_colors is None or update_colors is False:
             self.__update()
         else:
             try:
@@ -667,7 +699,7 @@ class TextCollectionVisual(TextVisual):
 
     def clear(self, update=False):
         """
-        Removes all shapes from colleciton
+        Removes all shapes from collection
         :param update: bool
             Set True to redraw collection
         """

+ 101 - 65
appGUI/preferences/PreferencesUIManager.py

@@ -119,6 +119,7 @@ class PreferencesUIManager:
             "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_multicolored": self.ui.gerber_defaults_form.gerber_gen_group.multicolored_cb,
+            "gerber_store_color_list": self.ui.gerber_defaults_form.gerber_gen_group.store_colors_cb,
             "gerber_circle_steps": self.ui.gerber_defaults_form.gerber_gen_group.circle_steps_entry,
             "gerber_def_units": self.ui.gerber_defaults_form.gerber_gen_group.gerber_units_radio,
             "gerber_def_zeros": self.ui.gerber_defaults_form.gerber_gen_group.gerber_zeros_radio,
@@ -171,6 +172,7 @@ class PreferencesUIManager:
             "excellon_plot": self.ui.excellon_defaults_form.excellon_gen_group.plot_cb,
             "excellon_solid": self.ui.excellon_defaults_form.excellon_gen_group.solid_cb,
             "excellon_multicolored": self.ui.excellon_defaults_form.excellon_gen_group.multicolored_cb,
+            "excellon_merge_fuse_tools": self.ui.excellon_defaults_form.excellon_gen_group.fuse_tools_cb,
             "excellon_format_upper_in":
                 self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_upper_in_entry,
             "excellon_format_lower_in":
@@ -193,34 +195,12 @@ class PreferencesUIManager:
 
             "excellon_milling_dia": self.ui.excellon_defaults_form.excellon_opt_group.mill_dia_entry,
 
-            "excellon_cutz": self.ui.excellon_defaults_form.excellon_opt_group.cutz_entry,
-            "excellon_multidepth": self.ui.excellon_defaults_form.excellon_opt_group.mpass_cb,
-            "excellon_depthperpass": self.ui.excellon_defaults_form.excellon_opt_group.maxdepth_entry,
-            "excellon_travelz": self.ui.excellon_defaults_form.excellon_opt_group.travelz_entry,
-            "excellon_endz": self.ui.excellon_defaults_form.excellon_opt_group.endz_entry,
-            "excellon_endxy": self.ui.excellon_defaults_form.excellon_opt_group.endxy_entry,
-
-            "excellon_feedrate_z": self.ui.excellon_defaults_form.excellon_opt_group.feedrate_z_entry,
-            "excellon_spindlespeed": self.ui.excellon_defaults_form.excellon_opt_group.spindlespeed_entry,
-            "excellon_dwell": self.ui.excellon_defaults_form.excellon_opt_group.dwell_cb,
-            "excellon_dwelltime": self.ui.excellon_defaults_form.excellon_opt_group.dwelltime_entry,
-            "excellon_toolchange": self.ui.excellon_defaults_form.excellon_opt_group.toolchange_cb,
-            "excellon_toolchangez": self.ui.excellon_defaults_form.excellon_opt_group.toolchangez_entry,
-            "excellon_ppname_e": self.ui.excellon_defaults_form.excellon_opt_group.pp_excellon_name_cb,
             "excellon_tooldia": self.ui.excellon_defaults_form.excellon_opt_group.tooldia_entry,
             "excellon_slot_tooldia": self.ui.excellon_defaults_form.excellon_opt_group.slot_tooldia_entry,
-            "excellon_gcode_type": self.ui.excellon_defaults_form.excellon_opt_group.excellon_gcode_type_radio,
 
             # Excellon Advanced Options
-            "excellon_offset":          self.ui.excellon_defaults_form.excellon_adv_opt_group.offset_entry,
-            "excellon_toolchangexy":    self.ui.excellon_defaults_form.excellon_adv_opt_group.toolchangexy_entry,
-            "excellon_startz":          self.ui.excellon_defaults_form.excellon_adv_opt_group.estartz_entry,
-            "excellon_feedrate_rapid":  self.ui.excellon_defaults_form.excellon_adv_opt_group.feedrate_rapid_entry,
-            "excellon_z_pdepth":        self.ui.excellon_defaults_form.excellon_adv_opt_group.pdepth_entry,
-            "excellon_feedrate_probe":  self.ui.excellon_defaults_form.excellon_adv_opt_group.feedrate_probe_entry,
-            "excellon_spindledir":      self.ui.excellon_defaults_form.excellon_adv_opt_group.spindledir_radio,
-            "excellon_f_plunge":        self.ui.excellon_defaults_form.excellon_adv_opt_group.fplunge_cb,
-            "excellon_f_retract":       self.ui.excellon_defaults_form.excellon_adv_opt_group.fretract_cb,
+            "excellon_tools_table_display": self.ui.excellon_defaults_form.excellon_adv_opt_group.table_visibility_cb,
+            "excellon_autoload_db":         self.ui.excellon_defaults_form.excellon_adv_opt_group.autoload_db_cb,
 
             # Excellon Export
             "excellon_exp_units":       self.ui.excellon_defaults_form.excellon_exp_group.excellon_units_radio,
@@ -261,11 +241,14 @@ class PreferencesUIManager:
                 self.ui.excellon_defaults_form.excellon_editor_group.slot_array_circular_angle_entry,
 
             # Geometry General
-            "geometry_plot":            self.ui.geometry_defaults_form.geometry_gen_group.plot_cb,
-            "geometry_multicolored":    self.ui.geometry_defaults_form.geometry_gen_group.multicolored_cb,
-            "geometry_circle_steps":    self.ui.geometry_defaults_form.geometry_gen_group.circle_steps_entry,
-            "geometry_cnctooldia":      self.ui.geometry_defaults_form.geometry_gen_group.cnctooldia_entry,
-            "geometry_plot_line":       self.ui.geometry_defaults_form.geometry_gen_group.line_color_entry,
+            "geometry_plot":                self.ui.geometry_defaults_form.geometry_gen_group.plot_cb,
+            "geometry_multicolored":        self.ui.geometry_defaults_form.geometry_gen_group.multicolored_cb,
+            "geometry_circle_steps":        self.ui.geometry_defaults_form.geometry_gen_group.circle_steps_entry,
+            "geometry_cnctooldia":          self.ui.geometry_defaults_form.geometry_gen_group.cnctooldia_entry,
+            "geometry_merge_fuse_tools":    self.ui.geometry_defaults_form.geometry_gen_group.fuse_tools_cb,
+            "geometry_plot_line":           self.ui.geometry_defaults_form.geometry_gen_group.line_color_entry,
+            "geometry_optimization_type":   self.ui.geometry_defaults_form.geometry_gen_group.opt_algorithm_radio,
+            "geometry_search_time":         self.ui.geometry_defaults_form.geometry_gen_group.optimization_time_entry,
 
             # Geometry Options
             "geometry_cutz":            self.ui.geometry_defaults_form.geometry_opt_group.cutz_entry,
@@ -306,8 +289,6 @@ class PreferencesUIManager:
 
             # CNCJob General
             "cncjob_plot":              self.ui.cncjob_defaults_form.cncjob_gen_group.plot_cb,
-            "cncjob_plot_kind":         self.ui.cncjob_defaults_form.cncjob_gen_group.cncplot_method_radio,
-            "cncjob_annotation":        self.ui.cncjob_defaults_form.cncjob_gen_group.annotation_cb,
 
             "cncjob_tooldia":           self.ui.cncjob_defaults_form.cncjob_gen_group.tooldia_entry,
             "cncjob_coords_type":       self.ui.cncjob_defaults_form.cncjob_gen_group.coords_type_radio,
@@ -321,14 +302,28 @@ class PreferencesUIManager:
             "cncjob_travel_fill":       self.ui.cncjob_defaults_form.cncjob_gen_group.tfill_color_entry,
 
             # CNC Job Options
-            "cncjob_prepend":   self.ui.cncjob_defaults_form.cncjob_opt_group.prepend_text,
-            "cncjob_append":    self.ui.cncjob_defaults_form.cncjob_opt_group.append_text,
+            "cncjob_plot_kind":         self.ui.cncjob_defaults_form.cncjob_opt_group.cncplot_method_radio,
+            "cncjob_annotation":        self.ui.cncjob_defaults_form.cncjob_opt_group.annotation_cb,
 
             # CNC Job Advanced Options
-            "cncjob_toolchange_macro":          self.ui.cncjob_defaults_form.cncjob_adv_opt_group.toolchange_text,
-            "cncjob_toolchange_macro_enable":   self.ui.cncjob_defaults_form.cncjob_adv_opt_group.toolchange_cb,
-            "cncjob_annotation_fontsize":  self.ui.cncjob_defaults_form.cncjob_adv_opt_group.annotation_fontsize_sp,
+            "cncjob_annotation_fontsize":   self.ui.cncjob_defaults_form.cncjob_adv_opt_group.annotation_fontsize_sp,
             "cncjob_annotation_fontcolor": self.ui.cncjob_defaults_form.cncjob_adv_opt_group.annotation_fontcolor_entry,
+            # Autolevelling
+            "cncjob_al_mode":               self.ui.cncjob_defaults_form.cncjob_adv_opt_group.al_mode_radio,
+            "cncjob_al_method":             self.ui.cncjob_defaults_form.cncjob_adv_opt_group.al_method_radio,
+            "cncjob_al_rows":               self.ui.cncjob_defaults_form.cncjob_adv_opt_group.al_rows_entry,
+            "cncjob_al_columns":            self.ui.cncjob_defaults_form.cncjob_adv_opt_group.al_columns_entry,
+            "cncjob_al_travelz":            self.ui.cncjob_defaults_form.cncjob_adv_opt_group.ptravelz_entry,
+            "cncjob_al_probe_depth":        self.ui.cncjob_defaults_form.cncjob_adv_opt_group.pdepth_entry,
+            "cncjob_al_probe_fr":           self.ui.cncjob_defaults_form.cncjob_adv_opt_group.feedrate_probe_entry,
+            "cncjob_al_controller":         self.ui.cncjob_defaults_form.cncjob_adv_opt_group.al_controller_combo,
+            "cncjob_al_grbl_jog_step":      self.ui.cncjob_defaults_form.cncjob_adv_opt_group.jog_step_entry,
+            "cncjob_al_grbl_jog_fr":        self.ui.cncjob_defaults_form.cncjob_adv_opt_group.jog_fr_entry,
+            "cncjob_al_grbl_travelz":       self.ui.cncjob_defaults_form.cncjob_adv_opt_group.jog_travelz_entry,
+
+            # CNC Job (GCode) Editor
+            "cncjob_prepend":               self.ui.cncjob_defaults_form.cncjob_editor_group.prepend_text,
+            "cncjob_append":                self.ui.cncjob_defaults_form.cncjob_editor_group.append_text,
 
             # Isolation Routing Tool
             "tools_iso_tooldia":        self.ui.tools_defaults_form.tools_iso_group.tool_dia_entry,
@@ -350,10 +345,42 @@ class PreferencesUIManager:
             "tools_iso_isoexcept":      self.ui.tools_defaults_form.tools_iso_group.except_cb,
             "tools_iso_selection":      self.ui.tools_defaults_form.tools_iso_group.select_combo,
             "tools_iso_poly_ints":      self.ui.tools_defaults_form.tools_iso_group.poly_int_cb,
-            "tools_iso_force":      self.ui.tools_defaults_form.tools_iso_group.force_iso_cb,
+            "tools_iso_force":          self.ui.tools_defaults_form.tools_iso_group.force_iso_cb,
             "tools_iso_area_shape":     self.ui.tools_defaults_form.tools_iso_group.area_shape_radio,
             "tools_iso_plotting":       self.ui.tools_defaults_form.tools_iso_group.plotting_radio,
 
+            # Drilling Tool
+            "tools_drill_tool_order":   self.ui.tools_defaults_form.tools_drill_group.order_radio,
+            "tools_drill_cutz":         self.ui.tools_defaults_form.tools_drill_group.cutz_entry,
+            "tools_drill_multidepth":   self.ui.tools_defaults_form.tools_drill_group.mpass_cb,
+            "tools_drill_depthperpass": self.ui.tools_defaults_form.tools_drill_group.maxdepth_entry,
+            "tools_drill_travelz":      self.ui.tools_defaults_form.tools_drill_group.travelz_entry,
+            "tools_drill_endz":         self.ui.tools_defaults_form.tools_drill_group.endz_entry,
+            "tools_drill_endxy":        self.ui.tools_defaults_form.tools_drill_group.endxy_entry,
+
+            "tools_drill_feedrate_z":   self.ui.tools_defaults_form.tools_drill_group.feedrate_z_entry,
+            "tools_drill_spindlespeed": self.ui.tools_defaults_form.tools_drill_group.spindlespeed_entry,
+            "tools_drill_dwell":        self.ui.tools_defaults_form.tools_drill_group.dwell_cb,
+            "tools_drill_dwelltime":    self.ui.tools_defaults_form.tools_drill_group.dwelltime_entry,
+            "tools_drill_toolchange":   self.ui.tools_defaults_form.tools_drill_group.toolchange_cb,
+            "tools_drill_toolchangez":  self.ui.tools_defaults_form.tools_drill_group.toolchangez_entry,
+            "tools_drill_ppname_e":     self.ui.tools_defaults_form.tools_drill_group.pp_excellon_name_cb,
+
+            "tools_drill_drill_slots":      self.ui.tools_defaults_form.tools_drill_group.drill_slots_cb,
+            "tools_drill_drill_overlap":    self.ui.tools_defaults_form.tools_drill_group.drill_overlap_entry,
+            "tools_drill_last_drill":       self.ui.tools_defaults_form.tools_drill_group.last_drill_cb,
+
+            # Advanced Options
+            "tools_drill_offset":           self.ui.tools_defaults_form.tools_drill_group.offset_entry,
+            "tools_drill_toolchangexy":     self.ui.tools_defaults_form.tools_drill_group.toolchangexy_entry,
+            "tools_drill_startz":           self.ui.tools_defaults_form.tools_drill_group.estartz_entry,
+            "tools_drill_feedrate_rapid":   self.ui.tools_defaults_form.tools_drill_group.feedrate_rapid_entry,
+            "tools_drill_z_pdepth":         self.ui.tools_defaults_form.tools_drill_group.pdepth_entry,
+            "tools_drill_feedrate_probe":   self.ui.tools_defaults_form.tools_drill_group.feedrate_probe_entry,
+            "tools_drill_spindledir":       self.ui.tools_defaults_form.tools_drill_group.spindledir_radio,
+            "tools_drill_f_plunge":         self.ui.tools_defaults_form.tools_drill_group.fplunge_cb,
+            "tools_drill_f_retract":        self.ui.tools_defaults_form.tools_drill_group.fretract_cb,
+
             # NCC Tool
             "tools_ncctools":           self.ui.tools_defaults_form.tools_ncc_group.ncc_tool_dia_entry,
             "tools_nccorder":           self.ui.tools_defaults_form.tools_ncc_group.ncc_order_radio,
@@ -376,39 +403,45 @@ class PreferencesUIManager:
             "tools_ncc_plotting":       self.ui.tools_defaults_form.tools_ncc_group.plotting_radio,
 
             # CutOut Tool
-            "tools_cutouttooldia": self.ui.tools_defaults_form.tools_cutout_group.cutout_tooldia_entry,
-            "tools_cutoutkind": self.ui.tools_defaults_form.tools_cutout_group.obj_kind_combo,
-            "tools_cutoutmargin": self.ui.tools_defaults_form.tools_cutout_group.cutout_margin_entry,
-            "tools_cutout_z": self.ui.tools_defaults_form.tools_cutout_group.cutz_entry,
-            "tools_cutout_depthperpass": self.ui.tools_defaults_form.tools_cutout_group.maxdepth_entry,
-            "tools_cutout_mdepth": self.ui.tools_defaults_form.tools_cutout_group.mpass_cb,
-            "tools_cutoutgapsize": self.ui.tools_defaults_form.tools_cutout_group.cutout_gap_entry,
-            "tools_gaps_ff": self.ui.tools_defaults_form.tools_cutout_group.gaps_combo,
-            "tools_cutout_convexshape": self.ui.tools_defaults_form.tools_cutout_group.convex_box,
+            "tools_cutout_tooldia":          self.ui.tools_defaults_form.tools_cutout_group.cutout_tooldia_entry,
+            "tools_cutout_kind":             self.ui.tools_defaults_form.tools_cutout_group.obj_kind_combo,
+            "tools_cutout_margin":          self.ui.tools_defaults_form.tools_cutout_group.cutout_margin_entry,
+            "tools_cutout_z":               self.ui.tools_defaults_form.tools_cutout_group.cutz_entry,
+            "tools_cutout_depthperpass":    self.ui.tools_defaults_form.tools_cutout_group.maxdepth_entry,
+            "tools_cutout_mdepth":          self.ui.tools_defaults_form.tools_cutout_group.mpass_cb,
+            "tools_cutout_gapsize":         self.ui.tools_defaults_form.tools_cutout_group.cutout_gap_entry,
+            "tools_cutout_gaps_ff":         self.ui.tools_defaults_form.tools_cutout_group.gaps_combo,
+            "tools_cutout_convexshape":     self.ui.tools_defaults_form.tools_cutout_group.convex_box,
+            "tools_cutout_big_cursor":      self.ui.tools_defaults_form.tools_cutout_group.big_cursor_cb,
+
+            "tools_cutout_gap_type":        self.ui.tools_defaults_form.tools_cutout_group.gaptype_radio,
+            "tools_cutout_gap_depth":       self.ui.tools_defaults_form.tools_cutout_group.thin_depth_entry,
+            "tools_cutout_mb_dia":          self.ui.tools_defaults_form.tools_cutout_group.mb_dia_entry,
+            "tools_cutout_mb_spacing":      self.ui.tools_defaults_form.tools_cutout_group.mb_spacing_entry,
 
             # Paint Area Tool
-            "tools_painttooldia": self.ui.tools_defaults_form.tools_paint_group.painttooldia_entry,
-            "tools_paintorder": self.ui.tools_defaults_form.tools_paint_group.paint_order_radio,
-            "tools_paintoverlap": self.ui.tools_defaults_form.tools_paint_group.paintoverlap_entry,
-            "tools_paintmargin": self.ui.tools_defaults_form.tools_paint_group.paintmargin_entry,
-            "tools_paintmethod": self.ui.tools_defaults_form.tools_paint_group.paintmethod_combo,
-            "tools_selectmethod": self.ui.tools_defaults_form.tools_paint_group.selectmethod_combo,
-            "tools_paint_area_shape": self.ui.tools_defaults_form.tools_paint_group.area_shape_radio,
-            "tools_pathconnect": self.ui.tools_defaults_form.tools_paint_group.pathconnect_cb,
-            "tools_paintcontour": self.ui.tools_defaults_form.tools_paint_group.contour_cb,
-            "tools_paint_plotting": self.ui.tools_defaults_form.tools_paint_group.paint_plotting_radio,
-
-            "tools_paintrest": self.ui.tools_defaults_form.tools_paint_group.rest_cb,
-            "tools_painttool_type": self.ui.tools_defaults_form.tools_paint_group.tool_type_radio,
-            "tools_paintcutz": self.ui.tools_defaults_form.tools_paint_group.cutz_entry,
-            "tools_painttipdia": self.ui.tools_defaults_form.tools_paint_group.tipdia_entry,
-            "tools_painttipangle": self.ui.tools_defaults_form.tools_paint_group.tipangle_entry,
-            "tools_paintnewdia": self.ui.tools_defaults_form.tools_paint_group.newdia_entry,
+            "tools_painttooldia":       self.ui.tools_defaults_form.tools_paint_group.painttooldia_entry,
+            "tools_paintorder":         self.ui.tools_defaults_form.tools_paint_group.paint_order_radio,
+            "tools_paintoverlap":       self.ui.tools_defaults_form.tools_paint_group.paintoverlap_entry,
+            "tools_paintoffset":        self.ui.tools_defaults_form.tools_paint_group.paintmargin_entry,
+            "tools_paintmethod":        self.ui.tools_defaults_form.tools_paint_group.paintmethod_combo,
+            "tools_selectmethod":       self.ui.tools_defaults_form.tools_paint_group.selectmethod_combo,
+            "tools_paint_area_shape":   self.ui.tools_defaults_form.tools_paint_group.area_shape_radio,
+            "tools_pathconnect":        self.ui.tools_defaults_form.tools_paint_group.pathconnect_cb,
+            "tools_paintcontour":       self.ui.tools_defaults_form.tools_paint_group.contour_cb,
+            "tools_paint_plotting":     self.ui.tools_defaults_form.tools_paint_group.paint_plotting_radio,
+
+            "tools_paintrest":          self.ui.tools_defaults_form.tools_paint_group.rest_cb,
+            "tools_painttool_type":     self.ui.tools_defaults_form.tools_paint_group.tool_type_radio,
+            "tools_paintcutz":          self.ui.tools_defaults_form.tools_paint_group.cutz_entry,
+            "tools_painttipdia":        self.ui.tools_defaults_form.tools_paint_group.tipdia_entry,
+            "tools_painttipangle":      self.ui.tools_defaults_form.tools_paint_group.tipangle_entry,
+            "tools_paintnewdia":        self.ui.tools_defaults_form.tools_paint_group.newdia_entry,
 
             # 2-sided Tool
             "tools_2sided_mirror_axis": self.ui.tools_defaults_form.tools_2sided_group.mirror_axis_radio,
-            "tools_2sided_axis_loc": self.ui.tools_defaults_form.tools_2sided_group.axis_location_radio,
-            "tools_2sided_drilldia": self.ui.tools_defaults_form.tools_2sided_group.drill_dia_entry,
+            "tools_2sided_axis_loc":    self.ui.tools_defaults_form.tools_2sided_group.axis_location_radio,
+            "tools_2sided_drilldia":    self.ui.tools_defaults_form.tools_2sided_group.drill_dia_entry,
             "tools_2sided_allign_axis": self.ui.tools_defaults_form.tools_2sided_group.align_axis_radio,
 
             # Film Tool
@@ -434,6 +467,7 @@ class PreferencesUIManager:
             "tools_panelize_spacing_rows": self.ui.tools_defaults_form.tools_panelize_group.pspacing_rows,
             "tools_panelize_columns": self.ui.tools_defaults_form.tools_panelize_group.pcolumns,
             "tools_panelize_rows": self.ui.tools_defaults_form.tools_panelize_group.prows,
+            "tools_panelize_optimization": self.ui.tools_defaults_form.tools_panelize_group.poptimization_cb,
             "tools_panelize_constrain": self.ui.tools_defaults_form.tools_panelize_group.pconstrain_cb,
             "tools_panelize_constrainx": self.ui.tools_defaults_form.tools_panelize_group.px_width_entry,
             "tools_panelize_constrainy": self.ui.tools_defaults_form.tools_panelize_group.py_height_entry,
@@ -858,6 +892,7 @@ class PreferencesUIManager:
 
         # restore the default stylesheet by setting a blank one
         self.ui.pref_apply_button.setStyleSheet("")
+        self.ui.pref_apply_button.setIcon(QtGui.QIcon(self.ui.app.resource_location + '/apply32.png'))
 
         self.inform.emit('%s' % _("Preferences applied."))
 
@@ -1082,6 +1117,7 @@ class PreferencesUIManager:
                     self.ui.plot_tab_area.tabBar.setTabTextColor(idx, QtGui.QColor('red'))
 
             self.ui.pref_apply_button.setStyleSheet("QPushButton {color: red;}")
+            self.ui.pref_apply_button.setIcon(QtGui.QIcon(self.ui.app.resource_location + '/apply_red32.png'))
 
             self.preferences_changed_flag = True
 

+ 158 - 122
appGUI/preferences/cncjob/CNCJobAdvOptPrefGroupUI.py

@@ -1,7 +1,7 @@
 from PyQt5 import QtWidgets, QtGui
 from PyQt5.QtCore import QSettings, Qt
 
-from appGUI.GUIElements import FCTextArea, FCCheckBox, FCComboBox, FCSpinner, FCColorEntry
+from appGUI.GUIElements import FCComboBox, FCSpinner, FCColorEntry, FCLabel, FCDoubleSpinner, RadioSet
 from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
 import gettext
 import appTranslation as fcTranslate
@@ -26,132 +26,29 @@ class CNCJobAdvOptPrefGroupUI(OptionsGroupUI):
 
         self.setTitle(str(_("CNC Job Adv. Options")))
 
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.layout.addLayout(grid0)
+
         # ## Export G-Code
-        self.export_gcode_label = QtWidgets.QLabel("<b>%s:</b>" % _("Export CNC Code"))
+        self.export_gcode_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
         self.export_gcode_label.setToolTip(
             _("Export and save G-Code to\n"
               "make this object to a file.")
         )
-        self.layout.addWidget(self.export_gcode_label)
-
-        # Prepend to G-Code
-        toolchangelabel = QtWidgets.QLabel('%s' % _('Toolchange G-Code'))
-        toolchangelabel.setToolTip(
-            _(
-                "Type here any G-Code commands you would\n"
-                "like to be executed when Toolchange event is encountered.\n"
-                "This will constitute a Custom Toolchange GCode,\n"
-                "or a Toolchange Macro.\n"
-                "The FlatCAM variables are surrounded by '%' symbol.\n\n"
-                "WARNING: it can be used only with a preprocessor file\n"
-                "that has 'toolchange_custom' in it's name and this is built\n"
-                "having as template the 'Toolchange Custom' posprocessor file."
-            )
-        )
-        self.layout.addWidget(toolchangelabel)
-
-        qsettings = QSettings("Open Source", "FlatCAM")
-        if qsettings.contains("textbox_font_size"):
-            tb_fsize = qsettings.value('textbox_font_size', type=int)
-        else:
-            tb_fsize = 10
-        font = QtGui.QFont()
-        font.setPointSize(tb_fsize)
-
-        self.toolchange_text = FCTextArea()
-        self.toolchange_text.setPlaceholderText(
-            _(
-                "Type here any G-Code commands you would "
-                "like to be executed when Toolchange event is encountered.\n"
-                "This will constitute a Custom Toolchange GCode, "
-                "or a Toolchange Macro.\n"
-                "The FlatCAM variables are surrounded by '%' symbol.\n"
-                "WARNING: it can be used only with a preprocessor file "
-                "that has 'toolchange_custom' in it's name."
-            )
-        )
-        self.layout.addWidget(self.toolchange_text)
-        self.toolchange_text.setFont(font)
-
-        hlay = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay)
-
-        # Toolchange Replacement GCode
-        self.toolchange_cb = FCCheckBox(label='%s' % _('Use Toolchange Macro'))
-        self.toolchange_cb.setToolTip(
-            _("Check this box if you want to use\n"
-              "a Custom Toolchange GCode (macro).")
-        )
-        hlay.addWidget(self.toolchange_cb)
-        hlay.addStretch()
-
-        hlay1 = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay1)
-
-        # Variable list
-        self.tc_variable_combo = FCComboBox()
-        self.tc_variable_combo.setToolTip(
-            _("A list of the FlatCAM variables that can be used\n"
-              "in the Toolchange event.\n"
-              "They have to be surrounded by the '%' symbol")
-        )
-        hlay1.addWidget(self.tc_variable_combo)
-
-        # Populate the Combo Box
-        variables = [_('Parameters'), 'tool', 'tooldia', 't_drills', 'x_toolchange', 'y_toolchange', 'z_toolchange',
-                     'z_cut', 'z_move', 'z_depthpercut', 'spindlespeed', 'dwelltime']
-        self.tc_variable_combo.addItems(variables)
-        self.tc_variable_combo.insertSeparator(1)
-
-        self.tc_variable_combo.setItemData(0, _("FlatCAM CNC parameters"), Qt.ToolTipRole)
-        fnt = QtGui.QFont()
-        fnt.setBold(True)
-        self.tc_variable_combo.setItemData(0, fnt, Qt.FontRole)
-
-        self.tc_variable_combo.setItemData(2, 'tool = %s' % _("tool number"), Qt.ToolTipRole)
-        self.tc_variable_combo.setItemData(3, 'tooldia = %s' % _("tool diameter"), Qt.ToolTipRole)
-        self.tc_variable_combo.setItemData(4, 't_drills = %s' % _("for Excellon, total number of drills"),
-                                           Qt.ToolTipRole)
-        self.tc_variable_combo.setItemData(5, 'x_toolchange = %s' % _("X coord for Toolchange"), Qt.ToolTipRole)
-        self.tc_variable_combo.setItemData(6, 'y_toolchange = %s' % _("Y coord for Toolchange"),
-                                           Qt.ToolTipRole)
-        self.tc_variable_combo.setItemData(7, 'z_toolchange = %s' % _("Z coord for Toolchange"), Qt.ToolTipRole)
-        self.tc_variable_combo.setItemData(8, 'z_cut = %s' % _("Z depth for the cut"), Qt.ToolTipRole)
-        self.tc_variable_combo.setItemData(9, 'z_move = %s' % _("Z height for travel"), Qt.ToolTipRole)
-        self.tc_variable_combo.setItemData(10, 'z_depthpercut = %s' % _("the step value for multidepth cut"),
-                                           Qt.ToolTipRole)
-        self.tc_variable_combo.setItemData(11, 'spindlesspeed = %s' % _("the value for the spindle speed"),
-                                           Qt.ToolTipRole)
-        self.tc_variable_combo.setItemData(12,
-                                           _("dwelltime = time to dwell to allow the spindle to reach it's set RPM"),
-                                           Qt.ToolTipRole)
-
-        # hlay1.addStretch()
-
-        # Insert Variable into the Toolchange G-Code Text Box
-        # self.tc_insert_buton = FCButton("Insert")
-        # self.tc_insert_buton.setToolTip(
-        #     "Insert the variable in the GCode Box\n"
-        #     "surrounded by the '%' symbol."
-        # )
-        # hlay1.addWidget(self.tc_insert_buton)
-
-        grid0 = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid0)
-
-        grid0.addWidget(QtWidgets.QLabel(''), 1, 0, 1, 2)
+        grid0.addWidget(self.export_gcode_label, 0, 0, 1, 2)
 
         # Annotation Font Size
         self.annotation_fontsize_label = QtWidgets.QLabel('%s:' % _("Annotation Size"))
         self.annotation_fontsize_label.setToolTip(
             _("The font size of the annotation text. In pixels.")
         )
-        grid0.addWidget(self.annotation_fontsize_label, 2, 0)
         self.annotation_fontsize_sp = FCSpinner()
         self.annotation_fontsize_sp.set_range(0, 9999)
 
+        grid0.addWidget(self.annotation_fontsize_label, 2, 0)
         grid0.addWidget(self.annotation_fontsize_sp, 2, 1)
-        grid0.addWidget(QtWidgets.QLabel(''), 2, 2)
 
         # Annotation Font Color
         self.annotation_color_label = QtWidgets.QLabel('%s:' % _('Annotation Color'))
@@ -160,20 +57,159 @@ class CNCJobAdvOptPrefGroupUI(OptionsGroupUI):
         )
         self.annotation_fontcolor_entry = FCColorEntry()
 
-        grid0.addWidget(self.annotation_color_label, 3, 0)
-        grid0.addWidget(self.annotation_fontcolor_entry, 3, 1)
+        grid0.addWidget(self.annotation_color_label, 4, 0)
+        grid0.addWidget(self.annotation_fontcolor_entry, 4, 1)
+
+        # ## Autolevelling
+        self.autolevelling_gcode_label = QtWidgets.QLabel("<b>%s</b>" % _("Autolevelling"))
+        self.autolevelling_gcode_label.setToolTip(
+            _("Parameters for the autolevelling.")
+        )
+        grid0.addWidget(self.autolevelling_gcode_label, 6, 0, 1, 2)
+
+        # Probe points mode
+        al_mode_lbl = FCLabel('%s:' % _("Mode"))
+        al_mode_lbl.setToolTip(_("Choose a mode for height map generation.\n"
+                                 "- Manual: will pick a selection of probe points by clicking on canvas\n"
+                                 "- Grid: will automatically generate a grid of probe points"))
+
+        self.al_mode_radio = RadioSet(
+            [
+                {'label': _('Manual'), 'value': 'manual'},
+                {'label': _('Grid'), 'value': 'grid'}
+            ])
+        grid0.addWidget(al_mode_lbl, 8, 0)
+        grid0.addWidget(self.al_mode_radio, 8, 1)
+
+        # AUTOLEVELL METHOD
+        self.al_method_lbl = FCLabel('%s:' % _("Method"))
+        self.al_method_lbl.setToolTip(_("Choose a method for approximation of heights from autolevelling data.\n"
+                                        "- Voronoi: will generate a Voronoi diagram\n"
+                                        "- Bilinear: will use bilinear interpolation. Usable only for grid mode."))
+
+        self.al_method_radio = RadioSet(
+            [
+                {'label': _('Voronoi'), 'value': 'v'},
+                {'label': _('Bilinear'), 'value': 'b'}
+            ])
+        grid0.addWidget(self.al_method_lbl, 9, 0)
+        grid0.addWidget(self.al_method_radio, 9, 1)
+
+        # ## Columns
+        self.al_columns_entry = FCSpinner()
+
+        self.al_columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
+        self.al_columns_label.setToolTip(
+            _("The number of grid columns.")
+        )
+        grid0.addWidget(self.al_columns_label, 10, 0)
+        grid0.addWidget(self.al_columns_entry, 10, 1)
+
+        # ## Rows
+        self.al_rows_entry = FCSpinner()
+
+        self.al_rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
+        self.al_rows_label.setToolTip(
+            _("The number of gird rows.")
+        )
+        grid0.addWidget(self.al_rows_label, 12, 0)
+        grid0.addWidget(self.al_rows_entry, 12, 1)
+
+        # Travel Z Probe
+        self.ptravelz_label = QtWidgets.QLabel('%s:' % _("Probe Z travel"))
+        self.ptravelz_label.setToolTip(
+            _("The safe Z for probe travelling between probe points.")
+        )
+        self.ptravelz_entry = FCDoubleSpinner()
+        self.ptravelz_entry.set_precision(self.decimals)
+        self.ptravelz_entry.set_range(0.0000, 9999.9999)
+
+        grid0.addWidget(self.ptravelz_label, 14, 0)
+        grid0.addWidget(self.ptravelz_entry, 14, 1)
+
+        # Probe depth
+        self.pdepth_label = QtWidgets.QLabel('%s:' % _("Probe Z depth"))
+        self.pdepth_label.setToolTip(
+            _("The maximum depth that the probe is allowed\n"
+              "to probe. Negative value, in current units.")
+        )
+        self.pdepth_entry = FCDoubleSpinner()
+        self.pdepth_entry.set_precision(self.decimals)
+        self.pdepth_entry.set_range(-99999.9999, 0.0000)
+
+        grid0.addWidget(self.pdepth_label, 16, 0)
+        grid0.addWidget(self.pdepth_entry, 16, 1)
+
+        # Probe feedrate
+        self.feedrate_probe_label = QtWidgets.QLabel('%s:' % _("Probe Feedrate"))
+        self.feedrate_probe_label.setToolTip(
+            _("The feedrate used while the probe is probing.")
+        )
+        self.feedrate_probe_entry = FCDoubleSpinner()
+        self.feedrate_probe_entry.set_precision(self.decimals)
+        self.feedrate_probe_entry.set_range(0, 99999.9999)
+
+        grid0.addWidget(self.feedrate_probe_label, 18, 0)
+        grid0.addWidget(self.feedrate_probe_entry, 18, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 20, 0, 1, 2)
+
+        self.al_controller_label = FCLabel('%s:' % _("Controller"))
+        self.al_controller_label.setToolTip(
+            _("The kind of controller for which to generate\n"
+              "height map gcode.")
+        )
+
+        self.al_controller_combo = FCComboBox()
+        self.al_controller_combo.addItems(["MACH3", "MACH4", "LinuxCNC", "GRBL"])
+        grid0.addWidget(self.al_controller_label, 22, 0)
+        grid0.addWidget(self.al_controller_combo, 22, 1)
+
+        # JOG Step
+        self.jog_step_label = FCLabel('%s:' % _("Step"))
+        self.jog_step_label.setToolTip(
+            _("Each jog action will move the axes with this value.")
+        )
+
+        self.jog_step_entry = FCDoubleSpinner()
+        self.jog_step_entry.set_precision(self.decimals)
+        self.jog_step_entry.set_range(0, 99999.9999)
+
+        grid0.addWidget(self.jog_step_label, 24, 0)
+        grid0.addWidget(self.jog_step_entry, 24, 1)
+
+        # JOG Feedrate
+        self.jog_fr_label = FCLabel('%s:' % _("Feedrate"))
+        self.jog_fr_label.setToolTip(
+            _("Feedrate when jogging.")
+        )
+
+        self.jog_fr_entry = FCDoubleSpinner()
+        self.jog_fr_entry.set_precision(self.decimals)
+        self.jog_fr_entry.set_range(0, 99999.9999)
+
+        grid0.addWidget(self.jog_fr_label, 26, 0)
+        grid0.addWidget(self.jog_fr_entry, 26, 1)
+
+        # JOG Travel Z
+        self.jog_travelz_label = FCLabel('%s:' % _("Travel Z"))
+        self.jog_travelz_label.setToolTip(
+            _("Safe height (Z) distance when jogging to origin.")
+        )
+
+        self.jog_travelz_entry = FCDoubleSpinner()
+        self.jog_travelz_entry.set_precision(self.decimals)
+        self.jog_travelz_entry.set_range(0, 99999.9999)
+
+        grid0.addWidget(self.jog_travelz_label, 28, 0)
+        grid0.addWidget(self.jog_travelz_entry, 28, 1)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 3, 2)
         self.layout.addStretch()
 
-        self.tc_variable_combo.currentIndexChanged[str].connect(self.on_cnc_custom_parameters)
         self.annotation_fontcolor_entry.editingFinished.connect(self.on_annotation_fontcolor_entry)
 
-    def on_cnc_custom_parameters(self, signal_text):
-        if signal_text == 'Parameters':
-            return
-        else:
-            self.toolchange_text.insertPlainText('%%%s%%' % signal_text)
-
     def on_annotation_fontcolor_entry(self):
         self.app.defaults['cncjob_annotation_fontcolor'] = self.annotation_fontcolor_entry.get_value()

+ 79 - 0
appGUI/preferences/cncjob/CNCJobEditorPrefGroupUI.py

@@ -0,0 +1,79 @@
+from PyQt5 import QtWidgets, QtGui
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCTextArea
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class CNCJobEditorPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "CNC Job Options Preferences", parent=None)
+        super(CNCJobEditorPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("CNC Job Editor")))
+        self.decimals = decimals
+
+        # Editor Parameters
+        self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.param_label.setToolTip(
+            _("A list of Editor parameters.")
+        )
+        self.layout.addWidget(self.param_label)
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("textbox_font_size"):
+            tb_fsize = qsettings.value('textbox_font_size', type=int)
+        else:
+            tb_fsize = 10
+        font = QtGui.QFont()
+        font.setPointSize(tb_fsize)
+
+        # Prepend to G-Code
+        prependlabel = QtWidgets.QLabel('%s:' % _('Prepend to G-Code'))
+        prependlabel.setToolTip(
+            _("Type here any G-Code commands you would\n"
+              "like to add at the beginning of the G-Code file.")
+        )
+        self.layout.addWidget(prependlabel)
+
+        self.prepend_text = FCTextArea()
+        self.prepend_text.setPlaceholderText(
+            _("Type here any G-Code commands you would "
+              "like to add at the beginning of the G-Code file.")
+        )
+        self.layout.addWidget(self.prepend_text)
+        self.prepend_text.setFont(font)
+
+        # Append text to G-Code
+        appendlabel = QtWidgets.QLabel('%s:' % _('Append to G-Code'))
+        appendlabel.setToolTip(
+            _("Type here any G-Code commands you would\n"
+              "like to append to the generated file.\n"
+              "I.e.: M2 (End of program)")
+        )
+        self.layout.addWidget(appendlabel)
+
+        self.append_text = FCTextArea()
+        self.append_text.setPlaceholderText(
+            _("Type here any G-Code commands you would "
+              "like to append to the generated file.\n"
+              "I.e.: M2 (End of program)")
+        )
+        self.layout.addWidget(self.append_text)
+        self.append_text.setFont(font)
+
+        self.layout.addStretch()

+ 0 - 30
appGUI/preferences/cncjob/CNCJobGenPrefGroupUI.py

@@ -41,36 +41,6 @@ class CNCJobGenPrefGroupUI(OptionsGroupUI):
         self.plot_cb.setToolTip(_("Plot (show) this object."))
         grid0.addWidget(self.plot_cb, 0, 0, 1, 2)
 
-        # Plot Kind
-        self.cncplot_method_label = QtWidgets.QLabel('%s:' % _("Plot kind"))
-        self.cncplot_method_label.setToolTip(
-            _("This selects the kind of geometries on the canvas to plot.\n"
-              "Those can be either of type 'Travel' which means the moves\n"
-              "above the work piece or it can be of type 'Cut',\n"
-              "which means the moves that cut into the material.")
-        )
-
-        self.cncplot_method_radio = RadioSet([
-            {"label": _("All"), "value": "all"},
-            {"label": _("Travel"), "value": "travel"},
-            {"label": _("Cut"), "value": "cut"}
-        ], orientation='vertical')
-
-        grid0.addWidget(self.cncplot_method_label, 1, 0)
-        grid0.addWidget(self.cncplot_method_radio, 1, 1)
-        grid0.addWidget(QtWidgets.QLabel(''), 1, 2)
-
-        # Display Annotation
-        self.annotation_cb = FCCheckBox(_("Display Annotation"))
-        self.annotation_cb.setToolTip(
-            _("This selects if to display text annotation on the plot.\n"
-              "When checked it will display numbers in order for each end\n"
-              "of a travel line."
-              )
-        )
-
-        grid0.addWidget(self.annotation_cb, 2, 0, 1, 3)
-
         # ###################################################################
         # Number of circle steps for circular aperture linear approximation #
         # ###################################################################

+ 31 - 30
appGUI/preferences/cncjob/CNCJobOptPrefGroupUI.py

@@ -1,7 +1,7 @@
 from PyQt5 import QtWidgets, QtGui
 from PyQt5.QtCore import QSettings
 
-from appGUI.GUIElements import FCTextArea
+from appGUI.GUIElements import RadioSet, FCCheckBox
 from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
@@ -43,38 +43,39 @@ class CNCJobOptPrefGroupUI(OptionsGroupUI):
         font = QtGui.QFont()
         font.setPointSize(tb_fsize)
 
-        # Prepend to G-Code
-        prependlabel = QtWidgets.QLabel('%s:' % _('Prepend to G-Code'))
-        prependlabel.setToolTip(
-            _("Type here any G-Code commands you would\n"
-              "like to add at the beginning of the G-Code file.")
-        )
-        self.layout.addWidget(prependlabel)
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
 
-        self.prepend_text = FCTextArea()
-        self.prepend_text.setPlaceholderText(
-            _("Type here any G-Code commands you would "
-              "like to add at the beginning of the G-Code file.")
-        )
-        self.layout.addWidget(self.prepend_text)
-        self.prepend_text.setFont(font)
-
-        # Append text to G-Code
-        appendlabel = QtWidgets.QLabel('%s:' % _('Append to G-Code'))
-        appendlabel.setToolTip(
-            _("Type here any G-Code commands you would\n"
-              "like to append to the generated file.\n"
-              "I.e.: M2 (End of program)")
+        # Plot Kind
+        self.cncplot_method_label = QtWidgets.QLabel('%s:' % _("Plot kind"))
+        self.cncplot_method_label.setToolTip(
+            _("This selects the kind of geometries on the canvas to plot.\n"
+              "Those can be either of type 'Travel' which means the moves\n"
+              "above the work piece or it can be of type 'Cut',\n"
+              "which means the moves that cut into the material.")
         )
-        self.layout.addWidget(appendlabel)
 
-        self.append_text = FCTextArea()
-        self.append_text.setPlaceholderText(
-            _("Type here any G-Code commands you would "
-              "like to append to the generated file.\n"
-              "I.e.: M2 (End of program)")
+        self.cncplot_method_radio = RadioSet([
+            {"label": _("All"), "value": "all"},
+            {"label": _("Travel"), "value": "travel"},
+            {"label": _("Cut"), "value": "cut"}
+        ], orientation='vertical', stretch=False)
+
+        grid0.addWidget(self.cncplot_method_label, 1, 0)
+        grid0.addWidget(self.cncplot_method_radio, 1, 1)
+        grid0.addWidget(QtWidgets.QLabel(''), 1, 2)
+
+        # Display Annotation
+        self.annotation_cb = FCCheckBox(_("Display Annotation"))
+        self.annotation_cb.setToolTip(
+            _("This selects if to display text annotation on the plot.\n"
+              "When checked it will display numbers in order for each end\n"
+              "of a travel line."
+              )
         )
-        self.layout.addWidget(self.append_text)
-        self.append_text.setFont(font)
+
+        grid0.addWidget(self.annotation_cb, 2, 0, 1, 3)
 
         self.layout.addStretch()

+ 11 - 2
appGUI/preferences/cncjob/CNCJobPreferencesUI.py

@@ -3,6 +3,7 @@ from PyQt5 import QtWidgets
 from appGUI.preferences.cncjob.CNCJobAdvOptPrefGroupUI import CNCJobAdvOptPrefGroupUI
 from appGUI.preferences.cncjob.CNCJobOptPrefGroupUI import CNCJobOptPrefGroupUI
 from appGUI.preferences.cncjob.CNCJobGenPrefGroupUI import CNCJobGenPrefGroupUI
+from appGUI.preferences.cncjob.CNCJobEditorPrefGroupUI import CNCJobEditorPrefGroupUI
 
 
 class CNCJobPreferencesUI(QtWidgets.QWidget):
@@ -15,13 +16,21 @@ class CNCJobPreferencesUI(QtWidgets.QWidget):
 
         self.cncjob_gen_group = CNCJobGenPrefGroupUI(decimals=self.decimals)
         self.cncjob_gen_group.setMinimumWidth(260)
+
         self.cncjob_opt_group = CNCJobOptPrefGroupUI(decimals=self.decimals)
         self.cncjob_opt_group.setMinimumWidth(260)
         self.cncjob_adv_opt_group = CNCJobAdvOptPrefGroupUI(decimals=self.decimals)
         self.cncjob_adv_opt_group.setMinimumWidth(260)
 
+        self.cncjob_editor_group = CNCJobEditorPrefGroupUI(decimals=self.decimals)
+        self.cncjob_editor_group.setMinimumWidth(260)
+
+        vlay = QtWidgets.QVBoxLayout()
+        vlay.addWidget(self.cncjob_opt_group)
+        vlay.addWidget(self.cncjob_adv_opt_group)
+
         self.layout.addWidget(self.cncjob_gen_group)
-        self.layout.addWidget(self.cncjob_opt_group)
-        self.layout.addWidget(self.cncjob_adv_opt_group)
+        self.layout.addLayout(vlay)
+        self.layout.addWidget(self.cncjob_editor_group)
 
         self.layout.addStretch()

+ 15 - 108
appGUI/preferences/excellon/ExcellonAdvOptPrefGroupUI.py

@@ -39,117 +39,24 @@ class ExcellonAdvOptPrefGroupUI(OptionsGroupUI):
         )
         self.layout.addWidget(self.exc_label)
 
-        grid1 = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid1)
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.layout.addLayout(grid0)
 
-        # Offset Z
-        offsetlabel = QtWidgets.QLabel('%s:' % _('Offset Z'))
-        offsetlabel.setToolTip(
-            _("Some drill bits (the larger ones) need to drill deeper\n"
-              "to create the desired exit hole diameter due of the tip shape.\n"
-              "The value here can compensate the Cut Z parameter."))
-        self.offset_entry = FCDoubleSpinner()
-        self.offset_entry.set_precision(self.decimals)
-        self.offset_entry.set_range(-999.9999, 999.9999)
-
-        grid1.addWidget(offsetlabel, 0, 0)
-        grid1.addWidget(self.offset_entry, 0, 1)
-
-        # ToolChange X,Y
-        toolchange_xy_label = QtWidgets.QLabel('%s:' % _('Toolchange X,Y'))
-        toolchange_xy_label.setToolTip(
-            _("Toolchange X,Y position.")
-        )
-        self.toolchangexy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
-
-        grid1.addWidget(toolchange_xy_label, 1, 0)
-        grid1.addWidget(self.toolchangexy_entry, 1, 1)
-
-        # Start Z
-        startzlabel = QtWidgets.QLabel('%s:' % _('Start Z'))
-        startzlabel.setToolTip(
-            _("Height of the tool just after start.\n"
-              "Delete the value if you don't need this feature.")
+        # Table Visibility CB
+        self.table_visibility_cb = FCCheckBox(label=_('Table Show/Hide'))
+        self.table_visibility_cb.setToolTip(
+            _("Toggle the display of the Tools Table.")
         )
-        self.estartz_entry = NumericalEvalEntry(border_color='#0069A9')
-
-        grid1.addWidget(startzlabel, 2, 0)
-        grid1.addWidget(self.estartz_entry, 2, 1)
+        grid0.addWidget(self.table_visibility_cb, 0, 0, 1, 2)
 
-        # Feedrate Rapids
-        fr_rapid_label = QtWidgets.QLabel('%s:' % _('Feedrate Rapids'))
-        fr_rapid_label.setToolTip(
-            _("Tool speed while drilling\n"
-              "(in units per minute).\n"
-              "This is for the rapid move G00.\n"
-              "It is useful only for Marlin,\n"
-              "ignore for any other cases.")
+        # Auto Load Tools from DB
+        self.autoload_db_cb = FCCheckBox('%s' % _("Auto load from DB"))
+        self.autoload_db_cb.setToolTip(
+            _("Automatic replacement of the tools from related application tools\n"
+              "with tools from DB that have a close diameter value.")
         )
-        self.feedrate_rapid_entry = FCDoubleSpinner()
-        self.feedrate_rapid_entry.set_precision(self.decimals)
-        self.feedrate_rapid_entry.set_range(0, 99999.9999)
-
-        grid1.addWidget(fr_rapid_label, 3, 0)
-        grid1.addWidget(self.feedrate_rapid_entry, 3, 1)
-
-        # Probe depth
-        self.pdepth_label = QtWidgets.QLabel('%s:' % _("Probe Z depth"))
-        self.pdepth_label.setToolTip(
-            _("The maximum depth that the probe is allowed\n"
-              "to probe. Negative value, in current units.")
-        )
-        self.pdepth_entry = FCDoubleSpinner()
-        self.pdepth_entry.set_precision(self.decimals)
-        self.pdepth_entry.set_range(-99999.9999, 0.0000)
-
-        grid1.addWidget(self.pdepth_label, 4, 0)
-        grid1.addWidget(self.pdepth_entry, 4, 1)
-
-        # Probe feedrate
-        self.feedrate_probe_label = QtWidgets.QLabel('%s:' % _("Feedrate Probe"))
-        self.feedrate_probe_label.setToolTip(
-           _("The feedrate used while the probe is probing.")
-        )
-        self.feedrate_probe_entry = FCDoubleSpinner()
-        self.feedrate_probe_entry.set_precision(self.decimals)
-        self.feedrate_probe_entry.set_range(0, 99999.9999)
-
-        grid1.addWidget(self.feedrate_probe_label, 5, 0)
-        grid1.addWidget(self.feedrate_probe_entry, 5, 1)
-
-        # Spindle direction
-        spindle_dir_label = QtWidgets.QLabel('%s:' % _('Spindle direction'))
-        spindle_dir_label.setToolTip(
-            _("This sets the direction that the spindle is rotating.\n"
-              "It can be either:\n"
-              "- CW = clockwise or\n"
-              "- CCW = counter clockwise")
-        )
-
-        self.spindledir_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
-                                          {'label': _('CCW'), 'value': 'CCW'}])
-        grid1.addWidget(spindle_dir_label, 6, 0)
-        grid1.addWidget(self.spindledir_radio, 6, 1)
-
-        self.fplunge_cb = FCCheckBox('%s' % _('Fast Plunge'))
-        self.fplunge_cb.setToolTip(
-            _("By checking this, the vertical move from\n"
-              "Z_Toolchange to Z_move is done with G0,\n"
-              "meaning the fastest speed available.\n"
-              "WARNING: the move is done at Toolchange X,Y coords.")
-        )
-        grid1.addWidget(self.fplunge_cb, 7, 0, 1, 2)
-
-        self.fretract_cb = FCCheckBox('%s' % _('Fast Retract'))
-        self.fretract_cb.setToolTip(
-            _("Exit hole strategy.\n"
-              " - When uncheked, while exiting the drilled hole the drill bit\n"
-              "will travel slow, with set feedrate (G1), up to zero depth and then\n"
-              "travel as fast as possible (G0) to the Z Move (travel height).\n"
-              " - When checked the travel from Z cut (cut depth) to Z_move\n"
-              "(travel height) is done as fast as possible (G0) in one move.")
-        )
-
-        grid1.addWidget(self.fretract_cb, 8, 0, 1, 2)
+        grid0.addWidget(self.autoload_db_cb, 1, 0, 1, 2)
 
         self.layout.addStretch()

+ 31 - 29
appGUI/preferences/excellon/ExcellonGenPrefGroupUI.py

@@ -1,9 +1,9 @@
 import platform
 
-from PyQt5 import QtWidgets, QtCore, QtGui
+from PyQt5 import QtWidgets, QtCore
 from PyQt5.QtCore import QSettings
 
-from appGUI.GUIElements import FCCheckBox, FCSpinner, RadioSet, FCEntry, FCSliderWithSpinner, FCColorEntry
+from appGUI.GUIElements import FCCheckBox, FCSpinner, RadioSet, FCSliderWithSpinner, FCColorEntry
 from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
 import gettext
 import appTranslation as fcTranslate
@@ -207,7 +207,7 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         grid2.addWidget(separator_line, 7, 0, 1, 2)
 
-        self.excellon_general_label = QtWidgets.QLabel("<b>%s:</b>" % _("Excellon Optimization"))
+        self.excellon_general_label = QtWidgets.QLabel("<b>%s:</b>" % _("Path Optimization"))
         grid2.addWidget(self.excellon_general_label, 8, 0, 1, 2)
 
         self.excellon_optimization_label = QtWidgets.QLabel(_('Algorithm:'))
@@ -219,25 +219,13 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
               "If <<TSA>> is checked then Travelling Salesman algorithm is used for\n"
               "drill path optimization.\n"
               "\n"
-              "If this control is disabled, then FlatCAM works in 32bit mode and it uses\n"
-              "Travelling Salesman algorithm for path optimization.")
+              "Some options are disabled when FlatCAM works in 32bit mode.")
         )
 
         self.excellon_optimization_radio = RadioSet([{'label': _('MetaHeuristic'), 'value': 'M'},
                                                      {'label': _('Basic'), 'value': 'B'},
                                                      {'label': _('TSA'), 'value': 'T'}],
                                                     orientation='vertical', stretch=False)
-        self.excellon_optimization_radio.setToolTip(
-            _("This sets the optimization type for the Excellon drill path.\n"
-              "If <<MetaHeuristic>> is checked then Google OR-Tools algorithm with\n"
-              "MetaHeuristic Guided Local Path is used. Default search time is 3sec.\n"
-              "If <<Basic>> is checked then Google OR-Tools Basic algorithm is used.\n"
-              "If <<TSA>> is checked then Travelling Salesman algorithm is used for\n"
-              "drill path optimization.\n"
-              "\n"
-              "If this control is disabled, then FlatCAM works in 32bit mode and it uses\n"
-              "Travelling Salesman algorithm for path optimization.")
-        )
 
         grid2.addWidget(self.excellon_optimization_label, 9, 0)
         grid2.addWidget(self.excellon_optimization_radio, 9, 1)
@@ -263,9 +251,25 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         grid2.addWidget(separator_line, 11, 0, 1, 2)
 
+        # Fuse Tools
+        self.join_geo_label = QtWidgets.QLabel('<b>%s</b>:' % _('Join Option'))
+        grid2.addWidget(self.join_geo_label, 12, 0, 1, 2)
+
+        self.fuse_tools_cb = FCCheckBox(_("Fuse Tools"))
+        self.fuse_tools_cb.setToolTip(
+            _("When checked the joined (merged) object tools\n"
+              "will be merged also but only if they share some of their attributes.")
+        )
+        grid2.addWidget(self.fuse_tools_cb, 13, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid2.addWidget(separator_line, 14, 0, 1, 2)
+
         # Excellon Object Color
         self.gerber_color_label = QtWidgets.QLabel('<b>%s</b>' % _('Object Color'))
-        grid2.addWidget(self.gerber_color_label, 12, 0, 1, 2)
+        grid2.addWidget(self.gerber_color_label, 17, 0, 1, 2)
 
         # Plot Line Color
         self.line_color_label = QtWidgets.QLabel('%s:' % _('Outline'))
@@ -274,8 +278,8 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         )
         self.line_color_entry = FCColorEntry()
 
-        grid2.addWidget(self.line_color_label, 13, 0)
-        grid2.addWidget(self.line_color_entry, 13, 1)
+        grid2.addWidget(self.line_color_label, 19, 0)
+        grid2.addWidget(self.line_color_entry, 19, 1)
 
         # Plot Fill Color
         self.fill_color_label = QtWidgets.QLabel('%s:' % _('Fill'))
@@ -286,8 +290,8 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         )
         self.fill_color_entry = FCColorEntry()
 
-        grid2.addWidget(self.fill_color_label, 14, 0)
-        grid2.addWidget(self.fill_color_entry, 14, 1)
+        grid2.addWidget(self.fill_color_label, 22, 0)
+        grid2.addWidget(self.fill_color_entry, 22, 1)
 
         # Plot Fill Transparency Level
         self.excellon_alpha_label = QtWidgets.QLabel('%s:' % _('Alpha'))
@@ -296,22 +300,18 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         )
         self.excellon_alpha_entry = FCSliderWithSpinner(0, 255, 1)
 
-        grid2.addWidget(self.excellon_alpha_label, 15, 0)
-        grid2.addWidget(self.excellon_alpha_entry, 15, 1)
+        grid2.addWidget(self.excellon_alpha_label, 24, 0)
+        grid2.addWidget(self.excellon_alpha_entry, 24, 1)
 
         self.layout.addStretch()
 
         current_platform = platform.architecture()[0]
         if current_platform == '64bit':
-            self.excellon_optimization_label.setDisabled(False)
-            self.excellon_optimization_radio.setDisabled(False)
+            self.excellon_optimization_radio.setOptionsDisabled([_('MetaHeuristic'), _('Basic')], False)
             self.optimization_time_label.setDisabled(False)
             self.optimization_time_entry.setDisabled(False)
-            self.excellon_optimization_radio.activated_custom.connect(self.optimization_selection)
-
         else:
-            self.excellon_optimization_label.setDisabled(True)
-            self.excellon_optimization_radio.setDisabled(True)
+            self.excellon_optimization_radio.setOptionsDisabled([_('MetaHeuristic'), _('Basic')], True)
             self.optimization_time_label.setDisabled(True)
             self.optimization_time_entry.setDisabled(True)
 
@@ -330,6 +330,8 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         # call it once to make sure it is updated at startup
         self.on_update_exc_export(state=self.app.defaults["excellon_update"])
 
+        self.excellon_optimization_radio.activated_custom.connect(self.optimization_selection)
+
     def optimization_selection(self):
         if self.excellon_optimization_radio.get_value() == 'M':
             self.optimization_time_label.setDisabled(False)

+ 0 - 195
appGUI/preferences/excellon/ExcellonOptPrefGroupUI.py

@@ -89,201 +89,6 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
         grid2.addWidget(self.mill_dia_label, 2, 0)
         grid2.addWidget(self.mill_dia_entry, 2, 1)
 
-        # Cut Z
-        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
-        cutzlabel.setToolTip(
-            _("Drill depth (negative)\n"
-              "below the copper surface.")
-        )
-
-        self.cutz_entry = FCDoubleSpinner()
-
-        if machinist_setting == 0:
-            self.cutz_entry.set_range(-9999.9999, 0.0000)
-        else:
-            self.cutz_entry.set_range(-9999.9999, 9999.9999)
-
-        self.cutz_entry.setSingleStep(0.1)
-        self.cutz_entry.set_precision(self.decimals)
-
-        grid2.addWidget(cutzlabel, 3, 0)
-        grid2.addWidget(self.cutz_entry, 3, 1)
-
-        # Multi-Depth
-        self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth"))
-        self.mpass_cb.setToolTip(
-            _(
-                "Use multiple passes to limit\n"
-                "the cut depth in each pass. Will\n"
-                "cut multiple times until Cut Z is\n"
-                "reached."
-            )
-        )
-
-        self.maxdepth_entry = FCDoubleSpinner()
-        self.maxdepth_entry.set_precision(self.decimals)
-        self.maxdepth_entry.set_range(0, 9999.9999)
-        self.maxdepth_entry.setSingleStep(0.1)
-
-        self.maxdepth_entry.setToolTip(_("Depth of each pass (positive)."))
-
-        grid2.addWidget(self.mpass_cb, 4, 0)
-        grid2.addWidget(self.maxdepth_entry, 4, 1)
-
-        # Travel Z
-        travelzlabel = QtWidgets.QLabel('%s:' % _('Travel Z'))
-        travelzlabel.setToolTip(
-            _("Tool height when travelling\n"
-              "across the XY plane.")
-        )
-
-        self.travelz_entry = FCDoubleSpinner()
-        self.travelz_entry.set_precision(self.decimals)
-
-        if machinist_setting == 0:
-            self.travelz_entry.set_range(0.0001, 9999.9999)
-        else:
-            self.travelz_entry.set_range(-9999.9999, 9999.9999)
-
-        grid2.addWidget(travelzlabel, 5, 0)
-        grid2.addWidget(self.travelz_entry, 5, 1)
-
-        # Tool change:
-        self.toolchange_cb = FCCheckBox('%s' % _("Tool change"))
-        self.toolchange_cb.setToolTip(
-            _("Include tool-change sequence\n"
-              "in G-Code (Pause for tool change).")
-        )
-        grid2.addWidget(self.toolchange_cb, 6, 0, 1, 2)
-
-        # Tool Change Z
-        toolchangezlabel = QtWidgets.QLabel('%s:' % _('Toolchange Z'))
-        toolchangezlabel.setToolTip(
-            _("Z-axis position (height) for\n"
-              "tool change.")
-        )
-
-        self.toolchangez_entry = FCDoubleSpinner()
-        self.toolchangez_entry.set_precision(self.decimals)
-
-        if machinist_setting == 0:
-            self.toolchangez_entry.set_range(0.0001, 9999.9999)
-        else:
-            self.toolchangez_entry.set_range(-9999.9999, 9999.9999)
-
-        grid2.addWidget(toolchangezlabel, 7, 0)
-        grid2.addWidget(self.toolchangez_entry, 7, 1)
-
-        # End Move Z
-        endz_label = QtWidgets.QLabel('%s:' % _('End move Z'))
-        endz_label.setToolTip(
-            _("Height of the tool after\n"
-              "the last move at the end of the job.")
-        )
-        self.endz_entry = FCDoubleSpinner()
-        self.endz_entry.set_precision(self.decimals)
-
-        if machinist_setting == 0:
-            self.endz_entry.set_range(0.0000, 9999.9999)
-        else:
-            self.endz_entry.set_range(-9999.9999, 9999.9999)
-
-        grid2.addWidget(endz_label, 8, 0)
-        grid2.addWidget(self.endz_entry, 8, 1)
-
-        # End Move X,Y
-        endmove_xy_label = QtWidgets.QLabel('%s:' % _('End move X,Y'))
-        endmove_xy_label.setToolTip(
-            _("End move X,Y position. In format (x,y).\n"
-              "If no value is entered then there is no move\n"
-              "on X,Y plane at the end of the job.")
-        )
-        self.endxy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
-
-        grid2.addWidget(endmove_xy_label, 9, 0)
-        grid2.addWidget(self.endxy_entry, 9, 1)
-
-        # Feedrate Z
-        frlabel = QtWidgets.QLabel('%s:' % _('Feedrate Z'))
-        frlabel.setToolTip(
-            _("Tool speed while drilling\n"
-              "(in units per minute).\n"
-              "So called 'Plunge' feedrate.\n"
-              "This is for linear move G01.")
-        )
-        self.feedrate_z_entry = FCDoubleSpinner()
-        self.feedrate_z_entry.set_precision(self.decimals)
-        self.feedrate_z_entry.set_range(0, 99999.9999)
-
-        grid2.addWidget(frlabel, 10, 0)
-        grid2.addWidget(self.feedrate_z_entry, 10, 1)
-
-        # Spindle speed
-        spdlabel = QtWidgets.QLabel('%s:' % _('Spindle Speed'))
-        spdlabel.setToolTip(
-            _("Speed of the spindle\n"
-              "in RPM (optional)")
-        )
-
-        self.spindlespeed_entry = FCSpinner()
-        self.spindlespeed_entry.set_range(0, 1000000)
-        self.spindlespeed_entry.set_step(100)
-
-        grid2.addWidget(spdlabel, 11, 0)
-        grid2.addWidget(self.spindlespeed_entry, 11, 1)
-
-        # Dwell
-        self.dwell_cb = FCCheckBox('%s' % _('Enable Dwell'))
-        self.dwell_cb .setToolTip(
-            _("Pause to allow the spindle to reach its\n"
-              "speed before cutting.")
-        )
-
-        grid2.addWidget(self.dwell_cb, 12, 0, 1, 2)
-
-        # Dwell Time
-        dwelltime = QtWidgets.QLabel('%s:' % _('Duration'))
-        dwelltime.setToolTip(_("Number of time units for spindle to dwell."))
-        self.dwelltime_entry = FCDoubleSpinner()
-        self.dwelltime_entry.set_precision(self.decimals)
-        self.dwelltime_entry.set_range(0, 99999.9999)
-
-        grid2.addWidget(dwelltime, 13, 0)
-        grid2.addWidget(self.dwelltime_entry, 13, 1)
-
-        self.ois_dwell_exc = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
-
-        # preprocessor selection
-        pp_excellon_label = QtWidgets.QLabel('%s:' % _("Preprocessor"))
-        pp_excellon_label.setToolTip(
-            _("The preprocessor JSON file that dictates\n"
-              "Gcode output.")
-        )
-
-        self.pp_excellon_name_cb = FCComboBox()
-        self.pp_excellon_name_cb.setFocusPolicy(Qt.StrongFocus)
-
-        grid2.addWidget(pp_excellon_label, 14, 0)
-        grid2.addWidget(self.pp_excellon_name_cb, 14, 1)
-
-        # ### Choose what to use for Gcode creation: Drills, Slots or Both
-        excellon_gcode_type_label = QtWidgets.QLabel('<b>%s</b>' % _('Gcode'))
-        excellon_gcode_type_label.setToolTip(
-            _("Choose what to use for GCode generation:\n"
-              "'Drills', 'Slots' or 'Both'.\n"
-              "When choosing 'Slots' or 'Both', slots will be\n"
-              "converted to drills.")
-        )
-        self.excellon_gcode_type_radio = RadioSet([{'label': 'Drills', 'value': 'drills'},
-                                                   {'label': 'Slots', 'value': 'slots'},
-                                                   {'label': 'Both', 'value': 'both'}])
-        grid2.addWidget(excellon_gcode_type_label, 15, 0)
-        grid2.addWidget(self.excellon_gcode_type_radio, 15, 1)
-
-        # until I decide to implement this feature those remain disabled
-        excellon_gcode_type_label.hide()
-        self.excellon_gcode_type_radio.setVisible(False)
-
         # ### Milling Holes ## ##
         self.mill_hole_label = QtWidgets.QLabel('<b>%s</b>' % _('Mill Holes'))
         self.mill_hole_label.setToolTip(

+ 1 - 1
appGUI/preferences/excellon/ExcellonPreferencesUI.py

@@ -43,11 +43,11 @@ class ExcellonPreferencesUI(QtWidgets.QWidget):
 
         self.vlay = QtWidgets.QVBoxLayout()
         self.vlay.addWidget(self.excellon_opt_group)
+        self.vlay.addWidget(self.excellon_adv_opt_group)
         self.vlay.addWidget(self.excellon_exp_group)
 
         self.layout.addWidget(self.excellon_gen_group)
         self.layout.addLayout(self.vlay)
-        self.layout.addWidget(self.excellon_adv_opt_group)
         self.layout.addWidget(self.excellon_editor_group)
 
         self.layout.addStretch()

+ 7 - 7
appGUI/preferences/general/GeneralAppPrefGroupUI.py

@@ -95,7 +95,7 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(separator_line, 4, 0, 1, 2)
 
         # Application Level for FlatCAM
-        self.app_level_label = QtWidgets.QLabel('<span style="color:red;"><b>%s:</b></span>' % _('APP. LEVEL'))
+        self.app_level_label = QtWidgets.QLabel('<span style="color:red;"><b>%s:</b></span>' % _('APPLICATION LEVEL'))
         self.app_level_label.setToolTip(_("Choose the default level of usage for FlatCAM.\n"
                                           "BASIC level -> reduced functionality, best for beginner's.\n"
                                           "ADVANCED level -> full functionality.\n\n"
@@ -104,8 +104,8 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
         self.app_level_radio = RadioSet([{'label': _('Basic'), 'value': 'b'},
                                          {'label': _('Advanced'), 'value': 'a'}])
 
-        grid0.addWidget(self.app_level_label, 5, 0)
-        grid0.addWidget(self.app_level_radio, 5, 1)
+        grid0.addWidget(self.app_level_label, 5, 0, 1, 2)
+        grid0.addWidget(self.app_level_radio, 6, 0, 1, 2)
 
         # Portability for FlatCAM
         self.portability_cb = FCCheckBox('%s' % _('Portable app'))
@@ -114,20 +114,20 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
                                          "which means that the preferences files will be saved\n"
                                          "in the application folder, in the lib\\config subfolder."))
 
-        grid0.addWidget(self.portability_cb, 6, 0, 1, 2)
+        grid0.addWidget(self.portability_cb, 7, 0, 1, 2)
 
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 7, 0, 1, 2)
+        grid0.addWidget(separator_line, 8, 0, 1, 2)
 
         # Languages for FlatCAM
         self.languagelabel = QtWidgets.QLabel('<b>%s</b>' % _('Languages'))
         self.languagelabel.setToolTip(_("Set the language used throughout FlatCAM."))
         self.language_cb = FCComboBox()
 
-        grid0.addWidget(self.languagelabel, 8, 0, 1, 2)
-        grid0.addWidget(self.language_cb, 9, 0, 1, 2)
+        grid0.addWidget(self.languagelabel, 9, 0, 1, 2)
+        grid0.addWidget(self.language_cb, 10, 0, 1, 2)
 
         self.language_apply_btn = FCButton(_("Apply Language"))
         self.language_apply_btn.setToolTip(_("Set the language used throughout FlatCAM.\n"

+ 16 - 2
appGUI/preferences/general/GeneralAppSettingsGroupUI.py

@@ -98,12 +98,26 @@ class GeneralAppSettingsGroupUI(OptionsGroupUI2):
         self.workspace_type_label = self.option_dict()["global_workspaceT"].label_widget
         self.workspace_orientation_field = self.option_dict()["global_workspace_orientation"].get_field()
         self.workspace_orientation_label = self.option_dict()["global_workspace_orientation"].label_widget
-        self.wks = OptionalInputSection(self.workspace_enabled_field, [self.workspace_type_label, self.workspace_type_field, self.workspace_orientation_label, self.workspace_orientation_field])
+        self.wks = OptionalInputSection(
+            self.workspace_enabled_field,
+            [
+                self.workspace_type_label,
+                self.workspace_type_field,
+                self.workspace_orientation_label,
+                self.workspace_orientation_field
+            ]
+        )
 
         self.mouse_cursor_color_enabled_field = self.option_dict()["global_cursor_color_enabled"].get_field()
         self.mouse_cursor_color_field = self.option_dict()["global_cursor_color"].get_field()
         self.mouse_cursor_color_label = self.option_dict()["global_cursor_color"].label_widget
-        self.mois = OptionalInputSection(self.mouse_cursor_color_enabled_field, [self.mouse_cursor_color_label, self.mouse_cursor_color_field])
+        self.mois = OptionalInputSection(
+            self.mouse_cursor_color_enabled_field,
+            [
+                self.mouse_cursor_color_label,
+                self.mouse_cursor_color_field
+            ]
+        )
         self.mouse_cursor_color_enabled_field.stateChanged.connect(self.on_mouse_cursor_color_enable)
         self.mouse_cursor_color_field.entry.editingFinished.connect(self.on_mouse_cursor_entry)
 

+ 9 - 5
appGUI/preferences/general/GeneralGUIPrefGroupUI.py

@@ -108,7 +108,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.style_combo, 5, 1)
 
         # Enable High DPI Support
-        self.hdpi_cb = FCCheckBox('%s' % _('Activate HDPI Support'))
+        self.hdpi_cb = FCCheckBox('%s' % _('HDPI Support'))
         self.hdpi_cb.setToolTip(
             _("Enable High DPI support for the application.\n"
               "It will be applied at the next app start.")
@@ -124,7 +124,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.hdpi_cb, 6, 0, 1, 3)
 
         # Enable Hover box
-        self.hover_cb = FCCheckBox('%s' % _('Display Hover Shape'))
+        self.hover_cb = FCCheckBox('%s' % _('Hover Shape'))
         self.hover_cb.setToolTip(
             _("Enable display of a hover shape for the application objects.\n"
               "It is displayed whenever the mouse cursor is hovering\n"
@@ -133,7 +133,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.hover_cb, 8, 0, 1, 3)
 
         # Enable Selection box
-        self.selection_cb = FCCheckBox('%s' % _('Display Selection Shape'))
+        self.selection_cb = FCCheckBox('%s' % _('Selection Shape'))
         self.selection_cb.setToolTip(
             _("Enable the display of a selection shape for the application objects.\n"
               "It is displayed whenever the mouse selects an object\n"
@@ -531,8 +531,12 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # add all the actions to the toolbars
         self.app.ui.populate_toolbars()
 
-        # reconnect all the signals to the toolbar actions
-        self.app.connect_toolbar_signals()
+        try:
+            # reconnect all the signals to the toolbar actions
+            self.app.connect_toolbar_signals(ui=self.app.ui)
+        except Exception as e:
+            self.app.log.debug(
+                "appGUI.preferences.general.GeneralGUIPrefGroupUI.on_layout() - connect toolbar signals -> %s" % str(e))
 
         self.app.ui.grid_snap_btn.setChecked(True)
 

+ 2 - 2
appGUI/preferences/geometry/GeometryEditorPrefGroupUI.py

@@ -27,10 +27,10 @@ class GeometryEditorPrefGroupUI(OptionsGroupUI):
         self.setTitle(str(_("Geometry Editor")))
         self.decimals = decimals
 
-        # Advanced Geometry Parameters
+        # Editor Parameters
         self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
         self.param_label.setToolTip(
-            _("A list of Geometry Editor parameters.")
+            _("A list of Editor parameters.")
         )
         self.layout.addWidget(self.param_label)
 

+ 89 - 5
appGUI/preferences/geometry/GeometryGenPrefGroupUI.py

@@ -1,9 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from appGUI.GUIElements import FCCheckBox, FCSpinner, FCEntry, FCColorEntry
+from appGUI.GUIElements import FCCheckBox, FCSpinner, FCEntry, FCColorEntry, RadioSet
 from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
+import platform
+
 import gettext
 import appTranslation as fcTranslate
 import builtins
@@ -86,9 +88,71 @@ class GeometryGenPrefGroupUI(OptionsGroupUI):
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         grid0.addWidget(separator_line, 9, 0, 1, 2)
 
+        self.opt_label = QtWidgets.QLabel("<b>%s:</b>" % _("Path Optimization"))
+        grid0.addWidget(self.opt_label, 10, 0, 1, 2)
+
+        self.opt_algorithm_label = QtWidgets.QLabel(_('Algorithm:'))
+        self.opt_algorithm_label.setToolTip(
+            _("This sets the path optimization algorithm.\n"
+              "- Rtre -> Rtree algorithm\n"
+              "- MetaHeuristic -> Google OR-Tools algorithm with\n"
+              "MetaHeuristic Guided Local Path is used. Default search time is 3sec.\n"
+              "- Basic -> Using Google OR-Tools Basic algorithm\n"
+              "- TSA -> Using Travelling Salesman algorithm\n"
+              "\n"
+              "Some options are disabled when FlatCAM works in 32bit mode.")
+        )
+
+        self.opt_algorithm_radio = RadioSet(
+            [
+                {'label': _('Rtree'), 'value': 'R'},
+                {'label': _('MetaHeuristic'), 'value': 'M'},
+                {'label': _('Basic'), 'value': 'B'},
+                {'label': _('TSA'), 'value': 'T'}
+            ], orientation='vertical', stretch=False)
+
+        grid0.addWidget(self.opt_algorithm_label, 12, 0)
+        grid0.addWidget(self.opt_algorithm_radio, 12, 1)
+
+        self.optimization_time_label = QtWidgets.QLabel('%s:' % _('Duration'))
+        self.optimization_time_label.setToolTip(
+            _("When OR-Tools Metaheuristic (MH) is enabled there is a\n"
+              "maximum threshold for how much time is spent doing the\n"
+              "path optimization. This max duration is set here.\n"
+              "In seconds.")
+
+        )
+
+        self.optimization_time_entry = FCSpinner()
+        self.optimization_time_entry.set_range(0, 999)
+
+        grid0.addWidget(self.optimization_time_label, 14, 0)
+        grid0.addWidget(self.optimization_time_entry, 14, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 16, 0, 1, 2)
+
+        # Fuse Tools
+        self.join_geo_label = QtWidgets.QLabel('<b>%s</b>:' % _('Join Option'))
+        grid0.addWidget(self.join_geo_label, 18, 0, 1, 2)
+
+        self.fuse_tools_cb = FCCheckBox(_("Fuse Tools"))
+        self.fuse_tools_cb.setToolTip(
+            _("When checked the joined (merged) object tools\n"
+              "will be merged also but only if they share some of their attributes.")
+        )
+        grid0.addWidget(self.fuse_tools_cb, 20, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 22, 0, 1, 2)
+
         # Geometry Object Color
-        self.gerber_color_label = QtWidgets.QLabel('<b>%s</b>' % _('Object Color'))
-        grid0.addWidget(self.gerber_color_label, 10, 0, 1, 2)
+        self.gerber_color_label = QtWidgets.QLabel('<b>%s</b>:' % _('Object Color'))
+        grid0.addWidget(self.gerber_color_label, 24, 0, 1, 2)
 
         # Plot Line Color
         self.line_color_label = QtWidgets.QLabel('%s:' % _('Outline'))
@@ -97,13 +161,33 @@ class GeometryGenPrefGroupUI(OptionsGroupUI):
         )
         self.line_color_entry = FCColorEntry()
 
-        grid0.addWidget(self.line_color_label, 11, 0)
-        grid0.addWidget(self.line_color_entry, 11, 1)
+        grid0.addWidget(self.line_color_label, 26, 0)
+        grid0.addWidget(self.line_color_entry, 26, 1)
 
         self.layout.addStretch()
 
+        current_platform = platform.architecture()[0]
+        if current_platform == '64bit':
+            self.opt_algorithm_radio.setOptionsDisabled([_('MetaHeuristic'), _('Basic')], False)
+            self.optimization_time_label.setDisabled(False)
+            self.optimization_time_entry.setDisabled(False)
+        else:
+            self.opt_algorithm_radio.setOptionsDisabled([_('MetaHeuristic'), _('Basic')], True)
+            self.optimization_time_label.setDisabled(True)
+            self.optimization_time_entry.setDisabled(True)
+
+        self.opt_algorithm_radio.activated_custom.connect(self.optimization_selection)
+
         # Setting plot colors signals
         self.line_color_entry.editingFinished.connect(self.on_line_color_entry)
 
     def on_line_color_entry(self):
         self.app.defaults['geometry_plot_line'] = self.line_color_entry.get_value()[:7] + 'FF'
+
+    def optimization_selection(self, val):
+        if val == 'M':
+            self.optimization_time_label.setDisabled(False)
+            self.optimization_time_entry.setDisabled(False)
+        else:
+            self.optimization_time_label.setDisabled(True)
+            self.optimization_time_entry.setDisabled(True)

+ 1 - 4
appGUI/preferences/gerber/GerberAdvOptPrefGroupUI.py

@@ -51,10 +51,7 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         # Aperture Table Visibility CB
         self.aperture_table_visibility_cb = FCCheckBox(label=_('Table Show/Hide'))
         self.aperture_table_visibility_cb.setToolTip(
-            _("Toggle the display of the Gerber Apertures Table.\n"
-              "Also, on hide, it will delete all mark shapes\n"
-              "that are drawn on canvas.")
-
+            _("Toggle the display of the Tools Table.")
         )
         grid0.addWidget(self.aperture_table_visibility_cb, 1, 0, 1, 2)
 

+ 31 - 9
appGUI/preferences/gerber/GerberGenPrefGroupUI.py

@@ -1,7 +1,7 @@
 from PyQt5 import QtWidgets, QtCore, QtGui
 from PyQt5.QtCore import QSettings
 
-from appGUI.GUIElements import FCCheckBox, FCSpinner, RadioSet, FCEntry, FCSliderWithSpinner, FCColorEntry
+from appGUI.GUIElements import FCCheckBox, FCSpinner, RadioSet, FCButton, FCSliderWithSpinner, FCColorEntry
 from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
@@ -140,14 +140,30 @@ class GerberGenPrefGroupUI(OptionsGroupUI):
         )
         grid0.addWidget(self.gerber_extra_buffering, 8, 0, 1, 3)
 
+        # Store colors
+        self.store_colors_cb = FCCheckBox(label='%s' % _('Store colors'))
+        self.store_colors_cb.setToolTip(
+            _("It will store the set colors for Gerber objects.\n"
+              "Those will be used each time the application is started.")
+        )
+        grid0.addWidget(self.store_colors_cb, 11, 0)
+
+        # Clear stored colors
+        self.clear_colors_button = FCButton('%s' % _('Clear Colors'))
+        self.clear_colors_button.setIcon(QtGui.QIcon(self.app.resource_location + '/trash32.png'))
+        self.clear_colors_button.setToolTip(
+            _("Reset the colors associated with Gerber objects.")
+        )
+        grid0.addWidget(self.clear_colors_button, 11, 1, 1, 2)
+
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 9, 0, 1, 3)
+        grid0.addWidget(separator_line, 13, 0, 1, 3)
 
         # Gerber Object Color
         self.gerber_color_label = QtWidgets.QLabel('<b>%s</b>' % _('Object Color'))
-        grid0.addWidget(self.gerber_color_label, 10, 0, 1, 3)
+        grid0.addWidget(self.gerber_color_label, 15, 0, 1, 3)
 
         # Plot Line Color
         self.line_color_label = QtWidgets.QLabel('%s:' % _('Outline'))
@@ -156,8 +172,8 @@ class GerberGenPrefGroupUI(OptionsGroupUI):
         )
         self.line_color_entry = FCColorEntry()
 
-        grid0.addWidget(self.line_color_label, 11, 0)
-        grid0.addWidget(self.line_color_entry, 11, 1, 1, 2)
+        grid0.addWidget(self.line_color_label, 17, 0)
+        grid0.addWidget(self.line_color_entry, 17, 1, 1, 2)
 
         # Plot Fill Color
         self.fill_color_label = QtWidgets.QLabel('%s:' % _('Fill'))
@@ -168,8 +184,8 @@ class GerberGenPrefGroupUI(OptionsGroupUI):
         )
         self.fill_color_entry = FCColorEntry()
 
-        grid0.addWidget(self.fill_color_label, 12, 0)
-        grid0.addWidget(self.fill_color_entry, 12, 1, 1, 2)
+        grid0.addWidget(self.fill_color_label, 20, 0)
+        grid0.addWidget(self.fill_color_entry, 20, 1, 1, 2)
 
         # Plot Fill Transparency Level
         self.gerber_alpha_label = QtWidgets.QLabel('%s:' % _('Alpha'))
@@ -178,8 +194,8 @@ class GerberGenPrefGroupUI(OptionsGroupUI):
         )
         self.gerber_alpha_entry = FCSliderWithSpinner(0, 255, 1)
 
-        grid0.addWidget(self.gerber_alpha_label, 13, 0)
-        grid0.addWidget(self.gerber_alpha_entry, 13, 1, 1, 2)
+        grid0.addWidget(self.gerber_alpha_label, 22, 0)
+        grid0.addWidget(self.gerber_alpha_entry, 22, 1, 1, 2)
 
         self.layout.addStretch()
 
@@ -189,6 +205,8 @@ class GerberGenPrefGroupUI(OptionsGroupUI):
 
         self.gerber_alpha_entry.valueChanged.connect(self.on_gerber_alpha_changed)     # alpha
 
+        self.clear_colors_button.clicked.connect(self.on_colors_clear_clicked)
+
     # Setting plot colors handlers
     def on_fill_color_changed(self):
         self.app.defaults['gerber_plot_fill'] = self.fill_color_entry.get_value()[:7] + \
@@ -205,3 +223,7 @@ class GerberGenPrefGroupUI(OptionsGroupUI):
     def on_line_color_changed(self):
         self.app.defaults['gerber_plot_line'] = self.line_color_entry.get_value()[:7] + \
                                                 self.app.defaults['gerber_plot_line'][7:9]
+
+    def on_colors_clear_clicked(self):
+        self.app.defaults['gerber_color_list'].clear()
+        self.app.inform.emit('[WARNING_NOTCL] %s' % _("Stored colors for Gerber objects are deleted."))

+ 1 - 1
appGUI/preferences/gerber/GerberPreferencesUI.py

@@ -43,12 +43,12 @@ class GerberPreferencesUI(QtWidgets.QWidget):
 
         self.vlay = QtWidgets.QVBoxLayout()
         self.vlay.addWidget(self.gerber_opt_group)
+        self.vlay.addWidget(self.gerber_adv_opt_group)
         self.vlay.addWidget(self.gerber_exp_group)
         self.vlay.addStretch()
 
         self.layout.addWidget(self.gerber_gen_group)
         self.layout.addLayout(self.vlay)
-        self.layout.addWidget(self.gerber_adv_opt_group)
         self.layout.addWidget(self.gerber_editor_group)
 
         self.layout.addStretch()

+ 18 - 7
appGUI/preferences/tools/Tools2sidedPrefGroupUI.py

@@ -71,19 +71,30 @@ class Tools2sidedPrefGroupUI(OptionsGroupUI):
             _("Mirror vertically (X) or horizontally (Y).")
         )
 
-        self.empty_lb1 = QtWidgets.QLabel("")
-        grid0.addWidget(self.empty_lb1, 2, 0)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 2, 0, 1, 2)
+
         grid0.addWidget(self.mirax_label, 3, 0)
         grid0.addWidget(self.mirror_axis_radio, 3, 1)
 
         # ## Axis Location
-        self.axis_location_radio = RadioSet([{'label': _('Point'), 'value': 'point'},
-                                             {'label': _('Box'), 'value': 'box'}])
+        self.axis_location_radio = RadioSet(
+            [
+                {'label': _('Point'), 'value': 'point'},
+                {'label': _('Box'), 'value': 'box'},
+                {'label': _('Hole Snap'), 'value': 'hole'},
+            ]
+        )
         self.axloc_label = QtWidgets.QLabel('%s:' % _("Axis Ref"))
         self.axloc_label.setToolTip(
-            _("The axis should pass through a <b>point</b> or cut\n "
-              "a specified <b>box</b> (in a FlatCAM object) through \n"
-              "the center.")
+            _("The coordinates used as reference for the mirror operation.\n"
+              "Can be:\n"
+              "- Point -> a set of coordinates (x,y) around which the object is mirrored\n"
+              "- Box -> a set of coordinates (x, y) obtained from the center of the\n"
+              "bounding box of another object selected below\n"
+              "- Hole Snap-> a point defined by the center of a drill hone in a Excellon object")
         )
 
         grid0.addWidget(self.axloc_label, 4, 0)

+ 81 - 13
appGUI/preferences/tools/ToolsCutoutPrefGroupUI.py

@@ -1,7 +1,7 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCComboBox
+from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCComboBox, FCLabel
 from appGUI.preferences import machinist_setting
 from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
@@ -29,7 +29,7 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI):
         self.decimals = decimals
 
         # ## Board cutout
-        self.board_cutout_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.board_cutout_label = FCLabel("<b>%s:</b>" % _("Parameters"))
         self.board_cutout_label.setToolTip(
             _("Create toolpaths to cut around\n"
               "the PCB and separate it from\n"
@@ -40,7 +40,7 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI):
         grid0 = QtWidgets.QGridLayout()
         self.layout.addLayout(grid0)
 
-        tdclabel = QtWidgets.QLabel('%s:' % _('Tool Diameter'))
+        tdclabel = FCLabel('%s:' % _('Tool Diameter'))
         tdclabel.setToolTip(
             _("Diameter of the tool used to cutout\n"
               "the PCB shape out of the surrounding material.")
@@ -55,7 +55,7 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.cutout_tooldia_entry, 0, 1)
 
         # Cut Z
-        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
+        cutzlabel = FCLabel('%s:' % _('Cut Z'))
         cutzlabel.setToolTip(
             _(
                 "Cutting depth (negative)\n"
@@ -97,7 +97,7 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.maxdepth_entry, 2, 1)
 
         # Object kind
-        kindlabel = QtWidgets.QLabel('%s:' % _('Object kind'))
+        kindlabel = FCLabel('%s:' % _('Object kind'))
         kindlabel.setToolTip(
             _("Choice of what kind the object we want to cutout is.<BR>"
               "- <B>Single</B>: contain a single PCB Gerber outline object.<BR>"
@@ -112,7 +112,7 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(kindlabel, 3, 0)
         grid0.addWidget(self.obj_kind_combo, 3, 1)
 
-        marginlabel = QtWidgets.QLabel('%s:' % _('Margin'))
+        marginlabel = FCLabel('%s:' % _('Margin'))
         marginlabel.setToolTip(
             _("Margin over bounds. A positive value here\n"
               "will make the cutout of the PCB further from\n"
@@ -126,8 +126,9 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI):
 
         grid0.addWidget(marginlabel, 4, 0)
         grid0.addWidget(self.cutout_margin_entry, 4, 1)
-
-        gaplabel = QtWidgets.QLabel('%s:' % _('Gap size'))
+        
+        # Gap Size
+        gaplabel = FCLabel('%s:' % _('Gap size'))
         gaplabel.setToolTip(
             _("The size of the bridge gaps in the cutout\n"
               "used to keep the board connected to\n"
@@ -142,8 +143,70 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI):
 
         grid0.addWidget(gaplabel, 5, 0)
         grid0.addWidget(self.cutout_gap_entry, 5, 1)
+        
+        # Gap Type
+        self.gaptype_label = FCLabel('%s:' % _("Gap type"))
+        self.gaptype_label.setToolTip(
+            _("The type of gap:\n"
+              "- Bridge -> the cutout will be interrupted by bridges\n"
+              "- Thin -> same as 'bridge' but it will be thinner by partially milling the gap\n"
+              "- M-Bites -> 'Mouse Bites' - same as 'bridge' but covered with drill holes")
+        )
+
+        self.gaptype_radio = RadioSet(
+            [
+                {'label': _('Bridge'),      'value': 'b'},
+                {'label': _('Thin'),        'value': 'bt'},
+                {'label': "M-Bites",        'value': 'mb'}
+            ],
+            stretch=True
+        )
 
-        gaps_label = QtWidgets.QLabel('%s:' % _('Gaps'))
+        grid0.addWidget(self.gaptype_label, 7, 0)
+        grid0.addWidget(self.gaptype_radio, 7, 1)
+
+        # Thin gaps Depth
+        self.thin_depth_label = FCLabel('%s:' % _("Depth"))
+        self.thin_depth_label.setToolTip(
+            _("The depth until the milling is done\n"
+              "in order to thin the gaps.")
+        )
+        self.thin_depth_entry = FCDoubleSpinner()
+        self.thin_depth_entry.set_precision(self.decimals)
+        if machinist_setting == 0:
+            self.thin_depth_entry.setRange(-9999.9999, -0.00001)
+        else:
+            self.thin_depth_entry.setRange(-9999.9999, 9999.9999)
+        self.thin_depth_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.thin_depth_label, 9, 0)
+        grid0.addWidget(self.thin_depth_entry, 9, 1)
+
+        # Mouse Bites Tool Diameter
+        self.mb_dia_label = FCLabel('%s:' % _("Tool Diameter"))
+        self.mb_dia_label.setToolTip(
+            _("The drill hole diameter when doing mpuse bites.")
+        )
+        self.mb_dia_entry = FCDoubleSpinner()
+        self.mb_dia_entry.set_precision(self.decimals)
+        self.mb_dia_entry.setRange(0, 100.0000)
+
+        grid0.addWidget(self.mb_dia_label, 11, 0)
+        grid0.addWidget(self.mb_dia_entry, 11, 1)
+
+        # Mouse Bites Holes Spacing
+        self.mb_spacing_label = FCLabel('%s:' % _("Spacing"))
+        self.mb_spacing_label.setToolTip(
+            _("The spacing between drill holes when doing mouse bites.")
+        )
+        self.mb_spacing_entry = FCDoubleSpinner()
+        self.mb_spacing_entry.set_precision(self.decimals)
+        self.mb_spacing_entry.setRange(0, 100.0000)
+
+        grid0.addWidget(self.mb_spacing_label, 13, 0)
+        grid0.addWidget(self.mb_spacing_entry, 13, 1)
+        
+        gaps_label = FCLabel('%s:' % _('Gaps'))
         gaps_label.setToolTip(
             _("Number of gaps used for the cutout.\n"
               "There can be maximum 8 bridges/gaps.\n"
@@ -158,13 +221,13 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI):
         )
 
         self.gaps_combo = FCComboBox()
-        grid0.addWidget(gaps_label, 6, 0)
-        grid0.addWidget(self.gaps_combo, 6, 1)
+        grid0.addWidget(gaps_label, 15, 0)
+        grid0.addWidget(self.gaps_combo, 15, 1)
 
         gaps_items = ['None', 'LR', 'TB', '4', '2LR', '2TB', '8']
         for it in gaps_items:
             self.gaps_combo.addItem(it)
-            self.gaps_combo.setStyleSheet('background-color: rgb(255,255,255)')
+            # self.gaps_combo.setStyleSheet('background-color: rgb(255,255,255)')
 
         # Surrounding convex box shape
         self.convex_box = FCCheckBox('%s' % _("Convex Shape"))
@@ -172,6 +235,11 @@ class ToolsCutoutPrefGroupUI(OptionsGroupUI):
             _("Create a convex shape surrounding the entire PCB.\n"
               "Used only if the source object type is Gerber.")
         )
-        grid0.addWidget(self.convex_box, 7, 0, 1, 2)
+        grid0.addWidget(self.convex_box, 17, 0, 1, 2)
+
+        self.big_cursor_cb = FCCheckBox('%s' % _("Big cursor"))
+        self.big_cursor_cb.setToolTip(
+            _("Use a big cursor when adding manual gaps."))
+        grid0.addWidget(self.big_cursor_cb, 19, 0, 1, 2)
 
         self.layout.addStretch()

+ 392 - 0
appGUI/preferences/tools/ToolsDrillPrefGroupUI.py

@@ -0,0 +1,392 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings, Qt
+
+from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, FCSpinner, NumericalEvalTupleEntry, \
+    OptionalInputSection, NumericalEvalEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsDrillPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        super(ToolsDrillPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Drilling Tool Options")))
+        self.decimals = decimals
+
+        # ## Clear non-copper regions
+        self.drill_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.drill_label.setToolTip(
+            _("Create CNCJob with toolpaths for drilling or milling holes.")
+        )
+        self.layout.addWidget(self.drill_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        # Tool order Radio Button
+        self.order_label = QtWidgets.QLabel('%s:' % _('Tool order'))
+        self.order_label.setToolTip(_("This set the way that the tools in the tools table are used.\n"
+                                      "'No' --> means that the used order is the one in the tool table\n"
+                                      "'Forward' --> means that the tools will be ordered from small to big\n"
+                                      "'Reverse' --> means that the tools will ordered from big to small\n\n"
+                                      "WARNING: using rest machining will automatically set the order\n"
+                                      "in reverse and disable this control."))
+
+        self.order_radio = RadioSet([{'label': _('No'), 'value': 'no'},
+                                     {'label': _('Forward'), 'value': 'fwd'},
+                                     {'label': _('Reverse'), 'value': 'rev'}])
+
+        grid0.addWidget(self.order_label, 1, 0)
+        grid0.addWidget(self.order_radio, 1, 1, 1, 2)
+
+        # Cut Z
+        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
+        cutzlabel.setToolTip(
+            _("Drill depth (negative)\n"
+              "below the copper surface.")
+        )
+
+        self.cutz_entry = FCDoubleSpinner()
+
+        if machinist_setting == 0:
+            self.cutz_entry.set_range(-9999.9999, 0.0000)
+        else:
+            self.cutz_entry.set_range(-9999.9999, 9999.9999)
+
+        self.cutz_entry.setSingleStep(0.1)
+        self.cutz_entry.set_precision(self.decimals)
+
+        grid0.addWidget(cutzlabel, 3, 0)
+        grid0.addWidget(self.cutz_entry, 3, 1, 1, 2)
+
+        # Multi-Depth
+        self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth"))
+        self.mpass_cb.setToolTip(
+            _(
+                "Use multiple passes to limit\n"
+                "the cut depth in each pass. Will\n"
+                "cut multiple times until Cut Z is\n"
+                "reached."
+            )
+        )
+
+        self.maxdepth_entry = FCDoubleSpinner()
+        self.maxdepth_entry.set_precision(self.decimals)
+        self.maxdepth_entry.set_range(0, 9999.9999)
+        self.maxdepth_entry.setSingleStep(0.1)
+
+        self.maxdepth_entry.setToolTip(_("Depth of each pass (positive)."))
+
+        grid0.addWidget(self.mpass_cb, 4, 0)
+        grid0.addWidget(self.maxdepth_entry, 4, 1, 1, 2)
+
+        # Travel Z
+        travelzlabel = QtWidgets.QLabel('%s:' % _('Travel Z'))
+        travelzlabel.setToolTip(
+            _("Tool height when travelling\n"
+              "across the XY plane.")
+        )
+
+        self.travelz_entry = FCDoubleSpinner()
+        self.travelz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.travelz_entry.set_range(0.0001, 9999.9999)
+        else:
+            self.travelz_entry.set_range(-9999.9999, 9999.9999)
+
+        grid0.addWidget(travelzlabel, 5, 0)
+        grid0.addWidget(self.travelz_entry, 5, 1, 1, 2)
+
+        # Tool change:
+        self.toolchange_cb = FCCheckBox('%s' % _("Tool change"))
+        self.toolchange_cb.setToolTip(
+            _("Include tool-change sequence\n"
+              "in G-Code (Pause for tool change).")
+        )
+        grid0.addWidget(self.toolchange_cb, 6, 0, 1, 3)
+
+        # Tool Change Z
+        toolchangezlabel = QtWidgets.QLabel('%s:' % _('Toolchange Z'))
+        toolchangezlabel.setToolTip(
+            _("Z-axis position (height) for\n"
+              "tool change.")
+        )
+
+        self.toolchangez_entry = FCDoubleSpinner()
+        self.toolchangez_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.toolchangez_entry.set_range(0.0001, 9999.9999)
+        else:
+            self.toolchangez_entry.set_range(-9999.9999, 9999.9999)
+
+        grid0.addWidget(toolchangezlabel, 7, 0)
+        grid0.addWidget(self.toolchangez_entry, 7, 1, 1, 2)
+
+        # End Move Z
+        endz_label = QtWidgets.QLabel('%s:' % _('End move Z'))
+        endz_label.setToolTip(
+            _("Height of the tool after\n"
+              "the last move at the end of the job.")
+        )
+        self.endz_entry = FCDoubleSpinner()
+        self.endz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.endz_entry.set_range(0.0000, 9999.9999)
+        else:
+            self.endz_entry.set_range(-9999.9999, 9999.9999)
+
+        grid0.addWidget(endz_label, 8, 0)
+        grid0.addWidget(self.endz_entry, 8, 1, 1, 2)
+
+        # End Move X,Y
+        endmove_xy_label = QtWidgets.QLabel('%s:' % _('End move X,Y'))
+        endmove_xy_label.setToolTip(
+            _("End move X,Y position. In format (x,y).\n"
+              "If no value is entered then there is no move\n"
+              "on X,Y plane at the end of the job.")
+        )
+        self.endxy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+
+        grid0.addWidget(endmove_xy_label, 9, 0)
+        grid0.addWidget(self.endxy_entry, 9, 1, 1, 2)
+
+        # Feedrate Z
+        frlabel = QtWidgets.QLabel('%s:' % _('Feedrate Z'))
+        frlabel.setToolTip(
+            _("Tool speed while drilling\n"
+              "(in units per minute).\n"
+              "So called 'Plunge' feedrate.\n"
+              "This is for linear move G01.")
+        )
+        self.feedrate_z_entry = FCDoubleSpinner()
+        self.feedrate_z_entry.set_precision(self.decimals)
+        self.feedrate_z_entry.set_range(0, 99999.9999)
+
+        grid0.addWidget(frlabel, 10, 0)
+        grid0.addWidget(self.feedrate_z_entry, 10, 1, 1, 2)
+
+        # Spindle speed
+        spdlabel = QtWidgets.QLabel('%s:' % _('Spindle Speed'))
+        spdlabel.setToolTip(
+            _("Speed of the spindle\n"
+              "in RPM (optional)")
+        )
+
+        self.spindlespeed_entry = FCSpinner()
+        self.spindlespeed_entry.set_range(0, 1000000)
+        self.spindlespeed_entry.set_step(100)
+
+        grid0.addWidget(spdlabel, 11, 0)
+        grid0.addWidget(self.spindlespeed_entry, 11, 1, 1, 2)
+
+        # Dwell
+        self.dwell_cb = FCCheckBox('%s' % _('Enable Dwell'))
+        self.dwell_cb.setToolTip(
+            _("Pause to allow the spindle to reach its\n"
+              "speed before cutting.")
+        )
+
+        grid0.addWidget(self.dwell_cb, 12, 0, 1, 3)
+
+        # Dwell Time
+        dwelltime = QtWidgets.QLabel('%s:' % _('Duration'))
+        dwelltime.setToolTip(_("Number of time units for spindle to dwell."))
+        self.dwelltime_entry = FCDoubleSpinner()
+        self.dwelltime_entry.set_precision(self.decimals)
+        self.dwelltime_entry.set_range(0, 99999.9999)
+
+        grid0.addWidget(dwelltime, 13, 0)
+        grid0.addWidget(self.dwelltime_entry, 13, 1, 1, 2)
+
+        self.ois_dwell_exc = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
+
+        # preprocessor selection
+        pp_excellon_label = QtWidgets.QLabel('%s:' % _("Preprocessor"))
+        pp_excellon_label.setToolTip(
+            _("The preprocessor JSON file that dictates\n"
+              "Gcode output.")
+        )
+
+        self.pp_excellon_name_cb = FCComboBox()
+        self.pp_excellon_name_cb.setFocusPolicy(Qt.StrongFocus)
+
+        grid0.addWidget(pp_excellon_label, 14, 0)
+        grid0.addWidget(self.pp_excellon_name_cb, 14, 1, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 16, 0, 1, 3)
+
+        # DRILL SLOTS LABEL
+        self.dslots_label = QtWidgets.QLabel('<b>%s:</b>' % _('Drilling Slots'))
+        grid0.addWidget(self.dslots_label, 18, 0, 1, 3)
+
+        # Drill slots
+        self.drill_slots_cb = FCCheckBox('%s' % _('Drill slots'))
+        self.drill_slots_cb.setToolTip(
+            _("If the selected tool has slots then they will be drilled.")
+        )
+        grid0.addWidget(self.drill_slots_cb, 20, 0, 1, 3)
+
+        # Drill Overlap
+        self.drill_overlap_label = QtWidgets.QLabel('%s:' % _('Overlap'))
+        self.drill_overlap_label.setToolTip(
+            _("How much (percentage) of the tool diameter to overlap previous drill hole.")
+        )
+
+        self.drill_overlap_entry = FCDoubleSpinner()
+        self.drill_overlap_entry.set_precision(self.decimals)
+        self.drill_overlap_entry.set_range(0.0, 9999.9999)
+        self.drill_overlap_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.drill_overlap_label, 22, 0)
+        grid0.addWidget(self.drill_overlap_entry, 22, 1, 1, 2)
+
+        # Last drill in slot
+        self.last_drill_cb = FCCheckBox('%s' % _('Last drill'))
+        self.last_drill_cb.setToolTip(
+            _("If the slot length is not completely covered by drill holes,\n"
+              "add a drill hole on the slot end point.")
+        )
+        grid0.addWidget(self.last_drill_cb, 24, 0, 1, 3)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 26, 0, 1, 3)
+
+        self.exc_label = QtWidgets.QLabel('<b>%s:</b>' % _('Advanced Options'))
+        self.exc_label.setToolTip(
+            _("A list of advanced parameters.")
+        )
+        grid0.addWidget(self.exc_label, 28, 0, 1, 3)
+
+        # Offset Z
+        offsetlabel = QtWidgets.QLabel('%s:' % _('Offset Z'))
+        offsetlabel.setToolTip(
+            _("Some drill bits (the larger ones) need to drill deeper\n"
+              "to create the desired exit hole diameter due of the tip shape.\n"
+              "The value here can compensate the Cut Z parameter."))
+        self.offset_entry = FCDoubleSpinner()
+        self.offset_entry.set_precision(self.decimals)
+        self.offset_entry.set_range(-999.9999, 999.9999)
+
+        grid0.addWidget(offsetlabel, 29, 0)
+        grid0.addWidget(self.offset_entry, 29, 1, 1, 2)
+
+        # ToolChange X,Y
+        toolchange_xy_label = QtWidgets.QLabel('%s:' % _('Toolchange X,Y'))
+        toolchange_xy_label.setToolTip(
+            _("Toolchange X,Y position.")
+        )
+        self.toolchangexy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+
+        grid0.addWidget(toolchange_xy_label, 31, 0)
+        grid0.addWidget(self.toolchangexy_entry, 31, 1, 1, 2)
+
+        # Start Z
+        startzlabel = QtWidgets.QLabel('%s:' % _('Start Z'))
+        startzlabel.setToolTip(
+            _("Height of the tool just after start.\n"
+              "Delete the value if you don't need this feature.")
+        )
+        self.estartz_entry = NumericalEvalEntry(border_color='#0069A9')
+
+        grid0.addWidget(startzlabel, 33, 0)
+        grid0.addWidget(self.estartz_entry, 33, 1, 1, 2)
+
+        # Feedrate Rapids
+        fr_rapid_label = QtWidgets.QLabel('%s:' % _('Feedrate Rapids'))
+        fr_rapid_label.setToolTip(
+            _("Tool speed while drilling\n"
+              "(in units per minute).\n"
+              "This is for the rapid move G00.\n"
+              "It is useful only for Marlin,\n"
+              "ignore for any other cases.")
+        )
+        self.feedrate_rapid_entry = FCDoubleSpinner()
+        self.feedrate_rapid_entry.set_precision(self.decimals)
+        self.feedrate_rapid_entry.set_range(0, 99999.9999)
+
+        grid0.addWidget(fr_rapid_label, 35, 0)
+        grid0.addWidget(self.feedrate_rapid_entry, 35, 1, 1, 2)
+
+        # Probe depth
+        self.pdepth_label = QtWidgets.QLabel('%s:' % _("Probe Z depth"))
+        self.pdepth_label.setToolTip(
+            _("The maximum depth that the probe is allowed\n"
+              "to probe. Negative value, in current units.")
+        )
+        self.pdepth_entry = FCDoubleSpinner()
+        self.pdepth_entry.set_precision(self.decimals)
+        self.pdepth_entry.set_range(-99999.9999, 0.0000)
+
+        grid0.addWidget(self.pdepth_label, 37, 0)
+        grid0.addWidget(self.pdepth_entry, 37, 1, 1, 2)
+
+        # Probe feedrate
+        self.feedrate_probe_label = QtWidgets.QLabel('%s:' % _("Feedrate Probe"))
+        self.feedrate_probe_label.setToolTip(
+           _("The feedrate used while the probe is probing.")
+        )
+        self.feedrate_probe_entry = FCDoubleSpinner()
+        self.feedrate_probe_entry.set_precision(self.decimals)
+        self.feedrate_probe_entry.set_range(0, 99999.9999)
+
+        grid0.addWidget(self.feedrate_probe_label, 38, 0)
+        grid0.addWidget(self.feedrate_probe_entry, 38, 1, 1, 2)
+
+        # Spindle direction
+        spindle_dir_label = QtWidgets.QLabel('%s:' % _('Spindle direction'))
+        spindle_dir_label.setToolTip(
+            _("This sets the direction that the spindle is rotating.\n"
+              "It can be either:\n"
+              "- CW = clockwise or\n"
+              "- CCW = counter clockwise")
+        )
+
+        self.spindledir_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
+                                          {'label': _('CCW'), 'value': 'CCW'}])
+        grid0.addWidget(spindle_dir_label, 40, 0)
+        grid0.addWidget(self.spindledir_radio, 40, 1, 1, 2)
+
+        self.fplunge_cb = FCCheckBox('%s' % _('Fast Plunge'))
+        self.fplunge_cb.setToolTip(
+            _("By checking this, the vertical move from\n"
+              "Z_Toolchange to Z_move is done with G0,\n"
+              "meaning the fastest speed available.\n"
+              "WARNING: the move is done at Toolchange X,Y coords.")
+        )
+        grid0.addWidget(self.fplunge_cb, 42, 0, 1, 3)
+
+        self.fretract_cb = FCCheckBox('%s' % _('Fast Retract'))
+        self.fretract_cb.setToolTip(
+            _("Exit hole strategy.\n"
+              " - When uncheked, while exiting the drilled hole the drill bit\n"
+              "will travel slow, with set feedrate (G1), up to zero depth and then\n"
+              "travel as fast as possible (G0) to the Z Move (travel height).\n"
+              " - When checked the travel from Z cut (cut depth) to Z_move\n"
+              "(travel height) is done as fast as possible (G0) in one move.")
+        )
+
+        grid0.addWidget(self.fretract_cb, 45, 0, 1, 3)
+
+        self.layout.addStretch()

+ 15 - 5
appGUI/preferences/tools/ToolsPanelizePrefGroupUI.py

@@ -106,6 +106,16 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.panel_type_label, 4, 0)
         grid0.addWidget(self.panel_type_radio, 4, 1)
 
+        # Path optimization
+        self.poptimization_cb = FCCheckBox('%s' % _("Path Optimization"))
+        self.poptimization_cb.setToolTip(
+            _("Active only for Geometry panel type.\n"
+              "When checked the application will find\n"
+              "any two overlapping Line elements in the panel\n"
+              "and remove the overlapping parts, keeping only one of them.")
+        )
+        grid0.addWidget(self.poptimization_cb, 5, 0, 1, 2)
+
         # ## Constrains
         self.pconstrain_cb = FCCheckBox('%s:' % _("Constrain within"))
         self.pconstrain_cb.setToolTip(
@@ -115,7 +125,7 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI):
               "the final panel will have as many columns and rows as\n"
               "they fit completely within selected area.")
         )
-        grid0.addWidget(self.pconstrain_cb, 5, 0, 1, 2)
+        grid0.addWidget(self.pconstrain_cb, 10, 0, 1, 2)
 
         self.px_width_entry = FCDoubleSpinner()
         self.px_width_entry.set_range(0.000001, 9999.9999)
@@ -127,8 +137,8 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI):
             _("The width (DX) within which the panel must fit.\n"
               "In current units.")
         )
-        grid0.addWidget(self.x_width_lbl, 6, 0)
-        grid0.addWidget(self.px_width_entry, 6, 1)
+        grid0.addWidget(self.x_width_lbl, 12, 0)
+        grid0.addWidget(self.px_width_entry, 12, 1)
 
         self.py_height_entry = FCDoubleSpinner()
         self.py_height_entry.set_range(0.000001, 9999.9999)
@@ -140,7 +150,7 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI):
             _("The height (DY)within which the panel must fit.\n"
               "In current units.")
         )
-        grid0.addWidget(self.y_height_lbl, 7, 0)
-        grid0.addWidget(self.py_height_entry, 7, 1)
+        grid0.addWidget(self.y_height_lbl, 17, 0)
+        grid0.addWidget(self.py_height_entry, 17, 1)
 
         self.layout.addStretch()

+ 12 - 6
appGUI/preferences/tools/ToolsPreferencesUI.py

@@ -14,6 +14,7 @@ from appGUI.preferences.tools.ToolsCutoutPrefGroupUI import ToolsCutoutPrefGroup
 from appGUI.preferences.tools.ToolsNCCPrefGroupUI import ToolsNCCPrefGroupUI
 from appGUI.preferences.tools.ToolsPaintPrefGroupUI import ToolsPaintPrefGroupUI
 from appGUI.preferences.tools.ToolsISOPrefGroupUI import ToolsISOPrefGroupUI
+from appGUI.preferences.tools.ToolsDrillPrefGroupUI import ToolsDrillPrefGroupUI
 
 import gettext
 import appTranslation as fcTranslate
@@ -41,6 +42,9 @@ class ToolsPreferencesUI(QtWidgets.QWidget):
         self.tools_iso_group = ToolsISOPrefGroupUI(decimals=self.decimals)
         self.tools_iso_group.setMinimumWidth(220)
 
+        self.tools_drill_group = ToolsDrillPrefGroupUI(decimals=self.decimals)
+        self.tools_drill_group.setMinimumWidth(220)
+
         self.tools_ncc_group = ToolsNCCPrefGroupUI(decimals=self.decimals)
         self.tools_ncc_group.setMinimumWidth(220)
 
@@ -75,17 +79,19 @@ class ToolsPreferencesUI(QtWidgets.QWidget):
         self.tools_sub_group.setMinimumWidth(200)
 
         self.vlay = QtWidgets.QVBoxLayout()
-        self.vlay.addWidget(self.tools_ncc_group)
-        self.vlay.addWidget(self.tools_cutout_group)
+
+        self.vlay.addWidget(self.tools_iso_group)
+        self.vlay.addWidget(self.tools_drill_group)
 
         self.vlay1 = QtWidgets.QVBoxLayout()
-        self.vlay1.addWidget(self.tools_paint_group)
-        self.vlay1.addWidget(self.tools_iso_group)
+        self.vlay1.addWidget(self.tools_ncc_group)
+        self.vlay1.addWidget(self.tools_2sided_group)
+        self.vlay1.addWidget(self.tools_cutout_group)
+        self.vlay1.addWidget(self.tools_sub_group)
 
         self.vlay2 = QtWidgets.QVBoxLayout()
+        self.vlay2.addWidget(self.tools_paint_group)
         self.vlay2.addWidget(self.tools_transform_group)
-        self.vlay2.addWidget(self.tools_2sided_group)
-        self.vlay2.addWidget(self.tools_sub_group)
 
         self.vlay3 = QtWidgets.QVBoxLayout()
         self.vlay3.addWidget(self.tools_film_group)

+ 119 - 29
appObjects/AppObject.py

@@ -67,18 +67,19 @@ class AppObject(QtCore.QObject):
               when appending it to the collection. There is no need to handle
               name conflicts here.
 
-        :param kind: The kind of object to create. One of 'gerber', 'excellon', 'cncjob' and 'geometry'.
-        :type kind: str
-        :param name: Name for the object.
-        :type name: str
-        :param initialize: Function to run after creation of the object but before it is attached to the application.
-        The function is called with 2 parameters: the new object and the App instance.
-        :type initialize: function
-        :param plot: If to plot the resulting object
-        :param autoselected: if the resulting object is autoselected in the Project tab and therefore in the
-        self.collection
-        :return: None
-        :rtype: None
+        :param kind:            The kind of object to create. One of 'gerber', 'excellon', 'cncjob' and 'geometry'.
+        :type kind:             str
+        :param name:            Name for the object.
+        :type name:             str
+        :param initialize:      Function to run after creation of the object but before it is attached to the
+                                application.
+                                The function is called with 2 parameters: the new object and the App instance.
+        :type initialize:       function
+        :param plot:            If to plot the resulting object
+        :param autoselected:    if the resulting object is autoselected in the Project tab and therefore in the
+                                self.collection
+        :return:                Either the object or the string 'fail'
+        :rtype:                 object
         """
 
         log.debug("AppObject.new_object()")
@@ -102,7 +103,12 @@ class AppObject(QtCore.QObject):
         # Object creation/instantiation
         obj = classdict[kind](name)
 
+        # ############################################################################################################
+        # adding object PROPERTIES
+        # ############################################################################################################
         obj.units = self.app.options["units"]
+        obj.isHovering = False
+        obj.notHovering = True
 
         # IMPORTANT
         # The key names in defaults and options dictionary's are not random:
@@ -113,13 +119,29 @@ class AppObject(QtCore.QObject):
         # let's say "excellon_toolchange", it will strip the excellon_) and to the obj.options the key will become
         # "toolchange"
 
+        # ############################################################################################################
+        # this section copies the application defaults related to the object to the object OPTIONS
+        # ############################################################################################################
         for option in self.app.options:
             if option.find(kind + "_") == 0:
                 oname = option[len(kind) + 1:]
                 obj.options[oname] = self.app.options[option]
 
-        obj.isHovering = False
-        obj.notHovering = True
+        # add some of the FlatCAM Tools related properties
+        if kind == 'excellon':
+            for option in self.app.options:
+                if option.find('tools_drill_') == 0 or option.find('tools_mill_') == 0:
+                    obj.options[option] = self.app.options[option]
+        if kind == 'gerber':
+            for option in self.app.options:
+                if option.find('tools_iso_') == 0:
+                    obj.options[option] = self.app.options[option]
+        if kind == 'geometry':
+            for option in self.app.options:
+                if option.find('tools_mill_') == 0:
+                    obj.options[option] = self.app.options[option]
+        # ############################################################################################################
+        # ############################################################################################################
 
         # Initialize as per user request
         # User must take care to implement initialize
@@ -127,6 +149,7 @@ class AppObject(QtCore.QObject):
         # have been invoked in a separate thread.
         t1 = time.time()
         log.debug("%f seconds before initialize()." % (t1 - t0))
+
         try:
             return_value = initialize(obj, self.app)
         except Exception as e:
@@ -145,16 +168,20 @@ class AppObject(QtCore.QObject):
             log.debug("Object (%s) parsing and/or geometry creation failed." % kind)
             return "fail"
 
+        # ############################################################################################################
         # Check units and convert if necessary
         # This condition CAN be true because initialize() can change obj.units
+        # ############################################################################################################
         if self.app.options["units"].upper() != obj.units.upper():
             self.app.inform.emit('%s: %s' % (_("Converting units to "), self.app.options["units"]))
             obj.convert_units(self.app.options["units"])
             t3 = time.time()
             log.debug("%f seconds converting units." % (t3 - t2))
 
+        # ############################################################################################################
         # Create the bounding box for the object and then add the results to the obj.options
         # But not for Scripts or for Documents
+        # ############################################################################################################
         if kind != 'document' and kind != 'script':
             try:
                 xmin, ymin, xmax, ymax = obj.bounds()
@@ -166,23 +193,16 @@ class AppObject(QtCore.QObject):
                 log.warning("AppObject.new_object() -> The object has no bounds properties. %s" % str(e))
                 return "fail"
 
-            try:
-                if kind == 'excellon':
-                    obj.fill_color = self.app.defaults["excellon_plot_fill"]
-                    obj.outline_color = self.app.defaults["excellon_plot_line"]
-
-                if kind == 'gerber':
-                    obj.fill_color = self.app.defaults["gerber_plot_fill"]
-                    obj.outline_color = self.app.defaults["gerber_plot_line"]
-            except Exception as e:
-                log.warning("AppObject.new_object() -> setting colors error. %s" % str(e))
-
+        # ############################################################################################################
         # update the KeyWords list with the name of the file
+        # ############################################################################################################
         self.app.myKeywords.append(obj.options['name'])
 
         log.debug("Moving new object back to main thread.")
 
+        # ############################################################################################################
         # Move the object to the main thread and let the app know that it is available.
+        # ############################################################################################################
         obj.moveToThread(self.app.main_thread)
         self.object_created.emit(obj, obj_plot, obj_autoselected)
 
@@ -203,11 +223,37 @@ class AppObject(QtCore.QObject):
 
         :return: None
         """
+        outname = 'new_geo'
 
         def initialize(obj, app):
-            obj.multitool = False
-
-        self.new_object('geometry', 'new_geo', initialize, plot=False)
+            obj.multitool = True
+            obj.multigeo = True
+            # store here the default data for Geometry Data
+            default_data = {}
+
+            for opt_key, opt_val in app.options.items():
+                if opt_key.find('geometry' + "_") == 0:
+                    oname = opt_key[len('geometry') + 1:]
+                    default_data[oname] = self.app.options[opt_key]
+                if opt_key.find('tools_mill' + "_") == 0:
+                    oname = opt_key[len('tools_mill') + 1:]
+                    default_data[oname] = self.app.options[opt_key]
+
+            obj.tools = {}
+            obj.tools.update({
+                1: {
+                    'tooldia': float(app.defaults["geometry_cnctooldia"]),
+                    'offset': 'Path',
+                    'offset_value': 0.0,
+                    'type': _('Rough'),
+                    'tool_type': 'C1',
+                    'data': deepcopy(default_data),
+                    'solid_geometry': []
+                }
+            })
+            obj.tools[1]['data']['name'] = outname
+
+        self.new_object('geometry', outname, initialize, plot=False)
 
     def new_gerber_object(self):
         """
@@ -288,6 +334,7 @@ class AppObject(QtCore.QObject):
         :param auto_select: if the newly created object to be autoselected after creation
         :return: None
         """
+
         t0 = time.time()  # DEBUG
         log.debug("on_object_created()")
 
@@ -299,6 +346,10 @@ class AppObject(QtCore.QObject):
 
         # self.app.inform.emit('[selected] %s created & selected: %s' %
         #                  (str(obj.kind).capitalize(), str(obj.options['name'])))
+
+        # #############################################################################################################
+        # ######################  Set colors for the message in the Status Bar  #######################################
+        # #############################################################################################################
         if obj.kind == 'gerber':
             self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
                 kind=obj.kind.capitalize(),
@@ -336,10 +387,47 @@ class AppObject(QtCore.QObject):
                 name=str(obj.options['name']), tx=_("created/selected"))
             )
 
+
+        # ############################################################################################################
+        # Set the colors for the objects that have geometry
+        # ############################################################################################################
+        if obj.kind != 'document' and obj.kind != 'script':
+            try:
+                if obj.kind == 'excellon':
+                    obj.fill_color = self.app.defaults["excellon_plot_fill"]
+                    obj.outline_color = self.app.defaults["excellon_plot_line"]
+
+                if obj.kind == 'gerber':
+                    if self.app.defaults["gerber_store_color_list"] is True:
+                        group = self.app.collection.group_items["gerber"]
+                        index = group.child_count() - 1
+
+                        # when loading a Gerber object always create a color tuple (line color, fill_color)
+                        # and add it to the self.app.defaults["gerber_color_list"] from where it will be picked and used
+                        try:
+                            colors = self.app.defaults["gerber_color_list"][index]
+                        except IndexError:
+                            obj.outline_color = self.app.defaults["gerber_plot_line"]
+                            obj.fill_color = self.app.defaults["gerber_plot_fill"]
+                            colors = (obj.outline_color, obj.fill_color)
+                            self.app.defaults["gerber_color_list"].append(colors)
+
+                        new_line_color = colors[0]
+                        new_fill = colors[1]
+                        obj.outline_color = new_line_color
+                        obj.fill_color = new_fill
+                    else:
+                        obj.outline_color = self.app.defaults["gerber_plot_line"]
+                        obj.fill_color = self.app.defaults["gerber_plot_fill"]
+            except Exception as e:
+                log.warning("AppObject.new_object() -> setting colors error. %s" % str(e))
+
+        # #############################################################################################################
         # update the SHELL auto-completer model with the name of the new object
+        # #############################################################################################################
         self.app.shell._edit.set_model_data(self.app.myKeywords)
 
-        if auto_select:
+        if auto_select or self.app.ui.notebook.currentWidget() is self.app.ui.properties_tab:
             # select the just opened object but deselect the previous ones
             self.app.collection.set_all_inactive()
             self.app.collection.set_active(obj.options["name"])
@@ -351,6 +439,8 @@ class AppObject(QtCore.QObject):
             with self.app.proc_container.new(_("Plotting")):
                 if t_obj.kind == 'cncjob':
                     t_obj.plot(kind=self.app.defaults["cncjob_plot_kind"])
+                if t_obj.kind == 'gerber':
+                    t_obj.plot(color=t_obj.outline_color, face_color=t_obj.fill_color)
                 else:
                     t_obj.plot()
 

Разница между файлами не показана из-за своего большого размера
+ 1410 - 37
appObjects/FlatCAMCNCJob.py


+ 8 - 10
appObjects/FlatCAMDocument.py

@@ -10,7 +10,7 @@
 # File modified by: Marius Stanciu                         #
 # ##########################################################
 
-from appEditors.FlatCAMTextEditor import TextEditor
+from appEditors.AppTextEditor import AppTextEditor
 from appObjects.FlatCAMObj import *
 
 import gettext
@@ -75,7 +75,7 @@ class DocumentObject(FlatCAMObj):
                 '<span style="color:red;"><b>Advanced</b></span>'
             ))
 
-        self.document_editor_tab = TextEditor(app=self.app)
+        self.document_editor_tab = AppTextEditor(app=self.app)
         stylesheet = """
                         QTextEdit {selection-background-color:%s;
                                    selection-color:white;
@@ -84,10 +84,6 @@ class DocumentObject(FlatCAMObj):
 
         self.document_editor_tab.code_editor.setStyleSheet(stylesheet)
 
-        # first clear previous text in text editor (if any)
-        self.document_editor_tab.code_editor.clear()
-        self.document_editor_tab.code_editor.setReadOnly(self._read_only)
-
         self.document_editor_tab.buttonRun.hide()
 
         self.ui.autocomplete_cb.set_value(self.app.defaults['document_autocompleter'])
@@ -138,14 +134,16 @@ class DocumentObject(FlatCAMObj):
 
         self.ui.font_size_cb.setCurrentIndex(int(self.app.defaults['document_font_size']))
 
-        self.document_editor_tab.handleTextChanged()
+        # self.document_editor_tab.handleTextChanged()
         self.ser_attrs = ['options', 'kind', 'source_file']
 
         if Qt.mightBeRichText(self.source_file):
-            self.document_editor_tab.code_editor.setHtml(self.source_file)
+            # self.document_editor_tab.code_editor.setHtml(self.source_file)
+            self.document_editor_tab.load_text(self.source_file, move_to_start=True, clear_text=True, as_html=True)
         else:
-            for line in self.source_file.splitlines():
-                self.document_editor_tab.code_editor.append(line)
+            # for line in self.source_file.splitlines():
+            #     self.document_editor_tab.code_editor.append(line)
+            self.document_editor_tab.load_text(self.source_file, move_to_start=True, clear_text=True, as_html=False)
 
         self.build_ui()
 

Разница между файлами не показана из-за своего большого размера
+ 208 - 616
appObjects/FlatCAMExcellon.py


+ 326 - 180
appObjects/FlatCAMGeometry.py

@@ -22,6 +22,8 @@ import math
 import numpy as np
 from copy import deepcopy
 import traceback
+from collections import defaultdict
+from functools import reduce
 
 import gettext
 import appTranslation as fcTranslate
@@ -38,6 +40,8 @@ class GeometryObject(FlatCAMObj, Geometry):
     format.
     """
     optionChanged = QtCore.pyqtSignal(str)
+    builduiSig = QtCore.pyqtSignal()
+
     ui_type = GeometryObjectUI
 
     def __init__(self, name):
@@ -152,13 +156,12 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.param_fields = {}
 
         # store here the state of the exclusion checkbox state to be restored after building the UI
-        # TODO add this in the sel.app.defaults dict and in Preferences
-        self.exclusion_area_cb_is_checked = False
+        self.exclusion_area_cb_is_checked = self.app.defaults["geometry_area_exclusion"]
 
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # from predecessors.
-        self.ser_attrs += ['options', 'kind', 'tools', 'multigeo']
+        self.ser_attrs += ['options', 'kind', 'multigeo', 'fill_color', 'outline_color', 'alpha_level']
 
     def build_ui(self):
         self.ui_disconnect()
@@ -175,70 +178,73 @@ class GeometryObject(FlatCAMObj, Geometry):
 
         self.units = self.app.defaults['units']
 
-        tool_idx = 0
+        row_idx = 0
 
         n = len(self.tools)
         self.ui.geo_tools_table.setRowCount(n)
 
         for tooluid_key, tooluid_value in self.tools.items():
-            tool_idx += 1
-            row_no = tool_idx - 1
 
-            tool_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
+            # -------------------- ID ------------------------------------------ #
+            tool_id = QtWidgets.QTableWidgetItem('%d' % int(row_idx + 1))
             tool_id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.ui.geo_tools_table.setItem(row_no, 0, tool_id)  # Tool name/id
+            self.ui.geo_tools_table.setItem(row_idx, 0, tool_id)  # Tool name/id
 
             # Make sure that the tool diameter when in MM is with no more than 2 decimals.
             # There are no tool bits in MM with more than 3 decimals diameter.
             # For INCH the decimals should be no more than 3. There are no tools under 10mils.
 
+            # -------------------- DIAMETER ------------------------------------- #
             dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(tooluid_value['tooldia'])))
             dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
+            self.ui.geo_tools_table.setItem(row_idx, 1, dia_item)  # Diameter
 
+            # -------------------- OFFSET   ------------------------------------- #
             offset_item = FCComboBox()
             for item in self.offset_item_options:
                 offset_item.addItem(item)
-            # offset_item.setStyleSheet('background-color: rgb(255,255,255)')
             idx = offset_item.findText(tooluid_value['offset'])
             offset_item.setCurrentIndex(idx)
+            self.ui.geo_tools_table.setCellWidget(row_idx, 2, offset_item)
 
+            # -------------------- TYPE     ------------------------------------- #
             type_item = FCComboBox()
             for item in self.type_item_options:
                 type_item.addItem(item)
-            # type_item.setStyleSheet('background-color: rgb(255,255,255)')
             idx = type_item.findText(tooluid_value['type'])
             type_item.setCurrentIndex(idx)
+            self.ui.geo_tools_table.setCellWidget(row_idx, 3, type_item)
 
+            # -------------------- TOOL TYPE ------------------------------------- #
             tool_type_item = FCComboBox()
             for item in self.tool_type_item_options:
                 tool_type_item.addItem(item)
-                # tool_type_item.setStyleSheet('background-color: rgb(255,255,255)')
             idx = tool_type_item.findText(tooluid_value['tool_type'])
             tool_type_item.setCurrentIndex(idx)
+            self.ui.geo_tools_table.setCellWidget(row_idx, 4, tool_type_item)
 
+            # -------------------- TOOL UID   ------------------------------------- #
             tool_uid_item = QtWidgets.QTableWidgetItem(str(tooluid_key))
+            # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
+            self.ui.geo_tools_table.setItem(row_idx, 5, tool_uid_item)  # Tool unique ID
 
+            # -------------------- PLOT       ------------------------------------- #
             plot_item = FCCheckBox()
             plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
             if self.ui.plot_cb.isChecked():
                 plot_item.setChecked(True)
+            self.ui.geo_tools_table.setCellWidget(row_idx, 6, plot_item)
 
-            self.ui.geo_tools_table.setItem(row_no, 1, dia_item)  # Diameter
-            self.ui.geo_tools_table.setCellWidget(row_no, 2, offset_item)
-            self.ui.geo_tools_table.setCellWidget(row_no, 3, type_item)
-            self.ui.geo_tools_table.setCellWidget(row_no, 4, tool_type_item)
-
-            # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
-            self.ui.geo_tools_table.setItem(row_no, 5, tool_uid_item)  # Tool unique ID
-            self.ui.geo_tools_table.setCellWidget(row_no, 6, plot_item)
-
+            # set an initial value for the OFFSET ENTRY
             try:
                 self.ui.tool_offset_entry.set_value(tooluid_value['offset_value'])
             except Exception as e:
                 log.debug("build_ui() --> Could not set the 'offset_value' key in self.tools. Error: %s" % str(e))
 
+            row_idx += 1
+
         # make the diameter column editable
-        for row in range(tool_idx):
+        for row in range(row_idx):
             self.ui.geo_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable |
                                                           QtCore.Qt.ItemIsEditable |
                                                           QtCore.Qt.ItemIsEnabled)
@@ -374,7 +380,9 @@ class GeometryObject(FlatCAMObj, Geometry):
         sel_rows = []
         sel_items = self.ui.geo_tools_table.selectedItems()
         for it in sel_items:
-            sel_rows.append(it.row())
+            new_row = it.row()
+            if new_row not in sel_rows:
+                sel_rows.append(new_row)
         if len(sel_rows) > 1:
             self.ui.tool_data_label.setText(
                 "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
@@ -391,9 +399,15 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.units = self.app.defaults['units'].upper()
         self.units_found = self.app.defaults['units']
 
+        # make sure the preprocessor combobox is clear
+        self.ui.pp_geometry_name_cb.clear()
         # populate preprocessor names in the combobox
         for name in list(self.app.preprocessors.keys()):
             self.ui.pp_geometry_name_cb.addItem(name)
+        # add tooltips
+        for it in range(self.ui.pp_geometry_name_cb.count()):
+            self.ui.pp_geometry_name_cb.setItemData(
+                it, self.ui.pp_geometry_name_cb.itemText(it), QtCore.Qt.ToolTipRole)
 
         self.form_fields.update({
             "plot": self.ui.plot_cb,
@@ -460,41 +474,19 @@ class GeometryObject(FlatCAMObj, Geometry):
 
         # store here the default data for Geometry Data
         self.default_data = {}
-        self.default_data.update({
-            "name": None,
-            "plot": None,
-            "cutz": None,
-            "vtipdia": None,
-            "vtipangle": None,
-            "travelz": None,
-            "feedrate": None,
-            "feedrate_z": None,
-            "feedrate_rapid": None,
-            "dwell": None,
-            "dwelltime": None,
-            "multidepth": None,
-            "ppname_g": None,
-            "depthperpass": None,
-            "extracut": None,
-            "extracut_length": None,
-            "toolchange": None,
-            "toolchangez": None,
-            "endz": None,
-            "endxy": '',
-            "area_exclusion": None,
-            "area_shape": None,
-            "area_strategy": None,
-            "area_overz": None,
-            "spindlespeed": 0,
-            "toolchangexy": None,
-            "startz": None
-        })
 
+        for opt_key, opt_val in self.app.options.items():
+            if opt_key.find('geometry' + "_") == 0:
+                oname = opt_key[len('geometry') + 1:]
+                self.default_data[oname] = self.app.options[opt_key]
+            if opt_key.find('tools_mill' + "_") == 0:
+                oname = opt_key[len('tools_mill') + 1:]
+                self.default_data[oname] = self.app.options[opt_key]
         # fill in self.default_data values from self.options
-        for def_key in self.default_data:
-            for opt_key, opt_val in self.options.items():
-                if def_key == opt_key:
-                    self.default_data[def_key] = deepcopy(opt_val)
+        # for def_key in self.default_data:
+        #     for opt_key, opt_val in self.options.items():
+        #         if def_key == opt_key:
+        #             self.default_data[def_key] = deepcopy(opt_val)
 
         if type(self.options["cnctooldia"]) == float:
             tools_list = [self.options["cnctooldia"]]
@@ -588,6 +580,8 @@ class GeometryObject(FlatCAMObj, Geometry):
         else:
             self.ui.level.setText('<span style="color:red;"><b>%s</b></span>' % _('Advanced'))
 
+        self.builduiSig.connect(self.build_ui)
+
         self.ui.e_cut_entry.setDisabled(False) if self.app.defaults['geometry_extracut'] else \
             self.ui.e_cut_entry.setDisabled(True)
         self.ui.extracut_cb.toggled.connect(lambda state: self.ui.e_cut_entry.setDisabled(not state))
@@ -595,6 +589,8 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
 
+        self.ui.editor_button.clicked.connect(self.app.object2editor)
+
         self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
         self.ui.paint_tool_button.clicked.connect(lambda: self.app.paint_tool.run(toggle=False))
         self.ui.generate_ncc_button.clicked.connect(lambda: self.app.ncclear_tool.run(toggle=False))
@@ -616,6 +612,28 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui.delete_sel_area_button.clicked.connect(self.on_delete_sel_areas)
         self.ui.strategy_radio.activated_custom.connect(self.on_strategy)
 
+        self.ui.geo_tools_table.drag_drop_sig.connect(self.rebuild_ui)
+
+    def rebuild_ui(self):
+        # read the table tools uid
+        current_uid_list = []
+        for row in range(self.ui.geo_tools_table.rowCount()):
+            uid = int(self.ui.geo_tools_table.item(row, 5).text())
+            current_uid_list.append(uid)
+
+        new_tools = {}
+        new_uid = 1
+
+        for current_uid in current_uid_list:
+            new_tools[new_uid] = deepcopy(self.tools[current_uid])
+            new_uid += 1
+
+        self.tools = new_tools
+
+        # the tools table changed therefore we need to reconnect the signals to the cellWidgets
+        self.ui_disconnect()
+        self.ui_connect()
+
     def on_cut_z_changed(self):
         self.old_cutz = self.ui.cutz_entry.get_value()
 
@@ -675,7 +693,8 @@ class GeometryObject(FlatCAMObj, Geometry):
             elif isinstance(current_widget, FCComboBox):
                 current_widget.currentIndexChanged.connect(self.gui_form_to_storage)
             elif isinstance(current_widget, FloatEntry) or isinstance(current_widget, LengthEntry) or \
-                    isinstance(current_widget, FCEntry) or isinstance(current_widget, IntEntry):
+                    isinstance(current_widget, FCEntry) or isinstance(current_widget, IntEntry) or \
+                    isinstance(current_widget, NumericalEvalTupleEntry):
                 current_widget.editingFinished.connect(self.gui_form_to_storage)
             elif isinstance(current_widget, FCSpinner) or isinstance(current_widget, FCDoubleSpinner):
                 current_widget.returnPressed.connect(self.gui_form_to_storage)
@@ -691,15 +710,15 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui.copytool_btn.clicked.connect(lambda: self.on_tool_copy())
         self.ui.deltool_btn.clicked.connect(lambda: self.on_tool_delete())
 
-        # self.ui.geo_tools_table.currentItemChanged.connect(self.on_row_selection_change)
         self.ui.geo_tools_table.clicked.connect(self.on_row_selection_change)
-        self.ui.geo_tools_table.horizontalHeader().sectionClicked.connect(self.on_row_selection_change)
+        self.ui.geo_tools_table.horizontalHeader().sectionClicked.connect(self.on_toggle_all_rows)
 
         self.ui.geo_tools_table.itemChanged.connect(self.on_tool_edit)
         self.ui.tool_offset_entry.returnPressed.connect(self.on_offset_value_edited)
 
         for row in range(self.ui.geo_tools_table.rowCount()):
             self.ui.geo_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
+
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
 
         # common parameters update
@@ -723,7 +742,8 @@ class GeometryObject(FlatCAMObj, Geometry):
                 except (TypeError, AttributeError):
                     pass
             elif isinstance(current_widget, LengthEntry) or isinstance(current_widget, IntEntry) or \
-                    isinstance(current_widget, FCEntry) or isinstance(current_widget, FloatEntry):
+                    isinstance(current_widget, FCEntry) or isinstance(current_widget, FloatEntry) or \
+                    isinstance(current_widget, NumericalEvalTupleEntry):
                 try:
                     current_widget.editingFinished.disconnect(self.gui_form_to_storage)
                 except (TypeError, AttributeError):
@@ -786,8 +806,44 @@ class GeometryObject(FlatCAMObj, Geometry):
         except (TypeError, AttributeError):
             pass
 
+    def on_toggle_all_rows(self):
+        """
+        will toggle the selection of all rows in Tools table
+
+        :return:
+        """
+        sel_model = self.ui.geo_tools_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        if len(sel_rows) == self.ui.geo_tools_table.rowCount():
+            self.ui.geo_tools_table.clearSelection()
+            self.ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("No Tool Selected"))
+            )
+        else:
+            self.ui.geo_tools_table.selectAll()
+            self.ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
+            )
+
     def on_row_selection_change(self):
-        self.update_ui()
+        sel_model = self.ui.geo_tools_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        # update UI only if only one row is selected otherwise having multiple rows selected will deform information
+        # for the rows other that the current one (first selected)
+        if len(sel_rows) == 1:
+            self.update_ui()
 
     def update_ui(self, row=None):
         self.ui_disconnect()
@@ -796,15 +852,26 @@ class GeometryObject(FlatCAMObj, Geometry):
             sel_rows = []
             sel_items = self.ui.geo_tools_table.selectedItems()
             for it in sel_items:
-                sel_rows.append(it.row())
+                new_row = it.row()
+                if new_row not in sel_rows:
+                    sel_rows.append(new_row)
         else:
             sel_rows = row if type(row) == list else [row]
 
         if not sel_rows:
-            sel_rows = [0]
+            # sel_rows = [0]
+            self.ui.generate_cnc_button.setDisabled(True)
+            self.ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("No Tool Selected"))
+            )
+            self.ui_connect()
+            return
+        else:
+            self.ui.generate_cnc_button.setDisabled(False)
 
-        for current_row in sel_rows:
-            self.set_tool_offset_visibility(current_row)
+        # update the QLabel that shows for which Tool we have the parameters in the UI form
+        if len(sel_rows) == 1:
+            current_row = sel_rows[0]
 
             # populate the form with the data from the tool associated with the row parameter
             try:
@@ -819,64 +886,69 @@ class GeometryObject(FlatCAMObj, Geometry):
                 self.ui_connect()
                 return
 
-            # update the QLabel that shows for which Tool we have the parameters in the UI form
-            if len(sel_rows) == 1:
-                self.ui.tool_data_label.setText(
-                    "<b>%s: <font color='#0000FF'>%s %d</font></b>" % (_('Parameters for'), _("Tool"), tooluid)
-                )
+            self.ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s %d</font></b>" % (_('Parameters for'), _("Tool"), tooluid)
+            )
+        else:
+            self.ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
+            )
 
-                # update the form with the V-Shape fields if V-Shape selected in the geo_tool_table
-                # also modify the Cut Z form entry to reflect the calculated Cut Z from values got from V-Shape Fields
-                try:
-                    item = self.ui.geo_tools_table.cellWidget(current_row, 4)
-                    if item is not None:
-                        tool_type_txt = item.currentText()
-                        self.ui_update_v_shape(tool_type_txt=tool_type_txt)
-                    else:
-                        self.ui_connect()
-                        return
-                except Exception as e:
-                    log.debug("Tool missing in ui_update_v_shape(). Add a tool in Geo Tool Table. %s" % str(e))
+        for current_row in sel_rows:
+            self.set_tool_offset_visibility(current_row)
+
+            # populate the form with the data from the tool associated with the row parameter
+            try:
+                item = self.ui.geo_tools_table.item(current_row, 5)
+                if type(item) is not None:
+                    tooluid = int(item.text())
+                else:
+                    self.ui_connect()
                     return
+            except Exception as e:
+                log.debug("Tool missing. Add a tool in Geo Tool Table. %s" % str(e))
+                self.ui_connect()
+                return
 
-                try:
-                    # set the form with data from the newly selected tool
-                    for tooluid_key, tooluid_value in list(self.tools.items()):
-                        if int(tooluid_key) == tooluid:
-                            for key, value in list(tooluid_value.items()):
-                                if key == 'data':
-                                    form_value_storage = tooluid_value['data']
-                                    self.update_form(form_value_storage)
-                                if key == 'offset_value':
-                                    # update the offset value in the entry even if the entry is hidden
-                                    self.ui.tool_offset_entry.set_value(tooluid_value['offset_value'])
-
-                                if key == 'tool_type' and value == 'V':
-                                    self.update_cutz()
-                except Exception as e:
-                    log.debug("GeometryObject.update_ui() -> %s " % str(e))
+            # update the form with the V-Shape fields if V-Shape selected in the geo_tool_table
+            # also modify the Cut Z form entry to reflect the calculated Cut Z from values got from V-Shape Fields
+            try:
+                item = self.ui.geo_tools_table.cellWidget(current_row, 4)
+                if item is not None:
+                    tool_type_txt = item.currentText()
+                    self.ui_update_v_shape(tool_type_txt=tool_type_txt)
+                else:
+                    self.ui_connect()
+                    return
+            except Exception as e:
+                log.debug("Tool missing in ui_update_v_shape(). Add a tool in Geo Tool Table. %s" % str(e))
+                return
 
-            else:
-                self.ui.tool_data_label.setText(
-                    "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
-                )
+            try:
+                # set the form with data from the newly selected tool
+                for tooluid_key, tooluid_value in list(self.tools.items()):
+                    if int(tooluid_key) == tooluid:
+                        for key, value in list(tooluid_value.items()):
+                            if key == 'data':
+                                form_value_storage = tooluid_value['data']
+                                self.update_form(form_value_storage)
+                            if key == 'offset_value':
+                                # update the offset value in the entry even if the entry is hidden
+                                self.ui.tool_offset_entry.set_value(tooluid_value['offset_value'])
+
+                            if key == 'tool_type' and value == 'V':
+                                self.update_cutz()
+            except Exception as e:
+                log.debug("GeometryObject.update_ui() -> %s " % str(e))
 
         self.ui_connect()
 
-    def on_tool_add(self, dia=None):
+    def on_tool_add(self, dia=None, new_geo=None):
         self.ui_disconnect()
 
         self.units = self.app.defaults['units'].upper()
 
-        if dia is not None:
-            tooldia = dia
-        else:
-            tooldia = float(self.ui.addtool_entry.get_value())
-
-        # construct a list of all 'tooluid' in the self.tools
-        # tool_uid_list = []
-        # for tooluid_key in self.tools:
-        #     tool_uid_list.append(int(tooluid_key))
+        tooldia = dia if dia is not None else float(self.ui.addtool_entry.get_value())
         tool_uid_list = [int(tooluid_key) for tooluid_key in self.tools]
 
         # find maximum from the temp_uid, add 1 and this is the new 'tooluid'
@@ -893,7 +965,8 @@ class GeometryObject(FlatCAMObj, Geometry):
             last_offset_value = self.tools[max_uid]['offset_value']
             last_type = self.tools[max_uid]['type']
             last_tool_type = self.tools[max_uid]['tool_type']
-            last_solid_geometry = self.tools[max_uid]['solid_geometry']
+
+            last_solid_geometry = self.tools[max_uid]['solid_geometry'] if new_geo is None else new_geo
 
             # if previous geometry was empty (it may happen for the first tool added)
             # then copy the object.solid_geometry
@@ -940,7 +1013,7 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui_connect()
         self.build_ui()
 
-        # if there is no tool left in the Tools Table, enable the parameters appGUI
+        # if there is at least one tool left in the Tools Table, enable the parameters GUI
         if self.ui.geo_tools_table.rowCount() != 0:
             self.ui.geo_param_frame.setDisabled(False)
 
@@ -958,9 +1031,9 @@ class GeometryObject(FlatCAMObj, Geometry):
                 break
         self.app.on_tools_database()
         self.app.tools_db_tab.ok_to_add = True
-        self.app.tools_db_tab.buttons_frame.hide()
-        self.app.tools_db_tab.add_tool_from_db.show()
-        self.app.tools_db_tab.cancel_tool_from_db.show()
+        self.app.tools_db_tab.ui.buttons_frame.hide()
+        self.app.tools_db_tab.ui.add_tool_from_db.show()
+        self.app.tools_db_tab.ui.cancel_tool_from_db.show()
 
     def on_tool_from_db_inserted(self, tool):
         """
@@ -1047,7 +1120,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                     except AttributeError:
                         self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to copy."))
                         self.ui_connect()
-                        self.build_ui()
+                        self.builduiSig.emit()
                         return
                     except Exception as e:
                         log.debug("on_tool_copy() --> " + str(e))
@@ -1056,7 +1129,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             else:
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to copy."))
                 self.ui_connect()
-                self.build_ui()
+                self.builduiSig.emit()
                 return
         else:
             # we copy all tools in geo_tools_table
@@ -1082,7 +1155,7 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ser_attrs.append('tools')
 
         self.ui_connect()
-        self.build_ui()
+        self.builduiSig.emit()
         self.app.inform.emit('[success] %s' % _("Tool was copied in Tool Table."))
 
     def on_tool_edit(self, current_item):
@@ -1098,6 +1171,9 @@ class GeometryObject(FlatCAMObj, Geometry):
             except ValueError:
                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
                 return
+        except AttributeError:
+            self.ui_connect()
+            return
 
         tool_dia = float('%.*f' % (self.decimals, d))
         tooluid = int(self.ui.geo_tools_table.item(current_row, 5).text())
@@ -1112,7 +1188,7 @@ class GeometryObject(FlatCAMObj, Geometry):
 
         self.app.inform.emit('[success] %s' % _("Tool was edited in Tool Table."))
         self.ui_connect()
-        self.build_ui()
+        self.builduiSig.emit()
 
     def on_tool_delete(self, all_tools=None):
         self.ui_disconnect()
@@ -1141,7 +1217,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                     except AttributeError:
                         self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to delete."))
                         self.ui_connect()
-                        self.build_ui()
+                        self.builduiSig.emit()
                         return
                     except Exception as e:
                         log.debug("on_tool_delete() --> " + str(e))
@@ -1150,7 +1226,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             else:
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to delete."))
                 self.ui_connect()
-                self.build_ui()
+                self.builduiSig.emit()
                 return
         else:
             # we delete all tools in geo_tools_table
@@ -1401,13 +1477,11 @@ class GeometryObject(FlatCAMObj, Geometry):
         widget_changed = self.sender()
         try:
             widget_idx = self.ui.grid3.indexOf(widget_changed)
+            # those are the indexes for the V-Tip Dia and V-Tip Angle, if edited calculate the new Cut Z
+            if widget_idx == 1 or widget_idx == 3:
+                self.update_cutz()
         except Exception as e:
             log.debug("GeometryObject.gui_form_to_storage() -- wdg index -> %s" % str(e))
-            return
-
-        # those are the indexes for the V-Tip Dia and V-Tip Angle, if edited calculate the new Cut Z
-        if widget_idx == 1 or widget_idx == 3:
-            self.update_cutz()
 
         # the original connect() function of the OptionalInputSelection is no longer working because of the
         # ui_diconnect() so I use this 'hack'
@@ -1714,16 +1788,6 @@ class GeometryObject(FlatCAMObj, Geometry):
         # test to see if we have tools available in the tool table
         if self.ui.geo_tools_table.selectedItems():
             for x in self.ui.geo_tools_table.selectedItems():
-                # try:
-                #     tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text())
-                # except ValueError:
-                #     # try to convert comma to decimal point. if it's still not working error message and return
-                #     try:
-                #         tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text().replace(',', '.'))
-                #     except ValueError:
-                #         self.app.inform.emit('[ERROR_NOTCL] %s' %
-                #                              _("Wrong value format entered, use a number."))
-                #         return
                 tooluid = int(self.ui.geo_tools_table.item(x.row(), 5).text())
 
                 for tooluid_key, tooluid_value in self.tools.items():
@@ -1759,7 +1823,7 @@ class GeometryObject(FlatCAMObj, Geometry):
         :param tools_dict:      a dictionary that holds the whole data needed to create the Gcode
                                 (including the solid_geometry)
         :param tools_in_use:    the tools that are used, needed by some preprocessors
-        :type  tools_in_use     list of lists, each list in the list is made out of row elements of tools table from appGUI
+        :type  tools_in_use     list of lists, each list in the list is made out of row elements of tools table from GUI
         :param segx:            number of segments on the X axis, for auto-levelling
         :param segy:            number of segments on the Y axis, for auto-levelling
         :param plot:            if True the generated object will be plotted; if False will not be plotted
@@ -1789,6 +1853,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             self.app.inform.emit(msg)
             return
 
+        self.multigeo = True
         # Object initialization function for app.app_obj.new_object()
         # RUNNING ON SEPARATE THREAD!
         def job_init_single_geometry(job_obj, app_obj):
@@ -1817,6 +1882,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             job_obj.z_pdepth = float(self.app.defaults["geometry_z_pdepth"])
             job_obj.feedrate_probe = float(self.app.defaults["geometry_feedrate_probe"])
 
+            total_gcode = ''
             for tooluid_key in list(tools_dict.keys()):
                 tool_cnt += 1
 
@@ -1906,6 +1972,8 @@ class GeometryObject(FlatCAMObj, Geometry):
                 else:
                     dia_cnc_dict['gcode'] = res
 
+                total_gcode += res
+
                 # tell gcode_parse from which point to start drawing the lines depending on what kind of
                 # object is the source of gcode
                 job_obj.toolchange_xy_type = "geometry"
@@ -1929,6 +1997,8 @@ class GeometryObject(FlatCAMObj, Geometry):
                 })
                 dia_cnc_dict.clear()
 
+            job_obj.source_file = total_gcode
+
         # Object initialization function for app.app_obj.new_object()
         # RUNNING ON SEPARATE THREAD!
         def job_init_multi_geometry(job_obj, app_obj):
@@ -1967,6 +2037,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                     self.app.inform.emit('[ERROR_NOTCL] %s...' % _('Cancelled. Empty file, it has no geometry'))
                     return 'fail'
 
+            total_gcode = ''
             for tooluid_key in list(tools_dict.keys()):
                 tool_cnt += 1
                 dia_cnc_dict = deepcopy(tools_dict[tooluid_key])
@@ -2039,22 +2110,23 @@ class GeometryObject(FlatCAMObj, Geometry):
                 # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
                 # to a value of 0.0005 which is 20 times less than 0.01
                 tol = float(self.app.defaults['global_tolerance']) / 20
-                res = job_obj.generate_from_multitool_geometry(
-                    tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset,
-                    tolerance=tol, z_cut=z_cut, z_move=z_move,
-                    feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
-                    spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime,
-                    multidepth=multidepth, depthpercut=depthpercut,
-                    extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy,
-                    toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
-                    pp_geometry_name=pp_geometry_name,
-                    tool_no=tool_cnt)
 
+                tool_lst = list(tools_dict.keys())
+                is_first = True if tooluid_key == tool_lst[0] else False
+                is_last = True if tooluid_key == tool_lst[-1] else False
+                res, start_gcode = job_obj.geometry_tool_gcode_gen(tooluid_key, tools_dict, first_pt=(0, 0),
+                                                                   tolerance = tol,
+                                                                   is_first=is_first, is_last=is_last,
+                                                                   toolchange = True)
                 if res == 'fail':
                     log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed")
                     return 'fail'
                 else:
                     dia_cnc_dict['gcode'] = res
+                total_gcode += res
+
+                if start_gcode != '':
+                    job_obj.gc_start = start_gcode
 
                 self.app.inform.emit('[success] %s' % _("G-Code parsing in progress..."))
                 dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
@@ -2081,6 +2153,8 @@ class GeometryObject(FlatCAMObj, Geometry):
                 })
                 dia_cnc_dict.clear()
 
+            job_obj.source_file = total_gcode
+
         if use_thread:
             # To be run in separate thread
             def job_thread(a_obj):
@@ -2103,11 +2177,11 @@ class GeometryObject(FlatCAMObj, Geometry):
             else:
                 self.app.app_obj.new_object("cncjob", outname, job_init_multi_geometry, plot=plot)
 
-    def generatecncjob(self, outname=None, dia=None, offset=None, z_cut=None, z_move=None,
-            feedrate=None, feedrate_z=None, feedrate_rapid=None, spindlespeed=None, dwell=None, dwelltime=None,
-            multidepth=None, dpp=None, toolchange=None, toolchangez=None, toolchangexy=None,
-            extracut=None, extracut_length=None, startz=None, endz=None, endxy=None, pp=None, segx=None, segy=None,
-            use_thread=True, plot=True):
+    def generatecncjob(self, outname=None, dia=None, offset=None, z_cut=None, z_move=None, feedrate=None,
+                       feedrate_z=None, feedrate_rapid=None, spindlespeed=None, dwell=None, dwelltime=None,
+                       multidepth=None, dpp=None, toolchange=None, toolchangez=None, toolchangexy=None,
+                       extracut=None, extracut_length=None, startz=None, endz=None, endxy=None, pp=None,
+                       segx=None, segy=None, use_thread=True, plot=True):
         """
         Only used by the TCL Command Cncjob.
         Creates a CNCJob out of this Geometry object. The actual
@@ -2220,17 +2294,18 @@ class GeometryObject(FlatCAMObj, Geometry):
             # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
             # to a value of 0.0005 which is 20 times less than 0.01
             tol = float(self.app.defaults['global_tolerance']) / 20
-            job_obj.generate_from_geometry_2(
-                self, tooldia=tooldia, offset=offset, tolerance=tol,
-                z_cut=z_cut, z_move=z_move,
-                feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
-                spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime,
-                multidepth=multidepth, depthpercut=depthperpass,
-                toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
-                extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy,
-                pp_geometry_name=ppname_g
+            res = job_obj.generate_from_geometry_2(self, tooldia=tooldia, offset=offset, tolerance=tol,
+                                                   z_cut=z_cut, z_move=z_move, feedrate=feedrate,
+                                                   feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
+                                                   spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime,
+                                                   multidepth=multidepth, depthpercut=depthperpass,
+                                                   toolchange=toolchange, toolchangez=toolchangez,
+                                                   toolchangexy=toolchangexy,
+                                                   extracut=extracut, extracut_length=extracut_length,
+                                                   startz=startz, endz=endz, endxy=endxy,
+                                                   pp_geometry_name=ppname_g
             )
-
+            job_obj.source_file = res
             # tell gcode_parse from which point to start drawing the lines depending on what kind of object is the
             # source of gcode
             job_obj.toolchange_xy_type = "geometry"
@@ -2654,13 +2729,15 @@ class GeometryObject(FlatCAMObj, Geometry):
             # if self.app.is_legacy is False:
             self.add_shape(shape=element, color=color, visible=visible, layer=0)
 
-    def plot(self, visible=None, kind=None):
+    def plot(self, visible=None, kind=None, plot_tool=None):
         """
         Plot the object.
 
-        :param visible: Controls if the added shape is visible of not
-        :param kind: added so there is no error when a project is loaded and it has both geometry and CNCJob, because
-        CNCJob require the 'kind' parameter. Perhaps the FlatCAMObj.plot() has to be rewrited
+        :param visible:     Controls if the added shape is visible of not
+        :param kind:        added so there is no error when a project is loaded and it has both geometry and CNCJob,
+                            because CNCJob require the 'kind' parameter. Perhaps the FlatCAMObj.plot()
+                            has to be rewritten
+        :param plot_tool:   plot a specific tool for multigeo objects
         :return:
         """
 
@@ -2694,17 +2771,33 @@ class GeometryObject(FlatCAMObj, Geometry):
             # plot solid geometries found as members of self.tools attribute dict
             # for MultiGeo
             if self.multigeo is True:  # geo multi tool usage
-                for tooluid_key in self.tools:
-                    solid_geometry = self.tools[tooluid_key]['solid_geometry']
-                    self.plot_element(solid_geometry, visible=visible,
-                                      color=random_color() if self.options['multicolored']
-                                      else self.app.defaults["geometry_plot_line"])
+                if plot_tool is None:
+                    for tooluid_key in self.tools:
+                        solid_geometry = self.tools[tooluid_key]['solid_geometry']
+                        if 'override_color' in self.tools[tooluid_key]['data']:
+                            color = self.tools[tooluid_key]['data']['override_color']
+                        else:
+                            color = random_color() if self.options['multicolored'] else \
+                                self.app.defaults["geometry_plot_line"]
+
+                        self.plot_element(solid_geometry, visible=visible, color=color)
+                else:
+                    solid_geometry = self.tools[plot_tool]['solid_geometry']
+                    if 'override_color' in self.tools[plot_tool]['data']:
+                        color = self.tools[plot_tool]['data']['override_color']
+                    else:
+                        color = random_color() if self.options['multicolored'] else \
+                            self.app.defaults["geometry_plot_line"]
+
+                    self.plot_element(solid_geometry, visible=visible, color=color)
             else:
                 # plot solid geometry that may be an direct attribute of the geometry object
                 # for SingleGeo
                 if self.solid_geometry:
-                    self.plot_element(self.solid_geometry, visible=visible,
-                                      color=self.app.defaults["geometry_plot_line"])
+                    solid_geometry = self.solid_geometry
+                    color = self.app.defaults["geometry_plot_line"]
+
+                    self.plot_element(solid_geometry, visible=visible, color=color)
 
             # self.plot_element(self.solid_geometry, visible=self.options['plot'])
 
@@ -2738,6 +2831,7 @@ class GeometryObject(FlatCAMObj, Geometry):
         check_row = 0
 
         self.shapes.clear(update=True)
+
         for tooluid_key in self.tools:
             solid_geometry = self.tools[tooluid_key]['solid_geometry']
 
@@ -2747,8 +2841,13 @@ class GeometryObject(FlatCAMObj, Geometry):
                 if tooluid_item == int(tooluid_key):
                     check_row = row
                     break
+
             if self.ui.geo_tools_table.cellWidget(check_row, 6).isChecked():
-                self.plot_element(element=solid_geometry, visible=True)
+                try:
+                    color = self.tools[tooluid_key]['data']['override_color']
+                    self.plot_element(element=solid_geometry, visible=True, color=color)
+                except KeyError:
+                    self.plot_element(element=solid_geometry, visible=True)
         self.shapes.redraw()
 
         # make sure that the general plot is disabled if one of the row plot's are disabled and
@@ -2773,13 +2872,14 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.plot()
 
     @staticmethod
-    def merge(geo_list, geo_final, multigeo=None):
+    def merge(geo_list, geo_final, multigeo=None, fuse_tools=None):
         """
         Merges the geometry of objects in grb_list into the geometry of geo_final.
 
-        :param geo_list: List of GerberObject Objects to join.
-        :param geo_final: Destination GerberObject object.
-        :param multigeo: if the merged geometry objects are of type MultiGeo
+        :param geo_list:    List of GerberObject Objects to join.
+        :param geo_final:   Destination GerberObject object.
+        :param multigeo:    if the merged geometry objects are of type MultiGeo
+        :param fuse_tools:  If True will try to fuse tools of the same type for the Geometry objects
         :return: None
         """
 
@@ -2833,7 +2933,53 @@ class GeometryObject(FlatCAMObj, Geometry):
 
         geo_final.options.update(new_options)
         geo_final.solid_geometry = new_solid_geometry
-        geo_final.tools = new_tools
+
+        if new_tools and fuse_tools is True:
+            # merge the geometries of the tools that share the same tool diameter and the same tool_type
+            # and the same type
+            final_tools = {}
+            same_dia = defaultdict(list)
+            same_type = defaultdict(list)
+            same_tool_type = defaultdict(list)
+
+            # find tools that have the same diameter and group them by diameter
+            for k, v in new_tools.items():
+                same_dia[v['tooldia']].append(k)
+
+            # find tools that have the same type and group them by type
+            for k, v in new_tools.items():
+                same_type[v['type']].append(k)
+
+            # find tools that have the same tool_type and group them by tool_type
+            for k, v in new_tools.items():
+                same_tool_type[v['tool_type']].append(k)
+
+            # find the intersections in the above groups
+            intersect_list = []
+            for dia, dia_list in same_dia.items():
+                for ty, type_list in same_type.items():
+                    for t_ty, tool_type_list in same_tool_type.items():
+                        intersection = reduce(np.intersect1d, (dia_list, type_list, tool_type_list)).tolist()
+                        if intersection:
+                            intersect_list.append(intersection)
+
+            new_tool_nr = 1
+            for i_lst in intersect_list:
+                new_solid_geo = []
+                for old_tool in i_lst:
+                    new_solid_geo += new_tools[old_tool]['solid_geometry']
+
+                if new_solid_geo:
+                    final_tools[new_tool_nr] = \
+                        {
+                            k: deepcopy(new_tools[old_tool][k]) for k in new_tools[old_tool] if k != 'solid_geometry'
+                        }
+                    final_tools[new_tool_nr]['solid_geometry'] = deepcopy(new_solid_geo)
+                    new_tool_nr += 1
+        else:
+            final_tools = new_tools
+
+        geo_final.tools = final_tools
 
     @staticmethod
     def get_pts(o):

+ 144 - 108
appObjects/FlatCAMGerber.py

@@ -41,66 +41,6 @@ class GerberObject(FlatCAMObj, Gerber):
 
     ui_type = GerberObjectUI
 
-    @staticmethod
-    def merge(grb_list, grb_final):
-        """
-        Merges the geometry of objects in geo_list into
-        the geometry of geo_final.
-
-        :param grb_list: List of GerberObject Objects to join.
-        :param grb_final: Destination GeometryObject object.
-        :return: None
-        """
-
-        if grb_final.solid_geometry is None:
-            grb_final.solid_geometry = []
-            grb_final.follow_geometry = []
-
-        if not grb_final.apertures:
-            grb_final.apertures = {}
-
-        if type(grb_final.solid_geometry) is not list:
-            grb_final.solid_geometry = [grb_final.solid_geometry]
-            grb_final.follow_geometry = [grb_final.follow_geometry]
-
-        for grb in grb_list:
-
-            # Expand lists
-            if type(grb) is list:
-                GerberObject.merge(grb_list=grb, grb_final=grb_final)
-            else:   # If not list, just append
-                for option in grb.options:
-                    if option != 'name':
-                        try:
-                            grb_final.options[option] = grb.options[option]
-                        except KeyError:
-                            log.warning("Failed to copy option.", option)
-
-                try:
-                    for geos in grb.solid_geometry:
-                        grb_final.solid_geometry.append(geos)
-                        grb_final.follow_geometry.append(geos)
-                except TypeError:
-                    grb_final.solid_geometry.append(grb.solid_geometry)
-                    grb_final.follow_geometry.append(grb.solid_geometry)
-
-                for ap in grb.apertures:
-                    if ap not in grb_final.apertures:
-                        grb_final.apertures[ap] = grb.apertures[ap]
-                    else:
-                        # create a list of integers out of the grb.apertures keys and find the max of that value
-                        # then, the aperture duplicate is assigned an id value incremented with 1,
-                        # and finally made string because the apertures dict keys are strings
-                        max_ap = str(max([int(k) for k in grb_final.apertures.keys()]) + 1)
-                        grb_final.apertures[max_ap] = {}
-                        grb_final.apertures[max_ap]['geometry'] = []
-
-                        for k, v in grb.apertures[ap].items():
-                            grb_final.apertures[max_ap][k] = deepcopy(v)
-
-        grb_final.solid_geometry = MultiPolygon(grb_final.solid_geometry)
-        grb_final.follow_geometry = MultiPolygon(grb_final.follow_geometry)
-
     def __init__(self, name):
         self.decimals = self.app.decimals
 
@@ -161,8 +101,8 @@ class GerberObject(FlatCAMObj, Gerber):
         # keep track if the UI is built so we don't have to build it every time
         self.ui_build = False
 
-        # build only once the aperture storage (takes time)
-        self.build_aperture_storage = False
+        # aperture marking storage
+        self.mark_shapes_storage = {}
 
         # Attributes to be included in serialization
         # Always append to it because it carries contents
@@ -200,12 +140,16 @@ class GerberObject(FlatCAMObj, Gerber):
         # Fill form fields only on object create
         self.to_form()
 
-        assert isinstance(self.ui, GerberObjectUI)
+        assert isinstance(self.ui, GerberObjectUI), \
+            "Expected a GerberObjectUI, got %s" % type(self.ui)
 
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
         self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
 
+        # Editor
+        self.ui.editor_button.clicked.connect(lambda: self.app.object2editor())
+
         # Tools
         self.ui.iso_button.clicked.connect(self.app.isolation_tool.run)
         self.ui.generate_ncc_button.clicked.connect(self.app.ncclear_tool.run)
@@ -844,27 +788,17 @@ class GerberObject(FlatCAMObj, Gerber):
     def on_aperture_table_visibility_change(self):
         if self.ui.aperture_table_visibility_cb.isChecked():
             # add the shapes storage for marking apertures
-            if self.build_aperture_storage is False:
-                self.build_aperture_storage = True
-
-                if self.app.is_legacy is False:
-                    for ap_code in self.apertures:
-                        self.mark_shapes[ap_code] = self.app.plotcanvas.new_shape_collection(layers=1)
-                else:
-                    for ap_code in self.apertures:
-                        self.mark_shapes[ap_code] = ShapeCollectionLegacy(obj=self, app=self.app,
-                                                                          name=self.options['name'] + str(ap_code))
+            for ap_code in self.apertures:
+                self.mark_shapes_storage[ap_code] = []
 
             self.ui.apertures_table.setVisible(True)
-            for ap in self.mark_shapes:
-                self.mark_shapes[ap].enabled = True
+            self.mark_shapes.enabled = True
 
             self.ui.mark_all_cb.setVisible(True)
             self.ui.mark_all_cb.setChecked(False)
             self.build_ui()
         else:
             self.ui.apertures_table.setVisible(False)
-
             self.ui.mark_all_cb.setVisible(False)
 
             # on hide disable all mark plots
@@ -872,10 +806,7 @@ class GerberObject(FlatCAMObj, Gerber):
                 for row in range(self.ui.apertures_table.rowCount()):
                     self.ui.apertures_table.cellWidget(row, 5).set_value(False)
                 self.clear_plot_apertures()
-
-                # for ap in list(self.mark_shapes.keys()):
-                #     # self.mark_shapes[ap].enabled = False
-                #     del self.mark_shapes[ap]
+                self.mark_shapes.enabled = False
             except Exception as e:
                 log.debug(" GerberObject.on_aperture_visibility_changed() --> %s" % str(e))
 
@@ -1005,7 +936,7 @@ class GerberObject(FlatCAMObj, Gerber):
             log.debug("GerberObject.plot() --> %s" % str(e))
 
     # experimental plot() when the solid_geometry is stored in the self.apertures
-    def plot_aperture(self, run_thread=True, **kwargs):
+    def plot_aperture(self, run_thread=False, **kwargs):
         """
 
         :param run_thread: if True run the aperture plot as a thread in a worker
@@ -1026,12 +957,12 @@ class GerberObject(FlatCAMObj, Gerber):
         else:
             color = self.app.defaults['gerber_plot_fill']
 
-        if 'marked_aperture' not in kwargs:
-            return
-        else:
+        if 'marked_aperture' in kwargs:
             aperture_to_plot_mark = kwargs['marked_aperture']
             if aperture_to_plot_mark is None:
                 return
+        else:
+            return
 
         if 'visible' not in kwargs:
             visibility = True
@@ -1046,15 +977,17 @@ class GerberObject(FlatCAMObj, Gerber):
                         for elem in self.apertures[aperture_to_plot_mark]['geometry']:
                             if 'solid' in elem:
                                 geo = elem['solid']
-                                if type(geo) == Polygon or type(geo) == LineString:
-                                    self.add_mark_shape(apid=aperture_to_plot_mark, shape=geo, color=color,
-                                                        face_color=color, visible=visibility)
-                                else:
+                                try:
                                     for el in geo:
-                                        self.add_mark_shape(apid=aperture_to_plot_mark, shape=el, color=color,
-                                                            face_color=color, visible=visibility)
+                                        shape_key = self.add_mark_shape(shape=el, color=color, face_color=color,
+                                                                        visible=visibility)
+                                        self.mark_shapes_storage[aperture_to_plot_mark].append(shape_key)
+                                except TypeError:
+                                    shape_key = self.add_mark_shape(shape=geo, color=color, face_color=color,
+                                                                    visible=visibility)
+                                    self.mark_shapes_storage[aperture_to_plot_mark].append(shape_key)
 
-                    self.mark_shapes[aperture_to_plot_mark].redraw()
+                    self.mark_shapes.redraw()
 
                 except (ObjectDeleted, AttributeError):
                     self.clear_plot_apertures()
@@ -1073,24 +1006,19 @@ class GerberObject(FlatCAMObj, Gerber):
         :return:
         """
 
-        if self.mark_shapes:
+        if self.mark_shapes_storage:
             if aperture == 'all':
-                for apid in list(self.apertures.keys()):
+                val = False if self.app.is_legacy is True else True
+                self.mark_shapes.clear(update=val)
+            else:
+                for shape_key in self.mark_shapes_storage[aperture]:
                     try:
-                        if self.app.is_legacy is True:
-                            self.mark_shapes[apid].clear(update=False)
-                        else:
-                            self.mark_shapes[apid].clear(update=True)
+                        self.mark_shapes.remove(shape_key)
                     except Exception as e:
-                        log.debug("GerberObject.clear_plot_apertures() 'all' --> %s" % str(e))
-            else:
-                try:
-                    if self.app.is_legacy is True:
-                        self.mark_shapes[aperture].clear(update=False)
-                    else:
-                        self.mark_shapes[aperture].clear(update=True)
-                except Exception as e:
-                    log.debug("GerberObject.clear_plot_apertures() 'aperture' --> %s" % str(e))
+                        log.debug("GerberObject.clear_plot_apertures() -> %s" % str(e))
+
+                self.mark_shapes_storage[aperture] = []
+                self.mark_shapes.redraw()
 
     def clear_mark_all(self):
         self.ui.mark_all_cb.set_value(False)
@@ -1117,6 +1045,7 @@ class GerberObject(FlatCAMObj, Gerber):
         try:
             aperture = self.ui.apertures_table.item(cw_row, 1).text()
         except AttributeError:
+            self.ui_connect()
             return
 
         if self.ui.apertures_table.cellWidget(cw_row, 5).isChecked():
@@ -1124,7 +1053,6 @@ class GerberObject(FlatCAMObj, Gerber):
             # self.plot_aperture(color='#2d4606bf', marked_aperture=aperture, visible=True)
             self.plot_aperture(color=self.app.defaults['global_sel_draw_color'] + 'AF',
                                marked_aperture=aperture, visible=True, run_thread=True)
-            # self.mark_shapes[aperture].redraw()
         else:
             self.marked_rows.append(False)
             self.clear_plot_apertures(aperture=aperture)
@@ -1228,7 +1156,8 @@ class GerberObject(FlatCAMObj, Gerber):
                     for geo_elem in self.apertures['0']['geometry']:
                         if 'solid' in geo_elem:
                             geo = geo_elem['solid']
-                            if not geo.is_empty:
+                            if not geo.is_empty and not isinstance(geo, LineString) and \
+                                    not isinstance(geo, MultiLineString) and not isinstance(geo, Point):
                                 gerber_code += 'G36*\n'
                                 geo_coords = list(geo.exterior.coords)
                                 # first command is a move with pen-up D02 at the beginning of the geo
@@ -1287,6 +1216,53 @@ class GerberObject(FlatCAMObj, Gerber):
                                         gerber_code += 'D02*\n'
                                         gerber_code += 'G37*\n'
                                     gerber_code += '%LPD*%\n'
+                            elif isinstance(geo, LineString) or isinstance(geo, MultiLineString) or \
+                                    isinstance(geo, Point):
+                                try:
+                                    if not geo.is_empty:
+                                        if isinstance(geo, Point):
+                                            if g_zeros == 'T':
+                                                x_formatted, y_formatted = tz_format(geo.x, geo.y, factor)
+                                                gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
+                                                                                               yform=y_formatted)
+                                            else:
+                                                x_formatted, y_formatted = lz_format(geo.x, geo.y, factor)
+                                                gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
+                                                                                               yform=y_formatted)
+                                        else:
+                                            geo_coords = list(geo.coords)
+                                            # first command is a move with pen-up D02 at the beginning of the geo
+                                            if g_zeros == 'T':
+                                                x_formatted, y_formatted = tz_format(
+                                                    geo_coords[0][0], geo_coords[0][1], factor)
+                                                gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
+                                                                                               yform=y_formatted)
+                                            else:
+                                                x_formatted, y_formatted = lz_format(
+                                                    geo_coords[0][0], geo_coords[0][1], factor)
+                                                gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
+                                                                                               yform=y_formatted)
+
+                                            prev_coord = geo_coords[0]
+                                            for coord in geo_coords[1:]:
+                                                if coord != prev_coord:
+                                                    if g_zeros == 'T':
+                                                        x_formatted, y_formatted = tz_format(coord[0], coord[1],
+                                                                                             factor)
+                                                        gerber_code += "X{xform}Y{yform}D01*\n".format(
+                                                            xform=x_formatted,
+                                                            yform=y_formatted)
+                                                    else:
+                                                        x_formatted, y_formatted = lz_format(coord[0], coord[1],
+                                                                                             factor)
+                                                        gerber_code += "X{xform}Y{yform}D01*\n".format(
+                                                            xform=x_formatted,
+                                                            yform=y_formatted)
+                                                prev_coord = coord
+
+                                            # gerber_code += "D02*\n"
+                                except Exception as e:
+                                    log.debug("FlatCAMObj.GerberObject.export_gerber() 'follow' --> %s" % str(e))
                         if 'clear' in geo_elem:
                             geo = geo_elem['clear']
                             if not geo.is_empty:
@@ -1483,6 +1459,66 @@ class GerberObject(FlatCAMObj, Gerber):
 
         return gerber_code
 
+    @staticmethod
+    def merge(grb_list, grb_final):
+        """
+        Merges the geometry of objects in geo_list into
+        the geometry of geo_final.
+
+        :param grb_list: List of GerberObject Objects to join.
+        :param grb_final: Destination GeometryObject object.
+        :return: None
+        """
+
+        if grb_final.solid_geometry is None:
+            grb_final.solid_geometry = []
+            grb_final.follow_geometry = []
+
+        if not grb_final.apertures:
+            grb_final.apertures = {}
+
+        if type(grb_final.solid_geometry) is not list:
+            grb_final.solid_geometry = [grb_final.solid_geometry]
+            grb_final.follow_geometry = [grb_final.follow_geometry]
+
+        for grb in grb_list:
+
+            # Expand lists
+            if type(grb) is list:
+                GerberObject.merge(grb_list=grb, grb_final=grb_final)
+            else:   # If not list, just append
+                for option in grb.options:
+                    if option != 'name':
+                        try:
+                            grb_final.options[option] = grb.options[option]
+                        except KeyError:
+                            log.warning("Failed to copy option.", option)
+
+                try:
+                    for geos in grb.solid_geometry:
+                        grb_final.solid_geometry.append(geos)
+                        grb_final.follow_geometry.append(geos)
+                except TypeError:
+                    grb_final.solid_geometry.append(grb.solid_geometry)
+                    grb_final.follow_geometry.append(grb.solid_geometry)
+
+                for ap in grb.apertures:
+                    if ap not in grb_final.apertures:
+                        grb_final.apertures[ap] = grb.apertures[ap]
+                    else:
+                        # create a list of integers out of the grb.apertures keys and find the max of that value
+                        # then, the aperture duplicate is assigned an id value incremented with 1,
+                        # and finally made string because the apertures dict keys are strings
+                        max_ap = str(max([int(k) for k in grb_final.apertures.keys()]) + 1)
+                        grb_final.apertures[max_ap] = {}
+                        grb_final.apertures[max_ap]['geometry'] = []
+
+                        for k, v in grb.apertures[ap].items():
+                            grb_final.apertures[max_ap][k] = deepcopy(v)
+
+        grb_final.solid_geometry = MultiPolygon(grb_final.solid_geometry)
+        grb_final.follow_geometry = MultiPolygon(grb_final.follow_geometry)
+
     def mirror(self, axis, point):
         Gerber.mirror(self, axis=axis, point=point)
         self.replotApertures.emit()

+ 10 - 6
appObjects/FlatCAMObj.py

@@ -14,8 +14,9 @@ import inspect  # TODO: For debugging only.
 
 from appGUI.ObjectUI import *
 
-from Common import LoudDict
+from appCommon.Common import LoudDict
 from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
+from appGUI.VisPyVisuals import ShapeCollection
 
 import sys
 
@@ -67,6 +68,9 @@ class FlatCAMObj(QtCore.QObject):
         # View
         self.ui = None
 
+        # set True by the collection.append() when the object load is complete
+        self.load_complete = None
+
         self.options = LoudDict(name=name)
         self.options.set_change_callback(self.on_options_change)
 
@@ -82,11 +86,11 @@ class FlatCAMObj(QtCore.QObject):
 
         if self.app.is_legacy is False:
             self.shapes = self.app.plotcanvas.new_shape_group()
+            self.mark_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
             # self.shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, pool=self.app.pool, layers=2)
         else:
             self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=name)
-
-        self.mark_shapes = {}
+            self.mark_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=name + "_mark_shapes")
 
         self.item = None  # Link with project view item
 
@@ -211,7 +215,7 @@ class FlatCAMObj(QtCore.QObject):
 
         self.app.ui.selected_scroll_area.setWidget(self.ui)
         # self.ui.setMinimumWidth(100)
-        # self.ui.setMaximumWidth(self.app.ui.selected_tab.sizeHint().width())
+        # self.ui.setMaximumWidth(self.app.ui.properties_tab.sizeHint().width())
 
         self.muted_ui = False
 
@@ -408,11 +412,11 @@ class FlatCAMObj(QtCore.QObject):
             key = self.shapes.add(tolerance=self.drawing_tolerance, **kwargs)
         return key
 
-    def add_mark_shape(self, apid, **kwargs):
+    def add_mark_shape(self, **kwargs):
         if self.deleted:
             raise ObjectDeleted()
         else:
-            key = self.mark_shapes[apid].add(tolerance=self.drawing_tolerance, layer=0, **kwargs)
+            key = self.mark_shapes.add(tolerance=self.drawing_tolerance, layer=0, **kwargs)
         return key
 
     def update_filters(self, last_ext, filter_string):

+ 4 - 7
appObjects/FlatCAMScript.py

@@ -10,7 +10,7 @@
 # File modified by: Marius Stanciu                         #
 # ##########################################################
 
-from appEditors.FlatCAMTextEditor import TextEditor
+from appEditors.AppTextEditor import AppTextEditor
 from appObjects.FlatCAMObj import *
 from appGUI.ObjectUI import *
 
@@ -86,7 +86,7 @@ class ScriptObject(FlatCAMObj):
                 '<span style="color:red;"><b>Advanced</b></span>'
             ))
 
-        self.script_editor_tab = TextEditor(app=self.app, plain_text=True, parent=self.app.ui)
+        self.script_editor_tab = AppTextEditor(app=self.app, plain_text=True, parent=self.app.ui)
 
         # tab_here = False
         # # try to not add too many times a tab that it is already installed
@@ -135,14 +135,11 @@ class ScriptObject(FlatCAMObj):
         self.script_editor_tab.t_frame.hide()
 
         try:
-            self.script_editor_tab.code_editor.setPlainText(self.source_file)
-            # for line in self.source_file.splitlines():
-            #     QtWidgets.QApplication.processEvents()
-            #     self.script_editor_tab.code_editor.append(line)
+            # self.script_editor_tab.code_editor.setPlainText(self.source_file)
+            self.script_editor_tab.load_text(self.source_file, move_to_end=True)
         except Exception as e:
             log.debug("ScriptObject.set_ui() --> %s" % str(e))
 
-        self.script_editor_tab.code_editor.moveCursor(QtGui.QTextCursor.End)
         self.script_editor_tab.t_frame.show()
 
         self.app.proc_container.view.set_idle()

+ 3 - 1
appObjects/ObjectCollection.py

@@ -570,6 +570,8 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         obj.options["name"] = name
 
         obj.set_ui(obj.ui_type(app=self.app))
+        # a way to signal that the object was fully loaded
+        obj.load_complete = True
 
         # Required before appending (Qt MVC)
         group = self.group_items[obj.kind]
@@ -1011,7 +1013,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
     def on_row_activated(self, index):
         if index.isValid():
             if index.internalPointer().parent_item != self.root_item:
-                self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+                self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
         self.on_item_activated(index)
 
     def on_row_selected(self, obj_name):

+ 17 - 5
appParsers/ParseDXF.py

@@ -7,6 +7,7 @@
 
 from shapely.geometry import LineString
 from shapely.affinity import rotate
+from ezdxf.math.vector import Vector as ezdxf_vector
 
 import logging
 
@@ -175,8 +176,7 @@ def dxfellipse2shapely(ellipse, ellipse_segments=100):
     ratio = ellipse.dxf.ratio
 
     points_list = []
-
-    major_axis = Vector(major_axis)
+    major_axis = Vector(list(major_axis))
 
     major_x = major_axis[0]
     major_y = major_axis[1]
@@ -248,9 +248,17 @@ def dxfsolid2shapely(solid):
 
 
 def dxfspline2shapely(spline):
-    with spline.edit_data() as spline_data:
-        ctrl_points = spline_data.control_points
-        knot_values = spline_data.knot_values
+    # for old version of ezdxf
+    # with spline.edit_data() as spline_data:
+    #     ctrl_points = spline_data.control_points
+    #     try:
+    #         # required if using old version of ezdxf
+    #         knot_values = spline_data.knot_values
+    #     except AttributeError:
+    #         knot_values = spline_data.knots
+
+    ctrl_points = spline.control_points
+    knot_values = spline.knots
     is_closed = spline.closed
     degree = spline.dxf.degree
 
@@ -322,6 +330,10 @@ def get_geo_from_insert(dxf_object, insert):
         if sx != 1 or sy != 1:
             geo = scale(geo, sx, sy)
         if phi != 0:
+            if isinstance(tr, str) and tr.lower() == 'c':
+                tr = 'center'
+            elif isinstance(tr, ezdxf_vector):
+                tr = list(tr)
             geo = rotate(geo, phi, origin=tr)
 
         geo_block_transformed.append(geo)

+ 9 - 10
appParsers/ParseDXF_Spline.py

@@ -22,18 +22,16 @@ def normalize_2(v):
 
 # ------------------------------------------------------------------------------
 # Convert a B-spline to polyline with a fixed number of segments
-#
-# FIXME to become adaptive
 # ------------------------------------------------------------------------------
 def spline2Polyline(xyz, degree, closed, segments, knots):
     """
-    :param xyz: DXF spline control points
-    :param degree: degree of the Spline curve
-    :param closed: closed Spline
-    :type closed: bool
-    :param segments: how many lines to use for Spline approximation
-    :param knots: DXF spline knots
-    :return: x,y,z coordinates (each is a list)
+    :param xyz:         DXF spline control points
+    :param degree:      degree of the Spline curve
+    :param closed:      closed Spline
+    :type closed:       bool
+    :param segments:    how many lines to use for Spline approximation
+    :param knots:       DXF spline knots
+    :return:            x,y,z coordinates (each is a list)
     """
 
     # Check if last point coincide with the first one
@@ -48,7 +46,8 @@ def spline2Polyline(xyz, degree, closed, segments, knots):
         knots = None
     else:
         # make base-1
-        knots.insert(0, 0)
+        # knots.insert(0, 0)
+        pass
 
     npts = len(xyz)
 

+ 285 - 227
appParsers/ParseExcellon.py

@@ -40,30 +40,13 @@ class Excellon(Geometry):
     ================  ====================================
     Key               Value
     ================  ====================================
-    C                 Diameter of the tool
-    solid_geometry    Geometry list for each tool
+    tooldia           Diameter of the tool
+    drills            List that store the Shapely Points for drill points
+    slots             List that store the Shapely Points for slots. Each is a tuple: (start_point, stop_point)
     data              dictionary which holds the options for each tool
-    Others            Not supported (Ignored).
-    ================  ====================================
-
-    * ``drills`` (list): Each is a dictionary:
-
-    ================  ====================================
-    Key               Value
-    ================  ====================================
-    point             (Shapely.Point) Where to drill
-    tool              (str) A key in ``tools``
+    solid_geometry    Geometry list for each tool
     ================  ====================================
 
-    * ``slots`` (list): Each is a dictionary
-
-    ================  ====================================
-    Key               Value
-    ================  ====================================
-    start             (Shapely.Point) Start point of the slot
-    stop              (Shapely.Point) Stop point of the slot
-    tool              (str) A key in ``tools``
-    ================  ====================================
     """
 
     defaults = {
@@ -96,10 +79,6 @@ class Excellon(Geometry):
 
         # dictionary to store tools, see above for description
         self.tools = {}
-        # list to store the drills, see above for description
-        self.drills = []
-        # self.slots (list) to store the slots; each is a dictionary
-        self.slots = []
 
         self.source_file = ''
 
@@ -110,9 +89,6 @@ class Excellon(Geometry):
         self.match_routing_start = None
         self.match_routing_stop = None
 
-        self.num_tools = []  # List for keeping the tools sorted
-        self.index_per_tool = {}  # Dictionary to store the indexed points for each tool
-
         # ## IN|MM -> Units are inherited from Geometry
         self.units = self.app.defaults['units']
         self.units_found = self.app.defaults['units']
@@ -142,9 +118,8 @@ class Excellon(Geometry):
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # from Geometry.
-        self.ser_attrs += ['tools', 'drills', 'zeros', 'excellon_format_upper_mm', 'excellon_format_lower_mm',
-                           'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'slots',
-                           'source_file']
+        self.ser_attrs += ['zeros', 'excellon_format_upper_mm', 'excellon_format_lower_mm',
+                           'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'source_file']
 
         # ### Patterns ####
         # Regex basics:
@@ -346,14 +321,22 @@ class Excellon(Geometry):
 
                         if match.group(2):
                             name_tool += 1
+
+                            # ----------  add a TOOL  ------------ #
+                            if name_tool not in self.tools:
+                                self.tools[name_tool] = {}
                             if line_units == 'MILS':
-                                spec = {"C": (float(match.group(2)) / 1000)}
-                                self.tools[str(name_tool)] = spec
-                                log.debug("Tool definition: %s %s" % (name_tool, spec))
+                                spec = {
+                                    'tooldia':  (float(match.group(2)) / 1000)
+                                }
+                                self.tools[name_tool]['tooldia'] = (float(match.group(2)) / 1000)
+                                log.debug("Tool definition: %d %s" % (name_tool, spec))
                             else:
-                                spec = {"C": float(match.group(2))}
-                                self.tools[str(name_tool)] = spec
-                                log.debug("Tool definition: %s %s" % (name_tool, spec))
+                                spec = {
+                                    'tooldia': float(match.group(2))
+                                }
+                                self.tools[name_tool]['tooldia'] = float(match.group(2))
+                                log.debug("Tool definition: %d %s" % (name_tool, spec))
                             spec['solid_geometry'] = []
                             continue
                     # search for Altium Excellon Format / Sprint Layout who is included as a comment
@@ -400,7 +383,7 @@ class Excellon(Geometry):
                         lower_tools = set()
                         if not self.excellon_units_found and self.tools:
                             for tool in self.tools:
-                                tool_dia = float(self.tools[tool]['C'])
+                                tool_dia = float(self.tools[tool]['tooldia'])
                                 lower_tools.add(tool_dia) if tool_dia <= 0.1 else greater_tools.add(tool_dia)
 
                             assumed_units = "IN" if len(lower_tools) > len(greater_tools) else "MM"
@@ -434,12 +417,12 @@ class Excellon(Geometry):
                     # ## Tool change ###
                     match = self.toolsel_re.search(eline)
                     if match:
-                        current_tool = str(int(match.group(1)))
+                        current_tool = int(match.group(1))
                         log.debug("Tool change: %s" % current_tool)
                         if bool(headerless):
                             match = self.toolset_hl_re.search(eline)
                             if match:
-                                name = str(int(match.group(1)))
+                                name = int(match.group(1))
                                 try:
                                     diam = float(match.group(2))
                                 except Exception:
@@ -467,8 +450,13 @@ class Excellon(Geometry):
                                     else:
                                         diam = (self.toolless_diam + (int(current_tool) - 1) / 100) / 25.4
 
-                                spec = {"C": diam, 'solid_geometry': []}
-                                self.tools[name] = spec
+                                # ----------  add a TOOL  ------------ #
+                                spec = {"tooldia": diam, 'solid_geometry': []}
+                                if name not in self.tools:
+                                    self.tools[name] = {}
+                                self.tools[name]['tooldia'] = diam
+                                self.tools[name]['solid_geometry'] = []
+
                                 log.debug("Tool definition out of header: %s %s" % (name, spec))
 
                         continue
@@ -479,8 +467,8 @@ class Excellon(Geometry):
                         match1 = self.stop_re.search(eline)
                         if match or match1:
                             name_tool += 1
-                            current_tool = str(name_tool)
-                            log.debug("Tool change for Allegro type of Excellon: %s" % current_tool)
+                            current_tool = name_tool
+                            log.debug("Tool change for Allegro type of Excellon: %d" % current_tool)
                             continue
 
                     # ## Slots parsing for drilled slots (contain G85)
@@ -546,7 +534,7 @@ class Excellon(Geometry):
                             # store current tool diameter as slot diameter
                             slot_dia = 0.05
                             try:
-                                slot_dia = float(self.tools[current_tool]['C'])
+                                slot_dia = float(self.tools[current_tool]['tooldia'])
                             except Exception:
                                 pass
                             log.debug(
@@ -556,13 +544,17 @@ class Excellon(Geometry):
                                 )
                             )
 
-                            self.slots.append(
-                                {
-                                    'start': Point(slot_start_x, slot_start_y),
-                                    'stop': Point(slot_stop_x, slot_stop_y),
-                                    'tool': current_tool
-                                }
+                            # ----------  add a slot  ------------ #
+                            slot = (
+                                Point(slot_start_x, slot_start_y),
+                                Point(slot_stop_x, slot_stop_y)
                             )
+                            if current_tool not in self.tools:
+                                self.tools[current_tool] = {}
+                            if 'slots' in self.tools[current_tool]:
+                                self.tools[current_tool]['slots'].append(slot)
+                            else:
+                                self.tools[current_tool]['slots'] = [slot]
                             continue
 
                         # Slot coordinates with period: Use literally. ###
@@ -616,7 +608,7 @@ class Excellon(Geometry):
                             # store current tool diameter as slot diameter
                             slot_dia = 0.05
                             try:
-                                slot_dia = float(self.tools[current_tool]['C'])
+                                slot_dia = float(self.tools[current_tool]['tooldia'])
                             except Exception:
                                 pass
                             log.debug(
@@ -626,13 +618,17 @@ class Excellon(Geometry):
                                 )
                             )
 
-                            self.slots.append(
-                                {
-                                    'start': Point(slot_start_x, slot_start_y),
-                                    'stop': Point(slot_stop_x, slot_stop_y),
-                                    'tool': current_tool
-                                }
+                            # ----------  add a Slot  ------------ #
+                            slot = (
+                                Point(slot_start_x, slot_start_y),
+                                Point(slot_stop_x, slot_stop_y)
                             )
+                            if current_tool not in self.tools:
+                                self.tools[current_tool] = {}
+                            if 'slots' in self.tools[current_tool]:
+                                self.tools[current_tool]['slots'].append(slot)
+                            else:
+                                self.tools[current_tool]['slots'] = [slot]
                         continue
 
                     # ## Coordinates without period # ##
@@ -660,7 +656,14 @@ class Excellon(Geometry):
                                     coordx += repeating_x
                                 if repeating_y:
                                     coordy += repeating_y
-                                self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool})
+
+                                # ----------  add a Drill  ------------ #
+                                if current_tool not in self.tools:
+                                    self.tools[current_tool] = {}
+                                if 'drills' in self.tools[current_tool]:
+                                    self.tools[current_tool]['drills'].append(Point((coordx, coordy)))
+                                else:
+                                    self.tools[current_tool]['drills'] = [Point((coordx, coordy))]
 
                                 repeat -= 1
                             current_x = coordx
@@ -710,19 +713,31 @@ class Excellon(Geometry):
                                     self.routing_flag = 1
                                     slot_stop_x = x
                                     slot_stop_y = y
-                                    self.slots.append(
-                                        {
-                                            'start': Point(slot_start_x, slot_start_y),
-                                            'stop': Point(slot_stop_x, slot_stop_y),
-                                            'tool': current_tool
-                                        }
+
+                                    # ----------  add a Slot  ------------ #
+                                    slot = (
+                                        Point(slot_start_x, slot_start_y),
+                                        Point(slot_stop_x, slot_stop_y)
                                     )
+                                    if current_tool not in self.tools:
+                                        self.tools[current_tool] = {}
+                                    if 'slots' in self.tools[current_tool]:
+                                        self.tools[current_tool]['slots'].append(slot)
+                                    else:
+                                        self.tools[current_tool]['slots'] = [slot]
                                     continue
 
                             if self.match_routing_start is None and self.match_routing_stop is None:
                                 # signal that there are drill operations
                                 self.defaults['excellon_drills'] = True
-                                self.drills.append({'point': Point((x, y)), 'tool': current_tool})
+
+                                # ----------  add a Drill  ------------ #
+                                if current_tool not in self.tools:
+                                    self.tools[current_tool] = {}
+                                if 'drills' in self.tools[current_tool]:
+                                    self.tools[current_tool]['drills'].append(Point((x, y)))
+                                else:
+                                    self.tools[current_tool]['drills'] = [Point((x, y))]
                                 # log.debug("{:15} {:8} {:8}".format(eline, x, y))
                                 continue
 
@@ -778,13 +793,18 @@ class Excellon(Geometry):
                                 self.routing_flag = 1
                                 slot_stop_x = x
                                 slot_stop_y = y
-                                self.slots.append(
-                                    {
-                                        'start': Point(slot_start_x, slot_start_y),
-                                        'stop': Point(slot_stop_x, slot_stop_y),
-                                        'tool': current_tool
-                                    }
+
+                                # ----------  add a Slot  ------------ #
+                                slot = (
+                                    Point(slot_start_x, slot_start_y),
+                                    Point(slot_stop_x, slot_stop_y)
                                 )
+                                if current_tool not in self.tools:
+                                    self.tools[current_tool] = {}
+                                if 'slots' in self.tools[current_tool]:
+                                    self.tools[current_tool]['slots'].append(slot)
+                                else:
+                                    self.tools[current_tool]['slots'] = [slot]
                                 continue
 
                         if self.match_routing_start is None and self.match_routing_stop is None:
@@ -792,7 +812,14 @@ class Excellon(Geometry):
                             if repeat == 0:
                                 # signal that there are drill operations
                                 self.defaults['excellon_drills'] = True
-                                self.drills.append({'point': Point((x, y)), 'tool': current_tool})
+
+                                # ----------  add a Drill  ------------ #
+                                if current_tool not in self.tools:
+                                    self.tools[current_tool] = {}
+                                if 'drills' in self.tools[current_tool]:
+                                    self.tools[current_tool]['drills'].append(Point((x, y)))
+                                else:
+                                    self.tools[current_tool]['drills'] = [Point((x, y))]
                             else:
                                 coordx = x
                                 coordy = y
@@ -801,7 +828,15 @@ class Excellon(Geometry):
                                         coordx = (repeat * x) + repeating_x
                                     if repeating_y:
                                         coordy = (repeat * y) + repeating_y
-                                    self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool})
+
+                                    # ----------  add a Drill  ------------ #
+                                    if current_tool not in self.tools:
+                                        self.tools[current_tool] = {}
+                                    if 'drills' in self.tools[current_tool]:
+                                        self.tools[current_tool]['drills'].append(Point((coordx, coordy)))
+                                    else:
+                                        self.tools[current_tool]['drills'] = [Point((coordx, coordy))]
+
                                     repeat -= 1
                             repeating_x = repeating_y = 0
                             # log.debug("{:15} {:8} {:8}".format(eline, x, y))
@@ -813,9 +848,14 @@ class Excellon(Geometry):
                     # ## Tool definitions # ##
                     match = self.toolset_re.search(eline)
                     if match:
-                        name = str(int(match.group(1)))
+                        # ----------  add a TOOL  ------------ #
+                        name = int(match.group(1))
                         spec = {"C": float(match.group(2)), 'solid_geometry': []}
-                        self.tools[name] = spec
+                        if name not in self.tools:
+                            self.tools[name] = {}
+                        self.tools[name]['tooldia'] = float(match.group(2))
+                        self.tools[name]['solid_geometry'] = []
+
                         log.debug("Tool definition: %s %s" % (name, spec))
                         continue
 
@@ -917,6 +957,15 @@ class Excellon(Geometry):
             # is finished since the tools definitions are spread in the Excellon body. We use as units the value
             # from self.defaults['excellon_units']
 
+            # the data structure of the Excellon object has to include bot the 'drills' and the 'slots' keys otherwise
+            # I will need to test for them everywhere.
+            # Even if there are not drills or slots I just add the storage there with an empty list
+            for tool in self.tools:
+                if 'drills' not in self.tools[tool]:
+                    self.tools[tool]['drills'] = []
+                if 'slots' not in self.tools[tool]:
+                    self.tools[tool]['slots'] = []
+
             log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
         except Exception:
             log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline))
@@ -977,8 +1026,9 @@ class Excellon(Geometry):
     def create_geometry(self):
         """
         Creates circles of the tool diameter at every point
-        specified in ``self.drills``. Also creates geometries (polygons)
-        for the slots as specified in ``self.slots``
+        specified in self.tools[tool]['drills'].
+        Also creates geometries (polygons)
+        for the slots as specified in self.tools[tool]['slots']
         All the resulting geometry is stored into self.solid_geometry list.
         The list self.solid_geometry has 2 elements: first is a dict with the drills geometry,
         and second element is another similar dict that contain the slots geometry.
@@ -1001,36 +1051,35 @@ class Excellon(Geometry):
                 self.tools[tool]['solid_geometry'] = []
                 self.tools[tool]['data'] = {}
 
-            for drill in self.drills:
-                # poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0)
-                if drill['tool'] == '':
-                    self.app.inform.emit('[WARNING] %s' %
-                                         _("Excellon.create_geometry() -> a drill location was skipped "
-                                           "due of not having a tool associated.\n"
-                                           "Check the resulting GCode."))
-                    log.debug("appParsers.ParseExcellon.Excellon.create_geometry() -> a drill location was skipped "
-                              "due of not having a tool associated")
-                    continue
-                tooldia = self.tools[drill['tool']]['C']
-                poly = drill['point'].buffer(tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
-                self.solid_geometry.append(poly)
-
-                tool_in_drills = drill['tool']
-                self.tools[tool_in_drills]['solid_geometry'].append(poly)
-                self.tools[tool_in_drills]['data'] = deepcopy(self.default_data)
-
-            for slot in self.slots:
-                slot_tooldia = self.tools[slot['tool']]['C']
-                start = slot['start']
-                stop = slot['stop']
-
-                lines_string = LineString([start, stop])
-                poly = lines_string.buffer(slot_tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
-                self.solid_geometry.append(poly)
-
-                tool_in_slots = slot['tool']
-                self.tools[tool_in_slots]['solid_geometry'].append(poly)
-                self.tools[tool_in_slots]['data'] = deepcopy(self.default_data)
+            for tool in self.tools:
+                tooldia = self.tools[tool]['tooldia']
+
+                if 'drills' in self.tools[tool]:
+                    for drill in self.tools[tool]['drills']:
+                        poly = drill.buffer(tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
+
+                        # add poly in the tools geometry
+                        self.tools[tool]['solid_geometry'].append(poly)
+                        self.tools[tool]['data'] = deepcopy(self.default_data)
+
+                        # add poly to the total solid geometry
+                        self.solid_geometry.append(poly)
+
+                if 'slots' in self.tools[tool]:
+                    for slot in self.tools[tool]['slots']:
+                        start = slot[0]
+                        stop = slot[1]
+
+                        lines_string = LineString([start, stop])
+                        poly = lines_string.buffer(tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
+
+                        # add poly in the tools geometry
+                        self.tools[tool]['solid_geometry'].append(poly)
+                        self.tools[tool]['data'] = deepcopy(self.default_data)
+
+                        # add poly to the total solid geometry
+                        self.solid_geometry.append(poly)
+
         except Exception as e:
             log.debug("appParsers.ParseExcellon.Excellon.create_geometry() -> "
                       "Excellon geometry creation failed due of ERROR: %s" % str(e))
@@ -1126,7 +1175,7 @@ class Excellon(Geometry):
 
         # Tools
         for tname in self.tools:
-            self.tools[tname]["C"] *= factor
+            self.tools[tname]["tooldia"] *= factor
 
         self.create_geometry()
         return factor
@@ -1173,31 +1222,40 @@ class Excellon(Geometry):
         # variables to display the percentage of work done
         self.geo_len = 0
         try:
-            self.geo_len = len(self.drills)
+            self.geo_len = len(self.tools)
         except TypeError:
             self.geo_len = 1
         self.old_disp_number = 0
         self.el_count = 0
 
-        # Drills
-        for drill in self.drills:
-            drill['point'] = affinity.scale(drill['point'], xfactor, yfactor, origin=(px, py))
+        for tool in self.tools:
+            # Scale Drills
+            if 'drills' in self.tools[tool]:
+                new_drills = []
+                for drill in self.tools[tool]['drills']:
+                    new_drills.append(affinity.scale(drill, xfactor, yfactor, origin=(px, py)))
+                self.tools[tool]['drills'] = new_drills
+
+            # Scale Slots
+            if 'slots' in self.tools[tool]:
+                new_slots = []
+                for slot in self.tools[tool]['slots']:
+                    new_start = affinity.scale(slot[0], xfactor, yfactor, origin=(px, py))
+                    new_stop = affinity.scale(slot[1], xfactor, yfactor, origin=(px, py))
+                    new_slot = (new_start, new_stop)
+                    new_slots.append(new_slot)
+                self.tools[tool]['slots'] = new_slots
+
+            # Scale solid_geometry
+            self.tools[tool]['solid_geometry'] = scale_geom(self.tools[tool]['solid_geometry'])
 
+            # update status display
             self.el_count += 1
             disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
             if self.old_disp_number < disp_number <= 100:
                 self.app.proc_container.update_view_text(' %d%%' % disp_number)
                 self.old_disp_number = disp_number
 
-        # scale solid_geometry
-        for tool in self.tools:
-            self.tools[tool]['solid_geometry'] = scale_geom(self.tools[tool]['solid_geometry'])
-
-        # Slots
-        for slot in self.slots:
-            slot['stop'] = affinity.scale(slot['stop'], xfactor, yfactor, origin=(px, py))
-            slot['start'] = affinity.scale(slot['start'], xfactor, yfactor, origin=(px, py))
-
         self.create_geometry()
         self.app.proc_container.new_text = ''
 
@@ -1231,31 +1289,40 @@ class Excellon(Geometry):
         # variables to display the percentage of work done
         self.geo_len = 0
         try:
-            self.geo_len = len(self.drills)
+            self.geo_len = len(self.tools)
         except TypeError:
             self.geo_len = 1
         self.old_disp_number = 0
         self.el_count = 0
 
-        # Drills
-        for drill in self.drills:
-            drill['point'] = affinity.translate(drill['point'], xoff=dx, yoff=dy)
+        for tool in self.tools:
+            # Offset Drills
+            if 'drills' in self.tools[tool]:
+                new_drills = []
+                for drill in self.tools[tool]['drills']:
+                    new_drills.append(affinity.translate(drill, xoff=dx, yoff=dy))
+                self.tools[tool]['drills'] = new_drills
+
+            # Offset Slots
+            if 'slots' in self.tools[tool]:
+                new_slots = []
+                for slot in self.tools[tool]['slots']:
+                    new_start = affinity.translate(slot[0], xoff=dx, yoff=dy)
+                    new_stop = affinity.translate(slot[1], xoff=dx, yoff=dy)
+                    new_slot = (new_start, new_stop)
+                    new_slots.append(new_slot)
+                self.tools[tool]['slots'] = new_slots
+
+            # Offset solid_geometry
+            self.tools[tool]['solid_geometry'] = offset_geom(self.tools[tool]['solid_geometry'])
 
+            # update status display
             self.el_count += 1
             disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
             if self.old_disp_number < disp_number <= 100:
                 self.app.proc_container.update_view_text(' %d%%' % disp_number)
                 self.old_disp_number = disp_number
 
-        # offset solid_geometry
-        for tool in self.tools:
-            self.tools[tool]['solid_geometry'] = offset_geom(self.tools[tool]['solid_geometry'])
-
-        # Slots
-        for slot in self.slots:
-            slot['stop'] = affinity.translate(slot['stop'], xoff=dx, yoff=dy)
-            slot['start'] = affinity.translate(slot['start'], xoff=dx, yoff=dy)
-
         # Recreate geometry
         self.create_geometry()
         self.app.proc_container.new_text = ''
@@ -1291,31 +1358,40 @@ class Excellon(Geometry):
         # variables to display the percentage of work done
         self.geo_len = 0
         try:
-            self.geo_len = len(self.drills)
+            self.geo_len = len(self.tools)
         except TypeError:
             self.geo_len = 1
         self.old_disp_number = 0
         self.el_count = 0
 
-        # Drills
-        for drill in self.drills:
-            drill['point'] = affinity.scale(drill['point'], xscale, yscale, origin=(px, py))
+        for tool in self.tools:
+            # Offset Drills
+            if 'drills' in self.tools[tool]:
+                new_drills = []
+                for drill in self.tools[tool]['drills']:
+                    new_drills.append(affinity.scale(drill, xscale, yscale, origin=(px, py)))
+                self.tools[tool]['drills'] = new_drills
+
+            # Offset Slots
+            if 'slots' in self.tools[tool]:
+                new_slots = []
+                for slot in self.tools[tool]['slots']:
+                    new_start = affinity.scale(slot[0], xscale, yscale, origin=(px, py))
+                    new_stop = affinity.scale(slot[1], xscale, yscale, origin=(px, py))
+                    new_slot = (new_start, new_stop)
+                    new_slots.append(new_slot)
+                self.tools[tool]['slots'] = new_slots
+
+            # Offset solid_geometry
+            self.tools[tool]['solid_geometry'] = mirror_geom(self.tools[tool]['solid_geometry'])
 
+            # update status display
             self.el_count += 1
             disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
             if self.old_disp_number < disp_number <= 100:
                 self.app.proc_container.update_view_text(' %d%%' % disp_number)
                 self.old_disp_number = disp_number
 
-        # mirror solid_geometry
-        for tool in self.tools:
-            self.tools[tool]['solid_geometry'] = mirror_geom(self.tools[tool]['solid_geometry'])
-
-        # Slots
-        for slot in self.slots:
-            slot['stop'] = affinity.scale(slot['stop'], xscale, yscale, origin=(px, py))
-            slot['start'] = affinity.scale(slot['start'], xscale, yscale, origin=(px, py))
-
         # Recreate geometry
         self.create_geometry()
         self.app.proc_container.new_text = ''
@@ -1361,7 +1437,7 @@ class Excellon(Geometry):
         # variables to display the percentage of work done
         self.geo_len = 0
         try:
-            self.geo_len = len(self.drills)
+            self.geo_len = len(self.tools)
         except TypeError:
             self.geo_len = 1
         self.old_disp_number = 0
@@ -1369,47 +1445,36 @@ class Excellon(Geometry):
 
         if point is None:
             px, py = 0, 0
-
-            # Drills
-            for drill in self.drills:
-                drill['point'] = affinity.skew(drill['point'], angle_x, angle_y,
-                                               origin=(px, py))
-
-                self.el_count += 1
-                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-                if self.old_disp_number < disp_number <= 100:
-                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                    self.old_disp_number = disp_number
-
-            # skew solid_geometry
-            for tool in self.tools:
-                self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
-
-            # Slots
-            for slot in self.slots:
-                slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(px, py))
-                slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(px, py))
         else:
             px, py = point
-            # Drills
-            for drill in self.drills:
-                drill['point'] = affinity.skew(drill['point'], angle_x, angle_y,
-                                               origin=(px, py))
-
-                self.el_count += 1
-                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-                if self.old_disp_number < disp_number <= 100:
-                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                    self.old_disp_number = disp_number
-
-            # skew solid_geometry
-            for tool in self.tools:
-                self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
 
-            # Slots
-            for slot in self.slots:
-                slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(px, py))
-                slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(px, py))
+        for tool in self.tools:
+            # Offset Drills
+            if 'drills' in self.tools[tool]:
+                new_drills = []
+                for drill in self.tools[tool]['drills']:
+                    new_drills.append(affinity.skew(drill, angle_x, angle_y, origin=(px, py)))
+                self.tools[tool]['drills'] = new_drills
+
+            # Offset Slots
+            if 'slots' in self.tools[tool]:
+                new_slots = []
+                for slot in self.tools[tool]['slots']:
+                    new_start = affinity.skew(slot[0], angle_x, angle_y, origin=(px, py))
+                    new_stop = affinity.skew(slot[1], angle_x, angle_y, origin=(px, py))
+                    new_slot = (new_start, new_stop)
+                    new_slots.append(new_slot)
+                self.tools[tool]['slots'] = new_slots
+
+            # Offset solid_geometry
+            self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
+
+            # update status display
+            self.el_count += 1
+            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+            if self.old_disp_number < disp_number <= 100:
+                self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                self.old_disp_number = disp_number
 
         self.create_geometry()
         self.app.proc_container.new_text = ''
@@ -1441,58 +1506,51 @@ class Excellon(Geometry):
                         return obj
                 else:
                     try:
-                        return affinity.rotate(obj, angle, origin=(px, py))
+                        return affinity.rotate(obj, angle, origin=orig)
                     except AttributeError:
                         return obj
 
         # variables to display the percentage of work done
         self.geo_len = 0
         try:
-            self.geo_len = len(self.drills)
+            self.geo_len = len(self.tools)
         except TypeError:
             self.geo_len = 1
         self.old_disp_number = 0
         self.el_count = 0
 
         if point is None:
-            # Drills
-            for drill in self.drills:
-                drill['point'] = affinity.rotate(drill['point'], angle, origin='center')
-
-            # rotate solid_geometry
-            for tool in self.tools:
-                self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'], origin='center')
-
-                self.el_count += 1
-                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-                if self.old_disp_number < disp_number <= 100:
-                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                    self.old_disp_number = disp_number
-
-            # Slots
-            for slot in self.slots:
-                slot['stop'] = affinity.rotate(slot['stop'], angle, origin='center')
-                slot['start'] = affinity.rotate(slot['start'], angle, origin='center')
+            orig = 'center'
         else:
-            px, py = point
-            # Drills
-            for drill in self.drills:
-                drill['point'] = affinity.rotate(drill['point'], angle, origin=(px, py))
-
-                self.el_count += 1
-                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-                if self.old_disp_number < disp_number <= 100:
-                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                    self.old_disp_number = disp_number
+            orig = point
 
-            # rotate solid_geometry
-            for tool in self.tools:
-                self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'])
-
-            # Slots
-            for slot in self.slots:
-                slot['stop'] = affinity.rotate(slot['stop'], angle, origin=(px, py))
-                slot['start'] = affinity.rotate(slot['start'], angle, origin=(px, py))
+        for tool in self.tools:
+            # Offset Drills
+            if 'drills' in self.tools[tool]:
+                new_drills = []
+                for drill in self.tools[tool]['drills']:
+                    new_drills.append(affinity.rotate(drill, angle, origin=orig))
+                self.tools[tool]['drills'] = new_drills
+
+            # Offset Slots
+            if 'slots' in self.tools[tool]:
+                new_slots = []
+                for slot in self.tools[tool]['slots']:
+                    new_start = affinity.rotate(slot[0], angle, origin=orig)
+                    new_stop = affinity.rotate(slot[1], angle, origin=orig)
+                    new_slot = (new_start, new_stop)
+                    new_slots.append(new_slot)
+                self.tools[tool]['slots'] = new_slots
+
+            # Offset solid_geometry
+            self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'], origin=orig)
+
+            # update status display
+            self.el_count += 1
+            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+            if self.old_disp_number < disp_number <= 100:
+                self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                self.old_disp_number = disp_number
 
         self.create_geometry()
         self.app.proc_container.new_text = ''
@@ -1534,8 +1592,8 @@ class Excellon(Geometry):
             except TypeError:
                 self.tools[tool]['solid_geometry'] = [res]
             if factor is None:
-                self.tools[tool]['C'] += distance
+                self.tools[tool]['tooldia'] += distance
             else:
-                self.tools[tool]['C'] *= distance
+                self.tools[tool]['tooldia'] *= distance
 
         self.create_geometry()

+ 116 - 27
appParsers/ParseGerber.py

@@ -2,20 +2,22 @@ from PyQt5 import QtWidgets
 from camlib import Geometry, arc, arc_angle, ApertureMacro, grace
 
 import numpy as np
-import re
-import logging
+# import re
+# import logging
 import traceback
 from copy import deepcopy
-import sys
+# import sys
 
-from shapely.ops import cascaded_union
-from shapely.affinity import scale, translate
+from shapely.ops import unary_union, linemerge
+# from shapely.affinity import scale, translate
 import shapely.affinity as affinity
-from shapely.geometry import box as shply_box, Polygon, LineString, Point, MultiPolygon
+from shapely.geometry import box as shply_box
 
 from lxml import etree as ET
-from appParsers.ParseSVG import svgparselength, getsvggeo
-import appTranslation as fcTranslate
+import ezdxf
+
+from appParsers.ParseDXF import *
+from appParsers.ParseSVG import svgparselength, getsvggeo, svgparse_viewbox
 
 import gettext
 import builtins
@@ -140,12 +142,6 @@ class Gerber(Geometry):
 
         self.source_file = ''
 
-        # Attributes to be included in serialization
-        # Always append to it because it carries contents
-        # from Geometry.
-        self.ser_attrs += ['int_digits', 'frac_digits', 'apertures',
-                           'aperture_macros', 'solid_geometry', 'source_file']
-
         # ### Parser patterns ## ##
         # FS - Format Specification
         # The format of X and Y must be the same!
@@ -232,6 +228,11 @@ class Gerber(Geometry):
 
         self.use_buffer_for_union = self.app.defaults["gerber_use_buffer_for_union"]
 
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from Geometry.
+        self.ser_attrs += ['apertures', 'int_digits', 'frac_digits', 'aperture_macros', 'solid_geometry', 'source_file']
+
     def aperture_parse(self, apertureId, apertureType, apParameters):
         """
         Parse gerber aperture definition into dictionary of apertures.
@@ -502,10 +503,10 @@ class Gerber(Geometry):
 
                     if buff_length > 0:
                         if current_polarity == 'D':
-                            self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
+                            self.solid_geometry = self.solid_geometry.union(unary_union(poly_buffer))
 
                         else:
-                            self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
+                            self.solid_geometry = self.solid_geometry.difference(unary_union(poly_buffer))
 
                         # follow_buffer = []
                         poly_buffer = []
@@ -949,6 +950,41 @@ class Gerber(Geometry):
                             # only add the point if it's a new one otherwise skip it (harder to process)
                             if path[-1] != [current_x, current_y]:
                                 path.append([current_x, current_y])
+                            elif len(path) == 1:
+                                # it's a flash that is done by moving with pen up D2 and then just a pen down D1
+                                # Reset path starting point
+                                path = [[current_x, current_y]]
+
+                                # --- BUFFERED ---
+                                # Draw the flash
+                                # this treats the case when we are storing geometry as paths
+                                geo_dict = {}
+                                geo_flash = Point([current_x, current_y])
+                                follow_buffer.append(geo_flash)
+                                geo_dict['follow'] = geo_flash
+
+                                # this treats the case when we are storing geometry as solids
+                                flash = self.create_flash_geometry(
+                                    Point([current_x, current_y]),
+                                    self.apertures[current_aperture],
+                                    self.steps_per_circle
+                                )
+                                if not flash.is_empty:
+                                    if self.app.defaults['gerber_simplification']:
+                                        poly_buffer.append(flash.simplify(s_tol))
+                                    else:
+                                        poly_buffer.append(flash)
+
+                                    if self.is_lpc is True:
+                                        geo_dict['clear'] = flash
+                                    else:
+                                        geo_dict['solid'] = flash
+
+                                if current_aperture not in self.apertures:
+                                    self.apertures[current_aperture] = {}
+                                if 'geometry' not in self.apertures[current_aperture]:
+                                    self.apertures[current_aperture]['geometry'] = []
+                                self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
 
                             if making_region is False:
                                 # if the aperture is rectangle then add a rectangular shape having as parameters the
@@ -1497,7 +1533,7 @@ class Gerber(Geometry):
 
             else:
                 log.debug("Union by union()...")
-                new_poly = cascaded_union(poly_buffer)
+                new_poly = unary_union(poly_buffer)
                 new_poly = new_poly.buffer(0, int(self.steps_per_circle / 4))
                 log.warning("Union done.")
 
@@ -1601,7 +1637,7 @@ class Gerber(Geometry):
                 p2 = Point(loc[0], loc[1] - 0.5 * (height - width))
                 c1 = p1.buffer(width * 0.5, int(steps_per_circle / 4))
                 c2 = p2.buffer(width * 0.5, int(steps_per_circle / 4))
-            return cascaded_union([c1, c2]).convex_hull
+            return unary_union([c1, c2]).convex_hull
 
         if aperture['type'] == 'P':  # Regular polygon
             loc = location.coords[0]
@@ -1750,16 +1786,16 @@ class Gerber(Geometry):
         self.scale(factor, factor)
         return factor
 
-    def import_svg(self, filename, object_type='gerber', flip=True, units='MM'):
+    def import_svg(self, filename, object_type='gerber', flip=True, units=None):
         """
         Imports shapes from an SVG file into the object's geometry.
 
-        :param filename: Path to the SVG file.
-        :type filename: str
-        :param object_type: parameter passed further along
-        :param flip: Flip the vertically.
-        :type flip: bool
-        :param units: FlatCAM units
+        :param filename:        Path to the SVG file.
+        :type filename:         str
+        :param object_type:     parameter passed further along
+        :param flip:            Flip the vertically.
+        :type flip:             bool
+        :param units:           FlatCAM units
         :return: None
         """
 
@@ -1774,7 +1810,10 @@ class Gerber(Geometry):
         # w = float(svg_root.get('width'))
         h = svgparselength(svg_root.get('height'))[0]  # TODO: No units support yet
 
-        geos = getsvggeo(svg_root, 'gerber')
+        units = self.app.defaults['units'] if units is None else units
+        res = self.app.defaults['gerber_circle_steps']
+        factor = svgparse_viewbox(svg_root)
+        geos = getsvggeo(svg_root, 'gerber', units=units, res=res, factor=factor)
         if flip:
             geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
 
@@ -1801,7 +1840,7 @@ class Gerber(Geometry):
                 geo_qrcode = []
                 geo_qrcode.append(Polygon(geos[0].exterior))
                 for i_el in geos[0].interiors:
-                    geo_qrcode.append(Polygon(i_el).buffer(0))
+                    geo_qrcode.append(Polygon(i_el).buffer(0, resolution=res))
                 for poly in geo_qrcode:
                     geos.append(poly)
 
@@ -1836,6 +1875,56 @@ class Gerber(Geometry):
             new_el['follow'] = pol.exterior
             self.apertures['0']['geometry'].append(new_el)
 
+    def import_dxf_as_gerber(self, filename, units='MM'):
+        """
+        Imports shapes from an DXF file into the Gerberobject geometry.
+
+        :param filename:    Path to the DXF file.
+        :type filename:     str
+        :param units:       Application units
+        :return: None
+        """
+
+        log.debug("Parsing DXF file geometry into a Gerber object geometry.")
+        # Parse into list of shapely objects
+        dxf = ezdxf.readfile(filename)
+        geos = getdxfgeo(dxf)
+        # trying to optimize the resulting geometry by merging contiguous lines
+        geos = linemerge(geos)
+
+        # Add to object
+        if self.solid_geometry is None:
+            self.solid_geometry = []
+
+        if type(self.solid_geometry) is list:
+            if type(geos) is list:
+                self.solid_geometry += geos
+            else:
+                self.solid_geometry.append(geos)
+        else:  # It's shapely geometry
+            self.solid_geometry = [self.solid_geometry, geos]
+
+        # flatten the self.solid_geometry list for import_dxf() to import DXF as Gerber
+        flat_geo = list(self.flatten_list(self.solid_geometry))
+        if flat_geo:
+            self.solid_geometry = unary_union(flat_geo)
+            self.follow_geometry = self.solid_geometry
+        else:
+            return "fail"
+
+        # create the self.apertures data structure
+        if '0' not in self.apertures:
+            self.apertures['0'] = {}
+            self.apertures['0']['type'] = 'REG'
+            self.apertures['0']['size'] = 0.0
+            self.apertures['0']['geometry'] = []
+
+        for pol in flat_geo:
+            new_el = {}
+            new_el['solid'] = pol
+            new_el['follow'] = pol
+            self.apertures['0']['geometry'].append(deepcopy(new_el))
+
     def scale(self, xfactor, yfactor=None, point=None):
         """
         Scales the objects' geometry on the XY plane by a given factor.

+ 63 - 43
appParsers/ParseHPGL2.py

@@ -52,49 +52,69 @@ class HPGL2:
 
         self.default_data = {}
         self.default_data.update({
-            "name": '_ncc',
-            "plot": self.app.defaults["geometry_plot"],
-            "cutz": self.app.defaults["geometry_cutz"],
-            "vtipdia": self.app.defaults["geometry_vtipdia"],
-            "vtipangle": self.app.defaults["geometry_vtipangle"],
-            "travelz": self.app.defaults["geometry_travelz"],
-            "feedrate": self.app.defaults["geometry_feedrate"],
-            "feedrate_z": self.app.defaults["geometry_feedrate_z"],
-            "feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
-            "dwell": self.app.defaults["geometry_dwell"],
-            "dwelltime": self.app.defaults["geometry_dwelltime"],
-            "multidepth": self.app.defaults["geometry_multidepth"],
-            "ppname_g": self.app.defaults["geometry_ppname_g"],
-            "depthperpass": self.app.defaults["geometry_depthperpass"],
-            "extracut": self.app.defaults["geometry_extracut"],
-            "extracut_length": self.app.defaults["geometry_extracut_length"],
-            "toolchange": self.app.defaults["geometry_toolchange"],
-            "toolchangez": self.app.defaults["geometry_toolchangez"],
-            "endz": self.app.defaults["geometry_endz"],
-            "endxy": self.app.defaults["geometry_endxy"],
-            "area_exclusion": self.app.defaults["geometry_area_exclusion"],
-            "area_shape": self.app.defaults["geometry_area_shape"],
-            "area_strategy": self.app.defaults["geometry_area_strategy"],
-            "area_overz": self.app.defaults["geometry_area_overz"],
-
-            "spindlespeed": self.app.defaults["geometry_spindlespeed"],
-            "toolchangexy": self.app.defaults["geometry_toolchangexy"],
-            "startz": self.app.defaults["geometry_startz"],
-
-            "tooldia": self.app.defaults["tools_painttooldia"],
-            "paintmargin": self.app.defaults["tools_paintmargin"],
-            "paintmethod": self.app.defaults["tools_paintmethod"],
-            "selectmethod": self.app.defaults["tools_selectmethod"],
-            "pathconnect": self.app.defaults["tools_pathconnect"],
-            "paintcontour": self.app.defaults["tools_paintcontour"],
-            "paintoverlap": self.app.defaults["tools_paintoverlap"],
-
-            "nccoverlap": self.app.defaults["tools_nccoverlap"],
-            "nccmargin": self.app.defaults["tools_nccmargin"],
-            "nccmethod": self.app.defaults["tools_nccmethod"],
-            "nccconnect": self.app.defaults["tools_nccconnect"],
-            "ncccontour": self.app.defaults["tools_ncccontour"],
-            "nccrest": self.app.defaults["tools_nccrest"]
+            "name":                     '_ncc',
+            "plot":                     self.app.defaults["geometry_plot"],
+            "cutz":                     self.app.defaults["geometry_cutz"],
+            "vtipdia":                  self.app.defaults["geometry_vtipdia"],
+            "vtipangle":                self.app.defaults["geometry_vtipangle"],
+            "travelz":                  self.app.defaults["geometry_travelz"],
+            "feedrate":                 self.app.defaults["geometry_feedrate"],
+            "feedrate_z":               self.app.defaults["geometry_feedrate_z"],
+            "feedrate_rapid":           self.app.defaults["geometry_feedrate_rapid"],
+            "dwell":                    self.app.defaults["geometry_dwell"],
+            "dwelltime":                self.app.defaults["geometry_dwelltime"],
+            "multidepth":               self.app.defaults["geometry_multidepth"],
+            "ppname_g":                 self.app.defaults["geometry_ppname_g"],
+            "depthperpass":             self.app.defaults["geometry_depthperpass"],
+            "extracut":                 self.app.defaults["geometry_extracut"],
+            "extracut_length":          self.app.defaults["geometry_extracut_length"],
+            "toolchange":               self.app.defaults["geometry_toolchange"],
+            "toolchangez":              self.app.defaults["geometry_toolchangez"],
+            "endz":                     self.app.defaults["geometry_endz"],
+            "endxy":                    self.app.defaults["geometry_endxy"],
+            "area_exclusion":           self.app.defaults["geometry_area_exclusion"],
+            "area_shape":               self.app.defaults["geometry_area_shape"],
+            "area_strategy":            self.app.defaults["geometry_area_strategy"],
+            "area_overz":               self.app.defaults["geometry_area_overz"],
+
+            "spindlespeed":             self.app.defaults["geometry_spindlespeed"],
+            "toolchangexy":             self.app.defaults["geometry_toolchangexy"],
+            "startz":                   self.app.defaults["geometry_startz"],
+
+            "tooldia":                  self.app.defaults["tools_painttooldia"],
+            "tools_paintoffset":        self.app.defaults["tools_paintoffset"],
+            "tools_paintmethod":        self.app.defaults["tools_paintmethod"],
+            "tools_selectmethod":       self.app.defaults["tools_selectmethod"],
+            "tools_pathconnect":        self.app.defaults["tools_pathconnect"],
+            "tools_paintcontour":       self.app.defaults["tools_paintcontour"],
+            "tools_paintoverlap":       self.app.defaults["tools_paintoverlap"],
+            "tools_paintrest":          self.app.defaults["tools_paintrest"],
+
+            "tools_nccoperation":       self.app.defaults["tools_nccoperation"],
+            "tools_nccmargin":          self.app.defaults["tools_nccmargin"],
+            "tools_nccmethod":          self.app.defaults["tools_nccmethod"],
+            "tools_nccconnect":         self.app.defaults["tools_nccconnect"],
+            "tools_ncccontour":         self.app.defaults["tools_ncccontour"],
+            "tools_nccoverlap":         self.app.defaults["tools_nccoverlap"],
+            "tools_nccrest":            self.app.defaults["tools_nccrest"],
+            "tools_nccref":             self.app.defaults["tools_nccref"],
+            "tools_ncc_offset_choice":  self.app.defaults["tools_ncc_offset_choice"],
+            "tools_ncc_offset_value":   self.app.defaults["tools_ncc_offset_value"],
+            "tools_nccmilling_type":    self.app.defaults["tools_nccmilling_type"],
+
+            "tools_iso_passes":         self.app.defaults["tools_iso_passes"],
+            "tools_iso_overlap":        self.app.defaults["tools_iso_overlap"],
+            "tools_iso_milling_type":   self.app.defaults["tools_iso_milling_type"],
+            "tools_iso_follow":         self.app.defaults["tools_iso_follow"],
+            "tools_iso_isotype":        self.app.defaults["tools_iso_isotype"],
+
+            "tools_iso_rest":           self.app.defaults["tools_iso_rest"],
+            "tools_iso_combine_passes": self.app.defaults["tools_iso_combine_passes"],
+            "tools_iso_isoexcept":      self.app.defaults["tools_iso_isoexcept"],
+            "tools_iso_selection":      self.app.defaults["tools_iso_selection"],
+            "tools_iso_poly_ints":      self.app.defaults["tools_iso_poly_ints"],
+            "tools_iso_force":          self.app.defaults["tools_iso_force"],
+            "tools_iso_area_shape":     self.app.defaults["tools_iso_area_shape"]
         })
 
         # will store the geometry here for compatibility reason

+ 1 - 1
appParsers/ParsePDF.py

@@ -7,7 +7,7 @@
 
 from PyQt5 import QtCore
 
-from Common import GracefulException as grace
+from appCommon.Common import GracefulException as grace
 
 from shapely.geometry import Polygon, LineString, MultiPolygon
 

+ 134 - 71
appParsers/ParseSVG.py

@@ -38,9 +38,9 @@ def svgparselength(lengthstr):
     Parse an SVG length string into a float and a units
     string, if any.
 
-    :param lengthstr: SVG length string.
-    :return: Number and units pair.
-    :rtype: tuple(float, str|None)
+    :param lengthstr:   SVG length string.
+    :return:            Number and units pair.
+    :rtype:             tuple(float, str|None)
     """
 
     integer_re_str = r'[+-]?[0-9]+'
@@ -48,14 +48,32 @@ def svgparselength(lengthstr):
                     r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
     length_re_str = r'(' + number_re_str + r')(em|ex|px|in|cm|mm|pt|pc|%)?'
 
-    match = re.search(length_re_str, lengthstr)
-    if match:
-        return float(match.group(1)), match.group(2)
+    if lengthstr:
+        match = re.search(length_re_str, lengthstr)
+        if match:
+            return float(match.group(1)), match.group(2)
+    else:
+        return 0, 0
 
     return
 
 
-def path2shapely(path, object_type, res=1.0):
+def svgparse_viewbox(root):
+    val = root.get('viewBox')
+    if val is None:
+        return 1.0
+
+    res = [float(x) for x in val.split()] or [float(x) for x in val.split(',')]
+    w = svgparselength(root.get('width'))[0]
+    # h = svgparselength(root.get('height'))[0]
+
+    v_w = res[2]
+    # v_h = res[3]
+
+    return w / v_w
+
+
+def path2shapely(path, object_type, res=1.0, units='MM', factor=1.0):
     """
     Converts an svg.path.Path into a Shapely
     Polygon or LinearString.
@@ -63,6 +81,10 @@ def path2shapely(path, object_type, res=1.0):
     :param path:        svg.path.Path instance
     :param object_type:
     :param res:         Resolution (minimum step along path)
+    :param units:       FlatCAM units
+    :type units:        str
+    :param factor:      correction factor due of virtual units
+    :type factor:       float
     :return:            Shapely geometry object
     :rtype :            Polygon
     :rtype :            LineString
@@ -82,7 +104,7 @@ def path2shapely(path, object_type, res=1.0):
             if len(points) == 0 or points[-1] != (x, y):
                 points.append((x, y))
             end = component.end
-            points.append((end.real, end.imag))
+            points.append((factor * end.real, factor * end.imag))
             continue
 
         # Arc, CubicBezier or QuadraticBezier
@@ -95,10 +117,14 @@ def path2shapely(path, object_type, res=1.0):
             # steps = int(length / res + 0.5)
             steps = int(length) * 2
 
+            if units == 'IN':
+                steps *= 25
+
             # solve error when step is below 1,
-            # it may cause other problems, but LineString needs at least  two points
-            if steps == 0:
-                steps = 1
+            # it may cause other problems, but LineString needs at least two points
+            # later edit: made the minimum nr of steps to be 10; left it like that to see that steps can be 0
+            if steps == 0 or steps < 10:
+                steps = 10
 
             frac = 1.0 / steps
 
@@ -107,9 +133,9 @@ def path2shapely(path, object_type, res=1.0):
                 point = component.point(i * frac)
                 x, y = point.real, point.imag
                 if len(points) == 0 or points[-1] != (x, y):
-                    points.append((x, y))
+                    points.append((factor * x, factor * y))
             end = component.point(1.0)
-            points.append((end.real, end.imag))
+            points.append((factor * end.real, factor * end.imag))
             continue
 
         # Move
@@ -124,7 +150,7 @@ def path2shapely(path, object_type, res=1.0):
                     closed = False
                     start = component.start
                     x, y = start.real, start.imag
-                    points = [(x, y)]
+                    points = [(factor * x, factor * y)]
             continue
 
         closed = False
@@ -145,6 +171,7 @@ def path2shapely(path, object_type, res=1.0):
 
     if points:
         rings.append(points)
+
     try:
         rings = MultiLineString(rings)
     except Exception as e:
@@ -174,30 +201,35 @@ def path2shapely(path, object_type, res=1.0):
     return geometry
 
 
-def svgrect2shapely(rect, n_points=32):
+def svgrect2shapely(rect, n_points=32, factor=1.0):
     """
     Converts an SVG rect into Shapely geometry.
 
     :param rect:        Rect Element
     :type rect:         xml.etree.ElementTree.Element
-    :param n_points:    number of points to approximate circles
+    :param n_points:    number of points to approximate rectangles corners when having rounded corners
     :type n_points:     int
+    :param factor:      correction factor due of virtual units
+    :type factor:       float
     :return:            shapely.geometry.polygon.LinearRing
     """
     w = svgparselength(rect.get('width'))[0]
     h = svgparselength(rect.get('height'))[0]
+
     x_obj = rect.get('x')
     if x_obj is not None:
-        x = svgparselength(x_obj)[0]
+        x = svgparselength(x_obj)[0] * factor
     else:
         x = 0
+
     y_obj = rect.get('y')
     if y_obj is not None:
-        y = svgparselength(y_obj)[0]
+        y = svgparselength(y_obj)[0] * factor
     else:
         y = 0
-    rxstr = rect.get('rx')
-    rystr = rect.get('ry')
+
+    rxstr = rect.get('rx') * factor
+    rystr = rect.get('ry') * factor
 
     if rxstr is None and rystr is None:  # Sharp corners
         pts = [
@@ -244,42 +276,44 @@ def svgrect2shapely(rect, n_points=32):
     # return LinearRing(pts)
 
 
-def svgcircle2shapely(circle):
+def svgcircle2shapely(circle, n_points=64, factor=1.0):
     """
     Converts an SVG circle into Shapely geometry.
 
-    :param circle: Circle Element
-    :type circle: xml.etree.ElementTree.Element
-    :return: Shapely representation of the circle.
-    :rtype: shapely.geometry.polygon.LinearRing
+    :param circle:      Circle Element
+    :type circle:       xml.etree.ElementTree.Element
+    :param n_points:    circle resolution; nr of points to b e used to approximate a circle
+    :type n_points:     int
+    :return:            Shapely representation of the circle.
+    :rtype:             shapely.geometry.polygon.LinearRing
     """
     # cx = float(circle.get('cx'))
     # cy = float(circle.get('cy'))
     # r = float(circle.get('r'))
-    cx = svgparselength(circle.get('cx'))[0]  # TODO: No units support yet
-    cy = svgparselength(circle.get('cy'))[0]  # TODO: No units support yet
-    r = svgparselength(circle.get('r'))[0]  # TODO: No units support yet
+    cx = svgparselength(circle.get('cx'))[0] * factor  # TODO: No units support yet
+    cy = svgparselength(circle.get('cy'))[0] * factor  # TODO: No units support yet
+    r = svgparselength(circle.get('r'))[0] * factor  # TODO: No units support yet
 
-    # TODO: No resolution specified.
-    return Point(cx, cy).buffer(r)
+    return Point(cx, cy).buffer(r, resolution=n_points)
 
 
-def svgellipse2shapely(ellipse, n_points=64):
+def svgellipse2shapely(ellipse, n_points=64, factor=1.0):
     """
     Converts an SVG ellipse into Shapely geometry
 
-    :param ellipse: Ellipse Element
-    :type ellipse: xml.etree.ElementTree.Element
-    :param n_points: Number of discrete points in output.
-    :return: Shapely representation of the ellipse.
-    :rtype: shapely.geometry.polygon.LinearRing
+    :param ellipse:     Ellipse Element
+    :type ellipse:      xml.etree.ElementTree.Element
+    :param n_points:    Number of discrete points in output.
+    :type n_points:     int
+    :return:            Shapely representation of the ellipse.
+    :rtype:             shapely.geometry.polygon.LinearRing
     """
 
-    cx = svgparselength(ellipse.get('cx'))[0]  # TODO: No units support yet
-    cy = svgparselength(ellipse.get('cy'))[0]  # TODO: No units support yet
+    cx = svgparselength(ellipse.get('cx'))[0] * factor  # TODO: No units support yet
+    cy = svgparselength(ellipse.get('cy'))[0] * factor  # TODO: No units support yet
 
-    rx = svgparselength(ellipse.get('rx'))[0]  # TODO: No units support yet
-    ry = svgparselength(ellipse.get('ry'))[0]  # TODO: No units support yet
+    rx = svgparselength(ellipse.get('rx'))[0] * factor  # TODO: No units support yet
+    ry = svgparselength(ellipse.get('ry'))[0] * factor  # TODO: No units support yet
 
     t = np.arange(n_points, dtype=float) / n_points
     x = cx + rx * np.cos(2 * np.pi * t)
@@ -290,47 +324,74 @@ def svgellipse2shapely(ellipse, n_points=64):
     # return LinearRing(pts)
 
 
-def svgline2shapely(line):
+def svgline2shapely(line, factor=1.0):
     """
 
-    :param line: Line element
-    :type line: xml.etree.ElementTree.Element
-    :return: Shapely representation on the line.
-    :rtype: shapely.geometry.polygon.LinearRing
+    :param line:        Line element
+    :type line:         xml.etree.ElementTree.Element
+    :param factor:      correction factor due of virtual units
+    :type factor:       float
+    :return:            Shapely representation on the line.
+    :rtype:             shapely.geometry.polygon.LineString
     """
 
-    x1 = svgparselength(line.get('x1'))[0]
-    y1 = svgparselength(line.get('y1'))[0]
-    x2 = svgparselength(line.get('x2'))[0]
-    y2 = svgparselength(line.get('y2'))[0]
+    x1 = svgparselength(line.get('x1'))[0] * factor
+    y1 = svgparselength(line.get('y1'))[0] * factor
+    x2 = svgparselength(line.get('x2'))[0] * factor
+    y2 = svgparselength(line.get('y2'))[0] * factor
 
     return LineString([(x1, y1), (x2, y2)])
 
 
-def svgpolyline2shapely(polyline):
+def svgpolyline2shapely(polyline, factor=1.0):
+    """
+
+    :param polyline:    Polyline element
+    :type polyline:     xml.etree.ElementTree.Element
+    :param factor:      correction factor due of virtual units
+    :type factor:       float
+    :return:            Shapely representation of the PolyLine
+    :rtype:             shapely.geometry.polygon.LineString
+    """
 
     ptliststr = polyline.get('points')
-    points = parse_svg_point_list(ptliststr)
+    points = parse_svg_point_list(ptliststr, factor)
 
     return LineString(points)
 
 
-def svgpolygon2shapely(polygon):
+def svgpolygon2shapely(polygon, n_points=64, factor=1.0):
+    """
+    Convert a SVG polygon to a Shapely Polygon.
+
+    :param polygon:
+    :type polygon:
+    :param n_points:    circle resolution; nr of points to b e used to approximate a circle
+    :type n_points:     int
+    :param factor:      correction factor due of virtual units
+    :type factor:       float
+    :return:            Shapely Polygon
+    """
 
     ptliststr = polygon.get('points')
-    points = parse_svg_point_list(ptliststr)
+    points = parse_svg_point_list(ptliststr, factor)
 
-    return Polygon(points).buffer(0)
+    return Polygon(points).buffer(0, resolution=n_points)
     # return LinearRing(points)
 
 
-def getsvggeo(node, object_type, root=None):
+def getsvggeo(node, object_type, root=None, units='MM', res=64, factor=1.0):
     """
     Extracts and flattens all geometry from an SVG node
     into a list of Shapely geometry.
 
     :param node:        xml.etree.ElementTree.Element
     :param object_type:
+    :param root:
+    :param units:       FlatCAM units
+    :param res:         resolution to be used for circles buffering
+    :param factor:      correction factor due of virtual units
+    :type factor:       float
     :return:            List of Shapely geometry
     :rtype:             list
     """
@@ -343,46 +404,45 @@ def getsvggeo(node, object_type, root=None):
     # Recurse
     if len(node) > 0:
         for child in node:
-            subgeo = getsvggeo(child, object_type, root)
+            subgeo = getsvggeo(child, object_type, root=root, units=units, res=res, factor=factor)
             if subgeo is not None:
                 geo += subgeo
-
     # Parse
     elif kind == 'path':
         log.debug("***PATH***")
         P = parse_path(node.get('d'))
-        P = path2shapely(P, object_type)
+        P = path2shapely(P, object_type, units=units, factor=factor)
         # for path, the resulting geometry is already a list so no need to create a new one
         geo = P
 
     elif kind == 'rect':
         log.debug("***RECT***")
-        R = svgrect2shapely(node)
+        R = svgrect2shapely(node, n_points=res, factor=factor)
         geo = [R]
 
     elif kind == 'circle':
         log.debug("***CIRCLE***")
-        C = svgcircle2shapely(node)
+        C = svgcircle2shapely(node, n_points=res, factor=factor)
         geo = [C]
 
     elif kind == 'ellipse':
         log.debug("***ELLIPSE***")
-        E = svgellipse2shapely(node)
+        E = svgellipse2shapely(node, n_points=res, factor=factor)
         geo = [E]
 
     elif kind == 'polygon':
         log.debug("***POLYGON***")
-        poly = svgpolygon2shapely(node)
+        poly = svgpolygon2shapely(node, n_points=res, factor=factor)
         geo = [poly]
 
     elif kind == 'line':
         log.debug("***LINE***")
-        line = svgline2shapely(node)
+        line = svgline2shapely(node, factor=factor)
         geo = [line]
 
     elif kind == 'polyline':
         log.debug("***POLYLINE***")
-        pline = svgpolyline2shapely(node)
+        pline = svgpolyline2shapely(node, factor=factor)
         geo = [pline]
 
     elif kind == 'use':
@@ -392,7 +452,7 @@ def getsvggeo(node, object_type, root=None):
         href = node.attrib['href'] if 'href' in node.attrib else node.attrib['{http://www.w3.org/1999/xlink}href']
         ref = root.find(".//*[@id='%s']" % href.replace('#', ''))
         if ref is not None:
-            geo = getsvggeo(ref, object_type, root)
+            geo = getsvggeo(ref, object_type, root=root, units=units, res=res, factor=factor)
 
     else:
         log.warning("Unknown kind: " + kind)
@@ -434,6 +494,7 @@ def getsvgtext(node, object_type, units='MM'):
 
     :param node:        xml.etree.ElementTree.Element
     :param object_type:
+    :param units:       FlatCAM units
     :return:            List of Shapely geometry
     :rtype:             list
     """
@@ -525,13 +586,15 @@ def getsvgtext(node, object_type, units='MM'):
     return geo
 
 
-def parse_svg_point_list(ptliststr):
+def parse_svg_point_list(ptliststr, factor):
     """
     Returns a list of coordinate pairs extracted from the "points"
     attribute in SVG polygons and polyline's.
 
-    :param ptliststr: "points" attribute string in polygon or polyline.
-    :return: List of tuples with coordinates.
+    :param ptliststr:       "points" attribute string in polygon or polyline.
+    :param factor:          correction factor due of virtual units
+    :type factor:           float
+    :return:                List of tuples with coordinates.
     """
 
     pairs = []
@@ -544,9 +607,9 @@ def parse_svg_point_list(ptliststr):
         val = float(ptliststr[pos:match.start()])
 
         if i % 2 == 1:
-            pairs.append((last, val))
+            pairs.append((factor * last, factor * val))
         else:
-            last = val
+            last = val * factor
 
         pos = match.end()
         i += 1
@@ -554,7 +617,7 @@ def parse_svg_point_list(ptliststr):
     # Check for last element
     val = float(ptliststr[pos:])
     if i % 2 == 1:
-        pairs.append((last, val))
+        pairs.append((factor * last, factor * val))
     else:
         log.warning("Incomplete coordinates.")
 

+ 199 - 162
appTools/ToolAlignObjects.py

@@ -5,10 +5,10 @@
 # MIT Licence                                              #
 # ##########################################################
 
-from PyQt5 import QtWidgets, QtCore
+from PyQt5 import QtWidgets, QtCore, QtGui
 from appTool import AppTool
 
-from appGUI.GUIElements import FCComboBox, RadioSet
+from appGUI.GUIElements import FCComboBox, RadioSet, FCLabel, FCButton
 
 import math
 
@@ -39,154 +39,17 @@ class AlignObjects(AppTool):
 
         self.canvas = self.app.plotcanvas
 
-        # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-
-        self.layout.addWidget(QtWidgets.QLabel(''))
-
-        # Form Layout
-        grid0 = QtWidgets.QGridLayout()
-        grid0.setColumnStretch(0, 0)
-        grid0.setColumnStretch(1, 1)
-        self.layout.addLayout(grid0)
-
-        self.aligned_label = QtWidgets.QLabel('<b>%s:</b>' % _("MOVING object"))
-        grid0.addWidget(self.aligned_label, 0, 0, 1, 2)
-
-        self.aligned_label.setToolTip(
-            _("Specify the type of object to be aligned.\n"
-              "It can be of type: Gerber or Excellon.\n"
-              "The selection here decide the type of objects that will be\n"
-              "in the Object combobox.")
-        )
-
-        # Type of object to be aligned
-        self.type_obj_radio = RadioSet([
-            {"label": _("Gerber"), "value": "grb"},
-            {"label": _("Excellon"), "value": "exc"},
-        ])
-
-        grid0.addWidget(self.type_obj_radio, 3, 0, 1, 2)
-
-        # Object to be aligned
-        self.object_combo = FCComboBox()
-        self.object_combo.setModel(self.app.collection)
-        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.object_combo.is_last = True
-
-        self.object_combo.setToolTip(
-            _("Object to be aligned.")
-        )
-
-        grid0.addWidget(self.object_combo, 4, 0, 1, 2)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 5, 0, 1, 2)
-
-        grid0.addWidget(QtWidgets.QLabel(''), 6, 0, 1, 2)
-
-        self.aligned_label = QtWidgets.QLabel('<b>%s:</b>' % _("DESTINATION object"))
-        self.aligned_label.setToolTip(
-            _("Specify the type of object to be aligned to.\n"
-              "It can be of type: Gerber or Excellon.\n"
-              "The selection here decide the type of objects that will be\n"
-              "in the Object combobox.")
-        )
-        grid0.addWidget(self.aligned_label, 7, 0, 1, 2)
-
-        # Type of object to be aligned to = aligner
-        self.type_aligner_obj_radio = RadioSet([
-            {"label": _("Gerber"), "value": "grb"},
-            {"label": _("Excellon"), "value": "exc"},
-        ])
-
-        grid0.addWidget(self.type_aligner_obj_radio, 8, 0, 1, 2)
-
-        # Object to be aligned to = aligner
-        self.aligner_object_combo = FCComboBox()
-        self.aligner_object_combo.setModel(self.app.collection)
-        self.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.aligner_object_combo.is_last = True
-
-        self.aligner_object_combo.setToolTip(
-            _("Object to be aligned to. Aligner.")
-        )
-
-        grid0.addWidget(self.aligner_object_combo, 9, 0, 1, 2)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 10, 0, 1, 2)
-
-        grid0.addWidget(QtWidgets.QLabel(''), 11, 0, 1, 2)
-
-        # Alignment Type
-        self.a_type_lbl = QtWidgets.QLabel('<b>%s:</b>' % _("Alignment Type"))
-        self.a_type_lbl.setToolTip(
-            _("The type of alignment can be:\n"
-              "- Single Point -> it require a single point of sync, the action will be a translation\n"
-              "- Dual Point -> it require two points of sync, the action will be translation followed by rotation")
-        )
-        self.a_type_radio = RadioSet(
-            [
-                {'label': _('Single Point'), 'value': 'sp'},
-                {'label': _('Dual Point'), 'value': 'dp'}
-            ])
-
-        grid0.addWidget(self.a_type_lbl, 12, 0, 1, 2)
-        grid0.addWidget(self.a_type_radio, 13, 0, 1, 2)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 14, 0, 1, 2)
-
-        # Buttons
-        self.align_object_button = QtWidgets.QPushButton(_("Align Object"))
-        self.align_object_button.setToolTip(
-            _("Align the specified object to the aligner object.\n"
-              "If only one point is used then it assumes translation.\n"
-              "If tho points are used it assume translation and rotation.")
-        )
-        self.align_object_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.align_object_button)
-
-        self.layout.addStretch()
-
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = AlignUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
         # Signals
-        self.align_object_button.clicked.connect(self.on_align)
-        self.type_obj_radio.activated_custom.connect(self.on_type_obj_changed)
-        self.type_aligner_obj_radio.activated_custom.connect(self.on_type_aligner_changed)
-        self.reset_button.clicked.connect(self.set_tool_ui)
+        self.ui.align_object_button.clicked.connect(self.on_align)
+        self.ui.type_obj_radio.activated_custom.connect(self.on_type_obj_changed)
+        self.ui.type_aligner_obj_radio.activated_custom.connect(self.on_type_aligner_changed)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
 
         self.mr = None
 
@@ -256,39 +119,39 @@ class AlignObjects(AppTool):
         self.aligned_old_fill_color = None
         self.aligned_old_line_color = None
 
-        self.a_type_radio.set_value(self.app.defaults["tools_align_objects_align_type"])
-        self.type_obj_radio.set_value('grb')
-        self.type_aligner_obj_radio.set_value('grb')
+        self.ui.a_type_radio.set_value(self.app.defaults["tools_align_objects_align_type"])
+        self.ui.type_obj_radio.set_value('grb')
+        self.ui.type_aligner_obj_radio.set_value('grb')
 
         if self.local_connected is True:
             self.disconnect_cal_events()
 
     def on_type_obj_changed(self, val):
         obj_type = {'grb': 0, 'exc': 1}[val]
-        self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        self.object_combo.setCurrentIndex(0)
-        self.object_combo.obj_type = {'grb': "Gerber", 'exc': "Excellon"}[val]
+        self.ui.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.ui.object_combo.setCurrentIndex(0)
+        self.ui.object_combo.obj_type = {'grb': "Gerber", 'exc': "Excellon"}[val]
 
     def on_type_aligner_changed(self, val):
         obj_type = {'grb': 0, 'exc': 1}[val]
-        self.aligner_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        self.aligner_object_combo.setCurrentIndex(0)
-        self.aligner_object_combo.obj_type = {'grb': "Gerber", 'exc': "Excellon"}[val]
+        self.ui.aligner_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.ui.aligner_object_combo.setCurrentIndex(0)
+        self.ui.aligner_object_combo.obj_type = {'grb': "Gerber", 'exc': "Excellon"}[val]
 
     def on_align(self):
         self.app.delete_selection_shape()
 
-        obj_sel_index = self.object_combo.currentIndex()
-        obj_model_index = self.app.collection.index(obj_sel_index, 0, self.object_combo.rootModelIndex())
+        obj_sel_index = self.ui.object_combo.currentIndex()
+        obj_model_index = self.app.collection.index(obj_sel_index, 0, self.ui.object_combo.rootModelIndex())
         try:
             self.aligned_obj = obj_model_index.internalPointer().obj
         except AttributeError:
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no aligned FlatCAM object selected..."))
             return
 
-        aligner_obj_sel_index = self.aligner_object_combo.currentIndex()
+        aligner_obj_sel_index = self.ui.aligner_object_combo.currentIndex()
         aligner_obj_model_index = self.app.collection.index(
-            aligner_obj_sel_index, 0, self.aligner_object_combo.rootModelIndex())
+            aligner_obj_sel_index, 0, self.ui.aligner_object_combo.rootModelIndex())
 
         try:
             self.aligner_obj = aligner_obj_model_index.internalPointer().obj
@@ -296,7 +159,7 @@ class AlignObjects(AppTool):
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no aligner FlatCAM object selected..."))
             return
 
-        self.align_type = self.a_type_radio.get_value()
+        self.align_type = self.ui.a_type_radio.get_value()
 
         # disengage the grid snapping since it will be hard to find the drills or pads on grid
         if self.app.ui.grid_snap_btn.isChecked():
@@ -488,5 +351,179 @@ class AlignObjects(AppTool):
         )
 
     def reset_fields(self):
+        self.ui.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ui.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+
+
+class AlignUI:
+
+    toolName = _("Align Objects")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        # ## Title
+        title_label = FCLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+
+        self.layout.addWidget(QtWidgets.QLabel(""))
+
+        # Form Layout
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.layout.addLayout(grid0)
+
+        self.aligned_label =FCLabel('<b>%s:</b>' % _("MOVING object"))
+        grid0.addWidget(self.aligned_label, 0, 0, 1, 2)
+
+        self.aligned_label.setToolTip(
+            _("Specify the type of object to be aligned.\n"
+              "It can be of type: Gerber or Excellon.\n"
+              "The selection here decide the type of objects that will be\n"
+              "in the Object combobox.")
+        )
+
+        # Type of object to be aligned
+        self.type_obj_radio = RadioSet([
+            {"label": _("Gerber"), "value": "grb"},
+            {"label": _("Excellon"), "value": "exc"},
+        ])
+
+        grid0.addWidget(self.type_obj_radio, 3, 0, 1, 2)
+
+        # Object to be aligned
+        self.object_combo = FCComboBox()
+        self.object_combo.setModel(self.app.collection)
         self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.object_combo.is_last = True
+
+        self.object_combo.setToolTip(
+            _("Object to be aligned.")
+        )
+
+        grid0.addWidget(self.object_combo, 4, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 5, 0, 1, 2)
+
+        grid0.addWidget(FCLabel(''), 6, 0, 1, 2)
+
+        self.aligned_label = FCLabel('<b>%s:</b>' % _("DESTINATION object"))
+        self.aligned_label.setToolTip(
+            _("Specify the type of object to be aligned to.\n"
+              "It can be of type: Gerber or Excellon.\n"
+              "The selection here decide the type of objects that will be\n"
+              "in the Object combobox.")
+        )
+        grid0.addWidget(self.aligned_label, 7, 0, 1, 2)
+
+        # Type of object to be aligned to = aligner
+        self.type_aligner_obj_radio = RadioSet([
+            {"label": _("Gerber"), "value": "grb"},
+            {"label": _("Excellon"), "value": "exc"},
+        ])
+
+        grid0.addWidget(self.type_aligner_obj_radio, 8, 0, 1, 2)
+
+        # Object to be aligned to = aligner
+        self.aligner_object_combo = FCComboBox()
+        self.aligner_object_combo.setModel(self.app.collection)
         self.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.aligner_object_combo.is_last = True
+
+        self.aligner_object_combo.setToolTip(
+            _("Object to be aligned to. Aligner.")
+        )
+
+        grid0.addWidget(self.aligner_object_combo, 9, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 10, 0, 1, 2)
+
+        grid0.addWidget(QtWidgets.QLabel(''), 11, 0, 1, 2)
+
+        # Alignment Type
+        self.a_type_lbl = FCLabel('<b>%s:</b>' % _("Alignment Type"))
+        self.a_type_lbl.setToolTip(
+            _("The type of alignment can be:\n"
+              "- Single Point -> it require a single point of sync, the action will be a translation\n"
+              "- Dual Point -> it require two points of sync, the action will be translation followed by rotation")
+        )
+        self.a_type_radio = RadioSet(
+            [
+                {'label': _('Single Point'), 'value': 'sp'},
+                {'label': _('Dual Point'), 'value': 'dp'}
+            ])
+
+        grid0.addWidget(self.a_type_lbl, 12, 0, 1, 2)
+        grid0.addWidget(self.a_type_radio, 13, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 14, 0, 1, 2)
+
+        # Buttons
+        self.align_object_button =FCButton(_("Align Object"))
+        self.align_object_button.setToolTip(
+            _("Align the specified object to the aligner object.\n"
+              "If only one point is used then it assumes translation.\n"
+              "If tho points are used it assume translation and rotation.")
+        )
+        self.align_object_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.align_object_button)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = FCButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
+
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 158 - 125
appTools/ToolCalculators.py

@@ -5,7 +5,7 @@
 # MIT Licence                                              #
 # ##########################################################
 
-from PyQt5 import QtWidgets
+from PyQt5 import QtWidgets, QtGui
 from appTool import AppTool
 from appGUI.GUIElements import FCSpinner, FCDoubleSpinner, NumericalEvalEntry
 import math
@@ -21,26 +21,152 @@ if '_' not in builtins.__dict__:
 
 class ToolCalculator(AppTool):
 
+    def __init__(self, app):
+        AppTool.__init__(self, app)
+
+        self.app = app
+        self.decimals = self.app.decimals
+
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = CalcUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
+
+        self.units = ''
+
+        # ## Signals
+        self.ui.cutDepth_entry.valueChanged.connect(self.on_calculate_tool_dia)
+        self.ui.cutDepth_entry.returnPressed.connect(self.on_calculate_tool_dia)
+        self.ui.tipDia_entry.returnPressed.connect(self.on_calculate_tool_dia)
+        self.ui.tipAngle_entry.returnPressed.connect(self.on_calculate_tool_dia)
+        self.ui.calculate_vshape_button.clicked.connect(self.on_calculate_tool_dia)
+
+        self.ui.mm_entry.editingFinished.connect(self.on_calculate_inch_units)
+        self.ui.inch_entry.editingFinished.connect(self.on_calculate_mm_units)
+
+        self.ui.calculate_plate_button.clicked.connect(self.on_calculate_eplate)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
+
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("ToolCalculators()")
+
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+
+        AppTool.run(self)
+
+        self.set_tool_ui()
+
+        self.app.ui.notebook.setTabText(2, _("Calc. Tool"))
+
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='Alt+C', **kwargs)
+
+    def set_tool_ui(self):
+        self.units = self.app.defaults['units'].upper()
+
+        # ## Initialize form
+        self.ui.mm_entry.set_value('%.*f' % (self.decimals, 0))
+        self.ui.inch_entry.set_value('%.*f' % (self.decimals, 0))
+
+        length = self.app.defaults["tools_calc_electro_length"]
+        width = self.app.defaults["tools_calc_electro_width"]
+        density = self.app.defaults["tools_calc_electro_cdensity"]
+        growth = self.app.defaults["tools_calc_electro_growth"]
+        self.ui.pcblength_entry.set_value(length)
+        self.ui.pcbwidth_entry.set_value(width)
+        self.ui.cdensity_entry.set_value(density)
+        self.ui.growth_entry.set_value(growth)
+        self.ui.cvalue_entry.set_value(0.00)
+        self.ui.time_entry.set_value(0.0)
+
+        tip_dia = self.app.defaults["tools_calc_vshape_tip_dia"]
+        tip_angle = self.app.defaults["tools_calc_vshape_tip_angle"]
+        cut_z = self.app.defaults["tools_calc_vshape_cut_z"]
+
+        self.ui.tipDia_entry.set_value(tip_dia)
+        self.ui.tipAngle_entry.set_value(tip_angle)
+        self.ui.cutDepth_entry.set_value(cut_z)
+        self.ui.effectiveToolDia_entry.set_value('0.0000')
+
+    def on_calculate_tool_dia(self):
+        # Calculation:
+        # Manufacturer gives total angle of the the tip but we need only half of it
+        # tangent(half_tip_angle) = opposite side / adjacent = part_of _real_dia / depth_of_cut
+        # effective_diameter = tip_diameter + part_of_real_dia_left_side + part_of_real_dia_right_side
+        # tool is symmetrical therefore: part_of_real_dia_left_side = part_of_real_dia_right_side
+        # effective_diameter = tip_diameter + (2 * part_of_real_dia_left_side)
+        # effective diameter = tip_diameter + (2 * depth_of_cut * tangent(half_tip_angle))
+
+        tip_diameter = float(self.ui.tipDia_entry.get_value())
+
+        half_tip_angle = float(self.ui.tipAngle_entry.get_value()) / 2.0
+
+        cut_depth = float(self.ui.cutDepth_entry.get_value())
+        cut_depth = -cut_depth if cut_depth < 0 else cut_depth
+
+        tool_diameter = tip_diameter + (2 * cut_depth * math.tan(math.radians(half_tip_angle)))
+        self.ui.effectiveToolDia_entry.set_value("%.*f" % (self.decimals, tool_diameter))
+
+    def on_calculate_inch_units(self):
+        mm_val = float(self.mm_entry.get_value())
+        self.ui.inch_entry.set_value('%.*f' % (self.decimals, (mm_val / 25.4)))
+
+    def on_calculate_mm_units(self):
+        inch_val = float(self.inch_entry.get_value())
+        self.ui.mm_entry.set_value('%.*f' % (self.decimals, (inch_val * 25.4)))
+
+    def on_calculate_eplate(self):
+        length = float(self.ui.pcblength_entry.get_value())
+        width = float(self.ui.pcbwidth_entry.get_value())
+        density = float(self.ui.cdensity_entry.get_value())
+        copper = float(self.ui.growth_entry.get_value())
+
+        calculated_current = (length * width * density) * 0.0021527820833419
+        calculated_time = copper * 2.142857142857143 * float(20 / density)
+
+        self.ui.cvalue_entry.set_value('%.2f' % calculated_current)
+        self.ui.time_entry.set_value('%.1f' % calculated_time)
+
+
+class CalcUI:
+
     toolName = _("Calculators")
     v_shapeName = _("V-Shape Tool Calculator")
     unitsName = _("Units Calculator")
     eplateName = _("ElectroPlating Calculator")
 
-    def __init__(self, app):
-        AppTool.__init__(self, app)
-
+    def __init__(self, layout, app):
         self.app = app
         self.decimals = self.app.decimals
+        self.layout = layout
 
         # ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
         self.layout.addWidget(title_label)
 
         # #####################
@@ -244,127 +370,34 @@ class ToolCalculator(AppTool):
 
         # ## Reset Tool
         self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
         self.reset_button.setToolTip(
             _("Will reset the tool parameters.")
         )
         self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
         self.layout.addWidget(self.reset_button)
 
-        self.units = ''
-
-        # ## Signals
-        self.cutDepth_entry.valueChanged.connect(self.on_calculate_tool_dia)
-        self.cutDepth_entry.returnPressed.connect(self.on_calculate_tool_dia)
-        self.tipDia_entry.returnPressed.connect(self.on_calculate_tool_dia)
-        self.tipAngle_entry.returnPressed.connect(self.on_calculate_tool_dia)
-        self.calculate_vshape_button.clicked.connect(self.on_calculate_tool_dia)
-
-        self.mm_entry.editingFinished.connect(self.on_calculate_inch_units)
-        self.inch_entry.editingFinished.connect(self.on_calculate_mm_units)
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
 
-        self.calculate_plate_button.clicked.connect(self.on_calculate_eplate)
-        self.reset_button.clicked.connect(self.set_tool_ui)
-
-    def run(self, toggle=True):
-        self.app.defaults.report_usage("ToolCalculators()")
-
-        if toggle:
-            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-            else:
-                try:
-                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        # if tab is populated with the tool but it does not have the focus, focus on it
-                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
-                            # focus on Tool Tab
-                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-                        else:
-                            self.app.ui.splitter.setSizes([0, 1])
-                except AttributeError:
-                    pass
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
         else:
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-
-        AppTool.run(self)
-
-        self.set_tool_ui()
-
-        self.app.ui.notebook.setTabText(2, _("Calc. Tool"))
-
-    def install(self, icon=None, separator=None, **kwargs):
-        AppTool.install(self, icon, separator, shortcut='Alt+C', **kwargs)
-
-    def set_tool_ui(self):
-        self.units = self.app.defaults['units'].upper()
-
-        # ## Initialize form
-        self.mm_entry.set_value('%.*f' % (self.decimals, 0))
-        self.inch_entry.set_value('%.*f' % (self.decimals, 0))
-
-        length = self.app.defaults["tools_calc_electro_length"]
-        width = self.app.defaults["tools_calc_electro_width"]
-        density = self.app.defaults["tools_calc_electro_cdensity"]
-        growth = self.app.defaults["tools_calc_electro_growth"]
-        self.pcblength_entry.set_value(length)
-        self.pcbwidth_entry.set_value(width)
-        self.cdensity_entry.set_value(density)
-        self.growth_entry.set_value(growth)
-        self.cvalue_entry.set_value(0.00)
-        self.time_entry.set_value(0.0)
-
-        tip_dia = self.app.defaults["tools_calc_vshape_tip_dia"]
-        tip_angle = self.app.defaults["tools_calc_vshape_tip_angle"]
-        cut_z = self.app.defaults["tools_calc_vshape_cut_z"]
-
-        self.tipDia_entry.set_value(tip_dia)
-        self.tipAngle_entry.set_value(tip_angle)
-        self.cutDepth_entry.set_value(cut_z)
-        self.effectiveToolDia_entry.set_value('0.0000')
-
-    def on_calculate_tool_dia(self):
-        # Calculation:
-        # Manufacturer gives total angle of the the tip but we need only half of it
-        # tangent(half_tip_angle) = opposite side / adjacent = part_of _real_dia / depth_of_cut
-        # effective_diameter = tip_diameter + part_of_real_dia_left_side + part_of_real_dia_right_side
-        # tool is symmetrical therefore: part_of_real_dia_left_side = part_of_real_dia_right_side
-        # effective_diameter = tip_diameter + (2 * part_of_real_dia_left_side)
-        # effective diameter = tip_diameter + (2 * depth_of_cut * tangent(half_tip_angle))
-
-        tip_diameter = float(self.tipDia_entry.get_value())
-
-        half_tip_angle = float(self.tipAngle_entry.get_value()) / 2.0
-
-        cut_depth = float(self.cutDepth_entry.get_value())
-        cut_depth = -cut_depth if cut_depth < 0 else cut_depth
-
-        tool_diameter = tip_diameter + (2 * cut_depth * math.tan(math.radians(half_tip_angle)))
-        self.effectiveToolDia_entry.set_value("%.*f" % (self.decimals, tool_diameter))
-
-    def on_calculate_inch_units(self):
-        mm_val = float(self.mm_entry.get_value())
-        self.inch_entry.set_value('%.*f' % (self.decimals, (mm_val / 25.4)))
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
 
-    def on_calculate_mm_units(self):
-        inch_val = float(self.inch_entry.get_value())
-        self.mm_entry.set_value('%.*f' % (self.decimals, (inch_val * 25.4)))
-
-    def on_calculate_eplate(self):
-        length = float(self.pcblength_entry.get_value())
-        width = float(self.pcbwidth_entry.get_value())
-        density = float(self.cdensity_entry.get_value())
-        copper = float(self.growth_entry.get_value())
-
-        calculated_current = (length * width * density) * 0.0021527820833419
-        calculated_time = copper * 2.142857142857143 * float(20 / density)
-
-        self.cvalue_entry.set_value('%.2f' % calculated_current)
-        self.time_entry.set_value('%.1f' % calculated_time)
-
-# end of file
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 1125 - 1100
appTools/ToolCalibration.py

@@ -10,7 +10,7 @@ from PyQt5 import QtWidgets, QtCore, QtGui
 from appTool import AppTool
 from appGUI.GUIElements import FCDoubleSpinner, EvalEntry, FCCheckBox, OptionalInputSection, FCEntry
 from appGUI.GUIElements import FCTable, FCComboBox, RadioSet
-from appEditors.FlatCAMTextEditor import TextEditor
+from appEditors.AppTextEditor import AppTextEditor
 
 from shapely.geometry import Point
 from shapely.geometry.base import *
@@ -34,8 +34,6 @@ log = logging.getLogger('base')
 
 class ToolCalibration(AppTool):
 
-    toolName = _("Calibration Tool")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
 
@@ -44,1340 +42,1367 @@ class ToolCalibration(AppTool):
 
         self.decimals = self.app.decimals
 
-        # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = CalibUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
-        self.layout.addWidget(QtWidgets.QLabel(''))
+        self.mr = None
+        self.units = ''
 
-        # ## Grid Layout
-        grid_lay = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid_lay)
-        grid_lay.setColumnStretch(0, 0)
-        grid_lay.setColumnStretch(1, 1)
-        grid_lay.setColumnStretch(2, 0)
+        # here store 4 points to be used for calibration
+        self.click_points = [[], [], [], []]
 
-        self.gcode_title_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
-        self.gcode_title_label.setToolTip(
-            _("Parameters used when creating the GCode in this tool.")
-        )
-        grid_lay.addWidget(self.gcode_title_label, 0, 0, 1, 3)
+        # store the status of the grid
+        self.grid_status_memory = None
 
-        # Travel Z entry
-        travelz_lbl = QtWidgets.QLabel('%s:' % _("Travel Z"))
-        travelz_lbl.setToolTip(
-            _("Height (Z) for travelling between the points.")
-        )
+        self.target_obj = None
 
-        self.travelz_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.travelz_entry.set_range(-9999.9999, 9999.9999)
-        self.travelz_entry.set_precision(self.decimals)
-        self.travelz_entry.setSingleStep(0.1)
+        # if the mouse events are connected to a local method set this True
+        self.local_connected = False
 
-        grid_lay.addWidget(travelz_lbl, 1, 0)
-        grid_lay.addWidget(self.travelz_entry, 1, 1, 1, 2)
+        # reference for the tab where to open and view the verification GCode
+        self.gcode_editor_tab = None
 
-        # Verification Z entry
-        verz_lbl = QtWidgets.QLabel('%s:' % _("Verification Z"))
-        verz_lbl.setToolTip(
-            _("Height (Z) for checking the point.")
-        )
+        # calibrated object
+        self.cal_object = None
 
-        self.verz_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.verz_entry.set_range(-9999.9999, 9999.9999)
-        self.verz_entry.set_precision(self.decimals)
-        self.verz_entry.setSingleStep(0.1)
+        # ## Signals
+        self.ui.cal_source_radio.activated_custom.connect(self.on_cal_source_radio)
+        self.ui.obj_type_combo.currentIndexChanged.connect(self.on_obj_type_combo)
+        self.ui.adj_object_type_combo.currentIndexChanged.connect(self.on_adj_obj_type_combo)
 
-        grid_lay.addWidget(verz_lbl, 2, 0)
-        grid_lay.addWidget(self.verz_entry, 2, 1, 1, 2)
+        self.ui.start_button.clicked.connect(self.on_start_collect_points)
 
-        # Zero the Z of the verification tool
-        self.zeroz_cb = FCCheckBox('%s' % _("Zero Z tool"))
-        self.zeroz_cb.setToolTip(
-            _("Include a sequence to zero the height (Z)\n"
-              "of the verification tool.")
-        )
+        self.ui.gcode_button.clicked.connect(self.generate_verification_gcode)
+        self.ui.adj_gcode_button.clicked.connect(self.generate_verification_gcode)
 
-        grid_lay.addWidget(self.zeroz_cb, 3, 0, 1, 3)
+        self.ui.generate_factors_button.clicked.connect(self.calculate_factors)
 
-        # Toolchange Z entry
-        toolchangez_lbl = QtWidgets.QLabel('%s:' % _("Toolchange Z"))
-        toolchangez_lbl.setToolTip(
-            _("Height (Z) for mounting the verification probe.")
-        )
+        self.ui.scale_button.clicked.connect(self.on_scale_button)
+        self.ui.skew_button.clicked.connect(self.on_skew_button)
 
-        self.toolchangez_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.toolchangez_entry.set_range(0.0000, 9999.9999)
-        self.toolchangez_entry.set_precision(self.decimals)
-        self.toolchangez_entry.setSingleStep(0.1)
+        self.ui.cal_button.clicked.connect(self.on_cal_button_click)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
 
-        grid_lay.addWidget(toolchangez_lbl, 4, 0)
-        grid_lay.addWidget(self.toolchangez_entry, 4, 1, 1, 2)
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("ToolCalibration()")
 
-        # Toolchange X-Y entry
-        toolchangexy_lbl = QtWidgets.QLabel('%s:' % _('Toolchange X-Y'))
-        toolchangexy_lbl.setToolTip(
-            _("Toolchange X,Y position.\n"
-              "If no value is entered then the current\n"
-              "(x, y) point will be used,")
-        )
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
 
-        self.toolchange_xy_entry = FCEntry()
+        AppTool.run(self)
 
-        grid_lay.addWidget(toolchangexy_lbl, 5, 0)
-        grid_lay.addWidget(self.toolchange_xy_entry, 5, 1, 1, 2)
+        self.set_tool_ui()
 
-        self.z_ois = OptionalInputSection(
-            self.zeroz_cb,
-            [
-                toolchangez_lbl,
-                self.toolchangez_entry,
-                toolchangexy_lbl,
-                self.toolchange_xy_entry
-            ]
-        )
+        self.app.ui.notebook.setTabText(2, _("Calibration Tool"))
 
-        separator_line1 = QtWidgets.QFrame()
-        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line1, 6, 0, 1, 3)
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='Alt+E', **kwargs)
 
-        # Second point choice
-        second_point_lbl = QtWidgets.QLabel('%s:' % _("Second point"))
-        second_point_lbl.setToolTip(
-            _("Second point in the Gcode verification can be:\n"
-              "- top-left -> the user will align the PCB vertically\n"
-              "- bottom-right -> the user will align the PCB horizontally")
-        )
-        self.second_point_radio = RadioSet([{'label': _('Top-Left'), 'value': 'tl'},
-                                            {'label': _('Bottom-Right'), 'value': 'br'}],
-                                           orientation='vertical')
+    def set_tool_ui(self):
+        self.units = self.app.defaults['units'].upper()
 
-        grid_lay.addWidget(second_point_lbl, 7, 0)
-        grid_lay.addWidget(self.second_point_radio, 7, 1, 1, 2)
+        if self.local_connected is True:
+            self.disconnect_cal_events()
 
-        separator_line1 = QtWidgets.QFrame()
-        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line1, 8, 0, 1, 3)
+        self.ui.bottom_left_coordx_found.set_value(_("Origin"))
+        self.ui.bottom_left_coordy_found.set_value(_("Origin"))
 
-        grid_lay.addWidget(QtWidgets.QLabel(''), 9, 0, 1, 3)
+        self.reset_calibration_points()
 
-        step_1 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 1: Acquire Calibration Points"))
-        step_1.setToolTip(
-            _("Pick four points by clicking on canvas.\n"
-              "Those four points should be in the four\n"
-              "(as much as possible) corners of the object.")
-        )
-        grid_lay.addWidget(step_1, 10, 0, 1, 3)
+        self.ui.cal_source_radio.set_value(self.app.defaults['tools_cal_calsource'])
+        self.ui.travelz_entry.set_value(self.app.defaults['tools_cal_travelz'])
+        self.ui.verz_entry.set_value(self.app.defaults['tools_cal_verz'])
+        self.ui.zeroz_cb.set_value(self.app.defaults['tools_cal_zeroz'])
+        self.ui.toolchangez_entry.set_value(self.app.defaults['tools_cal_toolchangez'])
+        self.ui.toolchange_xy_entry.set_value(self.app.defaults['tools_cal_toolchange_xy'])
 
-        self.cal_source_lbl = QtWidgets.QLabel("<b>%s:</b>" % _("Source Type"))
-        self.cal_source_lbl.setToolTip(_("The source of calibration points.\n"
-                                         "It can be:\n"
-                                         "- Object -> click a hole geo for Excellon or a pad for Gerber\n"
-                                         "- Free -> click freely on canvas to acquire the calibration points"))
-        self.cal_source_radio = RadioSet([{'label': _('Object'), 'value': 'object'},
-                                          {'label': _('Free'), 'value': 'free'}],
-                                         stretch=False)
+        self.ui.second_point_radio.set_value(self.app.defaults['tools_cal_sec_point'])
 
-        grid_lay.addWidget(self.cal_source_lbl, 11, 0)
-        grid_lay.addWidget(self.cal_source_radio, 11, 1, 1, 2)
+        self.ui.scalex_entry.set_value(1.0)
+        self.ui.scaley_entry.set_value(1.0)
+        self.ui.skewx_entry.set_value(0.0)
+        self.ui.skewy_entry.set_value(0.0)
 
-        self.obj_type_label = QtWidgets.QLabel("%s:" % _("Object Type"))
+        # default object selection is Excellon = index_1
+        self.ui.obj_type_combo.setCurrentIndex(1)
+        self.on_obj_type_combo()
 
-        self.obj_type_combo = FCComboBox()
-        self.obj_type_combo.addItem(_("Gerber"))
-        self.obj_type_combo.addItem(_("Excellon"))
+        self.ui.adj_object_type_combo.setCurrentIndex(0)
+        self.on_adj_obj_type_combo()
+        # self.adj_object_combo.setCurrentIndex(0)
 
-        self.obj_type_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
-        self.obj_type_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
+        # calibrated object
+        self.cal_object = None
 
-        grid_lay.addWidget(self.obj_type_label, 12, 0)
-        grid_lay.addWidget(self.obj_type_combo, 12, 1, 1, 2)
+        self.app.inform.emit('%s...' % _("Tool initialized"))
 
-        self.object_combo = FCComboBox()
-        self.object_combo.setModel(self.app.collection)
-        self.object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
-        self.object_combo.is_last = True
+    def on_obj_type_combo(self):
+        obj_type = self.ui.obj_type_combo.currentIndex()
+        self.ui.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        # self.object_combo.setCurrentIndex(0)
+        self.ui.object_combo.obj_type = {
+            _("Gerber"): "Gerber", _("Excellon"): "Excellon"
+        }[self.ui.obj_type_combo.get_value()]
 
-        self.object_label = QtWidgets.QLabel("%s:" % _("Source object selection"))
-        self.object_label.setToolTip(
-            _("FlatCAM Object to be used as a source for reference points.")
-        )
+    def on_adj_obj_type_combo(self):
+        obj_type = self.ui.adj_object_type_combo.currentIndex()
+        self.ui.adj_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        # self.adj_object_combo.setCurrentIndex(0)
+        self.ui.adj_object_combo.obj_type = {
+            _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
+        }[self.ui.adj_object_type_combo.get_value()]
 
-        grid_lay.addWidget(self.object_label, 13, 0, 1, 3)
-        grid_lay.addWidget(self.object_combo, 14, 0, 1, 3)
+    def on_cal_source_radio(self, val):
+        if val == 'object':
+            self.ui.obj_type_label.setDisabled(False)
+            self.ui.obj_type_combo.setDisabled(False)
+            self.ui.object_label.setDisabled(False)
+            self.ui.object_combo.setDisabled(False)
+        else:
+            self.ui.obj_type_label.setDisabled(True)
+            self.ui.obj_type_combo.setDisabled(True)
+            self.ui.object_label.setDisabled(True)
+            self.ui.object_combo.setDisabled(True)
 
-        self.points_table_label = QtWidgets.QLabel('<b>%s</b>' % _('Calibration Points'))
-        self.points_table_label.setToolTip(
-            _("Contain the expected calibration points and the\n"
-              "ones measured.")
-        )
-        grid_lay.addWidget(self.points_table_label, 15, 0, 1, 3)
+    def on_start_collect_points(self):
 
-        self.points_table = FCTable()
-        self.points_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
-        # self.points_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
-        grid_lay.addWidget(self.points_table, 16, 0, 1, 3)
+        if self.ui.cal_source_radio.get_value() == 'object':
+            selection_index = self.ui.object_combo.currentIndex()
+            model_index = self.app.collection.index(selection_index, 0, self.ui.object_combo.rootModelIndex())
+            try:
+                self.target_obj = model_index.internalPointer().obj
+            except AttributeError:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no source FlatCAM object selected..."))
+                return
 
-        self.points_table.setColumnCount(4)
-        self.points_table.setHorizontalHeaderLabels(
-            [
-                '#',
-                _("Name"),
-                _("Target"),
-                _("Found Delta")
-            ]
-        )
-        self.points_table.setRowCount(8)
-        row = 0
+        # disengage the grid snapping since it will be hard to find the drills on grid
+        if self.app.ui.grid_snap_btn.isChecked():
+            self.grid_status_memory = True
+            self.app.ui.grid_snap_btn.trigger()
+        else:
+            self.grid_status_memory = False
 
-        # BOTTOM LEFT
-        id_item_1 = QtWidgets.QTableWidgetItem('%d' % 1)
-        flags = QtCore.Qt.ItemIsEnabled
-        id_item_1.setFlags(flags)
-        self.points_table.setItem(row, 0, id_item_1)  # Tool name/id
+        self.mr = self.canvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
 
-        self.bottom_left_coordx_lbl = QtWidgets.QLabel('%s' % _('Bot Left X'))
-        self.points_table.setCellWidget(row, 1, self.bottom_left_coordx_lbl)
-        self.bottom_left_coordx_tgt = EvalEntry()
-        self.points_table.setCellWidget(row, 2, self.bottom_left_coordx_tgt)
-        self.bottom_left_coordx_tgt.setReadOnly(True)
-        self.bottom_left_coordx_found = EvalEntry()
-        self.points_table.setCellWidget(row, 3, self.bottom_left_coordx_found)
-        row += 1
+        if self.app.is_legacy is False:
+            self.canvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        else:
+            self.canvas.graph_event_disconnect(self.app.mr)
 
-        self.bottom_left_coordy_lbl = QtWidgets.QLabel('%s' % _('Bot Left Y'))
-        self.points_table.setCellWidget(row, 1, self.bottom_left_coordy_lbl)
-        self.bottom_left_coordy_tgt = EvalEntry()
-        self.points_table.setCellWidget(row, 2, self.bottom_left_coordy_tgt)
-        self.bottom_left_coordy_tgt.setReadOnly(True)
-        self.bottom_left_coordy_found = EvalEntry()
-        self.points_table.setCellWidget(row, 3, self.bottom_left_coordy_found)
+        self.local_connected = True
 
-        self.bottom_left_coordx_found.setDisabled(True)
-        self.bottom_left_coordy_found.setDisabled(True)
-        row += 1
+        self.reset_calibration_points()
 
-        # BOTTOM RIGHT
-        id_item_2 = QtWidgets.QTableWidgetItem('%d' % 2)
-        flags = QtCore.Qt.ItemIsEnabled
-        id_item_2.setFlags(flags)
-        self.points_table.setItem(row, 0, id_item_2)  # Tool name/id
+        self.app.inform.emit(_("Get First calibration point. Bottom Left..."))
 
-        self.bottom_right_coordx_lbl = QtWidgets.QLabel('%s' % _('Bot Right X'))
-        self.points_table.setCellWidget(row, 1, self.bottom_right_coordx_lbl)
-        self.bottom_right_coordx_tgt = EvalEntry()
-        self.points_table.setCellWidget(row, 2, self.bottom_right_coordx_tgt)
-        self.bottom_right_coordx_tgt.setReadOnly(True)
-        self.bottom_right_coordx_found = EvalEntry()
-        self.points_table.setCellWidget(row, 3, self.bottom_right_coordx_found)
+    def on_mouse_click_release(self, event):
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            right_button = 2
+            self.app.event_is_dragging = self.app.event_is_dragging
+        else:
+            event_pos = (event.xdata, event.ydata)
+            right_button = 3
+            self.app.event_is_dragging = self.app.ui.popMenu.mouse_is_panning
 
-        row += 1
+        pos_canvas = self.canvas.translate_coords(event_pos)
 
-        self.bottom_right_coordy_lbl = QtWidgets.QLabel('%s' % _('Bot Right Y'))
-        self.points_table.setCellWidget(row, 1, self.bottom_right_coordy_lbl)
-        self.bottom_right_coordy_tgt = EvalEntry()
-        self.points_table.setCellWidget(row, 2, self.bottom_right_coordy_tgt)
-        self.bottom_right_coordy_tgt.setReadOnly(True)
-        self.bottom_right_coordy_found = EvalEntry()
-        self.points_table.setCellWidget(row, 3, self.bottom_right_coordy_found)
-        row += 1
+        if event.button == 1:
+            click_pt = Point([pos_canvas[0], pos_canvas[1]])
 
-        # TOP LEFT
-        id_item_3 = QtWidgets.QTableWidgetItem('%d' % 3)
-        flags = QtCore.Qt.ItemIsEnabled
-        id_item_3.setFlags(flags)
-        self.points_table.setItem(row, 0, id_item_3)  # Tool name/id
+            if self.app.selection_type is not None:
+                # delete previous selection shape
+                self.app.delete_selection_shape()
+                self.app.selection_type = None
+            else:
+                if self.ui.cal_source_radio.get_value() == 'object':
+                    if self.target_obj.kind.lower() == 'excellon':
+                        for tool, tool_dict in self.target_obj.tools.items():
+                            for geo in tool_dict['solid_geometry']:
+                                if click_pt.within(geo):
+                                    center_pt = geo.centroid
+                                    self.click_points.append(
+                                        [
+                                            float('%.*f' % (self.decimals, center_pt.x)),
+                                            float('%.*f' % (self.decimals, center_pt.y))
+                                        ]
+                                    )
+                                    self.check_points()
+                    else:
+                        for apid, apid_val in self.target_obj.apertures.items():
+                            for geo_el in apid_val['geometry']:
+                                if 'solid' in geo_el:
+                                    if click_pt.within(geo_el['solid']):
+                                        if isinstance(geo_el['follow'], Point):
+                                            center_pt = geo_el['solid'].centroid
+                                            self.click_points.append(
+                                                [
+                                                    float('%.*f' % (self.decimals, center_pt.x)),
+                                                    float('%.*f' % (self.decimals, center_pt.y))
+                                                ]
+                                            )
+                                            self.check_points()
+                else:
+                    self.click_points.append(
+                        [
+                            float('%.*f' % (self.decimals, click_pt.x)),
+                            float('%.*f' % (self.decimals, click_pt.y))
+                        ]
+                    )
+                    self.check_points()
+        elif event.button == right_button and self.app.event_is_dragging is False:
+            if len(self.click_points) != 4:
+                self.reset_calibration_points()
+                self.disconnect_cal_events()
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled by user request."))
 
-        self.top_left_coordx_lbl = QtWidgets.QLabel('%s' % _('Top Left X'))
-        self.points_table.setCellWidget(row, 1, self.top_left_coordx_lbl)
-        self.top_left_coordx_tgt = EvalEntry()
-        self.points_table.setCellWidget(row, 2, self.top_left_coordx_tgt)
-        self.top_left_coordx_tgt.setReadOnly(True)
-        self.top_left_coordx_found = EvalEntry()
-        self.points_table.setCellWidget(row, 3, self.top_left_coordx_found)
-        row += 1
+    def check_points(self):
+        if len(self.click_points) == 1:
+            self.ui.bottom_left_coordx_tgt.set_value(self.click_points[0][0])
+            self.ui.bottom_left_coordy_tgt.set_value(self.click_points[0][1])
+            self.app.inform.emit(_("Get Second calibration point. Bottom Right (Top Left)..."))
+        elif len(self.click_points) == 2:
+            self.ui.bottom_right_coordx_tgt.set_value(self.click_points[1][0])
+            self.ui.bottom_right_coordy_tgt.set_value(self.click_points[1][1])
+            self.app.inform.emit(_("Get Third calibration point. Top Left (Bottom Right)..."))
+        elif len(self.click_points) == 3:
+            self.ui.top_left_coordx_tgt.set_value(self.click_points[2][0])
+            self.ui.top_left_coordy_tgt.set_value(self.click_points[2][1])
+            self.app.inform.emit(_("Get Forth calibration point. Top Right..."))
+        elif len(self.click_points) == 4:
+            self.ui.top_right_coordx_tgt.set_value(self.click_points[3][0])
+            self.ui.top_right_coordy_tgt.set_value(self.click_points[3][1])
+            self.app.inform.emit('[success] %s' % _("Done. All four points have been acquired."))
+            self.disconnect_cal_events()
 
-        self.top_left_coordy_lbl = QtWidgets.QLabel('%s' % _('Top Left Y'))
-        self.points_table.setCellWidget(row, 1, self.top_left_coordy_lbl)
-        self.top_left_coordy_tgt = EvalEntry()
-        self.points_table.setCellWidget(row, 2, self.top_left_coordy_tgt)
-        self.top_left_coordy_tgt.setReadOnly(True)
-        self.top_left_coordy_found = EvalEntry()
-        self.points_table.setCellWidget(row, 3, self.top_left_coordy_found)
-        row += 1
+    def reset_calibration_points(self):
+        self.click_points = []
 
-        # TOP RIGHT
-        id_item_4 = QtWidgets.QTableWidgetItem('%d' % 4)
-        flags = QtCore.Qt.ItemIsEnabled
-        id_item_4.setFlags(flags)
-        self.points_table.setItem(row, 0, id_item_4)  # Tool name/id
+        self.ui.bottom_left_coordx_tgt.set_value('')
+        self.ui.bottom_left_coordy_tgt.set_value('')
 
-        self.top_right_coordx_lbl = QtWidgets.QLabel('%s' % _('Top Right X'))
-        self.points_table.setCellWidget(row, 1, self.top_right_coordx_lbl)
-        self.top_right_coordx_tgt = EvalEntry()
-        self.points_table.setCellWidget(row, 2, self.top_right_coordx_tgt)
-        self.top_right_coordx_tgt.setReadOnly(True)
-        self.top_right_coordx_found = EvalEntry()
-        self.top_right_coordx_found.setDisabled(True)
-        self.points_table.setCellWidget(row, 3, self.top_right_coordx_found)
-        row += 1
+        self.ui.bottom_right_coordx_tgt.set_value('')
+        self.ui.bottom_right_coordy_tgt.set_value('')
 
-        self.top_right_coordy_lbl = QtWidgets.QLabel('%s' % _('Top Right Y'))
-        self.points_table.setCellWidget(row, 1, self.top_right_coordy_lbl)
-        self.top_right_coordy_tgt = EvalEntry()
-        self.points_table.setCellWidget(row, 2, self.top_right_coordy_tgt)
-        self.top_right_coordy_tgt.setReadOnly(True)
-        self.top_right_coordy_found = EvalEntry()
-        self.top_right_coordy_found.setDisabled(True)
-        self.points_table.setCellWidget(row, 3, self.top_right_coordy_found)
+        self.ui.top_left_coordx_tgt.set_value('')
+        self.ui.top_left_coordy_tgt.set_value('')
 
-        vertical_header = self.points_table.verticalHeader()
-        vertical_header.hide()
-        self.points_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+        self.ui.top_right_coordx_tgt.set_value('')
+        self.ui.top_right_coordy_tgt.set_value('')
 
-        horizontal_header = self.points_table.horizontalHeader()
-        horizontal_header.setMinimumSectionSize(10)
-        horizontal_header.setDefaultSectionSize(70)
+        self.ui.bottom_right_coordx_found.set_value('')
+        self.ui.bottom_right_coordy_found.set_value('')
 
-        self.points_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
-        # for x in range(4):
-        #     self.points_table.resizeColumnToContents(x)
-        self.points_table.resizeColumnsToContents()
-        self.points_table.resizeRowsToContents()
+        self.ui.top_left_coordx_found.set_value('')
+        self.ui.top_left_coordy_found.set_value('')
 
-        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(0, 20)
-        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
-        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.Stretch)
+    def gcode_header(self):
+        log.debug("ToolCalibration.gcode_header()")
+        time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
 
-        self.points_table.setMinimumHeight(self.points_table.getHeight() + 2)
-        self.points_table.setMaximumHeight(self.points_table.getHeight() + 3)
+        gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
+                (str(self.app.version), str(self.app.version_date)) + '\n'
 
-        # ## Get Points Button
-        self.start_button = QtWidgets.QPushButton(_("Get Points"))
-        self.start_button.setToolTip(
-            _("Pick four points by clicking on canvas if the source choice\n"
-              "is 'free' or inside the object geometry if the source is 'object'.\n"
-              "Those four points should be in the four squares of\n"
-              "the object.")
-        )
-        self.start_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        grid_lay.addWidget(self.start_button, 17, 0, 1, 3)
+        gcode += '(Name: ' + _('Verification GCode for FlatCAM Calibration Tool') + ')\n'
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line, 18, 0, 1, 3)
+        gcode += '(Units: ' + self.units.upper() + ')\n\n'
+        gcode += '(Created on ' + time_str + ')\n\n'
+        gcode += 'G20\n' if self.units.upper() == 'IN' else 'G21\n'
+        gcode += 'G90\n'
+        gcode += 'G17\n'
+        gcode += 'G94\n\n'
+        return gcode
 
-        grid_lay.addWidget(QtWidgets.QLabel(''), 19, 0)
+    def close_tab(self):
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.tabText(idx) == _("Gcode Viewer"):
+                wdg = self.app.ui.plot_tab_area.widget(idx)
+                wdg.deleteLater()
+                self.app.ui.plot_tab_area.removeTab(idx)
 
-        # STEP 2 #
-        step_2 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 2: Verification GCode"))
-        step_2.setToolTip(
-            _("Generate GCode file to locate and align the PCB by using\n"
-              "the four points acquired above.\n"
-              "The points sequence is:\n"
-              "- first point -> set the origin\n"
-              "- second point -> alignment point. Can be: top-left or bottom-right.\n"
-              "- third point -> check point. Can be: top-left or bottom-right.\n"
-              "- forth point -> final verification point. Just for evaluation.")
-        )
-        grid_lay.addWidget(step_2, 20, 0, 1, 3)
+    def generate_verification_gcode(self):
+        sec_point = self.ui.second_point_radio.get_value()
 
-        # ## GCode Button
-        self.gcode_button = QtWidgets.QPushButton(_("Generate GCode"))
-        self.gcode_button.setToolTip(
-            _("Generate GCode file to locate and align the PCB by using\n"
-              "the four points acquired above.\n"
-              "The points sequence is:\n"
-              "- first point -> set the origin\n"
-              "- second point -> alignment point. Can be: top-left or bottom-right.\n"
-              "- third point -> check point. Can be: top-left or bottom-right.\n"
-              "- forth point -> final verification point. Just for evaluation.")
-        )
-        self.gcode_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        grid_lay.addWidget(self.gcode_button, 21, 0, 1, 3)
+        travel_z = '%.*f' % (self.decimals, self.ui.travelz_entry.get_value())
+        toolchange_z = '%.*f' % (self.decimals, self.ui.toolchangez_entry.get_value())
+        toolchange_xy_temp = self.ui.toolchange_xy_entry.get_value().split(",")
+        toolchange_xy = [float(eval(a)) for a in toolchange_xy_temp if a != '']
 
-        separator_line1 = QtWidgets.QFrame()
-        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line1, 22, 0, 1, 3)
+        verification_z = '%.*f' % (self.decimals, self.ui.verz_entry.get_value())
 
-        grid_lay.addWidget(QtWidgets.QLabel(''), 23, 0, 1, 3)
+        if len(self.click_points) != 4:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Four points are needed for GCode generation."))
+            return 'fail'
 
-        # STEP 3 #
-        step_3 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 3: Adjustments"))
-        step_3.setToolTip(
-            _("Calculate Scale and Skew factors based on the differences (delta)\n"
-              "found when checking the PCB pattern. The differences must be filled\n"
-              "in the fields Found (Delta).")
-        )
-        grid_lay.addWidget(step_3, 24, 0, 1, 3)
+        gcode = self.gcode_header()
+        if self.ui.zeroz_cb.get_value():
+            gcode += 'M5\n'
+            gcode += 'G00 Z%s\n' % toolchange_z
+            if toolchange_xy:
+                gcode += 'G00 X%s Y%s\n' % (toolchange_xy[0], toolchange_xy[1])
+            gcode += 'M0\n'
+            gcode += 'G01 Z0\n'
+            gcode += 'M0\n'
+            gcode += 'G00 Z%s\n' % toolchange_z
+            gcode += 'M0\n'
 
-        # ## Factors Button
-        self.generate_factors_button = QtWidgets.QPushButton(_("Calculate Factors"))
-        self.generate_factors_button.setToolTip(
-            _("Calculate Scale and Skew factors based on the differences (delta)\n"
-              "found when checking the PCB pattern. The differences must be filled\n"
-              "in the fields Found (Delta).")
-        )
-        self.generate_factors_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        grid_lay.addWidget(self.generate_factors_button, 25, 0, 1, 3)
+        # first point: bottom - left -> ORIGIN set
+        gcode += 'G00 Z%s\n' % travel_z
+        gcode += 'G00 X%s Y%s\n' % (self.click_points[0][0], self.click_points[0][1])
+        gcode += 'G01 Z%s\n' % verification_z
+        gcode += 'M0\n'
 
-        separator_line1 = QtWidgets.QFrame()
-        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line1, 26, 0, 1, 3)
+        if sec_point == 'tl':
+            # second point: top - left -> align the PCB to this point
+            gcode += 'G00 Z%s\n' % travel_z
+            gcode += 'G00 X%s Y%s\n' % (self.click_points[2][0], self.click_points[2][1])
+            gcode += 'G01 Z%s\n' % verification_z
+            gcode += 'M0\n'
 
-        grid_lay.addWidget(QtWidgets.QLabel(''), 27, 0, 1, 3)
+            # third point: bottom - right -> check for scale on X axis or for skew on Y axis
+            gcode += 'G00 Z%s\n' % travel_z
+            gcode += 'G00 X%s Y%s\n' % (self.click_points[1][0], self.click_points[1][1])
+            gcode += 'G01 Z%s\n' % verification_z
+            gcode += 'M0\n'
 
-        # STEP 4 #
-        step_4 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 4: Adjusted GCode"))
-        step_4.setToolTip(
-            _("Generate verification GCode file adjusted with\n"
-              "the factors above.")
-        )
-        grid_lay.addWidget(step_4, 28, 0, 1, 3)
+            # forth point: top - right -> verification point
+            gcode += 'G00 Z%s\n' % travel_z
+            gcode += 'G00 X%s Y%s\n' % (self.click_points[3][0], self.click_points[3][1])
+            gcode += 'G01 Z%s\n' % verification_z
+            gcode += 'M0\n'
+        else:
+            # second point: bottom - right -> align the PCB to this point
+            gcode += 'G00 Z%s\n' % travel_z
+            gcode += 'G00 X%s Y%s\n' % (self.click_points[1][0], self.click_points[1][1])
+            gcode += 'G01 Z%s\n' % verification_z
+            gcode += 'M0\n'
 
-        self.scalex_label = QtWidgets.QLabel(_("Scale Factor X:"))
-        self.scalex_label.setToolTip(
-            _("Factor for Scale action over X axis.")
-        )
-        self.scalex_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.scalex_entry.set_range(0, 9999.9999)
-        self.scalex_entry.set_precision(self.decimals)
-        self.scalex_entry.setSingleStep(0.1)
+            # third point: top - left -> check for scale on Y axis or for skew on X axis
+            gcode += 'G00 Z%s\n' % travel_z
+            gcode += 'G00 X%s Y%s\n' % (self.click_points[2][0], self.click_points[2][1])
+            gcode += 'G01 Z%s\n' % verification_z
+            gcode += 'M0\n'
 
-        grid_lay.addWidget(self.scalex_label, 29, 0)
-        grid_lay.addWidget(self.scalex_entry, 29, 1, 1, 2)
+            # forth point: top - right -> verification point
+            gcode += 'G00 Z%s\n' % travel_z
+            gcode += 'G00 X%s Y%s\n' % (self.click_points[3][0], self.click_points[3][1])
+            gcode += 'G01 Z%s\n' % verification_z
+            gcode += 'M0\n'
 
-        self.scaley_label = QtWidgets.QLabel(_("Scale Factor Y:"))
-        self.scaley_label.setToolTip(
-            _("Factor for Scale action over Y axis.")
-        )
-        self.scaley_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.scaley_entry.set_range(0, 9999.9999)
-        self.scaley_entry.set_precision(self.decimals)
-        self.scaley_entry.setSingleStep(0.1)
+        # return to (toolchange_xy[0], toolchange_xy[1], toolchange_z) point for toolchange event
+        gcode += 'G00 Z%s\n' % travel_z
+        gcode += 'G00 X0 Y0\n'
+        gcode += 'G00 Z%s\n' % toolchange_z
+        if toolchange_xy:
+            gcode += 'G00 X%s Y%s\n' % (toolchange_xy[0], toolchange_xy[1])
 
-        grid_lay.addWidget(self.scaley_label, 30, 0)
-        grid_lay.addWidget(self.scaley_entry, 30, 1, 1, 2)
+        gcode += 'M2'
 
-        self.scale_button = QtWidgets.QPushButton(_("Apply Scale Factors"))
-        self.scale_button.setToolTip(
-            _("Apply Scale factors on the calibration points.")
-        )
-        self.scale_button.setStyleSheet("""
-                               QPushButton
-                               {
-                                   font-weight: bold;
-                               }
-                               """)
-        grid_lay.addWidget(self.scale_button, 31, 0, 1, 3)
+        self.gcode_editor_tab = AppTextEditor(app=self.app, plain_text=True)
 
-        self.skewx_label = QtWidgets.QLabel(_("Skew Angle X:"))
-        self.skewx_label.setToolTip(
-            _("Angle for Skew action, in degrees.\n"
-              "Float number between -360 and 359.")
-        )
-        self.skewx_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.skewx_entry.set_range(-360, 360)
-        self.skewx_entry.set_precision(self.decimals)
-        self.skewx_entry.setSingleStep(0.1)
+        # add the tab if it was closed
+        self.app.ui.plot_tab_area.addTab(self.gcode_editor_tab, '%s' % _("Gcode Viewer"))
+        self.gcode_editor_tab.setObjectName('gcode_viewer_tab')
 
-        grid_lay.addWidget(self.skewx_label, 32, 0)
-        grid_lay.addWidget(self.skewx_entry, 32, 1, 1, 2)
+        # delete the absolute and relative position and messages in the infobar
+        self.app.ui.position_label.setText("")
+        self.app.ui.rel_position_label.setText("")
 
-        self.skewy_label = QtWidgets.QLabel(_("Skew Angle Y:"))
-        self.skewy_label.setToolTip(
-            _("Angle for Skew action, in degrees.\n"
-              "Float number between -360 and 359.")
-        )
-        self.skewy_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.skewy_entry.set_range(-360, 360)
-        self.skewy_entry.set_precision(self.decimals)
-        self.skewy_entry.setSingleStep(0.1)
+        self.gcode_editor_tab.code_editor.completer_enable = False
+        self.gcode_editor_tab.buttonRun.hide()
 
-        grid_lay.addWidget(self.skewy_label, 33, 0)
-        grid_lay.addWidget(self.skewy_entry, 33, 1, 1, 2)
+        # Switch plot_area to CNCJob tab
+        self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_editor_tab)
 
-        self.skew_button = QtWidgets.QPushButton(_("Apply Skew Factors"))
-        self.skew_button.setToolTip(
-            _("Apply Skew factors on the calibration points.")
-        )
-        self.skew_button.setStyleSheet("""
-                               QPushButton
-                               {
-                                   font-weight: bold;
-                               }
-                               """)
-        grid_lay.addWidget(self.skew_button, 34, 0, 1, 3)
+        self.gcode_editor_tab.t_frame.hide()
+        # then append the text from GCode to the text editor
+        try:
+            self.gcode_editor_tab.load_text(gcode, move_to_start=True, clear_text=True)
+        except Exception as e:
+            self.app.inform.emit('[ERROR] %s %s' % ('ERROR -->', str(e)))
+            return
+
+        self.gcode_editor_tab.t_frame.show()
+        self.app.proc_container.view.set_idle()
 
-        # final_factors_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Final Factors"))
-        # final_factors_lbl.setToolTip(
-        #     _("Generate verification GCode file adjusted with\n"
-        #       "the factors above.")
-        # )
-        # grid_lay.addWidget(final_factors_lbl, 27, 0, 1, 3)
-        #
-        # self.fin_scalex_label = QtWidgets.QLabel(_("Scale Factor X:"))
-        # self.fin_scalex_label.setToolTip(
-        #     _("Final factor for Scale action over X axis.")
-        # )
-        # self.fin_scalex_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        # self.fin_scalex_entry.set_range(0, 9999.9999)
-        # self.fin_scalex_entry.set_precision(self.decimals)
-        # self.fin_scalex_entry.setSingleStep(0.1)
-        #
-        # grid_lay.addWidget(self.fin_scalex_label, 28, 0)
-        # grid_lay.addWidget(self.fin_scalex_entry, 28, 1, 1, 2)
-        #
-        # self.fin_scaley_label = QtWidgets.QLabel(_("Scale Factor Y:"))
-        # self.fin_scaley_label.setToolTip(
-        #     _("Final factor for Scale action over Y axis.")
-        # )
-        # self.fin_scaley_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        # self.fin_scaley_entry.set_range(0, 9999.9999)
-        # self.fin_scaley_entry.set_precision(self.decimals)
-        # self.fin_scaley_entry.setSingleStep(0.1)
-        #
-        # grid_lay.addWidget(self.fin_scaley_label, 29, 0)
-        # grid_lay.addWidget(self.fin_scaley_entry, 29, 1, 1, 2)
-        #
-        # self.fin_skewx_label = QtWidgets.QLabel(_("Skew Angle X:"))
-        # self.fin_skewx_label.setToolTip(
-        #     _("Final value for angle for Skew action, in degrees.\n"
-        #       "Float number between -360 and 359.")
-        # )
-        # self.fin_skewx_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        # self.fin_skewx_entry.set_range(-360, 360)
-        # self.fin_skewx_entry.set_precision(self.decimals)
-        # self.fin_skewx_entry.setSingleStep(0.1)
-        #
-        # grid_lay.addWidget(self.fin_skewx_label, 30, 0)
-        # grid_lay.addWidget(self.fin_skewx_entry, 30, 1, 1, 2)
-        #
-        # self.fin_skewy_label = QtWidgets.QLabel(_("Skew Angle Y:"))
-        # self.fin_skewy_label.setToolTip(
-        #     _("Final value for angle for Skew action, in degrees.\n"
-        #       "Float number between -360 and 359.")
-        # )
-        # self.fin_skewy_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        # self.fin_skewy_entry.set_range(-360, 360)
-        # self.fin_skewy_entry.set_precision(self.decimals)
-        # self.fin_skewy_entry.setSingleStep(0.1)
-        #
-        # grid_lay.addWidget(self.fin_skewy_label, 31, 0)
-        # grid_lay.addWidget(self.fin_skewy_entry, 31, 1, 1, 2)
+        self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor'))
 
-        # ## Adjusted GCode Button
+        _filter_ = "G-Code Files (*.nc);;All Files (*.*)"
+        self.gcode_editor_tab.buttonSave.clicked.disconnect()
+        self.gcode_editor_tab.buttonSave.clicked.connect(
+            lambda: self.gcode_editor_tab.handleSaveGCode(name='fc_ver_gcode', filt=_filter_, callback=self.close_tab))
 
-        self.adj_gcode_button = QtWidgets.QPushButton(_("Generate Adjusted GCode"))
-        self.adj_gcode_button.setToolTip(
-            _("Generate verification GCode file adjusted with\n"
-              "the factors set above.\n"
-              "The GCode parameters can be readjusted\n"
-              "before clicking this button.")
-        )
-        self.adj_gcode_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        grid_lay.addWidget(self.adj_gcode_button, 42, 0, 1, 3)
+    def calculate_factors(self):
+        origin_x = self.click_points[0][0]
+        origin_y = self.click_points[0][1]
 
-        separator_line1 = QtWidgets.QFrame()
-        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line1, 43, 0, 1, 3)
+        top_left_x = self.click_points[2][0]
+        top_left_y = self.click_points[2][1]
 
-        grid_lay.addWidget(QtWidgets.QLabel(''), 44, 0, 1, 3)
+        bot_right_x = self.click_points[1][0]
+        bot_right_y = self.click_points[1][1]
 
-        # STEP 5 #
-        step_5 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 5: Calibrate FlatCAM Objects"))
-        step_5.setToolTip(
-            _("Adjust the FlatCAM objects\n"
-              "with the factors determined and verified above.")
-        )
-        grid_lay.addWidget(step_5, 45, 0, 1, 3)
+        try:
+            top_left_dx = float(self.ui.top_left_coordx_found.get_value())
+        except TypeError:
+            top_left_dx = top_left_x
 
-        self.adj_object_type_combo = FCComboBox()
-        self.adj_object_type_combo.addItems([_("Gerber"), _("Excellon"), _("Geometry")])
+        try:
+            top_left_dy = float(self.ui.top_left_coordy_found.get_value())
+        except TypeError:
+            top_left_dy = top_left_y
 
-        self.adj_object_type_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
-        self.adj_object_type_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
-        self.adj_object_type_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
+        try:
+            bot_right_dx = float(self.ui.bottom_right_coordx_found.get_value())
+        except TypeError:
+            bot_right_dx = bot_right_x
 
-        self.adj_object_type_label = QtWidgets.QLabel("%s:" % _("Adjusted object type"))
-        self.adj_object_type_label.setToolTip(_("Type of the FlatCAM Object to be adjusted."))
+        try:
+            bot_right_dy = float(self.ui.bottom_right_coordy_found.get_value())
+        except TypeError:
+            bot_right_dy = bot_right_y
 
-        grid_lay.addWidget(self.adj_object_type_label, 46, 0, 1, 3)
-        grid_lay.addWidget(self.adj_object_type_combo, 47, 0, 1, 3)
+        # ------------------------------------------------------------------------------- #
+        # --------------------------- FACTORS CALCULUS ---------------------------------- #
+        # ------------------------------------------------------------------------------- #
+        if bot_right_dx != float('%.*f' % (self.decimals, bot_right_x)):
+            # we have scale on X
+            scale_x = (bot_right_dx / (bot_right_x - origin_x)) + 1
+            self.ui.scalex_entry.set_value(scale_x)
 
-        self.adj_object_combo = FCComboBox()
-        self.adj_object_combo.setModel(self.app.collection)
-        self.adj_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.adj_object_combo.is_last = True
-        self.adj_object_combo.obj_type = {
-            _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
-        }[self.adj_object_type_combo.get_value()]
+        if top_left_dy != float('%.*f' % (self.decimals, top_left_y)):
+            # we have scale on Y
+            scale_y = (top_left_dy / (top_left_y - origin_y)) + 1
+            self.ui.scaley_entry.set_value(scale_y)
 
-        self.adj_object_label = QtWidgets.QLabel("%s:" % _("Adjusted object selection"))
-        self.adj_object_label.setToolTip(
-            _("The FlatCAM Object to be adjusted.")
-        )
+        if top_left_dx != float('%.*f' % (self.decimals, top_left_x)):
+            # we have skew on X
+            dx = top_left_dx
+            dy = top_left_y - origin_y
+            skew_angle_x = math.degrees(math.atan(dx / dy))
+            self.ui.skewx_entry.set_value(skew_angle_x)
 
-        grid_lay.addWidget(self.adj_object_label, 48, 0, 1, 3)
-        grid_lay.addWidget(self.adj_object_combo, 49, 0, 1, 3)
+        if bot_right_dy != float('%.*f' % (self.decimals, bot_right_y)):
+            # we have skew on Y
+            dx = bot_right_x - origin_x
+            dy = bot_right_dy + origin_y
+            skew_angle_y = math.degrees(math.atan(dy / dx))
+            self.ui.skewy_entry.set_value(skew_angle_y)
 
-        # ## Adjust Objects Button
-        self.cal_button = QtWidgets.QPushButton(_("Calibrate"))
-        self.cal_button.setToolTip(
-            _("Adjust (scale and/or skew) the objects\n"
-              "with the factors determined above.")
-        )
-        self.cal_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        grid_lay.addWidget(self.cal_button, 50, 0, 1, 3)
+    @property
+    def target_values_in_table(self):
+        self.click_points[0][0] = self.ui.bottom_left_coordx_tgt.get_value()
+        self.click_points[0][1] = self.ui.bottom_left_coordy_tgt.get_value()
 
-        separator_line2 = QtWidgets.QFrame()
-        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line2, 51, 0, 1, 3)
+        self.click_points[1][0] = self.ui.bottom_right_coordx_tgt.get_value()
+        self.click_points[1][1] = self.ui.bottom_right_coordy_tgt.get_value()
 
-        grid_lay.addWidget(QtWidgets.QLabel(''), 52, 0, 1, 3)
+        self.click_points[2][0] = self.ui.top_left_coordx_tgt.get_value()
+        self.click_points[2][1] = self.ui.top_left_coordy_tgt.get_value()
 
-        self.layout.addStretch()
+        self.click_points[3][0] = self.ui.top_right_coordx_tgt.get_value()
+        self.click_points[3][1] = self.ui.top_right_coordy_tgt.get_value()
 
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
+        return self.click_points
 
-        self.mr = None
-        self.units = ''
+    @target_values_in_table.setter
+    def target_values_in_table(self, param):
+        bl_pt, br_pt, tl_pt, tr_pt = param
 
-        # here store 4 points to be used for calibration
-        self.click_points = [[], [], [], []]
+        self.click_points[0] = [bl_pt[0], bl_pt[1]]
+        self.click_points[1] = [br_pt[0], br_pt[1]]
+        self.click_points[2] = [tl_pt[0], tl_pt[1]]
+        self.click_points[3] = [tr_pt[0], tr_pt[1]]
 
-        # store the status of the grid
-        self.grid_status_memory = None
+        self.ui.bottom_left_coordx_tgt.set_value(float('%.*f' % (self.decimals, bl_pt[0])))
+        self.ui.bottom_left_coordy_tgt.set_value(float('%.*f' % (self.decimals, bl_pt[1])))
 
-        self.target_obj = None
+        self.ui.bottom_right_coordx_tgt.set_value(float('%.*f' % (self.decimals, br_pt[0])))
+        self.ui.bottom_right_coordy_tgt.set_value(float('%.*f' % (self.decimals, br_pt[1])))
 
-        # if the mouse events are connected to a local method set this True
-        self.local_connected = False
+        self.ui.top_left_coordx_tgt.set_value(float('%.*f' % (self.decimals, tl_pt[0])))
+        self.ui.top_left_coordy_tgt.set_value(float('%.*f' % (self.decimals, tl_pt[1])))
 
-        # reference for the tab where to open and view the verification GCode
-        self.gcode_editor_tab = None
+        self.ui.top_right_coordx_tgt.set_value(float('%.*f' % (self.decimals, tr_pt[0])))
+        self.ui.top_right_coordy_tgt.set_value(float('%.*f' % (self.decimals, tr_pt[1])))
 
-        # calibrated object
-        self.cal_object = None
+    def on_scale_button(self):
+        scalex_fact = self.ui.scalex_entry.get_value()
+        scaley_fact = self.ui.scaley_entry.get_value()
+        bl, br, tl, tr = self.target_values_in_table
 
-        # ## Signals
-        self.cal_source_radio.activated_custom.connect(self.on_cal_source_radio)
-        self.obj_type_combo.currentIndexChanged.connect(self.on_obj_type_combo)
-        self.adj_object_type_combo.currentIndexChanged.connect(self.on_adj_obj_type_combo)
+        bl_geo = Point(bl[0], bl[1])
+        br_geo = Point(br[0], br[1])
+        tl_geo = Point(tl[0], tl[1])
+        tr_geo = Point(tr[0], tr[1])
 
-        self.start_button.clicked.connect(self.on_start_collect_points)
+        bl_scaled = scale(bl_geo, xfact=scalex_fact, yfact=scaley_fact, origin=(bl[0], bl[1]))
+        br_scaled = scale(br_geo, xfact=scalex_fact, yfact=scaley_fact, origin=(bl[0], bl[1]))
+        tl_scaled = scale(tl_geo, xfact=scalex_fact, yfact=scaley_fact, origin=(bl[0], bl[1]))
+        tr_scaled = scale(tr_geo, xfact=scalex_fact, yfact=scaley_fact, origin=(bl[0], bl[1]))
 
-        self.gcode_button.clicked.connect(self.generate_verification_gcode)
-        self.adj_gcode_button.clicked.connect(self.generate_verification_gcode)
+        scaled_values = [
+            [bl_scaled.x, bl_scaled.y],
+            [br_scaled.x, br_scaled.y],
+            [tl_scaled.x, tl_scaled.y],
+            [tr_scaled.x, tr_scaled.y]
+        ]
+        self.target_values_in_table = scaled_values
 
-        self.generate_factors_button.clicked.connect(self.calculate_factors)
+    def on_skew_button(self):
+        skewx_angle = self.ui.skewx_entry.get_value()
+        skewy_angle = self.ui.skewy_entry.get_value()
+        bl, br, tl, tr = self.target_values_in_table
 
-        self.scale_button.clicked.connect(self.on_scale_button)
-        self.skew_button.clicked.connect(self.on_skew_button)
+        bl_geo = Point(bl[0], bl[1])
+        br_geo = Point(br[0], br[1])
+        tl_geo = Point(tl[0], tl[1])
+        tr_geo = Point(tr[0], tr[1])
 
-        self.cal_button.clicked.connect(self.on_cal_button_click)
-        self.reset_button.clicked.connect(self.set_tool_ui)
+        bl_skewed = skew(bl_geo, xs=skewx_angle, ys=skewy_angle, origin=(bl[0], bl[1]))
+        br_skewed = skew(br_geo, xs=skewx_angle, ys=skewy_angle, origin=(bl[0], bl[1]))
+        tl_skewed = skew(tl_geo, xs=skewx_angle, ys=skewy_angle, origin=(bl[0], bl[1]))
+        tr_skewed = skew(tr_geo, xs=skewx_angle, ys=skewy_angle, origin=(bl[0], bl[1]))
 
-    def run(self, toggle=True):
-        self.app.defaults.report_usage("ToolCalibration()")
+        skewed_values = [
+            [bl_skewed.x, bl_skewed.y],
+            [br_skewed.x, br_skewed.y],
+            [tl_skewed.x, tl_skewed.y],
+            [tr_skewed.x, tr_skewed.y]
+        ]
+        self.target_values_in_table = skewed_values
 
-        if toggle:
-            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-            else:
-                try:
-                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        # if tab is populated with the tool but it does not have the focus, focus on it
-                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
-                            # focus on Tool Tab
-                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-                        else:
-                            self.app.ui.splitter.setSizes([0, 1])
-                except AttributeError:
-                    pass
-        else:
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
+    def on_cal_button_click(self):
+        # get the FlatCAM object to calibrate
+        selection_index = self.ui.adj_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.ui.adj_object_combo.rootModelIndex())
 
-        AppTool.run(self)
+        try:
+            self.cal_object = model_index.internalPointer().obj
+        except Exception as e:
+            log.debug("ToolCalibration.on_cal_button_click() --> %s" % str(e))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no FlatCAM object selected..."))
+            return 'fail'
 
-        self.set_tool_ui()
+        obj_name = self.cal_object.options["name"] + "_calibrated"
+
+        self.app.worker_task.emit({'fcn': self.new_calibrated_object, 'params': [obj_name]})
 
-        self.app.ui.notebook.setTabText(2, _("Calibration Tool"))
+    def new_calibrated_object(self, obj_name):
 
-    def install(self, icon=None, separator=None, **kwargs):
-        AppTool.install(self, icon, separator, shortcut='Alt+E', **kwargs)
+        try:
+            origin_x = self.click_points[0][0]
+            origin_y = self.click_points[0][1]
+        except IndexError as e:
+            log.debug("ToolCalibration.new_calibrated_object() --> %s" % str(e))
+            return 'fail'
 
-    def set_tool_ui(self):
-        self.units = self.app.defaults['units'].upper()
+        scalex = self.ui.scalex_entry.get_value()
+        scaley = self.ui.scaley_entry.get_value()
 
-        if self.local_connected is True:
-            self.disconnect_cal_events()
+        skewx = self.ui.skewx_entry.get_value()
+        skewy = self.ui.skewy_entry.get_value()
 
-        self.bottom_left_coordx_found.set_value(_("Origin"))
-        self.bottom_left_coordy_found.set_value(_("Origin"))
+        # create a new object adjusted (calibrated)
+        def initialize_geometry(obj_init, app):
+            obj_init.solid_geometry = deepcopy(obj.solid_geometry)
+            try:
+                obj_init.follow_geometry = deepcopy(obj.follow_geometry)
+            except AttributeError:
+                pass
 
-        self.reset_calibration_points()
+            try:
+                obj_init.apertures = deepcopy(obj.apertures)
+            except AttributeError:
+                pass
 
-        self.cal_source_radio.set_value(self.app.defaults['tools_cal_calsource'])
-        self.travelz_entry.set_value(self.app.defaults['tools_cal_travelz'])
-        self.verz_entry.set_value(self.app.defaults['tools_cal_verz'])
-        self.zeroz_cb.set_value(self.app.defaults['tools_cal_zeroz'])
-        self.toolchangez_entry.set_value(self.app.defaults['tools_cal_toolchangez'])
-        self.toolchange_xy_entry.set_value(self.app.defaults['tools_cal_toolchange_xy'])
+            try:
+                if obj.tools:
+                    obj_init.tools = deepcopy(obj.tools)
+            except Exception as ee:
+                log.debug("ToolCalibration.new_calibrated_object.initialize_geometry() --> %s" % str(ee))
 
-        self.second_point_radio.set_value(self.app.defaults['tools_cal_sec_point'])
+            obj_init.scale(xfactor=scalex, yfactor=scaley, point=(origin_x, origin_y))
+            obj_init.skew(angle_x=skewx, angle_y=skewy, point=(origin_x, origin_y))
 
-        self.scalex_entry.set_value(1.0)
-        self.scaley_entry.set_value(1.0)
-        self.skewx_entry.set_value(0.0)
-        self.skewy_entry.set_value(0.0)
+            try:
+                obj_init.source_file = deepcopy(obj.source_file)
+            except (AttributeError, TypeError):
+                pass
 
-        # default object selection is Excellon = index_1
-        self.obj_type_combo.setCurrentIndex(1)
-        self.on_obj_type_combo()
+        def initialize_gerber(obj_init, app):
+            obj_init.solid_geometry = deepcopy(obj.solid_geometry)
+            try:
+                obj_init.follow_geometry = deepcopy(obj.follow_geometry)
+            except AttributeError:
+                pass
 
-        self.adj_object_type_combo.setCurrentIndex(0)
-        self.on_adj_obj_type_combo()
-        # self.adj_object_combo.setCurrentIndex(0)
+            try:
+                obj_init.apertures = deepcopy(obj.apertures)
+            except AttributeError:
+                pass
 
-        # calibrated object
-        self.cal_object = None
+            try:
+                if obj.tools:
+                    obj_init.tools = deepcopy(obj.tools)
+            except Exception as err:
+                log.debug("ToolCalibration.new_calibrated_object.initialize_gerber() --> %s" % str(err))
 
-        self.app.inform.emit('%s...' % _("Tool initialized"))
+            obj_init.scale(xfactor=scalex, yfactor=scaley, point=(origin_x, origin_y))
+            obj_init.skew(angle_x=skewx, angle_y=skewy, point=(origin_x, origin_y))
 
-    def on_obj_type_combo(self):
-        obj_type = self.obj_type_combo.currentIndex()
-        self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        # self.object_combo.setCurrentIndex(0)
-        self.object_combo.obj_type = {
-            _("Gerber"): "Gerber", _("Excellon"): "Excellon"
-        }[self.obj_type_combo.get_value()]
+            try:
+                obj_init.source_file = self.export_gerber(obj_name=obj_name, filename=None, local_use=obj_init,
+                                                          use_thread=False)
+            except (AttributeError, TypeError):
+                pass
 
-    def on_adj_obj_type_combo(self):
-        obj_type = self.adj_object_type_combo.currentIndex()
-        self.adj_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        # self.adj_object_combo.setCurrentIndex(0)
-        self.adj_object_combo.obj_type = {
-            _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
-        }[self.adj_object_type_combo.get_value()]
+        def initialize_excellon(obj_init, app):
+            obj_init.tools = deepcopy(obj.tools)
 
-    def on_cal_source_radio(self, val):
-        if val == 'object':
-            self.obj_type_label.setDisabled(False)
-            self.obj_type_combo.setDisabled(False)
-            self.object_label.setDisabled(False)
-            self.object_combo.setDisabled(False)
-        else:
-            self.obj_type_label.setDisabled(True)
-            self.obj_type_combo.setDisabled(True)
-            self.object_label.setDisabled(True)
-            self.object_combo.setDisabled(True)
+            # drills are offset, so they need to be deep copied
+            obj_init.drills = deepcopy(obj.drills)
+            # slots are offset, so they need to be deep copied
+            obj_init.slots = deepcopy(obj.slots)
 
-    def on_start_collect_points(self):
+            obj_init.scale(xfactor=scalex, yfactor=scaley, point=(origin_x, origin_y))
+            obj_init.skew(angle_x=skewx, angle_y=skewy, point=(origin_x, origin_y))
 
-        if self.cal_source_radio.get_value() == 'object':
-            selection_index = self.object_combo.currentIndex()
-            model_index = self.app.collection.index(selection_index, 0, self.object_combo.rootModelIndex())
-            try:
-                self.target_obj = model_index.internalPointer().obj
-            except AttributeError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no source FlatCAM object selected..."))
-                return
+            obj_init.create_geometry()
 
-        # disengage the grid snapping since it will be hard to find the drills on grid
-        if self.app.ui.grid_snap_btn.isChecked():
-            self.grid_status_memory = True
-            self.app.ui.grid_snap_btn.trigger()
-        else:
-            self.grid_status_memory = False
+            obj_init.source_file = self.app.export_excellon(obj_name=obj_name, local_use=obj, filename=None,
+                                                            use_thread=False)
 
-        self.mr = self.canvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
+        obj = self.cal_object
+        obj_name = obj_name
 
-        if self.app.is_legacy is False:
-            self.canvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
-        else:
-            self.canvas.graph_event_disconnect(self.app.mr)
+        if obj is None:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no FlatCAM object selected..."))
+            log.debug("ToolCalibration.new_calibrated_object() --> No object to calibrate")
+            return 'fail'
 
-        self.local_connected = True
+        try:
+            if obj.kind.lower() == 'excellon':
+                self.app.app_obj.new_object("excellon", str(obj_name), initialize_excellon)
+            elif obj.kind.lower() == 'gerber':
+                self.app.app_obj.new_object("gerber", str(obj_name), initialize_gerber)
+            elif obj.kind.lower() == 'geometry':
+                self.app.app_obj.new_object("geometry", str(obj_name), initialize_geometry)
+        except Exception as e:
+            log.debug("ToolCalibration.new_calibrated_object() --> %s" % str(e))
+            return "Operation failed: %s" % str(e)
 
-        self.reset_calibration_points()
+    def disconnect_cal_events(self):
+        # restore the Grid snapping if it was active before
+        if self.grid_status_memory is True:
+            self.app.ui.grid_snap_btn.trigger()
 
-        self.app.inform.emit(_("Get First calibration point. Bottom Left..."))
+        self.app.mr = self.canvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
 
-    def on_mouse_click_release(self, event):
         if self.app.is_legacy is False:
-            event_pos = event.pos
-            right_button = 2
-            self.app.event_is_dragging = self.app.event_is_dragging
+            self.canvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
         else:
-            event_pos = (event.xdata, event.ydata)
-            right_button = 3
-            self.app.event_is_dragging = self.app.ui.popMenu.mouse_is_panning
-
-        pos_canvas = self.canvas.translate_coords(event_pos)
+            self.canvas.graph_event_disconnect(self.mr)
 
-        if event.button == 1:
-            click_pt = Point([pos_canvas[0], pos_canvas[1]])
+        self.local_connected = False
 
-            if self.app.selection_type is not None:
-                # delete previous selection shape
-                self.app.delete_selection_shape()
-                self.app.selection_type = None
-            else:
-                if self.cal_source_radio.get_value() == 'object':
-                    if self.target_obj.kind.lower() == 'excellon':
-                        for tool, tool_dict in self.target_obj.tools.items():
-                            for geo in tool_dict['solid_geometry']:
-                                if click_pt.within(geo):
-                                    center_pt = geo.centroid
-                                    self.click_points.append(
-                                        [
-                                            float('%.*f' % (self.decimals, center_pt.x)),
-                                            float('%.*f' % (self.decimals, center_pt.y))
-                                        ]
-                                    )
-                                    self.check_points()
-                    else:
-                        for apid, apid_val in self.target_obj.apertures.items():
-                            for geo_el in apid_val['geometry']:
-                                if 'solid' in geo_el:
-                                    if click_pt.within(geo_el['solid']):
-                                        if isinstance(geo_el['follow'], Point):
-                                            center_pt = geo_el['solid'].centroid
-                                            self.click_points.append(
-                                                [
-                                                    float('%.*f' % (self.decimals, center_pt.x)),
-                                                    float('%.*f' % (self.decimals, center_pt.y))
-                                                ]
-                                            )
-                                            self.check_points()
-                else:
-                    self.click_points.append(
-                        [
-                            float('%.*f' % (self.decimals, click_pt.x)),
-                            float('%.*f' % (self.decimals, click_pt.y))
-                        ]
-                    )
-                    self.check_points()
-        elif event.button == right_button and self.app.event_is_dragging is False:
-            if len(self.click_points) != 4:
-                self.reset_calibration_points()
-                self.disconnect_cal_events()
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled by user request."))
+    def reset_fields(self):
+        self.ui.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ui.adj_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
 
-    def check_points(self):
-        if len(self.click_points) == 1:
-            self.bottom_left_coordx_tgt.set_value(self.click_points[0][0])
-            self.bottom_left_coordy_tgt.set_value(self.click_points[0][1])
-            self.app.inform.emit(_("Get Second calibration point. Bottom Right (Top Left)..."))
-        elif len(self.click_points) == 2:
-            self.bottom_right_coordx_tgt.set_value(self.click_points[1][0])
-            self.bottom_right_coordy_tgt.set_value(self.click_points[1][1])
-            self.app.inform.emit(_("Get Third calibration point. Top Left (Bottom Right)..."))
-        elif len(self.click_points) == 3:
-            self.top_left_coordx_tgt.set_value(self.click_points[2][0])
-            self.top_left_coordy_tgt.set_value(self.click_points[2][1])
-            self.app.inform.emit(_("Get Forth calibration point. Top Right..."))
-        elif len(self.click_points) == 4:
-            self.top_right_coordx_tgt.set_value(self.click_points[3][0])
-            self.top_right_coordy_tgt.set_value(self.click_points[3][1])
-            self.app.inform.emit('[success] %s' % _("Done. All four points have been acquired."))
-            self.disconnect_cal_events()
 
-    def reset_calibration_points(self):
-        self.click_points = []
+class CalibUI:
 
-        self.bottom_left_coordx_tgt.set_value('')
-        self.bottom_left_coordy_tgt.set_value('')
+    toolName = _("Calibration Tool")
 
-        self.bottom_right_coordx_tgt.set_value('')
-        self.bottom_right_coordy_tgt.set_value('')
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
 
-        self.top_left_coordx_tgt.set_value('')
-        self.top_left_coordy_tgt.set_value('')
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
 
-        self.top_right_coordx_tgt.set_value('')
-        self.top_right_coordy_tgt.set_value('')
+        self.layout.addWidget(QtWidgets.QLabel(""))
 
-        self.bottom_right_coordx_found.set_value('')
-        self.bottom_right_coordy_found.set_value('')
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+        grid_lay.setColumnStretch(2, 0)
 
-        self.top_left_coordx_found.set_value('')
-        self.top_left_coordy_found.set_value('')
+        self.gcode_title_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
+        self.gcode_title_label.setToolTip(
+            _("Parameters used when creating the GCode in this tool.")
+        )
+        grid_lay.addWidget(self.gcode_title_label, 0, 0, 1, 3)
 
-    def gcode_header(self):
-        log.debug("ToolCalibration.gcode_header()")
-        time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
+        # Travel Z entry
+        travelz_lbl = QtWidgets.QLabel('%s:' % _("Travel Z"))
+        travelz_lbl.setToolTip(
+            _("Height (Z) for travelling between the points.")
+        )
 
-        gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
-                (str(self.app.version), str(self.app.version_date)) + '\n'
+        self.travelz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.travelz_entry.set_range(-9999.9999, 9999.9999)
+        self.travelz_entry.set_precision(self.decimals)
+        self.travelz_entry.setSingleStep(0.1)
 
-        gcode += '(Name: ' + _('Verification GCode for FlatCAM Calibration Tool') + ')\n'
+        grid_lay.addWidget(travelz_lbl, 1, 0)
+        grid_lay.addWidget(self.travelz_entry, 1, 1, 1, 2)
 
-        gcode += '(Units: ' + self.units.upper() + ')\n\n'
-        gcode += '(Created on ' + time_str + ')\n\n'
-        gcode += 'G20\n' if self.units.upper() == 'IN' else 'G21\n'
-        gcode += 'G90\n'
-        gcode += 'G17\n'
-        gcode += 'G94\n\n'
-        return gcode
+        # Verification Z entry
+        verz_lbl = QtWidgets.QLabel('%s:' % _("Verification Z"))
+        verz_lbl.setToolTip(
+            _("Height (Z) for checking the point.")
+        )
 
-    def close_tab(self):
-        for idx in range(self.app.ui.plot_tab_area.count()):
-            if self.app.ui.plot_tab_area.tabText(idx) == _("Gcode Viewer"):
-                wdg = self.app.ui.plot_tab_area.widget(idx)
-                wdg.deleteLater()
-                self.app.ui.plot_tab_area.removeTab(idx)
+        self.verz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.verz_entry.set_range(-9999.9999, 9999.9999)
+        self.verz_entry.set_precision(self.decimals)
+        self.verz_entry.setSingleStep(0.1)
 
-    def generate_verification_gcode(self):
-        sec_point = self.second_point_radio.get_value()
+        grid_lay.addWidget(verz_lbl, 2, 0)
+        grid_lay.addWidget(self.verz_entry, 2, 1, 1, 2)
 
-        travel_z = '%.*f' % (self.decimals, self.travelz_entry.get_value())
-        toolchange_z = '%.*f' % (self.decimals, self.toolchangez_entry.get_value())
-        toolchange_xy_temp = self.toolchange_xy_entry.get_value().split(",")
-        toolchange_xy = [float(eval(a)) for a in toolchange_xy_temp if a != '']
+        # Zero the Z of the verification tool
+        self.zeroz_cb = FCCheckBox('%s' % _("Zero Z tool"))
+        self.zeroz_cb.setToolTip(
+            _("Include a sequence to zero the height (Z)\n"
+              "of the verification tool.")
+        )
 
-        verification_z = '%.*f' % (self.decimals, self.verz_entry.get_value())
+        grid_lay.addWidget(self.zeroz_cb, 3, 0, 1, 3)
 
-        if len(self.click_points) != 4:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Four points are needed for GCode generation."))
-            return 'fail'
+        # Toolchange Z entry
+        toolchangez_lbl = QtWidgets.QLabel('%s:' % _("Toolchange Z"))
+        toolchangez_lbl.setToolTip(
+            _("Height (Z) for mounting the verification probe.")
+        )
 
-        gcode = self.gcode_header()
-        if self.zeroz_cb.get_value():
-            gcode += 'M5\n'
-            gcode += 'G00 Z%s\n' % toolchange_z
-            if toolchange_xy:
-                gcode += 'G00 X%s Y%s\n' % (toolchange_xy[0], toolchange_xy[1])
-            gcode += 'M0\n'
-            gcode += 'G01 Z0\n'
-            gcode += 'M0\n'
-            gcode += 'G00 Z%s\n' % toolchange_z
-            gcode += 'M0\n'
+        self.toolchangez_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.toolchangez_entry.set_range(0.0000, 9999.9999)
+        self.toolchangez_entry.set_precision(self.decimals)
+        self.toolchangez_entry.setSingleStep(0.1)
 
-        # first point: bottom - left -> ORIGIN set
-        gcode += 'G00 Z%s\n' % travel_z
-        gcode += 'G00 X%s Y%s\n' % (self.click_points[0][0], self.click_points[0][1])
-        gcode += 'G01 Z%s\n' % verification_z
-        gcode += 'M0\n'
+        grid_lay.addWidget(toolchangez_lbl, 4, 0)
+        grid_lay.addWidget(self.toolchangez_entry, 4, 1, 1, 2)
 
-        if sec_point == 'tl':
-            # second point: top - left -> align the PCB to this point
-            gcode += 'G00 Z%s\n' % travel_z
-            gcode += 'G00 X%s Y%s\n' % (self.click_points[2][0], self.click_points[2][1])
-            gcode += 'G01 Z%s\n' % verification_z
-            gcode += 'M0\n'
+        # Toolchange X-Y entry
+        toolchangexy_lbl = QtWidgets.QLabel('%s:' % _('Toolchange X-Y'))
+        toolchangexy_lbl.setToolTip(
+            _("Toolchange X,Y position.\n"
+              "If no value is entered then the current\n"
+              "(x, y) point will be used,")
+        )
 
-            # third point: bottom - right -> check for scale on X axis or for skew on Y axis
-            gcode += 'G00 Z%s\n' % travel_z
-            gcode += 'G00 X%s Y%s\n' % (self.click_points[1][0], self.click_points[1][1])
-            gcode += 'G01 Z%s\n' % verification_z
-            gcode += 'M0\n'
+        self.toolchange_xy_entry = FCEntry()
 
-            # forth point: top - right -> verification point
-            gcode += 'G00 Z%s\n' % travel_z
-            gcode += 'G00 X%s Y%s\n' % (self.click_points[3][0], self.click_points[3][1])
-            gcode += 'G01 Z%s\n' % verification_z
-            gcode += 'M0\n'
-        else:
-            # second point: bottom - right -> align the PCB to this point
-            gcode += 'G00 Z%s\n' % travel_z
-            gcode += 'G00 X%s Y%s\n' % (self.click_points[1][0], self.click_points[1][1])
-            gcode += 'G01 Z%s\n' % verification_z
-            gcode += 'M0\n'
+        grid_lay.addWidget(toolchangexy_lbl, 5, 0)
+        grid_lay.addWidget(self.toolchange_xy_entry, 5, 1, 1, 2)
 
-            # third point: top - left -> check for scale on Y axis or for skew on X axis
-            gcode += 'G00 Z%s\n' % travel_z
-            gcode += 'G00 X%s Y%s\n' % (self.click_points[2][0], self.click_points[2][1])
-            gcode += 'G01 Z%s\n' % verification_z
-            gcode += 'M0\n'
+        self.z_ois = OptionalInputSection(
+            self.zeroz_cb,
+            [
+                toolchangez_lbl,
+                self.toolchangez_entry,
+                toolchangexy_lbl,
+                self.toolchange_xy_entry
+            ]
+        )
 
-            # forth point: top - right -> verification point
-            gcode += 'G00 Z%s\n' % travel_z
-            gcode += 'G00 X%s Y%s\n' % (self.click_points[3][0], self.click_points[3][1])
-            gcode += 'G01 Z%s\n' % verification_z
-            gcode += 'M0\n'
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line1, 6, 0, 1, 3)
 
-        # return to (toolchange_xy[0], toolchange_xy[1], toolchange_z) point for toolchange event
-        gcode += 'G00 Z%s\n' % travel_z
-        gcode += 'G00 X0 Y0\n'
-        gcode += 'G00 Z%s\n' % toolchange_z
-        if toolchange_xy:
-            gcode += 'G00 X%s Y%s\n' % (toolchange_xy[0], toolchange_xy[1])
+        # Second point choice
+        second_point_lbl = QtWidgets.QLabel('%s:' % _("Second point"))
+        second_point_lbl.setToolTip(
+            _("Second point in the Gcode verification can be:\n"
+              "- top-left -> the user will align the PCB vertically\n"
+              "- bottom-right -> the user will align the PCB horizontally")
+        )
+        self.second_point_radio = RadioSet([{'label': _('Top-Left'), 'value': 'tl'},
+                                            {'label': _('Bottom-Right'), 'value': 'br'}],
+                                           orientation='vertical')
 
-        gcode += 'M2'
+        grid_lay.addWidget(second_point_lbl, 7, 0)
+        grid_lay.addWidget(self.second_point_radio, 7, 1, 1, 2)
 
-        self.gcode_editor_tab = TextEditor(app=self.app, plain_text=True)
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line1, 8, 0, 1, 3)
 
-        # add the tab if it was closed
-        self.app.ui.plot_tab_area.addTab(self.gcode_editor_tab, '%s' % _("Gcode Viewer"))
-        self.gcode_editor_tab.setObjectName('gcode_viewer_tab')
+        grid_lay.addWidget(QtWidgets.QLabel(''), 9, 0, 1, 3)
 
-        # delete the absolute and relative position and messages in the infobar
-        self.app.ui.position_label.setText("")
-        # self.app.ui.rel_position_label.setText("")
+        step_1 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 1: Acquire Calibration Points"))
+        step_1.setToolTip(
+            _("Pick four points by clicking on canvas.\n"
+              "Those four points should be in the four\n"
+              "(as much as possible) corners of the object.")
+        )
+        grid_lay.addWidget(step_1, 10, 0, 1, 3)
 
-        # first clear previous text in text editor (if any)
-        self.gcode_editor_tab.code_editor.clear()
-        self.gcode_editor_tab.code_editor.setReadOnly(False)
+        self.cal_source_lbl = QtWidgets.QLabel("<b>%s:</b>" % _("Source Type"))
+        self.cal_source_lbl.setToolTip(_("The source of calibration points.\n"
+                                         "It can be:\n"
+                                         "- Object -> click a hole geo for Excellon or a pad for Gerber\n"
+                                         "- Free -> click freely on canvas to acquire the calibration points"))
+        self.cal_source_radio = RadioSet([{'label': _('Object'), 'value': 'object'},
+                                          {'label': _('Free'), 'value': 'free'}],
+                                         stretch=False)
 
-        self.gcode_editor_tab.code_editor.completer_enable = False
-        self.gcode_editor_tab.buttonRun.hide()
+        grid_lay.addWidget(self.cal_source_lbl, 11, 0)
+        grid_lay.addWidget(self.cal_source_radio, 11, 1, 1, 2)
 
-        # Switch plot_area to CNCJob tab
-        self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_editor_tab)
+        self.obj_type_label = QtWidgets.QLabel("%s:" % _("Object Type"))
 
-        self.gcode_editor_tab.t_frame.hide()
-        # then append the text from GCode to the text editor
-        try:
-            self.gcode_editor_tab.code_editor.setPlainText(gcode)
-        except Exception as e:
-            self.app.inform.emit('[ERROR] %s %s' % ('ERROR -->', str(e)))
-            return
+        self.obj_type_combo = FCComboBox()
+        self.obj_type_combo.addItem(_("Gerber"))
+        self.obj_type_combo.addItem(_("Excellon"))
 
-        self.gcode_editor_tab.code_editor.moveCursor(QtGui.QTextCursor.Start)
+        self.obj_type_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
+        self.obj_type_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
 
-        self.gcode_editor_tab.t_frame.show()
-        self.app.proc_container.view.set_idle()
+        grid_lay.addWidget(self.obj_type_label, 12, 0)
+        grid_lay.addWidget(self.obj_type_combo, 12, 1, 1, 2)
 
-        self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor'))
+        self.object_combo = FCComboBox()
+        self.object_combo.setModel(self.app.collection)
+        self.object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.object_combo.is_last = True
 
-        _filter_ = "G-Code Files (*.nc);;All Files (*.*)"
-        self.gcode_editor_tab.buttonSave.clicked.disconnect()
-        self.gcode_editor_tab.buttonSave.clicked.connect(
-            lambda: self.gcode_editor_tab.handleSaveGCode(name='fc_ver_gcode', filt=_filter_, callback=self.close_tab))
+        self.object_label = QtWidgets.QLabel("%s:" % _("Source object selection"))
+        self.object_label.setToolTip(
+            _("FlatCAM Object to be used as a source for reference points.")
+        )
 
-    def calculate_factors(self):
-        origin_x = self.click_points[0][0]
-        origin_y = self.click_points[0][1]
+        grid_lay.addWidget(self.object_label, 13, 0, 1, 3)
+        grid_lay.addWidget(self.object_combo, 14, 0, 1, 3)
 
-        top_left_x = self.click_points[2][0]
-        top_left_y = self.click_points[2][1]
+        self.points_table_label = QtWidgets.QLabel('<b>%s</b>' % _('Calibration Points'))
+        self.points_table_label.setToolTip(
+            _("Contain the expected calibration points and the\n"
+              "ones measured.")
+        )
+        grid_lay.addWidget(self.points_table_label, 15, 0, 1, 3)
 
-        bot_right_x = self.click_points[1][0]
-        bot_right_y = self.click_points[1][1]
+        self.points_table = FCTable()
+        self.points_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        # self.points_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+        grid_lay.addWidget(self.points_table, 16, 0, 1, 3)
 
-        try:
-            top_left_dx = float(self.top_left_coordx_found.get_value())
-        except TypeError:
-            top_left_dx = top_left_x
+        self.points_table.setColumnCount(4)
+        self.points_table.setHorizontalHeaderLabels(
+            [
+                '#',
+                _("Name"),
+                _("Target"),
+                _("Found Delta")
+            ]
+        )
+        self.points_table.setRowCount(8)
+        row = 0
 
-        try:
-            top_left_dy = float(self.top_left_coordy_found.get_value())
-        except TypeError:
-            top_left_dy = top_left_y
+        # BOTTOM LEFT
+        id_item_1 = QtWidgets.QTableWidgetItem('%d' % 1)
+        flags = QtCore.Qt.ItemIsEnabled
+        id_item_1.setFlags(flags)
+        self.points_table.setItem(row, 0, id_item_1)  # Tool name/id
 
-        try:
-            bot_right_dx = float(self.bottom_right_coordx_found.get_value())
-        except TypeError:
-            bot_right_dx = bot_right_x
+        self.bottom_left_coordx_lbl = QtWidgets.QLabel('%s' % _('Bot Left X'))
+        self.points_table.setCellWidget(row, 1, self.bottom_left_coordx_lbl)
+        self.bottom_left_coordx_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.bottom_left_coordx_tgt)
+        self.bottom_left_coordx_tgt.setReadOnly(True)
+        self.bottom_left_coordx_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.bottom_left_coordx_found)
+        row += 1
 
-        try:
-            bot_right_dy = float(self.bottom_right_coordy_found.get_value())
-        except TypeError:
-            bot_right_dy = bot_right_y
+        self.bottom_left_coordy_lbl = QtWidgets.QLabel('%s' % _('Bot Left Y'))
+        self.points_table.setCellWidget(row, 1, self.bottom_left_coordy_lbl)
+        self.bottom_left_coordy_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.bottom_left_coordy_tgt)
+        self.bottom_left_coordy_tgt.setReadOnly(True)
+        self.bottom_left_coordy_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.bottom_left_coordy_found)
 
-        # ------------------------------------------------------------------------------- #
-        # --------------------------- FACTORS CALCULUS ---------------------------------- #
-        # ------------------------------------------------------------------------------- #
-        if bot_right_dx != float('%.*f' % (self.decimals, bot_right_x)):
-            # we have scale on X
-            scale_x = (bot_right_dx / (bot_right_x - origin_x)) + 1
-            self.scalex_entry.set_value(scale_x)
+        self.bottom_left_coordx_found.setDisabled(True)
+        self.bottom_left_coordy_found.setDisabled(True)
+        row += 1
+
+        # BOTTOM RIGHT
+        id_item_2 = QtWidgets.QTableWidgetItem('%d' % 2)
+        flags = QtCore.Qt.ItemIsEnabled
+        id_item_2.setFlags(flags)
+        self.points_table.setItem(row, 0, id_item_2)  # Tool name/id
+
+        self.bottom_right_coordx_lbl = QtWidgets.QLabel('%s' % _('Bot Right X'))
+        self.points_table.setCellWidget(row, 1, self.bottom_right_coordx_lbl)
+        self.bottom_right_coordx_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.bottom_right_coordx_tgt)
+        self.bottom_right_coordx_tgt.setReadOnly(True)
+        self.bottom_right_coordx_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.bottom_right_coordx_found)
 
-        if top_left_dy != float('%.*f' % (self.decimals, top_left_y)):
-            # we have scale on Y
-            scale_y = (top_left_dy / (top_left_y - origin_y)) + 1
-            self.scaley_entry.set_value(scale_y)
+        row += 1
 
-        if top_left_dx != float('%.*f' % (self.decimals, top_left_x)):
-            # we have skew on X
-            dx = top_left_dx
-            dy = top_left_y - origin_y
-            skew_angle_x = math.degrees(math.atan(dx / dy))
-            self.skewx_entry.set_value(skew_angle_x)
+        self.bottom_right_coordy_lbl = QtWidgets.QLabel('%s' % _('Bot Right Y'))
+        self.points_table.setCellWidget(row, 1, self.bottom_right_coordy_lbl)
+        self.bottom_right_coordy_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.bottom_right_coordy_tgt)
+        self.bottom_right_coordy_tgt.setReadOnly(True)
+        self.bottom_right_coordy_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.bottom_right_coordy_found)
+        row += 1
 
-        if bot_right_dy != float('%.*f' % (self.decimals, bot_right_y)):
-            # we have skew on Y
-            dx = bot_right_x - origin_x
-            dy = bot_right_dy + origin_y
-            skew_angle_y = math.degrees(math.atan(dy / dx))
-            self.skewy_entry.set_value(skew_angle_y)
+        # TOP LEFT
+        id_item_3 = QtWidgets.QTableWidgetItem('%d' % 3)
+        flags = QtCore.Qt.ItemIsEnabled
+        id_item_3.setFlags(flags)
+        self.points_table.setItem(row, 0, id_item_3)  # Tool name/id
 
-    @property
-    def target_values_in_table(self):
-        self.click_points[0][0] = self.bottom_left_coordx_tgt.get_value()
-        self.click_points[0][1] = self.bottom_left_coordy_tgt.get_value()
+        self.top_left_coordx_lbl = QtWidgets.QLabel('%s' % _('Top Left X'))
+        self.points_table.setCellWidget(row, 1, self.top_left_coordx_lbl)
+        self.top_left_coordx_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.top_left_coordx_tgt)
+        self.top_left_coordx_tgt.setReadOnly(True)
+        self.top_left_coordx_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.top_left_coordx_found)
+        row += 1
 
-        self.click_points[1][0] = self.bottom_right_coordx_tgt.get_value()
-        self.click_points[1][1] = self.bottom_right_coordy_tgt.get_value()
+        self.top_left_coordy_lbl = QtWidgets.QLabel('%s' % _('Top Left Y'))
+        self.points_table.setCellWidget(row, 1, self.top_left_coordy_lbl)
+        self.top_left_coordy_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.top_left_coordy_tgt)
+        self.top_left_coordy_tgt.setReadOnly(True)
+        self.top_left_coordy_found = EvalEntry()
+        self.points_table.setCellWidget(row, 3, self.top_left_coordy_found)
+        row += 1
 
-        self.click_points[2][0] = self.top_left_coordx_tgt.get_value()
-        self.click_points[2][1] = self.top_left_coordy_tgt.get_value()
+        # TOP RIGHT
+        id_item_4 = QtWidgets.QTableWidgetItem('%d' % 4)
+        flags = QtCore.Qt.ItemIsEnabled
+        id_item_4.setFlags(flags)
+        self.points_table.setItem(row, 0, id_item_4)  # Tool name/id
 
-        self.click_points[3][0] = self.top_right_coordx_tgt.get_value()
-        self.click_points[3][1] = self.top_right_coordy_tgt.get_value()
+        self.top_right_coordx_lbl = QtWidgets.QLabel('%s' % _('Top Right X'))
+        self.points_table.setCellWidget(row, 1, self.top_right_coordx_lbl)
+        self.top_right_coordx_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.top_right_coordx_tgt)
+        self.top_right_coordx_tgt.setReadOnly(True)
+        self.top_right_coordx_found = EvalEntry()
+        self.top_right_coordx_found.setDisabled(True)
+        self.points_table.setCellWidget(row, 3, self.top_right_coordx_found)
+        row += 1
 
-        return self.click_points
+        self.top_right_coordy_lbl = QtWidgets.QLabel('%s' % _('Top Right Y'))
+        self.points_table.setCellWidget(row, 1, self.top_right_coordy_lbl)
+        self.top_right_coordy_tgt = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.top_right_coordy_tgt)
+        self.top_right_coordy_tgt.setReadOnly(True)
+        self.top_right_coordy_found = EvalEntry()
+        self.top_right_coordy_found.setDisabled(True)
+        self.points_table.setCellWidget(row, 3, self.top_right_coordy_found)
 
-    @target_values_in_table.setter
-    def target_values_in_table(self, param):
-        bl_pt, br_pt, tl_pt, tr_pt = param
+        vertical_header = self.points_table.verticalHeader()
+        vertical_header.hide()
+        self.points_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
 
-        self.click_points[0] = [bl_pt[0], bl_pt[1]]
-        self.click_points[1] = [br_pt[0], br_pt[1]]
-        self.click_points[2] = [tl_pt[0], tl_pt[1]]
-        self.click_points[3] = [tr_pt[0], tr_pt[1]]
+        horizontal_header = self.points_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
 
-        self.bottom_left_coordx_tgt.set_value(float('%.*f' % (self.decimals, bl_pt[0])))
-        self.bottom_left_coordy_tgt.set_value(float('%.*f' % (self.decimals, bl_pt[1])))
+        self.points_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+        # for x in range(4):
+        #     self.points_table.resizeColumnToContents(x)
+        self.points_table.resizeColumnsToContents()
+        self.points_table.resizeRowsToContents()
 
-        self.bottom_right_coordx_tgt.set_value(float('%.*f' % (self.decimals, br_pt[0])))
-        self.bottom_right_coordy_tgt.set_value(float('%.*f' % (self.decimals, br_pt[1])))
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.Stretch)
 
-        self.top_left_coordx_tgt.set_value(float('%.*f' % (self.decimals, tl_pt[0])))
-        self.top_left_coordy_tgt.set_value(float('%.*f' % (self.decimals, tl_pt[1])))
+        self.points_table.setMinimumHeight(self.points_table.getHeight() + 2)
+        self.points_table.setMaximumHeight(self.points_table.getHeight() + 3)
 
-        self.top_right_coordx_tgt.set_value(float('%.*f' % (self.decimals, tr_pt[0])))
-        self.top_right_coordy_tgt.set_value(float('%.*f' % (self.decimals, tr_pt[1])))
+        # ## Get Points Button
+        self.start_button = QtWidgets.QPushButton(_("Get Points"))
+        self.start_button.setToolTip(
+            _("Pick four points by clicking on canvas if the source choice\n"
+              "is 'free' or inside the object geometry if the source is 'object'.\n"
+              "Those four points should be in the four squares of\n"
+              "the object.")
+        )
+        self.start_button.setStyleSheet("""
+                               QPushButton
+                               {
+                                   font-weight: bold;
+                               }
+                               """)
+        grid_lay.addWidget(self.start_button, 17, 0, 1, 3)
 
-    def on_scale_button(self):
-        scalex_fact = self.scalex_entry.get_value()
-        scaley_fact = self.scaley_entry.get_value()
-        bl, br, tl, tr = self.target_values_in_table
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 18, 0, 1, 3)
 
-        bl_geo = Point(bl[0], bl[1])
-        br_geo = Point(br[0], br[1])
-        tl_geo = Point(tl[0], tl[1])
-        tr_geo = Point(tr[0], tr[1])
+        grid_lay.addWidget(QtWidgets.QLabel(''), 19, 0)
 
-        bl_scaled = scale(bl_geo, xfact=scalex_fact, yfact=scaley_fact, origin=(bl[0], bl[1]))
-        br_scaled = scale(br_geo, xfact=scalex_fact, yfact=scaley_fact, origin=(bl[0], bl[1]))
-        tl_scaled = scale(tl_geo, xfact=scalex_fact, yfact=scaley_fact, origin=(bl[0], bl[1]))
-        tr_scaled = scale(tr_geo, xfact=scalex_fact, yfact=scaley_fact, origin=(bl[0], bl[1]))
+        # STEP 2 #
+        step_2 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 2: Verification GCode"))
+        step_2.setToolTip(
+            _("Generate GCode file to locate and align the PCB by using\n"
+              "the four points acquired above.\n"
+              "The points sequence is:\n"
+              "- first point -> set the origin\n"
+              "- second point -> alignment point. Can be: top-left or bottom-right.\n"
+              "- third point -> check point. Can be: top-left or bottom-right.\n"
+              "- forth point -> final verification point. Just for evaluation.")
+        )
+        grid_lay.addWidget(step_2, 20, 0, 1, 3)
 
-        scaled_values = [
-            [bl_scaled.x, bl_scaled.y],
-            [br_scaled.x, br_scaled.y],
-            [tl_scaled.x, tl_scaled.y],
-            [tr_scaled.x, tr_scaled.y]
-        ]
-        self.target_values_in_table = scaled_values
+        # ## GCode Button
+        self.gcode_button = QtWidgets.QPushButton(_("Generate GCode"))
+        self.gcode_button.setToolTip(
+            _("Generate GCode file to locate and align the PCB by using\n"
+              "the four points acquired above.\n"
+              "The points sequence is:\n"
+              "- first point -> set the origin\n"
+              "- second point -> alignment point. Can be: top-left or bottom-right.\n"
+              "- third point -> check point. Can be: top-left or bottom-right.\n"
+              "- forth point -> final verification point. Just for evaluation.")
+        )
+        self.gcode_button.setStyleSheet("""
+                               QPushButton
+                               {
+                                   font-weight: bold;
+                               }
+                               """)
+        grid_lay.addWidget(self.gcode_button, 21, 0, 1, 3)
 
-    def on_skew_button(self):
-        skewx_angle = self.skewx_entry.get_value()
-        skewy_angle = self.skewy_entry.get_value()
-        bl, br, tl, tr = self.target_values_in_table
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line1, 22, 0, 1, 3)
 
-        bl_geo = Point(bl[0], bl[1])
-        br_geo = Point(br[0], br[1])
-        tl_geo = Point(tl[0], tl[1])
-        tr_geo = Point(tr[0], tr[1])
+        grid_lay.addWidget(QtWidgets.QLabel(''), 23, 0, 1, 3)
 
-        bl_skewed = skew(bl_geo, xs=skewx_angle, ys=skewy_angle, origin=(bl[0], bl[1]))
-        br_skewed = skew(br_geo, xs=skewx_angle, ys=skewy_angle, origin=(bl[0], bl[1]))
-        tl_skewed = skew(tl_geo, xs=skewx_angle, ys=skewy_angle, origin=(bl[0], bl[1]))
-        tr_skewed = skew(tr_geo, xs=skewx_angle, ys=skewy_angle, origin=(bl[0], bl[1]))
+        # STEP 3 #
+        step_3 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 3: Adjustments"))
+        step_3.setToolTip(
+            _("Calculate Scale and Skew factors based on the differences (delta)\n"
+              "found when checking the PCB pattern. The differences must be filled\n"
+              "in the fields Found (Delta).")
+        )
+        grid_lay.addWidget(step_3, 24, 0, 1, 3)
 
-        skewed_values = [
-            [bl_skewed.x, bl_skewed.y],
-            [br_skewed.x, br_skewed.y],
-            [tl_skewed.x, tl_skewed.y],
-            [tr_skewed.x, tr_skewed.y]
-        ]
-        self.target_values_in_table = skewed_values
+        # ## Factors Button
+        self.generate_factors_button = QtWidgets.QPushButton(_("Calculate Factors"))
+        self.generate_factors_button.setToolTip(
+            _("Calculate Scale and Skew factors based on the differences (delta)\n"
+              "found when checking the PCB pattern. The differences must be filled\n"
+              "in the fields Found (Delta).")
+        )
+        self.generate_factors_button.setStyleSheet("""
+                               QPushButton
+                               {
+                                   font-weight: bold;
+                               }
+                               """)
+        grid_lay.addWidget(self.generate_factors_button, 25, 0, 1, 3)
 
-    def on_cal_button_click(self):
-        # get the FlatCAM object to calibrate
-        selection_index = self.adj_object_combo.currentIndex()
-        model_index = self.app.collection.index(selection_index, 0, self.adj_object_combo.rootModelIndex())
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line1, 26, 0, 1, 3)
 
-        try:
-            self.cal_object = model_index.internalPointer().obj
-        except Exception as e:
-            log.debug("ToolCalibration.on_cal_button_click() --> %s" % str(e))
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no FlatCAM object selected..."))
-            return 'fail'
+        grid_lay.addWidget(QtWidgets.QLabel(''), 27, 0, 1, 3)
 
-        obj_name = self.cal_object.options["name"] + "_calibrated"
+        # STEP 4 #
+        step_4 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 4: Adjusted GCode"))
+        step_4.setToolTip(
+            _("Generate verification GCode file adjusted with\n"
+              "the factors above.")
+        )
+        grid_lay.addWidget(step_4, 28, 0, 1, 3)
 
-        self.app.worker_task.emit({'fcn': self.new_calibrated_object, 'params': [obj_name]})
+        self.scalex_label = QtWidgets.QLabel(_("Scale Factor X:"))
+        self.scalex_label.setToolTip(
+            _("Factor for Scale action over X axis.")
+        )
+        self.scalex_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.scalex_entry.set_range(0, 9999.9999)
+        self.scalex_entry.set_precision(self.decimals)
+        self.scalex_entry.setSingleStep(0.1)
 
-    def new_calibrated_object(self, obj_name):
+        grid_lay.addWidget(self.scalex_label, 29, 0)
+        grid_lay.addWidget(self.scalex_entry, 29, 1, 1, 2)
 
-        try:
-            origin_x = self.click_points[0][0]
-            origin_y = self.click_points[0][1]
-        except IndexError as e:
-            log.debug("ToolCalibration.new_calibrated_object() --> %s" % str(e))
-            return 'fail'
+        self.scaley_label = QtWidgets.QLabel(_("Scale Factor Y:"))
+        self.scaley_label.setToolTip(
+            _("Factor for Scale action over Y axis.")
+        )
+        self.scaley_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.scaley_entry.set_range(0, 9999.9999)
+        self.scaley_entry.set_precision(self.decimals)
+        self.scaley_entry.setSingleStep(0.1)
 
-        scalex = self.scalex_entry.get_value()
-        scaley = self.scaley_entry.get_value()
+        grid_lay.addWidget(self.scaley_label, 30, 0)
+        grid_lay.addWidget(self.scaley_entry, 30, 1, 1, 2)
 
-        skewx = self.skewx_entry.get_value()
-        skewy = self.skewy_entry.get_value()
+        self.scale_button = QtWidgets.QPushButton(_("Apply Scale Factors"))
+        self.scale_button.setToolTip(
+            _("Apply Scale factors on the calibration points.")
+        )
+        self.scale_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid_lay.addWidget(self.scale_button, 31, 0, 1, 3)
 
-        # create a new object adjusted (calibrated)
-        def initialize_geometry(obj_init, app):
-            obj_init.solid_geometry = deepcopy(obj.solid_geometry)
-            try:
-                obj_init.follow_geometry = deepcopy(obj.follow_geometry)
-            except AttributeError:
-                pass
+        self.skewx_label = QtWidgets.QLabel(_("Skew Angle X:"))
+        self.skewx_label.setToolTip(
+            _("Angle for Skew action, in degrees.\n"
+              "Float number between -360 and 359.")
+        )
+        self.skewx_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.skewx_entry.set_range(-360, 360)
+        self.skewx_entry.set_precision(self.decimals)
+        self.skewx_entry.setSingleStep(0.1)
 
-            try:
-                obj_init.apertures = deepcopy(obj.apertures)
-            except AttributeError:
-                pass
+        grid_lay.addWidget(self.skewx_label, 32, 0)
+        grid_lay.addWidget(self.skewx_entry, 32, 1, 1, 2)
 
-            try:
-                if obj.tools:
-                    obj_init.tools = deepcopy(obj.tools)
-            except Exception as ee:
-                log.debug("ToolCalibration.new_calibrated_object.initialize_geometry() --> %s" % str(ee))
+        self.skewy_label = QtWidgets.QLabel(_("Skew Angle Y:"))
+        self.skewy_label.setToolTip(
+            _("Angle for Skew action, in degrees.\n"
+              "Float number between -360 and 359.")
+        )
+        self.skewy_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.skewy_entry.set_range(-360, 360)
+        self.skewy_entry.set_precision(self.decimals)
+        self.skewy_entry.setSingleStep(0.1)
 
-            obj_init.scale(xfactor=scalex, yfactor=scaley, point=(origin_x, origin_y))
-            obj_init.skew(angle_x=skewx, angle_y=skewy, point=(origin_x, origin_y))
+        grid_lay.addWidget(self.skewy_label, 33, 0)
+        grid_lay.addWidget(self.skewy_entry, 33, 1, 1, 2)
 
-            try:
-                obj_init.source_file = deepcopy(obj.source_file)
-            except (AttributeError, TypeError):
-                pass
+        self.skew_button = QtWidgets.QPushButton(_("Apply Skew Factors"))
+        self.skew_button.setToolTip(
+            _("Apply Skew factors on the calibration points.")
+        )
+        self.skew_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid_lay.addWidget(self.skew_button, 34, 0, 1, 3)
 
-        def initialize_gerber(obj_init, app):
-            obj_init.solid_geometry = deepcopy(obj.solid_geometry)
-            try:
-                obj_init.follow_geometry = deepcopy(obj.follow_geometry)
-            except AttributeError:
-                pass
+        # final_factors_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Final Factors"))
+        # final_factors_lbl.setToolTip(
+        #     _("Generate verification GCode file adjusted with\n"
+        #       "the factors above.")
+        # )
+        # grid_lay.addWidget(final_factors_lbl, 27, 0, 1, 3)
+        #
+        # self.fin_scalex_label = QtWidgets.QLabel(_("Scale Factor X:"))
+        # self.fin_scalex_label.setToolTip(
+        #     _("Final factor for Scale action over X axis.")
+        # )
+        # self.fin_scalex_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.fin_scalex_entry.set_range(0, 9999.9999)
+        # self.fin_scalex_entry.set_precision(self.decimals)
+        # self.fin_scalex_entry.setSingleStep(0.1)
+        #
+        # grid_lay.addWidget(self.fin_scalex_label, 28, 0)
+        # grid_lay.addWidget(self.fin_scalex_entry, 28, 1, 1, 2)
+        #
+        # self.fin_scaley_label = QtWidgets.QLabel(_("Scale Factor Y:"))
+        # self.fin_scaley_label.setToolTip(
+        #     _("Final factor for Scale action over Y axis.")
+        # )
+        # self.fin_scaley_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.fin_scaley_entry.set_range(0, 9999.9999)
+        # self.fin_scaley_entry.set_precision(self.decimals)
+        # self.fin_scaley_entry.setSingleStep(0.1)
+        #
+        # grid_lay.addWidget(self.fin_scaley_label, 29, 0)
+        # grid_lay.addWidget(self.fin_scaley_entry, 29, 1, 1, 2)
+        #
+        # self.fin_skewx_label = QtWidgets.QLabel(_("Skew Angle X:"))
+        # self.fin_skewx_label.setToolTip(
+        #     _("Final value for angle for Skew action, in degrees.\n"
+        #       "Float number between -360 and 359.")
+        # )
+        # self.fin_skewx_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.fin_skewx_entry.set_range(-360, 360)
+        # self.fin_skewx_entry.set_precision(self.decimals)
+        # self.fin_skewx_entry.setSingleStep(0.1)
+        #
+        # grid_lay.addWidget(self.fin_skewx_label, 30, 0)
+        # grid_lay.addWidget(self.fin_skewx_entry, 30, 1, 1, 2)
+        #
+        # self.fin_skewy_label = QtWidgets.QLabel(_("Skew Angle Y:"))
+        # self.fin_skewy_label.setToolTip(
+        #     _("Final value for angle for Skew action, in degrees.\n"
+        #       "Float number between -360 and 359.")
+        # )
+        # self.fin_skewy_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.fin_skewy_entry.set_range(-360, 360)
+        # self.fin_skewy_entry.set_precision(self.decimals)
+        # self.fin_skewy_entry.setSingleStep(0.1)
+        #
+        # grid_lay.addWidget(self.fin_skewy_label, 31, 0)
+        # grid_lay.addWidget(self.fin_skewy_entry, 31, 1, 1, 2)
 
-            try:
-                obj_init.apertures = deepcopy(obj.apertures)
-            except AttributeError:
-                pass
+        # ## Adjusted GCode Button
 
-            try:
-                if obj.tools:
-                    obj_init.tools = deepcopy(obj.tools)
-            except Exception as err:
-                log.debug("ToolCalibration.new_calibrated_object.initialize_gerber() --> %s" % str(err))
+        self.adj_gcode_button = QtWidgets.QPushButton(_("Generate Adjusted GCode"))
+        self.adj_gcode_button.setToolTip(
+            _("Generate verification GCode file adjusted with\n"
+              "the factors set above.\n"
+              "The GCode parameters can be readjusted\n"
+              "before clicking this button.")
+        )
+        self.adj_gcode_button.setStyleSheet("""
+                               QPushButton
+                               {
+                                   font-weight: bold;
+                               }
+                               """)
+        grid_lay.addWidget(self.adj_gcode_button, 42, 0, 1, 3)
 
-            obj_init.scale(xfactor=scalex, yfactor=scaley, point=(origin_x, origin_y))
-            obj_init.skew(angle_x=skewx, angle_y=skewy, point=(origin_x, origin_y))
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line1, 43, 0, 1, 3)
 
-            try:
-                obj_init.source_file = self.export_gerber(obj_name=obj_name, filename=None, local_use=obj_init,
-                                                          use_thread=False)
-            except (AttributeError, TypeError):
-                pass
+        grid_lay.addWidget(QtWidgets.QLabel(''), 44, 0, 1, 3)
 
-        def initialize_excellon(obj_init, app):
-            obj_init.tools = deepcopy(obj.tools)
+        # STEP 5 #
+        step_5 = QtWidgets.QLabel('<b>%s</b>' % _("STEP 5: Calibrate FlatCAM Objects"))
+        step_5.setToolTip(
+            _("Adjust the FlatCAM objects\n"
+              "with the factors determined and verified above.")
+        )
+        grid_lay.addWidget(step_5, 45, 0, 1, 3)
 
-            # drills are offset, so they need to be deep copied
-            obj_init.drills = deepcopy(obj.drills)
-            # slots are offset, so they need to be deep copied
-            obj_init.slots = deepcopy(obj.slots)
+        self.adj_object_type_combo = FCComboBox()
+        self.adj_object_type_combo.addItems([_("Gerber"), _("Excellon"), _("Geometry")])
 
-            obj_init.scale(xfactor=scalex, yfactor=scaley, point=(origin_x, origin_y))
-            obj_init.skew(angle_x=skewx, angle_y=skewy, point=(origin_x, origin_y))
+        self.adj_object_type_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
+        self.adj_object_type_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
+        self.adj_object_type_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
 
-            obj_init.create_geometry()
+        self.adj_object_type_label = QtWidgets.QLabel("%s:" % _("Adjusted object type"))
+        self.adj_object_type_label.setToolTip(_("Type of the FlatCAM Object to be adjusted."))
 
-            obj_init.source_file = self.app.export_excellon(obj_name=obj_name, local_use=obj, filename=None,
-                                                            use_thread=False)
+        grid_lay.addWidget(self.adj_object_type_label, 46, 0, 1, 3)
+        grid_lay.addWidget(self.adj_object_type_combo, 47, 0, 1, 3)
 
-        obj = self.cal_object
-        obj_name = obj_name
+        self.adj_object_combo = FCComboBox()
+        self.adj_object_combo.setModel(self.app.collection)
+        self.adj_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.adj_object_combo.is_last = True
+        self.adj_object_combo.obj_type = {
+            _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
+        }[self.adj_object_type_combo.get_value()]
 
-        if obj is None:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no FlatCAM object selected..."))
-            log.debug("ToolCalibration.new_calibrated_object() --> No object to calibrate")
-            return 'fail'
+        self.adj_object_label = QtWidgets.QLabel("%s:" % _("Adjusted object selection"))
+        self.adj_object_label.setToolTip(
+            _("The FlatCAM Object to be adjusted.")
+        )
 
-        try:
-            if obj.kind.lower() == 'excellon':
-                self.app.app_obj.new_object("excellon", str(obj_name), initialize_excellon)
-            elif obj.kind.lower() == 'gerber':
-                self.app.app_obj.new_object("gerber", str(obj_name), initialize_gerber)
-            elif obj.kind.lower() == 'geometry':
-                self.app.app_obj.new_object("geometry", str(obj_name), initialize_geometry)
-        except Exception as e:
-            log.debug("ToolCalibration.new_calibrated_object() --> %s" % str(e))
-            return "Operation failed: %s" % str(e)
+        grid_lay.addWidget(self.adj_object_label, 48, 0, 1, 3)
+        grid_lay.addWidget(self.adj_object_combo, 49, 0, 1, 3)
 
-    def disconnect_cal_events(self):
-        # restore the Grid snapping if it was active before
-        if self.grid_status_memory is True:
-            self.app.ui.grid_snap_btn.trigger()
+        # ## Adjust Objects Button
+        self.cal_button = QtWidgets.QPushButton(_("Calibrate"))
+        self.cal_button.setToolTip(
+            _("Adjust (scale and/or skew) the objects\n"
+              "with the factors determined above.")
+        )
+        self.cal_button.setStyleSheet("""
+                               QPushButton
+                               {
+                                   font-weight: bold;
+                               }
+                               """)
+        grid_lay.addWidget(self.cal_button, 50, 0, 1, 3)
 
-        self.app.mr = self.canvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line2, 51, 0, 1, 3)
 
-        if self.app.is_legacy is False:
-            self.canvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
-        else:
-            self.canvas.graph_event_disconnect(self.mr)
+        grid_lay.addWidget(QtWidgets.QLabel(''), 52, 0, 1, 3)
 
-        self.local_connected = False
+        self.layout.addStretch()
 
-    def reset_fields(self):
-        self.object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
-        self.adj_exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
-        self.adj_geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                               QPushButton
+                               {
+                                   font-weight: bold;
+                               }
+                               """)
+        self.layout.addWidget(self.reset_button)
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
 
-# end of file
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 1313 - 1284
appTools/ToolCopperThieving.py

@@ -5,7 +5,7 @@
 # MIT Licence                                              #
 # ##########################################################
 
-from PyQt5 import QtWidgets, QtCore
+from PyQt5 import QtWidgets, QtCore, QtGui
 
 from camlib import grace
 from appTool import AppTool
@@ -36,8 +36,6 @@ log = logging.getLogger('base')
 class ToolCopperThieving(AppTool):
     work_finished = QtCore.pyqtSignal()
 
-    toolName = _("Copper Thieving Tool")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
 
@@ -47,1533 +45,1564 @@ class ToolCopperThieving(AppTool):
         self.decimals = self.app.decimals
         self.units = self.app.defaults['units']
 
-        # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-        self.layout.addWidget(QtWidgets.QLabel(''))
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = ThievingUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
-        # ## Grid Layout
-        i_grid_lay = QtWidgets.QGridLayout()
-        self.layout.addLayout(i_grid_lay)
-        i_grid_lay.setColumnStretch(0, 0)
-        i_grid_lay.setColumnStretch(1, 1)
+        # Objects involved in Copper thieving
+        self.grb_object = None
+        self.ref_obj = None
+        self.sel_rect = []
+        self.sm_object = None
 
-        self.grb_object_combo = FCComboBox()
-        self.grb_object_combo.setModel(self.app.collection)
-        self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.grb_object_combo.is_last = True
-        self.grb_object_combo.obj_type = 'Gerber'
+        # store the flattened geometry here:
+        self.flat_geometry = []
 
-        self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
-        self.grbobj_label.setToolTip(
-            _("Gerber Object to which will be added a copper thieving.")
-        )
+        # Events ID
+        self.mr = None
+        self.mm = None
 
-        i_grid_lay.addWidget(self.grbobj_label, 0, 0)
-        i_grid_lay.addWidget(self.grb_object_combo, 1, 0, 1, 2)
+        # Mouse cursor positions
+        self.mouse_is_dragging = False
+        self.cursor_pos = (0, 0)
+        self.first_click = False
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        i_grid_lay.addWidget(separator_line, 2, 0, 1, 2)
+        self.area_method = False
 
-        # ## Grid Layout
-        grid_lay = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid_lay)
-        grid_lay.setColumnStretch(0, 0)
-        grid_lay.setColumnStretch(1, 1)
+        # Tool properties
+        self.clearance_val = None
+        self.margin_val = None
+        self.geo_steps_per_circle = 128
 
-        self.copper_fill_label = QtWidgets.QLabel('<b>%s</b>' % _('Parameters'))
-        self.copper_fill_label.setToolTip(
-            _("Parameters used for this tool.")
-        )
-        grid_lay.addWidget(self.copper_fill_label, 0, 0, 1, 2)
+        # Thieving geometry storage
+        self.new_solid_geometry = []
 
-        # CLEARANCE #
-        self.clearance_label = QtWidgets.QLabel('%s:' % _("Clearance"))
-        self.clearance_label.setToolTip(
-            _("This set the distance between the copper thieving components\n"
-              "(the polygon fill may be split in multiple polygons)\n"
-              "and the copper traces in the Gerber file.")
-        )
-        self.clearance_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.clearance_entry.set_range(0.00001, 9999.9999)
-        self.clearance_entry.set_precision(self.decimals)
-        self.clearance_entry.setSingleStep(0.1)
+        # Robber bar geometry storage
+        self.robber_geo = None
+        self.robber_line = None
 
-        grid_lay.addWidget(self.clearance_label, 1, 0)
-        grid_lay.addWidget(self.clearance_entry, 1, 1)
+        self.rb_thickness = None
 
-        # MARGIN #
-        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
-        self.margin_label.setToolTip(
-            _("Bounding box margin.")
-        )
-        self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.margin_entry.set_range(0.0, 9999.9999)
-        self.margin_entry.set_precision(self.decimals)
-        self.margin_entry.setSingleStep(0.1)
+        # SIGNALS
+        self.ui.ref_combo_type.currentIndexChanged.connect(self.on_ref_combo_type_change)
+        self.ui.reference_radio.group_toggle_fn = self.on_toggle_reference
+        self.ui.fill_type_radio.activated_custom.connect(self.on_thieving_type)
 
-        grid_lay.addWidget(self.margin_label, 2, 0)
-        grid_lay.addWidget(self.margin_entry, 2, 1)
+        self.ui.fill_button.clicked.connect(self.execute)
+        self.ui.rb_button.clicked.connect(self.add_robber_bar)
+        self.ui.ppm_button.clicked.connect(self.on_add_ppm)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
 
-        # Reference #
-        self.reference_radio = RadioSet([
-            {'label': _('Itself'), 'value': 'itself'},
-            {"label": _("Area Selection"), "value": "area"},
-            {'label':  _("Reference Object"), 'value': 'box'}
-        ], orientation='vertical', stretch=False)
-        self.reference_label = QtWidgets.QLabel(_("Reference:"))
-        self.reference_label.setToolTip(
-            _("- 'Itself' - the copper thieving extent is based on the object extent.\n"
-              "- 'Area Selection' - left mouse click to start selection of the area to be filled.\n"
-              "- 'Reference Object' - will do copper thieving within the area specified by another object.")
-        )
-        grid_lay.addWidget(self.reference_label, 3, 0)
-        grid_lay.addWidget(self.reference_radio, 3, 1)
+        self.work_finished.connect(self.on_new_pattern_plating_object)
 
-        self.ref_combo_type_label = QtWidgets.QLabel('%s:' % _("Ref. Type"))
-        self.ref_combo_type_label.setToolTip(
-            _("The type of FlatCAM object to be used as copper thieving reference.\n"
-              "It can be Gerber, Excellon or Geometry.")
-        )
-        self.ref_combo_type = FCComboBox()
-        self.ref_combo_type.addItems([_("Gerber"), _("Excellon"), _("Geometry")])
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("ToolCopperThieving()")
 
-        grid_lay.addWidget(self.ref_combo_type_label, 4, 0)
-        grid_lay.addWidget(self.ref_combo_type, 4, 1)
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
 
-        self.ref_combo_label = QtWidgets.QLabel('%s:' % _("Ref. Object"))
-        self.ref_combo_label.setToolTip(
-            _("The FlatCAM object to be used as non copper clearing reference.")
-        )
-        self.ref_combo = FCComboBox()
-        self.ref_combo.setModel(self.app.collection)
-        self.ref_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.ref_combo.is_last = True
-        self.ref_combo.obj_type = {
-            _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
-        }[self.ref_combo_type.get_value()]
+        AppTool.run(self)
 
-        grid_lay.addWidget(self.ref_combo_label, 5, 0)
-        grid_lay.addWidget(self.ref_combo, 5, 1)
+        self.set_tool_ui()
 
-        self.ref_combo.hide()
-        self.ref_combo_label.hide()
-        self.ref_combo_type.hide()
-        self.ref_combo_type_label.hide()
+        self.app.ui.notebook.setTabText(2, _("Copper Thieving Tool"))
 
-        # Bounding Box Type #
-        self.bbox_type_radio = RadioSet([
-            {'label': _('Rectangular'), 'value': 'rect'},
-            {"label": _("Minimal"), "value": "min"}
-        ], stretch=False)
-        self.bbox_type_label = QtWidgets.QLabel(_("Box Type:"))
-        self.bbox_type_label.setToolTip(
-            _("- 'Rectangular' - the bounding box will be of rectangular shape.\n"
-              "- 'Minimal' - the bounding box will be the convex hull shape.")
-        )
-        grid_lay.addWidget(self.bbox_type_label, 6, 0)
-        grid_lay.addWidget(self.bbox_type_radio, 6, 1)
-        self.bbox_type_label.hide()
-        self.bbox_type_radio.hide()
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='Alt+J', **kwargs)
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line, 7, 0, 1, 2)
+    def set_tool_ui(self):
+        self.units = self.app.defaults['units']
+        self.geo_steps_per_circle = int(self.app.defaults["tools_copper_thieving_circle_steps"])
 
-        # Fill Type
-        self.fill_type_radio = RadioSet([
-            {'label': _('Solid'), 'value': 'solid'},
-            {"label": _("Dots Grid"), "value": "dot"},
-            {"label": _("Squares Grid"), "value": "square"},
-            {"label": _("Lines Grid"), "value": "line"}
-        ], orientation='vertical', stretch=False)
-        self.fill_type_label = QtWidgets.QLabel(_("Fill Type:"))
-        self.fill_type_label.setToolTip(
-            _("- 'Solid' - copper thieving will be a solid polygon.\n"
-              "- 'Dots Grid' - the empty area will be filled with a pattern of dots.\n"
-              "- 'Squares Grid' - the empty area will be filled with a pattern of squares.\n"
-              "- 'Lines Grid' - the empty area will be filled with a pattern of lines.")
-        )
-        grid_lay.addWidget(self.fill_type_label, 8, 0)
-        grid_lay.addWidget(self.fill_type_radio, 8, 1)
+        self.ui.clearance_entry.set_value(float(self.app.defaults["tools_copper_thieving_clearance"]))
+        self.ui.margin_entry.set_value(float(self.app.defaults["tools_copper_thieving_margin"]))
+        self.ui.reference_radio.set_value(self.app.defaults["tools_copper_thieving_reference"])
+        self.ui.bbox_type_radio.set_value(self.app.defaults["tools_copper_thieving_box_type"])
+        self.ui.fill_type_radio.set_value(self.app.defaults["tools_copper_thieving_fill_type"])
 
-        # DOTS FRAME
-        self.dots_frame = QtWidgets.QFrame()
-        self.dots_frame.setContentsMargins(0, 0, 0, 0)
-        self.layout.addWidget(self.dots_frame)
-        dots_grid = QtWidgets.QGridLayout()
-        dots_grid.setColumnStretch(0, 0)
-        dots_grid.setColumnStretch(1, 1)
-        dots_grid.setContentsMargins(0, 0, 0, 0)
-        self.dots_frame.setLayout(dots_grid)
-        self.dots_frame.hide()
+        self.ui.dot_dia_entry.set_value(self.app.defaults["tools_copper_thieving_dots_dia"])
+        self.ui.dot_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_dots_spacing"])
+        self.ui.square_size_entry.set_value(self.app.defaults["tools_copper_thieving_squares_size"])
+        self.ui.squares_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_squares_spacing"])
+        self.ui.line_size_entry.set_value(self.app.defaults["tools_copper_thieving_lines_size"])
+        self.ui.lines_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_lines_spacing"])
 
-        self.dots_label = QtWidgets.QLabel('<b>%s</b>:' % _("Dots Grid Parameters"))
-        dots_grid.addWidget(self.dots_label, 0, 0, 1, 2)
+        self.ui.rb_margin_entry.set_value(self.app.defaults["tools_copper_thieving_rb_margin"])
+        self.ui.rb_thickness_entry.set_value(self.app.defaults["tools_copper_thieving_rb_thickness"])
+        self.ui.clearance_ppm_entry.set_value(self.app.defaults["tools_copper_thieving_mask_clearance"])
 
-        # Dot diameter #
-        self.dotdia_label = QtWidgets.QLabel('%s:' % _("Dia"))
-        self.dotdia_label.setToolTip(
-            _("Dot diameter in Dots Grid.")
-        )
-        self.dot_dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.dot_dia_entry.set_range(0.0, 9999.9999)
-        self.dot_dia_entry.set_precision(self.decimals)
-        self.dot_dia_entry.setSingleStep(0.1)
+        # INIT SECTION
+        self.area_method = False
+        self.robber_geo = None
+        self.robber_line = None
+        self.new_solid_geometry = None
 
-        dots_grid.addWidget(self.dotdia_label, 1, 0)
-        dots_grid.addWidget(self.dot_dia_entry, 1, 1)
+    def on_ref_combo_type_change(self):
+        obj_type = self.ui.ref_combo_type.currentIndex()
+        self.ui.ref_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.ui.ref_combo.setCurrentIndex(0)
+        self.ui.ref_combo.obj_type = {
+            _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
+        }[self.ui.ref_combo_type.get_value()]
 
-        # Dot spacing #
-        self.dotspacing_label = QtWidgets.QLabel('%s:' % _("Spacing"))
-        self.dotspacing_label.setToolTip(
-            _("Distance between each two dots in Dots Grid.")
-        )
-        self.dot_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.dot_spacing_entry.set_range(0.0, 9999.9999)
-        self.dot_spacing_entry.set_precision(self.decimals)
-        self.dot_spacing_entry.setSingleStep(0.1)
+    def on_toggle_reference(self):
+        if self.ui.reference_radio.get_value() == "itself" or self.ui.reference_radio.get_value() == "area":
+            self.ui.ref_combo.hide()
+            self.ui.ref_combo_label.hide()
+            self.ui.ref_combo_type.hide()
+            self.ui.ref_combo_type_label.hide()
+        else:
+            self.ui.ref_combo.show()
+            self.ui.ref_combo_label.show()
+            self.ui.ref_combo_type.show()
+            self.ui.ref_combo_type_label.show()
+
+        if self.ui.reference_radio.get_value() == "itself":
+            self.ui.bbox_type_label.show()
+            self.ui.bbox_type_radio.show()
+        else:
+            if self.ui.fill_type_radio.get_value() == 'line':
+                self.ui.reference_radio.set_value('itself')
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Lines Grid works only for 'itself' reference ..."))
+                return
 
-        dots_grid.addWidget(self.dotspacing_label, 2, 0)
-        dots_grid.addWidget(self.dot_spacing_entry, 2, 1)
+            self.ui.bbox_type_label.hide()
+            self.ui.bbox_type_radio.hide()
 
-        # SQUARES FRAME
-        self.squares_frame = QtWidgets.QFrame()
-        self.squares_frame.setContentsMargins(0, 0, 0, 0)
-        self.layout.addWidget(self.squares_frame)
-        squares_grid = QtWidgets.QGridLayout()
-        squares_grid.setColumnStretch(0, 0)
-        squares_grid.setColumnStretch(1, 1)
-        squares_grid.setContentsMargins(0, 0, 0, 0)
-        self.squares_frame.setLayout(squares_grid)
-        self.squares_frame.hide()
+    def on_thieving_type(self, choice):
+        if choice == 'solid':
+            self.ui.dots_frame.hide()
+            self.ui.squares_frame.hide()
+            self.ui.lines_frame.hide()
+            self.app.inform.emit(_("Solid fill selected."))
+        elif choice == 'dot':
+            self.ui.dots_frame.show()
+            self.ui.squares_frame.hide()
+            self.ui.lines_frame.hide()
+            self.app.inform.emit(_("Dots grid fill selected."))
+        elif choice == 'square':
+            self.ui.dots_frame.hide()
+            self.ui.squares_frame.show()
+            self.ui.lines_frame.hide()
+            self.app.inform.emit(_("Squares grid fill selected."))
+        else:
+            if self.ui.reference_radio.get_value() != 'itself':
+                self.ui.reference_radio.set_value('itself')
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Lines Grid works only for 'itself' reference ..."))
+            self.ui.dots_frame.hide()
+            self.ui.squares_frame.hide()
+            self.ui.lines_frame.show()
 
-        self.squares_label = QtWidgets.QLabel('<b>%s</b>:' % _("Squares Grid Parameters"))
-        squares_grid.addWidget(self.squares_label, 0, 0, 1, 2)
+    def add_robber_bar(self):
+        rb_margin = self.ui.rb_margin_entry.get_value()
+        self.rb_thickness = self.ui.rb_thickness_entry.get_value()
 
-        # Square Size #
-        self.square_size_label = QtWidgets.QLabel('%s:' % _("Size"))
-        self.square_size_label.setToolTip(
-            _("Square side size in Squares Grid.")
-        )
-        self.square_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.square_size_entry.set_range(0.0, 9999.9999)
-        self.square_size_entry.set_precision(self.decimals)
-        self.square_size_entry.setSingleStep(0.1)
+        # get the Gerber object on which the Robber bar will be inserted
+        selection_index = self.ui.grb_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.ui.grb_object_combo.rootModelIndex())
 
-        squares_grid.addWidget(self.square_size_label, 1, 0)
-        squares_grid.addWidget(self.square_size_entry, 1, 1)
+        try:
+            self.grb_object = model_index.internalPointer().obj
+        except Exception as e:
+            log.debug("ToolCopperThieving.add_robber_bar() --> %s" % str(e))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return
 
-        # Squares spacing #
-        self.squares_spacing_label = QtWidgets.QLabel('%s:' % _("Spacing"))
-        self.squares_spacing_label.setToolTip(
-            _("Distance between each two squares in Squares Grid.")
-        )
-        self.squares_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.squares_spacing_entry.set_range(0.0, 9999.9999)
-        self.squares_spacing_entry.set_precision(self.decimals)
-        self.squares_spacing_entry.setSingleStep(0.1)
+        try:
+            outline_pol = self.grb_object.solid_geometry.envelope
+        except TypeError:
+            outline_pol = MultiPolygon(self.grb_object.solid_geometry).envelope
 
-        squares_grid.addWidget(self.squares_spacing_label, 2, 0)
-        squares_grid.addWidget(self.squares_spacing_entry, 2, 1)
+        rb_distance = rb_margin + (self.rb_thickness / 2.0)
+        self.robber_line = outline_pol.buffer(rb_distance).exterior
 
-        # LINES FRAME
-        self.lines_frame = QtWidgets.QFrame()
-        self.lines_frame.setContentsMargins(0, 0, 0, 0)
-        self.layout.addWidget(self.lines_frame)
-        lines_grid = QtWidgets.QGridLayout()
-        lines_grid.setColumnStretch(0, 0)
-        lines_grid.setColumnStretch(1, 1)
-        lines_grid.setContentsMargins(0, 0, 0, 0)
-        self.lines_frame.setLayout(lines_grid)
-        self.lines_frame.hide()
+        self.robber_geo = self.robber_line.buffer(self.rb_thickness / 2.0)
 
-        self.lines_label = QtWidgets.QLabel('<b>%s</b>:' % _("Lines Grid Parameters"))
-        lines_grid.addWidget(self.lines_label, 0, 0, 1, 2)
+        self.app.proc_container.update_view_text(' %s' % _("Append geometry"))
 
-        # Square Size #
-        self.line_size_label = QtWidgets.QLabel('%s:' % _("Size"))
-        self.line_size_label.setToolTip(
-            _("Line thickness size in Lines Grid.")
-        )
-        self.line_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.line_size_entry.set_range(0.0, 9999.9999)
-        self.line_size_entry.set_precision(self.decimals)
-        self.line_size_entry.setSingleStep(0.1)
+        aperture_found = None
+        for ap_id, ap_val in self.grb_object.apertures.items():
+            if ap_val['type'] == 'C' and ap_val['size'] == self.rb_thickness:
+                aperture_found = ap_id
+                break
 
-        lines_grid.addWidget(self.line_size_label, 1, 0)
-        lines_grid.addWidget(self.line_size_entry, 1, 1)
+        if aperture_found:
+            geo_elem = {}
+            geo_elem['solid'] = self.robber_geo
+            geo_elem['follow'] = self.robber_line
+            self.grb_object.apertures[aperture_found]['geometry'].append(deepcopy(geo_elem))
+        else:
+            ap_keys = list(self.grb_object.apertures.keys())
+            if ap_keys:
+                new_apid = str(int(max(ap_keys)) + 1)
+            else:
+                new_apid = '10'
 
-        # Lines spacing #
-        self.lines_spacing_label = QtWidgets.QLabel('%s:' % _("Spacing"))
-        self.lines_spacing_label.setToolTip(
-            _("Distance between each two lines in Lines Grid.")
-        )
-        self.lines_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.lines_spacing_entry.set_range(0.0, 9999.9999)
-        self.lines_spacing_entry.set_precision(self.decimals)
-        self.lines_spacing_entry.setSingleStep(0.1)
+            self.grb_object.apertures[new_apid] = {}
+            self.grb_object.apertures[new_apid]['type'] = 'C'
+            self.grb_object.apertures[new_apid]['size'] = self.rb_thickness
+            self.grb_object.apertures[new_apid]['geometry'] = []
 
-        lines_grid.addWidget(self.lines_spacing_label, 2, 0)
-        lines_grid.addWidget(self.lines_spacing_entry, 2, 1)
+            geo_elem = {}
+            geo_elem['solid'] = self.robber_geo
+            geo_elem['follow'] = self.robber_line
+            self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
 
-        # ## Insert Copper Thieving
-        self.fill_button = QtWidgets.QPushButton(_("Insert Copper thieving"))
-        self.fill_button.setToolTip(
-            _("Will add a polygon (may be split in multiple parts)\n"
-              "that will surround the actual Gerber traces at a certain distance.")
-        )
-        self.fill_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.fill_button)
+        geo_obj = self.grb_object.solid_geometry
+        if isinstance(geo_obj, MultiPolygon):
+            s_list = []
+            for pol in geo_obj.geoms:
+                s_list.append(pol)
+            s_list.append(self.robber_geo)
+            geo_obj = MultiPolygon(s_list)
+        elif isinstance(geo_obj, list):
+            geo_obj.append(self.robber_geo)
+        elif isinstance(geo_obj, Polygon):
+            geo_obj = MultiPolygon([geo_obj, self.robber_geo])
 
-        # ## Grid Layout
-        grid_lay_1 = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid_lay_1)
-        grid_lay_1.setColumnStretch(0, 0)
-        grid_lay_1.setColumnStretch(1, 1)
-        grid_lay_1.setColumnStretch(2, 0)
+        self.grb_object.solid_geometry = geo_obj
 
-        separator_line_1 = QtWidgets.QFrame()
-        separator_line_1.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line_1.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay_1.addWidget(separator_line_1, 0, 0, 1, 3)
+        self.app.proc_container.update_view_text(' %s' % _("Append source file"))
+        # update the source file with the new geometry:
+        self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'],
+                                                             filename=None,
+                                                             local_use=self.grb_object,
+                                                             use_thread=False)
+        self.app.proc_container.update_view_text(' %s' % '')
+        self.on_exit()
+        self.app.inform.emit('[success] %s' % _("Copper Thieving Tool done."))
 
-        grid_lay_1.addWidget(QtWidgets.QLabel(''))
+    def execute(self):
+        self.app.call_source = "copper_thieving_tool"
 
-        self.robber_bar_label = QtWidgets.QLabel('<b>%s</b>' % _('Robber Bar Parameters'))
-        self.robber_bar_label.setToolTip(
-            _("Parameters used for the robber bar.\n"
-              "Robber bar = copper border to help in pattern hole plating.")
-        )
-        grid_lay_1.addWidget(self.robber_bar_label, 1, 0, 1, 3)
+        self.clearance_val = self.ui.clearance_entry.get_value()
+        self.margin_val = self.ui.margin_entry.get_value()
+        reference_method = self.ui.reference_radio.get_value()
 
-        # ROBBER BAR MARGIN #
-        self.rb_margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
-        self.rb_margin_label.setToolTip(
-            _("Bounding box margin for robber bar.")
-        )
-        self.rb_margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.rb_margin_entry.set_range(-9999.9999, 9999.9999)
-        self.rb_margin_entry.set_precision(self.decimals)
-        self.rb_margin_entry.setSingleStep(0.1)
+        # get the Gerber object on which the Copper thieving will be inserted
+        selection_index = self.ui.grb_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.ui.grb_object_combo.rootModelIndex())
 
-        grid_lay_1.addWidget(self.rb_margin_label, 2, 0)
-        grid_lay_1.addWidget(self.rb_margin_entry, 2, 1, 1, 2)
+        try:
+            self.grb_object = model_index.internalPointer().obj
+        except Exception as e:
+            log.debug("ToolCopperThieving.execute() --> %s" % str(e))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return
 
-        # THICKNESS #
-        self.rb_thickness_label = QtWidgets.QLabel('%s:' % _("Thickness"))
-        self.rb_thickness_label.setToolTip(
-            _("The robber bar thickness.")
-        )
-        self.rb_thickness_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.rb_thickness_entry.set_range(0.0000, 9999.9999)
-        self.rb_thickness_entry.set_precision(self.decimals)
-        self.rb_thickness_entry.setSingleStep(0.1)
+        if reference_method == 'itself':
+            bound_obj_name = self.ui.grb_object_combo.currentText()
 
-        grid_lay_1.addWidget(self.rb_thickness_label, 3, 0)
-        grid_lay_1.addWidget(self.rb_thickness_entry, 3, 1, 1, 2)
+            # Get reference object.
+            try:
+                self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
+            except Exception as e:
+                self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(e)))
+                return "Could not retrieve object: %s" % self.obj_name
 
-        # ## Insert Robber Bar
-        self.rb_button = QtWidgets.QPushButton(_("Insert Robber Bar"))
-        self.rb_button.setToolTip(
-            _("Will add a polygon with a defined thickness\n"
-              "that will surround the actual Gerber object\n"
-              "at a certain distance.\n"
-              "Required when doing holes pattern plating.")
-        )
-        self.rb_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        grid_lay_1.addWidget(self.rb_button, 4, 0, 1, 3)
+            self.on_copper_thieving(
+                thieving_obj=self.grb_object,
+                c_val=self.clearance_val,
+                margin=self.margin_val
+            )
 
-        separator_line_2 = QtWidgets.QFrame()
-        separator_line_2.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay_1.addWidget(separator_line_2, 5, 0, 1, 3)
+        elif reference_method == 'area':
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
 
-        self.patern_mask_label = QtWidgets.QLabel('<b>%s</b>' % _('Pattern Plating Mask'))
-        self.patern_mask_label.setToolTip(
-            _("Generate a mask for pattern plating.")
-        )
-        grid_lay_1.addWidget(self.patern_mask_label, 6, 0, 1, 3)
+            self.area_method = True
 
-        self.sm_obj_label = QtWidgets.QLabel("%s:" % _("Select Soldermask object"))
-        self.sm_obj_label.setToolTip(
-            _("Gerber Object with the soldermask.\n"
-              "It will be used as a base for\n"
-              "the pattern plating mask.")
-        )
+            if self.app.is_legacy is False:
+                self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
+                self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
+                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+            else:
+                self.app.plotcanvas.graph_event_disconnect(self.app.mp)
+                self.app.plotcanvas.graph_event_disconnect(self.app.mm)
+                self.app.plotcanvas.graph_event_disconnect(self.app.mr)
 
-        self.sm_object_combo = FCComboBox()
-        self.sm_object_combo.setModel(self.app.collection)
-        self.sm_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.sm_object_combo.is_last = True
-        self.sm_object_combo.obj_type = 'Gerber'
+            self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
+            self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
 
-        grid_lay_1.addWidget(self.sm_obj_label, 7, 0, 1, 3)
-        grid_lay_1.addWidget(self.sm_object_combo, 8, 0, 1, 3)
+        elif reference_method == 'box':
+            bound_obj_name = self.ui.ref_combo.currentText()
 
-        # Openings CLEARANCE #
-        self.clearance_ppm_label = QtWidgets.QLabel('%s:' % _("Clearance"))
-        self.clearance_ppm_label.setToolTip(
-            _("The distance between the possible copper thieving elements\n"
-              "and/or robber bar and the actual openings in the mask.")
-        )
-        self.clearance_ppm_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.clearance_ppm_entry.set_range(-9999.9999, 9999.9999)
-        self.clearance_ppm_entry.set_precision(self.decimals)
-        self.clearance_ppm_entry.setSingleStep(0.1)
+            # Get reference object.
+            try:
+                self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
+            except Exception:
+                self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), bound_obj_name))
+                return
 
-        grid_lay_1.addWidget(self.clearance_ppm_label, 9, 0)
-        grid_lay_1.addWidget(self.clearance_ppm_entry, 9, 1, 1, 2)
+            self.on_copper_thieving(
+                thieving_obj=self.grb_object,
+                ref_obj=self.ref_obj,
+                c_val=self.clearance_val,
+                margin=self.margin_val
+            )
 
-        # Plated area
-        self.plated_area_label = QtWidgets.QLabel('%s:' % _("Plated area"))
-        self.plated_area_label.setToolTip(
-            _("The area to be plated by pattern plating.\n"
-              "Basically is made from the openings in the plating mask.\n\n"
-              "<<WARNING>> - the calculated area is actually a bit larger\n"
-              "due of the fact that the soldermask openings are by design\n"
-              "a bit larger than the copper pads, and this area is\n"
-              "calculated from the soldermask openings.")
-        )
-        self.plated_area_entry = FCEntry()
-        self.plated_area_entry.setDisabled(True)
+        # To be called after clicking on the plot.
 
-        if self.units.upper() == 'MM':
-            self.units_area_label = QtWidgets.QLabel('%s<sup>2</sup>' % _("mm"))
+    def on_mouse_release(self, event):
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            # event_is_dragging = event.is_dragging
+            right_button = 2
         else:
-            self.units_area_label = QtWidgets.QLabel('%s<sup>2</sup>' % _("in"))
-
-        grid_lay_1.addWidget(self.plated_area_label, 10, 0)
-        grid_lay_1.addWidget(self.plated_area_entry, 10, 1)
-        grid_lay_1.addWidget(self.units_area_label, 10, 2)
-
-        # ## Pattern Plating Mask
-        self.ppm_button = QtWidgets.QPushButton(_("Generate pattern plating mask"))
-        self.ppm_button.setToolTip(
-            _("Will add to the soldermask gerber geometry\n"
-              "the geometries of the copper thieving and/or\n"
-              "the robber bar if those were generated.")
-        )
-        self.ppm_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        grid_lay_1.addWidget(self.ppm_button, 11, 0, 1, 3)
+            event_pos = (event.xdata, event.ydata)
+            # event_is_dragging = self.app.plotcanvas.is_dragging
+            right_button = 3
 
-        self.layout.addStretch()
+        event_pos = self.app.plotcanvas.translate_coords(event_pos)
 
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
+        # do clear area only for left mouse clicks
+        if event.button == 1:
+            if self.first_click is False:
+                self.first_click = True
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the filling area."))
 
-        # Objects involved in Copper thieving
-        self.grb_object = None
-        self.ref_obj = None
-        self.sel_rect = []
-        self.sm_object = None
+                self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
+                if self.app.grid_status() is True:
+                    self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
+            else:
+                self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
+                self.app.delete_selection_shape()
 
-        # store the flattened geometry here:
-        self.flat_geometry = []
+                if self.app.grid_status() is True:
+                    curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
+                else:
+                    curr_pos = (event_pos[0], event_pos[1])
 
-        # Events ID
-        self.mr = None
-        self.mm = None
+                x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
+                x1, y1 = curr_pos[0], curr_pos[1]
+                pt1 = (x0, y0)
+                pt2 = (x1, y0)
+                pt3 = (x1, y1)
+                pt4 = (x0, y1)
 
-        # Mouse cursor positions
-        self.mouse_is_dragging = False
-        self.cursor_pos = (0, 0)
-        self.first_click = False
+                new_rectangle = Polygon([pt1, pt2, pt3, pt4])
+                self.sel_rect.append(new_rectangle)
 
-        self.area_method = False
+                # add a temporary shape on canvas
+                self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(x1, y1))
+                self.first_click = False
+                return
 
-        # Tool properties
-        self.clearance_val = None
-        self.margin_val = None
-        self.geo_steps_per_circle = 128
+        elif event.button == right_button and self.mouse_is_dragging is False:
+            self.area_method = False
+            self.first_click = False
 
-        # Thieving geometry storage
-        self.new_solid_geometry = []
+            self.delete_tool_selection_shape()
 
-        # Robber bar geometry storage
-        self.robber_geo = None
-        self.robber_line = None
+            if self.app.is_legacy is False:
+                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
+                self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+            else:
+                self.app.plotcanvas.graph_event_disconnect(self.mr)
+                self.app.plotcanvas.graph_event_disconnect(self.mm)
 
-        self.rb_thickness = None
+            self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
+                                                                  self.app.on_mouse_click_over_plot)
+            self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
+                                                                  self.app.on_mouse_move_over_plot)
+            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                                  self.app.on_mouse_click_release_over_plot)
 
-        # SIGNALS
-        self.ref_combo_type.currentIndexChanged.connect(self.on_ref_combo_type_change)
-        self.reference_radio.group_toggle_fn = self.on_toggle_reference
-        self.fill_type_radio.activated_custom.connect(self.on_thieving_type)
+            if len(self.sel_rect) == 0:
+                return
 
-        self.fill_button.clicked.connect(self.execute)
-        self.rb_button.clicked.connect(self.add_robber_bar)
-        self.ppm_button.clicked.connect(self.on_add_ppm)
-        self.reset_button.clicked.connect(self.set_tool_ui)
+            self.sel_rect = cascaded_union(self.sel_rect)
 
-        self.work_finished.connect(self.on_new_pattern_plating_object)
+            if not isinstance(self.sel_rect, Iterable):
+                self.sel_rect = [self.sel_rect]
 
-    def run(self, toggle=True):
-        self.app.defaults.report_usage("ToolCopperThieving()")
+            self.on_copper_thieving(
+                thieving_obj=self.grb_object,
+                ref_obj=self.sel_rect,
+                c_val=self.clearance_val,
+                margin=self.margin_val
+            )
 
-        if toggle:
-            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-            else:
-                try:
-                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        # if tab is populated with the tool but it does not have the focus, focus on it
-                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
-                            # focus on Tool Tab
-                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-                        else:
-                            self.app.ui.splitter.setSizes([0, 1])
-                except AttributeError:
-                    pass
+    # called on mouse move
+    def on_mouse_move(self, event):
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            event_is_dragging = event.is_dragging
+            # right_button = 2
         else:
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
+            event_pos = (event.xdata, event.ydata)
+            event_is_dragging = self.app.plotcanvas.is_dragging
+            # right_button = 3
 
-        AppTool.run(self)
+        curr_pos = self.app.plotcanvas.translate_coords(event_pos)
 
-        self.set_tool_ui()
+        # detect mouse dragging motion
+        if event_is_dragging is True:
+            self.mouse_is_dragging = True
+        else:
+            self.mouse_is_dragging = False
 
-        self.app.ui.notebook.setTabText(2, _("Copper Thieving Tool"))
+        # update the cursor position
+        if self.app.grid_status() is True:
+            # Update cursor
+            curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
 
-    def install(self, icon=None, separator=None, **kwargs):
-        AppTool.install(self, icon, separator, shortcut='Alt+J', **kwargs)
+            self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
+                                         symbol='++', edge_color=self.app.cursor_color_3D,
+                                         edge_width=self.app.defaults["global_cursor_width"],
+                                         size=self.app.defaults["global_cursor_size"])
 
-    def set_tool_ui(self):
-        self.units = self.app.defaults['units']
-        self.clearance_entry.set_value(float(self.app.defaults["tools_copper_thieving_clearance"]))
-        self.margin_entry.set_value(float(self.app.defaults["tools_copper_thieving_margin"]))
-        self.reference_radio.set_value(self.app.defaults["tools_copper_thieving_reference"])
-        self.bbox_type_radio.set_value(self.app.defaults["tools_copper_thieving_box_type"])
-        self.fill_type_radio.set_value(self.app.defaults["tools_copper_thieving_fill_type"])
-        self.geo_steps_per_circle = int(self.app.defaults["tools_copper_thieving_circle_steps"])
+        if self.cursor_pos is None:
+            self.cursor_pos = (0, 0)
 
-        self.dot_dia_entry.set_value(self.app.defaults["tools_copper_thieving_dots_dia"])
-        self.dot_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_dots_spacing"])
-        self.square_size_entry.set_value(self.app.defaults["tools_copper_thieving_squares_size"])
-        self.squares_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_squares_spacing"])
-        self.line_size_entry.set_value(self.app.defaults["tools_copper_thieving_lines_size"])
-        self.lines_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_lines_spacing"])
+        self.app.dx = curr_pos[0] - float(self.cursor_pos[0])
+        self.app.dy = curr_pos[1] - float(self.cursor_pos[1])
 
-        self.rb_margin_entry.set_value(self.app.defaults["tools_copper_thieving_rb_margin"])
-        self.rb_thickness_entry.set_value(self.app.defaults["tools_copper_thieving_rb_thickness"])
-        self.clearance_ppm_entry.set_value(self.app.defaults["tools_copper_thieving_mask_clearance"])
+        # # update the positions on status bar
+        self.app.ui.position_label.setText("&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
+                                           "<b>Y</b>: %.4f&nbsp;" % (curr_pos[0], curr_pos[1]))
+        self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                               "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
 
-        # INIT SECTION
-        self.area_method = False
-        self.robber_geo = None
-        self.robber_line = None
-        self.new_solid_geometry = None
+        units = self.app.defaults["units"].lower()
+        self.app.plotcanvas.text_hud.text = \
+            'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX:  \t{:<.4f} [{:s}]\nY:  \t{:<.4f} [{:s}]'.format(
+                self.app.dx, units, self.app.dy, units, curr_pos[0], units, curr_pos[1], units)
 
-    def on_ref_combo_type_change(self):
-        obj_type = self.ref_combo_type.currentIndex()
-        self.ref_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        self.ref_combo.setCurrentIndex(0)
-        self.ref_combo.obj_type = {
-            _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
-        }[self.ref_combo_type.get_value()]
+        # draw the utility geometry
+        if self.first_click:
+            self.app.delete_selection_shape()
+            self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
+                                                 coords=(curr_pos[0], curr_pos[1]))
 
-    def on_toggle_reference(self):
-        if self.reference_radio.get_value() == "itself" or self.reference_radio.get_value() == "area":
-            self.ref_combo.hide()
-            self.ref_combo_label.hide()
-            self.ref_combo_type.hide()
-            self.ref_combo_type_label.hide()
-        else:
-            self.ref_combo.show()
-            self.ref_combo_label.show()
-            self.ref_combo_type.show()
-            self.ref_combo_type_label.show()
-
-        if self.reference_radio.get_value() == "itself":
-            self.bbox_type_label.show()
-            self.bbox_type_radio.show()
-        else:
-            if self.fill_type_radio.get_value() == 'line':
-                self.reference_radio.set_value('itself')
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Lines Grid works only for 'itself' reference ..."))
-                return
+    def on_copper_thieving(self, thieving_obj, ref_obj=None, c_val=None, margin=None, run_threaded=True):
+        """
 
-            self.bbox_type_label.hide()
-            self.bbox_type_radio.hide()
+        :param thieving_obj:
+        :param ref_obj:
+        :param c_val:
+        :param margin:
+        :param run_threaded:
+        :return:
+        """
 
-    def on_thieving_type(self, choice):
-        if choice == 'solid':
-            self.dots_frame.hide()
-            self.squares_frame.hide()
-            self.lines_frame.hide()
-            self.app.inform.emit(_("Solid fill selected."))
-        elif choice == 'dot':
-            self.dots_frame.show()
-            self.squares_frame.hide()
-            self.lines_frame.hide()
-            self.app.inform.emit(_("Dots grid fill selected."))
-        elif choice == 'square':
-            self.dots_frame.hide()
-            self.squares_frame.show()
-            self.lines_frame.hide()
-            self.app.inform.emit(_("Squares grid fill selected."))
+        if run_threaded:
+            self.app.proc_container.new('%s ...' % _("Thieving"))
         else:
-            if self.reference_radio.get_value() != 'itself':
-                self.reference_radio.set_value('itself')
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Lines Grid works only for 'itself' reference ..."))
-            self.dots_frame.hide()
-            self.squares_frame.hide()
-            self.lines_frame.show()
+            QtWidgets.QApplication.processEvents()
 
-    def add_robber_bar(self):
-        rb_margin = self.rb_margin_entry.get_value()
-        self.rb_thickness = self.rb_thickness_entry.get_value()
+        self.app.proc_container.view.set_busy('%s ...' % _("Thieving"))
 
-        # get the Gerber object on which the Robber bar will be inserted
-        selection_index = self.grb_object_combo.currentIndex()
-        model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
+        # #####################################################################
+        # ####### Read the parameters #########################################
+        # #####################################################################
 
-        try:
-            self.grb_object = model_index.internalPointer().obj
-        except Exception as e:
-            log.debug("ToolCopperThieving.add_robber_bar() --> %s" % str(e))
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
-            return 'fail'
+        log.debug("Copper Thieving Tool started. Reading parameters.")
+        self.app.inform.emit(_("Copper Thieving Tool started. Reading parameters."))
 
-        try:
-            outline_pol = self.grb_object.solid_geometry.envelope
-        except TypeError:
-            outline_pol = MultiPolygon(self.grb_object.solid_geometry).envelope
+        ref_selected = self.ui.reference_radio.get_value()
+        if c_val is None:
+            c_val = float(self.app.defaults["tools_copperfill_clearance"])
+        if margin is None:
+            margin = float(self.app.defaults["tools_copperfill_margin"])
 
-        rb_distance = rb_margin + (self.rb_thickness / 2.0)
-        self.robber_line = outline_pol.buffer(rb_distance).exterior
+        fill_type = self.ui.fill_type_radio.get_value()
+        dot_dia = self.ui.dot_dia_entry.get_value()
+        dot_spacing = self.ui.dot_spacing_entry.get_value()
+        square_size = self.ui.square_size_entry.get_value()
+        square_spacing = self.ui.squares_spacing_entry.get_value()
+        line_size = self.ui.line_size_entry.get_value()
+        line_spacing = self.ui.lines_spacing_entry.get_value()
 
-        self.robber_geo = self.robber_line.buffer(self.rb_thickness / 2.0)
+        # make sure that the source object solid geometry is an Iterable
+        if not isinstance(self.grb_object.solid_geometry, Iterable):
+            self.grb_object.solid_geometry = [self.grb_object.solid_geometry]
 
-        self.app.proc_container.update_view_text(' %s' % _("Append geometry"))
+        def job_thread_thieving(app_obj):
+            # #########################################################################################
+            # Prepare isolation polygon. This will create the clearance over the Gerber features ######
+            # #########################################################################################
+            log.debug("Copper Thieving Tool. Preparing isolation polygons.")
+            app_obj.app.inform.emit(_("Copper Thieving Tool. Preparing isolation polygons."))
 
-        aperture_found = None
-        for ap_id, ap_val in self.grb_object.apertures.items():
-            if ap_val['type'] == 'C' and ap_val['size'] == self.rb_thickness:
-                aperture_found = ap_id
-                break
-
-        if aperture_found:
-            geo_elem = {}
-            geo_elem['solid'] = self.robber_geo
-            geo_elem['follow'] = self.robber_line
-            self.grb_object.apertures[aperture_found]['geometry'].append(deepcopy(geo_elem))
-        else:
-            ap_keys = list(self.grb_object.apertures.keys())
-            if ap_keys:
-                new_apid = str(int(max(ap_keys)) + 1)
-            else:
-                new_apid = '10'
-
-            self.grb_object.apertures[new_apid] = {}
-            self.grb_object.apertures[new_apid]['type'] = 'C'
-            self.grb_object.apertures[new_apid]['size'] = self.rb_thickness
-            self.grb_object.apertures[new_apid]['geometry'] = []
-
-            geo_elem = {}
-            geo_elem['solid'] = self.robber_geo
-            geo_elem['follow'] = self.robber_line
-            self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
-
-        geo_obj = self.grb_object.solid_geometry
-        if isinstance(geo_obj, MultiPolygon):
-            s_list = []
-            for pol in geo_obj.geoms:
-                s_list.append(pol)
-            s_list.append(self.robber_geo)
-            geo_obj = MultiPolygon(s_list)
-        elif isinstance(geo_obj, list):
-            geo_obj.append(self.robber_geo)
-        elif isinstance(geo_obj, Polygon):
-            geo_obj = MultiPolygon([geo_obj, self.robber_geo])
+            # variables to display the percentage of work done
+            geo_len = 0
+            try:
+                for pol in app_obj.grb_object.solid_geometry:
+                    geo_len += 1
+            except TypeError:
+                geo_len = 1
 
-        self.grb_object.solid_geometry = geo_obj
+            old_disp_number = 0
+            pol_nr = 0
 
-        self.app.proc_container.update_view_text(' %s' % _("Append source file"))
-        # update the source file with the new geometry:
-        self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'],
-                                                             filename=None,
-                                                             local_use=self.grb_object,
-                                                             use_thread=False)
-        self.app.proc_container.update_view_text(' %s' % '')
-        self.on_exit()
-        self.app.inform.emit('[success] %s' % _("Copper Thieving Tool done."))
+            clearance_geometry = []
+            try:
+                for pol in app_obj.grb_object.solid_geometry:
+                    if app_obj.app.abort_flag:
+                        # graceful abort requested by the user
+                        raise grace
 
-    def execute(self):
-        self.app.call_source = "copper_thieving_tool"
+                    clearance_geometry.append(
+                        pol.buffer(c_val, int(int(app_obj.geo_steps_per_circle) / 4))
+                    )
 
-        self.clearance_val = self.clearance_entry.get_value()
-        self.margin_val = self.margin_entry.get_value()
-        reference_method = self.reference_radio.get_value()
+                    pol_nr += 1
+                    disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
 
-        # get the Gerber object on which the Copper thieving will be inserted
-        selection_index = self.grb_object_combo.currentIndex()
-        model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
+                    if old_disp_number < disp_number <= 100:
+                        app_obj.app.proc_container.update_view_text(' %s ... %d%%' %
+                                                                    (_("Thieving"), int(disp_number)))
+                        old_disp_number = disp_number
+            except TypeError:
+                # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
+                # MultiPolygon (not an iterable)
+                clearance_geometry.append(
+                    app_obj.grb_object.solid_geometry.buffer(c_val, int(int(app_obj.geo_steps_per_circle) / 4))
+                )
 
-        try:
-            self.grb_object = model_index.internalPointer().obj
-        except Exception as e:
-            log.debug("ToolCopperThieving.execute() --> %s" % str(e))
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
-            return 'fail'
+            app_obj.app.proc_container.update_view_text(' %s ...' % _("Buffering"))
+            clearance_geometry = unary_union(clearance_geometry)
 
-        if reference_method == 'itself':
-            bound_obj_name = self.grb_object_combo.currentText()
+            # #########################################################################################
+            # Prepare the area to fill with copper. ###################################################
+            # #########################################################################################
+            log.debug("Copper Thieving Tool. Preparing areas to fill with copper.")
+            app_obj.app.inform.emit(_("Copper Thieving Tool. Preparing areas to fill with copper."))
 
-            # Get reference object.
             try:
-                self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
+                if ref_obj is None or ref_obj == 'itself':
+                    working_obj = thieving_obj
+                else:
+                    working_obj = ref_obj
             except Exception as e:
-                self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(e)))
-                return "Could not retrieve object: %s" % self.obj_name
-
-            self.on_copper_thieving(
-                thieving_obj=self.grb_object,
-                c_val=self.clearance_val,
-                margin=self.margin_val
-            )
+                log.debug("ToolCopperThieving.on_copper_thieving() --> %s" % str(e))
+                return 'fail'
 
-        elif reference_method == 'area':
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
+            app_obj.app.proc_container.update_view_text(' %s' % _("Working..."))
+            if ref_selected == 'itself':
+                geo_n = working_obj.solid_geometry
 
-            self.area_method = True
+                try:
+                    if app_obj.ui.bbox_type_radio.get_value() == 'min':
+                        if isinstance(geo_n, MultiPolygon):
+                            env_obj = geo_n.convex_hull
+                        elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \
+                                (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon):
+                            env_obj = cascaded_union(geo_n)
+                        else:
+                            env_obj = cascaded_union(geo_n)
+                            env_obj = env_obj.convex_hull
+                        bounding_box = env_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
+                    else:
+                        if isinstance(geo_n, Polygon):
+                            bounding_box = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre).exterior
+                        elif isinstance(geo_n, list):
+                            geo_n = unary_union(geo_n)
+                            bounding_box = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre).exterior
+                        elif isinstance(geo_n, MultiPolygon):
+                            x0, y0, x1, y1 = geo_n.bounds
+                            geo = box(x0, y0, x1, y1)
+                            bounding_box = geo.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
+                        else:
+                            app_obj.app.inform.emit(
+                                '[ERROR_NOTCL] %s: %s' % (_("Geometry not supported for bounding box"), type(geo_n))
+                            )
+                            return 'fail'
 
-            if self.app.is_legacy is False:
-                self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
-                self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
-                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
-            else:
-                self.app.plotcanvas.graph_event_disconnect(self.app.mp)
-                self.app.plotcanvas.graph_event_disconnect(self.app.mm)
-                self.app.plotcanvas.graph_event_disconnect(self.app.mr)
+                except Exception as e:
+                    log.debug("ToolCopperFIll.on_copper_thieving()  'itself'  --> %s" % str(e))
+                    app_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("No object available."))
+                    return 'fail'
+            elif ref_selected == 'area':
+                geo_buff_list = []
+                try:
+                    for poly in working_obj:
+                        if app_obj.app.abort_flag:
+                            # graceful abort requested by the user
+                            raise grace
+                        geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
+                except TypeError:
+                    geo_buff_list.append(working_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
 
-            self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
-            self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
+                bounding_box = MultiPolygon(geo_buff_list)
+            else:   # ref_selected == 'box'
+                geo_n = working_obj.solid_geometry
 
-        elif reference_method == 'box':
-            bound_obj_name = self.ref_combo.currentText()
+                if working_obj.kind == 'geometry':
+                    try:
+                        __ = iter(geo_n)
+                    except Exception as e:
+                        log.debug("ToolCopperFIll.on_copper_thieving() 'box' --> %s" % str(e))
+                        geo_n = [geo_n]
 
-            # Get reference object.
-            try:
-                self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
-            except Exception as e:
-                self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), bound_obj_name))
-                return "Could not retrieve object: %s. Error: %s" % (bound_obj_name, str(e))
+                    geo_buff_list = []
+                    for poly in geo_n:
+                        if app_obj.app.abort_flag:
+                            # graceful abort requested by the user
+                            raise grace
+                        geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
 
-            self.on_copper_thieving(
-                thieving_obj=self.grb_object,
-                ref_obj=self.ref_obj,
-                c_val=self.clearance_val,
-                margin=self.margin_val
-            )
+                    bounding_box = cascaded_union(geo_buff_list)
+                elif working_obj.kind == 'gerber':
+                    geo_n = cascaded_union(geo_n).convex_hull
+                    bounding_box = cascaded_union(thieving_obj.solid_geometry).convex_hull.intersection(geo_n)
+                    bounding_box = bounding_box.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
+                else:
+                    app_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))
+                    return 'fail'
 
-        # To be called after clicking on the plot.
+            log.debug("Copper Thieving Tool. Finished creating areas to fill with copper.")
 
-    def on_mouse_release(self, event):
-        if self.app.is_legacy is False:
-            event_pos = event.pos
-            # event_is_dragging = event.is_dragging
-            right_button = 2
-        else:
-            event_pos = (event.xdata, event.ydata)
-            # event_is_dragging = self.app.plotcanvas.is_dragging
-            right_button = 3
+            app_obj.app.inform.emit(_("Copper Thieving Tool. Appending new geometry and buffering."))
 
-        event_pos = self.app.plotcanvas.translate_coords(event_pos)
+            # #########################################################################################
+            # ########## Generate filling geometry. ###################################################
+            # #########################################################################################
 
-        # do clear area only for left mouse clicks
-        if event.button == 1:
-            if self.first_click is False:
-                self.first_click = True
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the filling area."))
+            app_obj.new_solid_geometry = bounding_box.difference(clearance_geometry)
 
-                self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
-                if self.app.grid_status() is True:
-                    self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
-            else:
-                self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
-                self.app.delete_selection_shape()
+            # determine the bounding box polygon for the entire Gerber object to which we add copper thieving
+            # if isinstance(geo_n, list):
+            #     env_obj = unary_union(geo_n).buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
+            # else:
+            #     env_obj = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
+            #
+            # x0, y0, x1, y1 = env_obj.bounds
+            # bounding_box = box(x0, y0, x1, y1)
+            app_obj.app.proc_container.update_view_text(' %s' % _("Create geometry"))
 
-                if self.app.grid_status() is True:
-                    curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
-                else:
-                    curr_pos = (event_pos[0], event_pos[1])
+            bounding_box = thieving_obj.solid_geometry.envelope.buffer(
+                distance=margin,
+                join_style=base.JOIN_STYLE.mitre
+            )
+            x0, y0, x1, y1 = bounding_box.bounds
 
-                x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
-                x1, y1 = curr_pos[0], curr_pos[1]
-                pt1 = (x0, y0)
-                pt2 = (x1, y0)
-                pt3 = (x1, y1)
-                pt4 = (x0, y1)
+            if fill_type == 'dot' or fill_type == 'square':
+                # build the MultiPolygon of dots/squares that will fill the entire bounding box
+                thieving_list = []
 
-                new_rectangle = Polygon([pt1, pt2, pt3, pt4])
-                self.sel_rect.append(new_rectangle)
+                if fill_type == 'dot':
+                    radius = dot_dia / 2.0
+                    new_x = x0 + radius
+                    new_y = y0 + radius
+                    while new_x <= x1 - radius:
+                        while new_y <= y1 - radius:
+                            dot_geo = Point((new_x, new_y)).buffer(radius, resolution=64)
+                            thieving_list.append(dot_geo)
+                            new_y += dot_dia + dot_spacing
+                        new_x += dot_dia + dot_spacing
+                        new_y = y0 + radius
+                else:
+                    h_size = square_size / 2.0
+                    new_x = x0 + h_size
+                    new_y = y0 + h_size
+                    while new_x <= x1 - h_size:
+                        while new_y <= y1 - h_size:
+                            a, b, c, d = (Point((new_x, new_y)).buffer(h_size)).bounds
+                            square_geo = box(a, b, c, d)
+                            thieving_list.append(square_geo)
+                            new_y += square_size + square_spacing
+                        new_x += square_size + square_spacing
+                        new_y = y0 + h_size
 
-                # add a temporary shape on canvas
-                self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(x1, y1))
-                self.first_click = False
-                return
+                thieving_box_geo = MultiPolygon(thieving_list)
+                dx = bounding_box.centroid.x - thieving_box_geo.centroid.x
+                dy = bounding_box.centroid.y - thieving_box_geo.centroid.y
 
-        elif event.button == right_button and self.mouse_is_dragging is False:
-            self.area_method = False
-            self.first_click = False
+                thieving_box_geo = affinity.translate(thieving_box_geo, xoff=dx, yoff=dy)
 
-            self.delete_tool_selection_shape()
+                try:
+                    _it = iter(app_obj.new_solid_geometry)
+                except TypeError:
+                    app_obj.new_solid_geometry = [app_obj.new_solid_geometry]
 
-            if self.app.is_legacy is False:
-                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
-                self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
-            else:
-                self.app.plotcanvas.graph_event_disconnect(self.mr)
-                self.app.plotcanvas.graph_event_disconnect(self.mm)
+                try:
+                    _it = iter(thieving_box_geo)
+                except TypeError:
+                    thieving_box_geo = [thieving_box_geo]
 
-            self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
-                                                                  self.app.on_mouse_click_over_plot)
-            self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
-                                                                  self.app.on_mouse_move_over_plot)
-            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
-                                                                  self.app.on_mouse_click_release_over_plot)
+                thieving_geo = []
+                for dot_geo in thieving_box_geo:
+                    for geo_t in app_obj.new_solid_geometry:
+                        if dot_geo.within(geo_t):
+                            thieving_geo.append(dot_geo)
 
-            if len(self.sel_rect) == 0:
-                return
+                app_obj.new_solid_geometry = thieving_geo
 
-            self.sel_rect = cascaded_union(self.sel_rect)
+            if fill_type == 'line':
+                half_thick_line = line_size / 2.0
 
-            if not isinstance(self.sel_rect, Iterable):
-                self.sel_rect = [self.sel_rect]
+                # create a thick polygon-line that surrounds the copper features
+                outline_geometry = []
+                try:
+                    for pol in app_obj.grb_object.solid_geometry:
+                        if app_obj.app.abort_flag:
+                            # graceful abort requested by the user
+                            raise grace
 
-            self.on_copper_thieving(
-                thieving_obj=self.grb_object,
-                ref_obj=self.sel_rect,
-                c_val=self.clearance_val,
-                margin=self.margin_val
-            )
+                        outline_geometry.append(
+                            pol.buffer(c_val+half_thick_line, int(int(app_obj.geo_steps_per_circle) / 4))
+                        )
 
-    # called on mouse move
-    def on_mouse_move(self, event):
-        if self.app.is_legacy is False:
-            event_pos = event.pos
-            event_is_dragging = event.is_dragging
-            # right_button = 2
-        else:
-            event_pos = (event.xdata, event.ydata)
-            event_is_dragging = self.app.plotcanvas.is_dragging
-            # right_button = 3
+                        pol_nr += 1
+                        disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
 
-        curr_pos = self.app.plotcanvas.translate_coords(event_pos)
+                        if old_disp_number < disp_number <= 100:
+                            app_obj.app.proc_container.update_view_text(' %s ... %d%%' %
+                                                                        (_("Buffering"), int(disp_number)))
+                            old_disp_number = disp_number
+                except TypeError:
+                    # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
+                    # MultiPolygon (not an iterable)
+                    outline_geometry.append(
+                        app_obj.grb_object.solid_geometry.buffer(
+                            c_val+half_thick_line,
+                            int(int(app_obj.geo_steps_per_circle) / 4)
+                        )
+                    )
 
-        # detect mouse dragging motion
-        if event_is_dragging is True:
-            self.mouse_is_dragging = True
-        else:
-            self.mouse_is_dragging = False
+                app_obj.app.proc_container.update_view_text(' %s' % _("Buffering"))
+                outline_geometry = unary_union(outline_geometry)
 
-        # update the cursor position
-        if self.app.grid_status() is True:
-            # Update cursor
-            curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
+                outline_line = []
+                try:
+                    for geo_o in outline_geometry:
+                        outline_line.append(
+                            geo_o.exterior.buffer(
+                                half_thick_line, resolution=int(int(app_obj.geo_steps_per_circle) / 4)
+                            )
+                        )
+                except TypeError:
+                    outline_line.append(
+                        outline_geometry.exterior.buffer(
+                            half_thick_line, resolution=int(int(app_obj.geo_steps_per_circle) / 4)
+                        )
+                    )
 
-            self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
-                                         symbol='++', edge_color=self.app.cursor_color_3D,
-                                         edge_width=self.app.defaults["global_cursor_width"],
-                                         size=self.app.defaults["global_cursor_size"])
+                outline_geometry = unary_union(outline_line)
 
-        if self.cursor_pos is None:
-            self.cursor_pos = (0, 0)
+                # create a polygon-line that surrounds in the inside the bounding box polygon of the target Gerber
+                box_outline_geo = box(x0, y0, x1, y1).buffer(-half_thick_line)
+                box_outline_geo_exterior = box_outline_geo.exterior
+                box_outline_geometry = box_outline_geo_exterior.buffer(
+                    half_thick_line,
+                    resolution=int(int(app_obj.geo_steps_per_circle) / 4)
+                )
 
-        self.app.dx = curr_pos[0] - float(self.cursor_pos[0])
-        self.app.dy = curr_pos[1] - float(self.cursor_pos[1])
+                bx0, by0, bx1, by1 = box_outline_geo.bounds
+                thieving_lines_geo = []
+                new_x = bx0
+                new_y = by0
+                while new_x <= x1 - half_thick_line:
+                    line_geo = LineString([(new_x, by0), (new_x, by1)]).buffer(
+                        half_thick_line,
+                        resolution=int(int(app_obj.geo_steps_per_circle) / 4)
+                    )
+                    thieving_lines_geo.append(line_geo)
+                    new_x += line_size + line_spacing
 
-        # # update the positions on status bar
-        self.app.ui.position_label.setText("&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
-                                           "<b>Y</b>: %.4f&nbsp;" % (curr_pos[0], curr_pos[1]))
-        # self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
-        #                                        "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
+                while new_y <= y1 - half_thick_line:
+                    line_geo = LineString([(bx0, new_y), (bx1, new_y)]).buffer(
+                        half_thick_line,
+                        resolution=int(int(app_obj.geo_steps_per_circle) / 4)
+                    )
+                    thieving_lines_geo.append(line_geo)
+                    new_y += line_size + line_spacing
 
-        units = self.app.defaults["units"].lower()
-        self.app.plotcanvas.text_hud.text = \
-            'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX:  \t{:<.4f} [{:s}]\nY:  \t{:<.4f} [{:s}]'.format(
-                self.app.dx, units, self.app.dy, units, curr_pos[0], units, curr_pos[1], units)
+                # merge everything together
+                diff_lines_geo = []
+                for line_poly in thieving_lines_geo:
+                    rest_line = line_poly.difference(clearance_geometry)
+                    diff_lines_geo.append(rest_line)
+                app_obj.flatten([outline_geometry, box_outline_geometry, diff_lines_geo])
+                app_obj.new_solid_geometry = app_obj.flat_geometry
 
-        # draw the utility geometry
-        if self.first_click:
-            self.app.delete_selection_shape()
-            self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
-                                                 coords=(curr_pos[0], curr_pos[1]))
+            app_obj.app.proc_container.update_view_text(' %s' % _("Append geometry"))
+            geo_list = app_obj.grb_object.solid_geometry
+            if isinstance(app_obj.grb_object.solid_geometry, MultiPolygon):
+                geo_list = list(app_obj.grb_object.solid_geometry.geoms)
 
-    def on_copper_thieving(self, thieving_obj, ref_obj=None, c_val=None, margin=None, run_threaded=True):
-        """
+            if '0' not in app_obj.grb_object.apertures:
+                app_obj.grb_object.apertures['0'] = {}
+                app_obj.grb_object.apertures['0']['geometry'] = []
+                app_obj.grb_object.apertures['0']['type'] = 'REG'
+                app_obj.grb_object.apertures['0']['size'] = 0.0
 
-        :param thieving_obj:
-        :param ref_obj:
-        :param c_val:
-        :param margin:
-        :param run_threaded:
-        :return:
-        """
+            try:
+                for poly in app_obj.new_solid_geometry:
+                    # append to the new solid geometry
+                    geo_list.append(poly)
 
-        if run_threaded:
-            proc = self.app.proc_container.new('%s ...' % _("Thieving"))
-        else:
-            QtWidgets.QApplication.processEvents()
+                    # append into the '0' aperture
+                    geo_elem = {}
+                    geo_elem['solid'] = poly
+                    geo_elem['follow'] = poly.exterior
+                    app_obj.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem))
+            except TypeError:
+                # append to the new solid geometry
+                geo_list.append(app_obj.new_solid_geometry)
 
-        self.app.proc_container.view.set_busy('%s ...' % _("Thieving"))
+                # append into the '0' aperture
+                geo_elem = {}
+                geo_elem['solid'] = app_obj.new_solid_geometry
+                geo_elem['follow'] = app_obj.new_solid_geometry.exterior
+                app_obj.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem))
 
-        # #####################################################################
-        # ####### Read the parameters #########################################
-        # #####################################################################
+            app_obj.grb_object.solid_geometry = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001)
 
-        log.debug("Copper Thieving Tool started. Reading parameters.")
-        self.app.inform.emit(_("Copper Thieving Tool started. Reading parameters."))
+            app_obj.app.proc_container.update_view_text(' %s' % _("Append source file"))
+            # update the source file with the new geometry:
+            app_obj.grb_object.source_file = app_obj.app.export_gerber(obj_name=app_obj.grb_object.options['name'],
+                                                                       filename=None,
+                                                                       local_use=app_obj.grb_object,
+                                                                       use_thread=False)
+            app_obj.app.proc_container.update_view_text(' %s' % '')
+            app_obj.on_exit()
+            app_obj.app.inform.emit('[success] %s' % _("Copper Thieving Tool done."))
 
-        ref_selected = self.reference_radio.get_value()
-        if c_val is None:
-            c_val = float(self.app.defaults["tools_copperfill_clearance"])
-        if margin is None:
-            margin = float(self.app.defaults["tools_copperfill_margin"])
+        if run_threaded:
+            self.app.worker_task.emit({'fcn': job_thread_thieving, 'params': [self]})
+        else:
+            job_thread_thieving(self)
 
-        fill_type = self.fill_type_radio.get_value()
-        dot_dia = self.dot_dia_entry.get_value()
-        dot_spacing = self.dot_spacing_entry.get_value()
-        square_size = self.square_size_entry.get_value()
-        square_spacing = self.squares_spacing_entry.get_value()
-        line_size = self.line_size_entry.get_value()
-        line_spacing = self.lines_spacing_entry.get_value()
+    def on_add_ppm(self):
+        run_threaded = True
 
-        # make sure that the source object solid geometry is an Iterable
-        if not isinstance(self.grb_object.solid_geometry, Iterable):
-            self.grb_object.solid_geometry = [self.grb_object.solid_geometry]
+        if run_threaded:
+            self.app.proc_container.new('%s ...' % _("P-Plating Mask"))
+        else:
+            QtWidgets.QApplication.processEvents()
 
-        def job_thread_thieving(app_obj):
-            # #########################################################################################
-            # Prepare isolation polygon. This will create the clearance over the Gerber features ######
-            # #########################################################################################
-            log.debug("Copper Thieving Tool. Preparing isolation polygons.")
-            app_obj.app.inform.emit(_("Copper Thieving Tool. Preparing isolation polygons."))
+        self.app.proc_container.view.set_busy('%s ...' % _("P-Plating Mask"))
 
-            # variables to display the percentage of work done
-            geo_len = 0
-            try:
-                for pol in app_obj.grb_object.solid_geometry:
-                    geo_len += 1
-            except TypeError:
-                geo_len = 1
+        if run_threaded:
+            self.app.worker_task.emit({'fcn': self.on_new_pattern_plating_object, 'params': []})
+        else:
+            self.on_new_pattern_plating_object()
 
-            old_disp_number = 0
-            pol_nr = 0
+    def on_new_pattern_plating_object(self):
+        # get the Gerber object on which the Copper thieving will be inserted
+        selection_index = self.ui.sm_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.ui.sm_object_combo.rootModelIndex())
 
-            clearance_geometry = []
-            try:
-                for pol in app_obj.grb_object.solid_geometry:
-                    if app_obj.app.abort_flag:
-                        # graceful abort requested by the user
-                        raise grace
+        try:
+            self.sm_object = model_index.internalPointer().obj
+        except Exception as e:
+            log.debug("ToolCopperThieving.on_add_ppm() --> %s" % str(e))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return
 
-                    clearance_geometry.append(
-                        pol.buffer(c_val, int(int(app_obj.geo_steps_per_circle) / 4))
-                    )
+        ppm_clearance = self.ui.clearance_ppm_entry.get_value()
+        rb_thickness = self.rb_thickness
 
-                    pol_nr += 1
-                    disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
+        self.app.proc_container.update_view_text(' %s' % _("Append PP-M geometry"))
+        geo_list = self.sm_object.solid_geometry
+        if isinstance(self.sm_object.solid_geometry, MultiPolygon):
+            geo_list = list(self.sm_object.solid_geometry.geoms)
 
-                    if old_disp_number < disp_number <= 100:
-                        app_obj.app.proc_container.update_view_text(' %s ... %d%%' %
-                                                                    (_("Thieving"), int(disp_number)))
-                        old_disp_number = disp_number
-            except TypeError:
-                # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
-                # MultiPolygon (not an iterable)
-                clearance_geometry.append(
-                    app_obj.grb_object.solid_geometry.buffer(c_val, int(int(app_obj.geo_steps_per_circle) / 4))
-                )
+        # if the clearance is negative apply it to the original soldermask too
+        if ppm_clearance < 0:
+            temp_geo_list = []
+            for geo in geo_list:
+                temp_geo_list.append(geo.buffer(ppm_clearance))
+            geo_list = temp_geo_list
 
-            app_obj.app.proc_container.update_view_text(' %s ...' % _("Buffering"))
-            clearance_geometry = unary_union(clearance_geometry)
+        plated_area = 0.0
+        for geo in geo_list:
+            plated_area += geo.area
 
-            # #########################################################################################
-            # Prepare the area to fill with copper. ###################################################
-            # #########################################################################################
-            log.debug("Copper Thieving Tool. Preparing areas to fill with copper.")
-            app_obj.app.inform.emit(_("Copper Thieving Tool. Preparing areas to fill with copper."))
+        if self.new_solid_geometry:
+            for geo in self.new_solid_geometry:
+                plated_area += geo.area
+        if self.robber_geo:
+            plated_area += self.robber_geo.area
+        self.ui.plated_area_entry.set_value(plated_area)
 
-            try:
-                if ref_obj is None or ref_obj == 'itself':
-                    working_obj = thieving_obj
-                else:
-                    working_obj = ref_obj
-            except Exception as e:
-                log.debug("ToolCopperThieving.on_copper_thieving() --> %s" % str(e))
-                return 'fail'
+        thieving_solid_geo = self.new_solid_geometry
+        robber_solid_geo = self.robber_geo
+        robber_line = self.robber_line
 
-            app_obj.app.proc_container.update_view_text(' %s' % _("Working..."))
-            if ref_selected == 'itself':
-                geo_n = working_obj.solid_geometry
+        def obj_init(grb_obj, app_obj):
+            grb_obj.multitool = False
+            grb_obj.source_file = []
+            grb_obj.multigeo = False
+            grb_obj.follow = False
+            grb_obj.apertures = {}
+            grb_obj.solid_geometry = []
 
-                try:
-                    if app_obj.bbox_type_radio.get_value() == 'min':
-                        if isinstance(geo_n, MultiPolygon):
-                            env_obj = geo_n.convex_hull
-                        elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \
-                                (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon):
-                            env_obj = cascaded_union(geo_n)
-                        else:
-                            env_obj = cascaded_union(geo_n)
-                            env_obj = env_obj.convex_hull
-                        bounding_box = env_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
-                    else:
-                        if isinstance(geo_n, Polygon):
-                            bounding_box = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre).exterior
-                        elif isinstance(geo_n, list):
-                            geo_n = unary_union(geo_n)
-                            bounding_box = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre).exterior
-                        elif isinstance(geo_n, MultiPolygon):
-                            x0, y0, x1, y1 = geo_n.bounds
-                            geo = box(x0, y0, x1, y1)
-                            bounding_box = geo.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
-                        else:
-                            app_obj.app.inform.emit(
-                                '[ERROR_NOTCL] %s: %s' % (_("Geometry not supported for bounding box"), type(geo_n))
-                            )
-                            return 'fail'
+            # if we have copper thieving geometry, add it
+            if thieving_solid_geo:
+                if '0' not in grb_obj.apertures:
+                    grb_obj.apertures['0'] = {}
+                    grb_obj.apertures['0']['geometry'] = []
+                    grb_obj.apertures['0']['type'] = 'REG'
+                    grb_obj.apertures['0']['size'] = 0.0
 
-                except Exception as e:
-                    log.debug("ToolCopperFIll.on_copper_thieving()  'itself'  --> %s" % str(e))
-                    app_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("No object available."))
-                    return 'fail'
-            elif ref_selected == 'area':
-                geo_buff_list = []
                 try:
-                    for poly in working_obj:
-                        if app_obj.app.abort_flag:
-                            # graceful abort requested by the user
-                            raise grace
-                        geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
-                except TypeError:
-                    geo_buff_list.append(working_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
+                    for poly in thieving_solid_geo:
+                        poly_b = poly.buffer(ppm_clearance)
 
-                bounding_box = MultiPolygon(geo_buff_list)
-            else:   # ref_selected == 'box'
-                geo_n = working_obj.solid_geometry
+                        # append to the new solid geometry
+                        geo_list.append(poly_b)
 
-                if working_obj.kind == 'geometry':
-                    try:
-                        __ = iter(geo_n)
-                    except Exception as e:
-                        log.debug("ToolCopperFIll.on_copper_thieving() 'box' --> %s" % str(e))
-                        geo_n = [geo_n]
+                        # append into the '0' aperture
+                        geo_elem = {}
+                        geo_elem['solid'] = poly_b
+                        geo_elem['follow'] = poly_b.exterior
+                        grb_obj.apertures['0']['geometry'].append(deepcopy(geo_elem))
+                except TypeError:
+                    # append to the new solid geometry
+                    geo_list.append(thieving_solid_geo.buffer(ppm_clearance))
 
-                    geo_buff_list = []
-                    for poly in geo_n:
-                        if app_obj.app.abort_flag:
-                            # graceful abort requested by the user
-                            raise grace
-                        geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
+                    # append into the '0' aperture
+                    geo_elem = {}
+                    geo_elem['solid'] = thieving_solid_geo.buffer(ppm_clearance)
+                    geo_elem['follow'] = thieving_solid_geo.buffer(ppm_clearance).exterior
+                    grb_obj.apertures['0']['geometry'].append(deepcopy(geo_elem))
 
-                    bounding_box = cascaded_union(geo_buff_list)
-                elif working_obj.kind == 'gerber':
-                    geo_n = cascaded_union(geo_n).convex_hull
-                    bounding_box = cascaded_union(thieving_obj.solid_geometry).convex_hull.intersection(geo_n)
-                    bounding_box = bounding_box.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
+            # if we have robber bar geometry, add it
+            if robber_solid_geo:
+                aperture_found = None
+                for ap_id, ap_val in grb_obj.apertures.items():
+                    if ap_val['type'] == 'C' and ap_val['size'] == app_obj.rb_thickness + ppm_clearance:
+                        aperture_found = ap_id
+                        break
+
+                if aperture_found:
+                    geo_elem = {}
+                    geo_elem['solid'] = robber_solid_geo
+                    geo_elem['follow'] = robber_line
+                    grb_obj.apertures[aperture_found]['geometry'].append(deepcopy(geo_elem))
                 else:
-                    app_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))
-                    return 'fail'
+                    ap_keys = list(grb_obj.apertures.keys())
+                    max_apid = int(max(ap_keys))
+                    if ap_keys and max_apid != 0:
+                        new_apid = str(max_apid + 1)
+                    else:
+                        new_apid = '10'
 
-            log.debug("Copper Thieving Tool. Finished creating areas to fill with copper.")
+                    grb_obj.apertures[new_apid] = {}
+                    grb_obj.apertures[new_apid]['type'] = 'C'
+                    grb_obj.apertures[new_apid]['size'] = rb_thickness + ppm_clearance
+                    grb_obj.apertures[new_apid]['geometry'] = []
 
-            app_obj.app.inform.emit(_("Copper Thieving Tool. Appending new geometry and buffering."))
+                    geo_elem = {}
+                    geo_elem['solid'] = robber_solid_geo.buffer(ppm_clearance)
+                    geo_elem['follow'] = Polygon(robber_line).buffer(ppm_clearance / 2.0).exterior
+                    grb_obj.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
 
-            # #########################################################################################
-            # ########## Generate filling geometry. ###################################################
-            # #########################################################################################
+                geo_list.append(robber_solid_geo.buffer(ppm_clearance))
 
-            app_obj.new_solid_geometry = bounding_box.difference(clearance_geometry)
+            grb_obj.solid_geometry = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001)
 
-            # determine the bounding box polygon for the entire Gerber object to which we add copper thieving
-            # if isinstance(geo_n, list):
-            #     env_obj = unary_union(geo_n).buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
-            # else:
-            #     env_obj = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
-            #
-            # x0, y0, x1, y1 = env_obj.bounds
-            # bounding_box = box(x0, y0, x1, y1)
-            app_obj.app.proc_container.update_view_text(' %s' % _("Create geometry"))
+            app_obj.proc_container.update_view_text(' %s' % _("Append source file"))
+            # update the source file with the new geometry:
+            grb_obj.source_file = app_obj.export_gerber(obj_name=name,
+                                                        filename=None,
+                                                        local_use=grb_obj,
+                                                        use_thread=False)
+            app_obj.proc_container.update_view_text(' %s' % '')
 
-            bounding_box = thieving_obj.solid_geometry.envelope.buffer(
-                distance=margin,
-                join_style=base.JOIN_STYLE.mitre
-            )
-            x0, y0, x1, y1 = bounding_box.bounds
+        # Object name
+        obj_name, separatpr, obj_extension = self.sm_object.options['name'].rpartition('.')
+        name = '%s_%s.%s' % (obj_name, 'plating_mask', obj_extension)
 
-            if fill_type == 'dot' or fill_type == 'square':
-                # build the MultiPolygon of dots/squares that will fill the entire bounding box
-                thieving_list = []
+        self.app.app_obj.new_object('gerber', name, obj_init, autoselected=False)
 
-                if fill_type == 'dot':
-                    radius = dot_dia / 2.0
-                    new_x = x0 + radius
-                    new_y = y0 + radius
-                    while new_x <= x1 - radius:
-                        while new_y <= y1 - radius:
-                            dot_geo = Point((new_x, new_y)).buffer(radius, resolution=64)
-                            thieving_list.append(dot_geo)
-                            new_y += dot_dia + dot_spacing
-                        new_x += dot_dia + dot_spacing
-                        new_y = y0 + radius
-                else:
-                    h_size = square_size / 2.0
-                    new_x = x0 + h_size
-                    new_y = y0 + h_size
-                    while new_x <= x1 - h_size:
-                        while new_y <= y1 - h_size:
-                            a, b, c, d = (Point((new_x, new_y)).buffer(h_size)).bounds
-                            square_geo = box(a, b, c, d)
-                            thieving_list.append(square_geo)
-                            new_y += square_size + square_spacing
-                        new_x += square_size + square_spacing
-                        new_y = y0 + h_size
-
-                thieving_box_geo = MultiPolygon(thieving_list)
-                dx = bounding_box.centroid.x - thieving_box_geo.centroid.x
-                dy = bounding_box.centroid.y - thieving_box_geo.centroid.y
+        # Register recent file
+        self.app.file_opened.emit("gerber", name)
 
-                thieving_box_geo = affinity.translate(thieving_box_geo, xoff=dx, yoff=dy)
+        self.on_exit()
+        self.app.inform.emit('[success] %s' % _("Generating Pattern Plating Mask done."))
 
-                try:
-                    _it = iter(app_obj.new_solid_geometry)
-                except TypeError:
-                    app_obj.new_solid_geometry = [app_obj.new_solid_geometry]
+    def replot(self, obj):
+        def worker_task():
+            with self.app.proc_container.new('%s...' % _("Plotting")):
+                obj.plot()
 
-                try:
-                    _it = iter(thieving_box_geo)
-                except TypeError:
-                    thieving_box_geo = [thieving_box_geo]
+        self.app.worker_task.emit({'fcn': worker_task, 'params': []})
 
-                thieving_geo = []
-                for dot_geo in thieving_box_geo:
-                    for geo_t in app_obj.new_solid_geometry:
-                        if dot_geo.within(geo_t):
-                            thieving_geo.append(dot_geo)
+    def on_exit(self):
+        # plot the objects
+        if self.grb_object:
+            self.replot(obj=self.grb_object)
 
-                app_obj.new_solid_geometry = thieving_geo
+        if self.sm_object:
+            self.replot(obj=self.sm_object)
 
-            if fill_type == 'line':
-                half_thick_line = line_size / 2.0
+        # update the bounding box values
+        try:
+            a, b, c, d = self.grb_object.bounds()
+            self.grb_object.options['xmin'] = a
+            self.grb_object.options['ymin'] = b
+            self.grb_object.options['xmax'] = c
+            self.grb_object.options['ymax'] = d
+        except Exception as e:
+            log.debug("ToolCopperThieving.on_exit() bounds -> copper thieving Gerber error --> %s" % str(e))
 
-                # create a thick polygon-line that surrounds the copper features
-                outline_geometry = []
-                try:
-                    for pol in app_obj.grb_object.solid_geometry:
-                        if app_obj.app.abort_flag:
-                            # graceful abort requested by the user
-                            raise grace
+        # update the bounding box values
+        try:
+            a, b, c, d = self.sm_object.bounds()
+            self.sm_object.options['xmin'] = a
+            self.sm_object.options['ymin'] = b
+            self.sm_object.options['xmax'] = c
+            self.sm_object.options['ymax'] = d
+        except Exception as e:
+            log.debug("ToolCopperThieving.on_exit() bounds -> pattern plating mask error --> %s" % str(e))
 
-                        outline_geometry.append(
-                            pol.buffer(c_val+half_thick_line, int(int(app_obj.geo_steps_per_circle) / 4))
-                        )
+        # reset the variables
+        self.grb_object = None
+        self.sm_object = None
+        self.ref_obj = None
+        self.sel_rect = []
 
-                        pol_nr += 1
-                        disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
+        # Events ID
+        self.mr = None
+        self.mm = None
 
-                        if old_disp_number < disp_number <= 100:
-                            app_obj.app.proc_container.update_view_text(' %s ... %d%%' %
-                                                                        (_("Buffering"), int(disp_number)))
-                            old_disp_number = disp_number
-                except TypeError:
-                    # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
-                    # MultiPolygon (not an iterable)
-                    outline_geometry.append(
-                        app_obj.grb_object.solid_geometry.buffer(
-                            c_val+half_thick_line,
-                            int(int(app_obj.geo_steps_per_circle) / 4)
-                        )
-                    )
+        # Mouse cursor positions
+        self.mouse_is_dragging = False
+        self.cursor_pos = (0, 0)
+        self.first_click = False
 
-                app_obj.app.proc_container.update_view_text(' %s' % _("Buffering"))
-                outline_geometry = unary_union(outline_geometry)
+        # if True it means we exited from tool in the middle of area adding therefore disconnect the events
+        if self.area_method is True:
+            self.app.delete_selection_shape()
+            self.area_method = False
 
-                outline_line = []
-                try:
-                    for geo_o in outline_geometry:
-                        outline_line.append(
-                            geo_o.exterior.buffer(
-                                half_thick_line, resolution=int(int(app_obj.geo_steps_per_circle) / 4)
-                            )
-                        )
-                except TypeError:
-                    outline_line.append(
-                        outline_geometry.exterior.buffer(
-                            half_thick_line, resolution=int(int(app_obj.geo_steps_per_circle) / 4)
-                        )
-                    )
+            if self.app.is_legacy is False:
+                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
+                self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+            else:
+                self.app.plotcanvas.graph_event_disconnect(self.mr)
+                self.app.plotcanvas.graph_event_disconnect(self.mm)
 
-                outline_geometry = unary_union(outline_line)
+            self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
+                                                                  self.app.on_mouse_click_over_plot)
+            self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
+                                                                  self.app.on_mouse_move_over_plot)
+            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                                  self.app.on_mouse_click_release_over_plot)
 
-                # create a polygon-line that surrounds in the inside the bounding box polygon of the target Gerber
-                box_outline_geo = box(x0, y0, x1, y1).buffer(-half_thick_line)
-                box_outline_geo_exterior = box_outline_geo.exterior
-                box_outline_geometry = box_outline_geo_exterior.buffer(
-                    half_thick_line,
-                    resolution=int(int(app_obj.geo_steps_per_circle) / 4)
-                )
+        self.app.call_source = "app"
+        self.app.inform.emit('[success] %s' % _("Copper Thieving Tool exit."))
 
-                bx0, by0, bx1, by1 = box_outline_geo.bounds
-                thieving_lines_geo = []
-                new_x = bx0
-                new_y = by0
-                while new_x <= x1 - half_thick_line:
-                    line_geo = LineString([(new_x, by0), (new_x, by1)]).buffer(
-                        half_thick_line,
-                        resolution=int(int(app_obj.geo_steps_per_circle) / 4)
-                    )
-                    thieving_lines_geo.append(line_geo)
-                    new_x += line_size + line_spacing
+    def flatten(self, geometry):
+        """
+        Creates a list of non-iterable linear geometry objects.
+        :param geometry: Shapely type or list or list of list of such.
 
-                while new_y <= y1 - half_thick_line:
-                    line_geo = LineString([(bx0, new_y), (bx1, new_y)]).buffer(
-                        half_thick_line,
-                        resolution=int(int(app_obj.geo_steps_per_circle) / 4)
-                    )
-                    thieving_lines_geo.append(line_geo)
-                    new_y += line_size + line_spacing
+        Results are placed in self.flat_geometry
+        """
 
-                # merge everything together
-                diff_lines_geo = []
-                for line_poly in thieving_lines_geo:
-                    rest_line = line_poly.difference(clearance_geometry)
-                    diff_lines_geo.append(rest_line)
-                app_obj.flatten([outline_geometry, box_outline_geometry, diff_lines_geo])
-                app_obj.new_solid_geometry = app_obj.flat_geometry
+        # ## If iterable, expand recursively.
+        try:
+            for geo in geometry:
+                if geo is not None:
+                    self.flatten(geometry=geo)
 
-            app_obj.app.proc_container.update_view_text(' %s' % _("Append geometry"))
-            geo_list = app_obj.grb_object.solid_geometry
-            if isinstance(app_obj.grb_object.solid_geometry, MultiPolygon):
-                geo_list = list(app_obj.grb_object.solid_geometry.geoms)
+        # ## Not iterable, do the actual indexing and add.
+        except TypeError:
+            self.flat_geometry.append(geometry)
 
-            if '0' not in app_obj.grb_object.apertures:
-                app_obj.grb_object.apertures['0'] = {}
-                app_obj.grb_object.apertures['0']['geometry'] = []
-                app_obj.grb_object.apertures['0']['type'] = 'REG'
-                app_obj.grb_object.apertures['0']['size'] = 0.0
+        return self.flat_geometry
 
-            try:
-                for poly in app_obj.new_solid_geometry:
-                    # append to the new solid geometry
-                    geo_list.append(poly)
 
-                    # append into the '0' aperture
-                    geo_elem = {}
-                    geo_elem['solid'] = poly
-                    geo_elem['follow'] = poly.exterior
-                    app_obj.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem))
-            except TypeError:
-                # append to the new solid geometry
-                geo_list.append(app_obj.new_solid_geometry)
+class ThievingUI:
 
-                # append into the '0' aperture
-                geo_elem = {}
-                geo_elem['solid'] = app_obj.new_solid_geometry
-                geo_elem['follow'] = app_obj.new_solid_geometry.exterior
-                app_obj.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem))
+    toolName = _("Copper Thieving Tool")
 
-            app_obj.grb_object.solid_geometry = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001)
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.units = self.app.defaults['units']
+        self.layout = layout
 
-            app_obj.app.proc_container.update_view_text(' %s' % _("Append source file"))
-            # update the source file with the new geometry:
-            app_obj.grb_object.source_file = app_obj.app.export_gerber(obj_name=app_obj.grb_object.options['name'],
-                                                                       filename=None,
-                                                                       local_use=app_obj.grb_object,
-                                                                       use_thread=False)
-            app_obj.app.proc_container.update_view_text(' %s' % '')
-            app_obj.on_exit()
-            app_obj.app.inform.emit('[success] %s' % _("Copper Thieving Tool done."))
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(QtWidgets.QLabel(""))
 
-        if run_threaded:
-            self.app.worker_task.emit({'fcn': job_thread_thieving, 'params': [self]})
-        else:
-            job_thread_thieving(self)
+        # ## Grid Layout
+        i_grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(i_grid_lay)
+        i_grid_lay.setColumnStretch(0, 0)
+        i_grid_lay.setColumnStretch(1, 1)
 
-    def on_add_ppm(self):
-        run_threaded = True
+        self.grb_object_combo = FCComboBox()
+        self.grb_object_combo.setModel(self.app.collection)
+        self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.grb_object_combo.is_last = True
+        self.grb_object_combo.obj_type = 'Gerber'
 
-        if run_threaded:
-            proc = self.app.proc_container.new('%s ...' % _("P-Plating Mask"))
-        else:
-            QtWidgets.QApplication.processEvents()
+        self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
+        self.grbobj_label.setToolTip(
+            _("Gerber Object to which will be added a copper thieving.")
+        )
 
-        self.app.proc_container.view.set_busy('%s ...' % _("P-Plating Mask"))
+        i_grid_lay.addWidget(self.grbobj_label, 0, 0)
+        i_grid_lay.addWidget(self.grb_object_combo, 1, 0, 1, 2)
 
-        if run_threaded:
-            self.app.worker_task.emit({'fcn': self.on_new_pattern_plating_object, 'params': []})
-        else:
-            self.on_new_pattern_plating_object()
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        i_grid_lay.addWidget(separator_line, 2, 0, 1, 2)
 
-    def on_new_pattern_plating_object(self):
-        # get the Gerber object on which the Copper thieving will be inserted
-        selection_index = self.sm_object_combo.currentIndex()
-        model_index = self.app.collection.index(selection_index, 0, self.sm_object_combo.rootModelIndex())
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
 
-        try:
-            self.sm_object = model_index.internalPointer().obj
-        except Exception as e:
-            log.debug("ToolCopperThieving.on_add_ppm() --> %s" % str(e))
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
-            return 'fail'
+        self.copper_fill_label = QtWidgets.QLabel('<b>%s</b>' % _('Parameters'))
+        self.copper_fill_label.setToolTip(
+            _("Parameters used for this tool.")
+        )
+        grid_lay.addWidget(self.copper_fill_label, 0, 0, 1, 2)
 
-        ppm_clearance = self.clearance_ppm_entry.get_value()
-        rb_thickness = self.rb_thickness
+        # CLEARANCE #
+        self.clearance_label = QtWidgets.QLabel('%s:' % _("Clearance"))
+        self.clearance_label.setToolTip(
+            _("This set the distance between the copper thieving components\n"
+              "(the polygon fill may be split in multiple polygons)\n"
+              "and the copper traces in the Gerber file.")
+        )
+        self.clearance_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.clearance_entry.set_range(0.00001, 9999.9999)
+        self.clearance_entry.set_precision(self.decimals)
+        self.clearance_entry.setSingleStep(0.1)
 
-        self.app.proc_container.update_view_text(' %s' % _("Append PP-M geometry"))
-        geo_list = self.sm_object.solid_geometry
-        if isinstance(self.sm_object.solid_geometry, MultiPolygon):
-            geo_list = list(self.sm_object.solid_geometry.geoms)
+        grid_lay.addWidget(self.clearance_label, 1, 0)
+        grid_lay.addWidget(self.clearance_entry, 1, 1)
 
-        # if the clearance is negative apply it to the original soldermask too
-        if ppm_clearance < 0:
-            temp_geo_list = []
-            for geo in geo_list:
-                temp_geo_list.append(geo.buffer(ppm_clearance))
-            geo_list = temp_geo_list
+        # MARGIN #
+        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
+        self.margin_label.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.margin_entry.set_range(0.0, 9999.9999)
+        self.margin_entry.set_precision(self.decimals)
+        self.margin_entry.setSingleStep(0.1)
 
-        plated_area = 0.0
-        for geo in geo_list:
-            plated_area += geo.area
+        grid_lay.addWidget(self.margin_label, 2, 0)
+        grid_lay.addWidget(self.margin_entry, 2, 1)
 
-        if self.new_solid_geometry:
-            for geo in self.new_solid_geometry:
-                plated_area += geo.area
-        if self.robber_geo:
-            plated_area += self.robber_geo.area
-        self.plated_area_entry.set_value(plated_area)
+        # Reference #
+        self.reference_radio = RadioSet([
+            {'label': _('Itself'), 'value': 'itself'},
+            {"label": _("Area Selection"), "value": "area"},
+            {'label': _("Reference Object"), 'value': 'box'}
+        ], orientation='vertical', stretch=False)
+        self.reference_label = QtWidgets.QLabel(_("Reference:"))
+        self.reference_label.setToolTip(
+            _("- 'Itself' - the copper thieving extent is based on the object extent.\n"
+              "- 'Area Selection' - left mouse click to start selection of the area to be filled.\n"
+              "- 'Reference Object' - will do copper thieving within the area specified by another object.")
+        )
+        grid_lay.addWidget(self.reference_label, 3, 0)
+        grid_lay.addWidget(self.reference_radio, 3, 1)
 
-        thieving_solid_geo = self.new_solid_geometry
-        robber_solid_geo = self.robber_geo
-        robber_line = self.robber_line
+        self.ref_combo_type_label = QtWidgets.QLabel('%s:' % _("Ref. Type"))
+        self.ref_combo_type_label.setToolTip(
+            _("The type of FlatCAM object to be used as copper thieving reference.\n"
+              "It can be Gerber, Excellon or Geometry.")
+        )
+        self.ref_combo_type = FCComboBox()
+        self.ref_combo_type.addItems([_("Gerber"), _("Excellon"), _("Geometry")])
 
-        def obj_init(grb_obj, app_obj):
-            grb_obj.multitool = False
-            grb_obj.source_file = []
-            grb_obj.multigeo = False
-            grb_obj.follow = False
-            grb_obj.apertures = {}
-            grb_obj.solid_geometry = []
+        grid_lay.addWidget(self.ref_combo_type_label, 4, 0)
+        grid_lay.addWidget(self.ref_combo_type, 4, 1)
 
-            # 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.ref_combo_label = QtWidgets.QLabel('%s:' % _("Ref. Object"))
+        self.ref_combo_label.setToolTip(
+            _("The FlatCAM object to be used as non copper clearing reference.")
+        )
+        self.ref_combo = FCComboBox()
+        self.ref_combo.setModel(self.app.collection)
+        self.ref_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ref_combo.is_last = True
+        self.ref_combo.obj_type = {
+            _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
+        }[self.ref_combo_type.get_value()]
 
-            # if we have copper thieving geometry, add it
-            if thieving_solid_geo:
-                if '0' not in grb_obj.apertures:
-                    grb_obj.apertures['0'] = {}
-                    grb_obj.apertures['0']['geometry'] = []
-                    grb_obj.apertures['0']['type'] = 'REG'
-                    grb_obj.apertures['0']['size'] = 0.0
+        grid_lay.addWidget(self.ref_combo_label, 5, 0)
+        grid_lay.addWidget(self.ref_combo, 5, 1)
 
-                try:
-                    for poly in thieving_solid_geo:
-                        poly_b = poly.buffer(ppm_clearance)
+        self.ref_combo.hide()
+        self.ref_combo_label.hide()
+        self.ref_combo_type.hide()
+        self.ref_combo_type_label.hide()
 
-                        # append to the new solid geometry
-                        geo_list.append(poly_b)
+        # Bounding Box Type #
+        self.bbox_type_radio = RadioSet([
+            {'label': _('Rectangular'), 'value': 'rect'},
+            {"label": _("Minimal"), "value": "min"}
+        ], stretch=False)
+        self.bbox_type_label = QtWidgets.QLabel(_("Box Type:"))
+        self.bbox_type_label.setToolTip(
+            _("- 'Rectangular' - the bounding box will be of rectangular shape.\n"
+              "- 'Minimal' - the bounding box will be the convex hull shape.")
+        )
+        grid_lay.addWidget(self.bbox_type_label, 6, 0)
+        grid_lay.addWidget(self.bbox_type_radio, 6, 1)
+        self.bbox_type_label.hide()
+        self.bbox_type_radio.hide()
 
-                        # append into the '0' aperture
-                        geo_elem = {}
-                        geo_elem['solid'] = poly_b
-                        geo_elem['follow'] = poly_b.exterior
-                        grb_obj.apertures['0']['geometry'].append(deepcopy(geo_elem))
-                except TypeError:
-                    # append to the new solid geometry
-                    geo_list.append(thieving_solid_geo.buffer(ppm_clearance))
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 7, 0, 1, 2)
 
-                    # append into the '0' aperture
-                    geo_elem = {}
-                    geo_elem['solid'] = thieving_solid_geo.buffer(ppm_clearance)
-                    geo_elem['follow'] = thieving_solid_geo.buffer(ppm_clearance).exterior
-                    grb_obj.apertures['0']['geometry'].append(deepcopy(geo_elem))
+        # Fill Type
+        self.fill_type_radio = RadioSet([
+            {'label': _('Solid'), 'value': 'solid'},
+            {"label": _("Dots Grid"), "value": "dot"},
+            {"label": _("Squares Grid"), "value": "square"},
+            {"label": _("Lines Grid"), "value": "line"}
+        ], orientation='vertical', stretch=False)
+        self.fill_type_label = QtWidgets.QLabel(_("Fill Type:"))
+        self.fill_type_label.setToolTip(
+            _("- 'Solid' - copper thieving will be a solid polygon.\n"
+              "- 'Dots Grid' - the empty area will be filled with a pattern of dots.\n"
+              "- 'Squares Grid' - the empty area will be filled with a pattern of squares.\n"
+              "- 'Lines Grid' - the empty area will be filled with a pattern of lines.")
+        )
+        grid_lay.addWidget(self.fill_type_label, 8, 0)
+        grid_lay.addWidget(self.fill_type_radio, 8, 1)
 
-            # if we have robber bar geometry, add it
-            if robber_solid_geo:
-                aperture_found = None
-                for ap_id, ap_val in grb_obj.apertures.items():
-                    if ap_val['type'] == 'C' and ap_val['size'] == app_obj.rb_thickness + ppm_clearance:
-                        aperture_found = ap_id
-                        break
+        # DOTS FRAME
+        self.dots_frame = QtWidgets.QFrame()
+        self.dots_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.dots_frame)
+        dots_grid = QtWidgets.QGridLayout()
+        dots_grid.setColumnStretch(0, 0)
+        dots_grid.setColumnStretch(1, 1)
+        dots_grid.setContentsMargins(0, 0, 0, 0)
+        self.dots_frame.setLayout(dots_grid)
+        self.dots_frame.hide()
+
+        self.dots_label = QtWidgets.QLabel('<b>%s</b>:' % _("Dots Grid Parameters"))
+        dots_grid.addWidget(self.dots_label, 0, 0, 1, 2)
+
+        # Dot diameter #
+        self.dotdia_label = QtWidgets.QLabel('%s:' % _("Dia"))
+        self.dotdia_label.setToolTip(
+            _("Dot diameter in Dots Grid.")
+        )
+        self.dot_dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.dot_dia_entry.set_range(0.0, 9999.9999)
+        self.dot_dia_entry.set_precision(self.decimals)
+        self.dot_dia_entry.setSingleStep(0.1)
+
+        dots_grid.addWidget(self.dotdia_label, 1, 0)
+        dots_grid.addWidget(self.dot_dia_entry, 1, 1)
+
+        # Dot spacing #
+        self.dotspacing_label = QtWidgets.QLabel('%s:' % _("Spacing"))
+        self.dotspacing_label.setToolTip(
+            _("Distance between each two dots in Dots Grid.")
+        )
+        self.dot_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.dot_spacing_entry.set_range(0.0, 9999.9999)
+        self.dot_spacing_entry.set_precision(self.decimals)
+        self.dot_spacing_entry.setSingleStep(0.1)
+
+        dots_grid.addWidget(self.dotspacing_label, 2, 0)
+        dots_grid.addWidget(self.dot_spacing_entry, 2, 1)
+
+        # SQUARES FRAME
+        self.squares_frame = QtWidgets.QFrame()
+        self.squares_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.squares_frame)
+        squares_grid = QtWidgets.QGridLayout()
+        squares_grid.setColumnStretch(0, 0)
+        squares_grid.setColumnStretch(1, 1)
+        squares_grid.setContentsMargins(0, 0, 0, 0)
+        self.squares_frame.setLayout(squares_grid)
+        self.squares_frame.hide()
+
+        self.squares_label = QtWidgets.QLabel('<b>%s</b>:' % _("Squares Grid Parameters"))
+        squares_grid.addWidget(self.squares_label, 0, 0, 1, 2)
+
+        # Square Size #
+        self.square_size_label = QtWidgets.QLabel('%s:' % _("Size"))
+        self.square_size_label.setToolTip(
+            _("Square side size in Squares Grid.")
+        )
+        self.square_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.square_size_entry.set_range(0.0, 9999.9999)
+        self.square_size_entry.set_precision(self.decimals)
+        self.square_size_entry.setSingleStep(0.1)
+
+        squares_grid.addWidget(self.square_size_label, 1, 0)
+        squares_grid.addWidget(self.square_size_entry, 1, 1)
+
+        # Squares spacing #
+        self.squares_spacing_label = QtWidgets.QLabel('%s:' % _("Spacing"))
+        self.squares_spacing_label.setToolTip(
+            _("Distance between each two squares in Squares Grid.")
+        )
+        self.squares_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.squares_spacing_entry.set_range(0.0, 9999.9999)
+        self.squares_spacing_entry.set_precision(self.decimals)
+        self.squares_spacing_entry.setSingleStep(0.1)
+
+        squares_grid.addWidget(self.squares_spacing_label, 2, 0)
+        squares_grid.addWidget(self.squares_spacing_entry, 2, 1)
+
+        # LINES FRAME
+        self.lines_frame = QtWidgets.QFrame()
+        self.lines_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.lines_frame)
+        lines_grid = QtWidgets.QGridLayout()
+        lines_grid.setColumnStretch(0, 0)
+        lines_grid.setColumnStretch(1, 1)
+        lines_grid.setContentsMargins(0, 0, 0, 0)
+        self.lines_frame.setLayout(lines_grid)
+        self.lines_frame.hide()
+
+        self.lines_label = QtWidgets.QLabel('<b>%s</b>:' % _("Lines Grid Parameters"))
+        lines_grid.addWidget(self.lines_label, 0, 0, 1, 2)
+
+        # Square Size #
+        self.line_size_label = QtWidgets.QLabel('%s:' % _("Size"))
+        self.line_size_label.setToolTip(
+            _("Line thickness size in Lines Grid.")
+        )
+        self.line_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.line_size_entry.set_range(0.0, 9999.9999)
+        self.line_size_entry.set_precision(self.decimals)
+        self.line_size_entry.setSingleStep(0.1)
 
-                if aperture_found:
-                    geo_elem = {}
-                    geo_elem['solid'] = robber_solid_geo
-                    geo_elem['follow'] = robber_line
-                    grb_obj.apertures[aperture_found]['geometry'].append(deepcopy(geo_elem))
-                else:
-                    ap_keys = list(grb_obj.apertures.keys())
-                    max_apid = int(max(ap_keys))
-                    if ap_keys and max_apid != 0:
-                        new_apid = str(max_apid + 1)
-                    else:
-                        new_apid = '10'
+        lines_grid.addWidget(self.line_size_label, 1, 0)
+        lines_grid.addWidget(self.line_size_entry, 1, 1)
 
-                    grb_obj.apertures[new_apid] = {}
-                    grb_obj.apertures[new_apid]['type'] = 'C'
-                    grb_obj.apertures[new_apid]['size'] = rb_thickness + ppm_clearance
-                    grb_obj.apertures[new_apid]['geometry'] = []
+        # Lines spacing #
+        self.lines_spacing_label = QtWidgets.QLabel('%s:' % _("Spacing"))
+        self.lines_spacing_label.setToolTip(
+            _("Distance between each two lines in Lines Grid.")
+        )
+        self.lines_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.lines_spacing_entry.set_range(0.0, 9999.9999)
+        self.lines_spacing_entry.set_precision(self.decimals)
+        self.lines_spacing_entry.setSingleStep(0.1)
 
-                    geo_elem = {}
-                    geo_elem['solid'] = robber_solid_geo.buffer(ppm_clearance)
-                    geo_elem['follow'] = Polygon(robber_line).buffer(ppm_clearance / 2.0).exterior
-                    grb_obj.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
+        lines_grid.addWidget(self.lines_spacing_label, 2, 0)
+        lines_grid.addWidget(self.lines_spacing_entry, 2, 1)
 
-                geo_list.append(robber_solid_geo.buffer(ppm_clearance))
+        # ## Insert Copper Thieving
+        self.fill_button = QtWidgets.QPushButton(_("Insert Copper thieving"))
+        self.fill_button.setToolTip(
+            _("Will add a polygon (may be split in multiple parts)\n"
+              "that will surround the actual Gerber traces at a certain distance.")
+        )
+        self.fill_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.fill_button)
 
-            grb_obj.solid_geometry = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001)
+        # ## Grid Layout
+        grid_lay_1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay_1)
+        grid_lay_1.setColumnStretch(0, 0)
+        grid_lay_1.setColumnStretch(1, 1)
+        grid_lay_1.setColumnStretch(2, 0)
 
-            app_obj.proc_container.update_view_text(' %s' % _("Append source file"))
-            # update the source file with the new geometry:
-            grb_obj.source_file = app_obj.export_gerber(obj_name=name,
-                                                        filename=None,
-                                                        local_use=grb_obj,
-                                                        use_thread=False)
-            app_obj.proc_container.update_view_text(' %s' % '')
+        separator_line_1 = QtWidgets.QFrame()
+        separator_line_1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line_1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay_1.addWidget(separator_line_1, 0, 0, 1, 3)
 
-        # Object name
-        obj_name, separatpr, obj_extension = self.sm_object.options['name'].rpartition('.')
-        name = '%s_%s.%s' % (obj_name, 'plating_mask', obj_extension)
+        grid_lay_1.addWidget(QtWidgets.QLabel(''))
 
-        self.app.app_obj.new_object('gerber', name, obj_init, autoselected=False)
+        self.robber_bar_label = QtWidgets.QLabel('<b>%s</b>' % _('Robber Bar Parameters'))
+        self.robber_bar_label.setToolTip(
+            _("Parameters used for the robber bar.\n"
+              "Robber bar = copper border to help in pattern hole plating.")
+        )
+        grid_lay_1.addWidget(self.robber_bar_label, 1, 0, 1, 3)
 
-        # Register recent file
-        self.app.file_opened.emit("gerber", name)
+        # ROBBER BAR MARGIN #
+        self.rb_margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
+        self.rb_margin_label.setToolTip(
+            _("Bounding box margin for robber bar.")
+        )
+        self.rb_margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.rb_margin_entry.set_range(-9999.9999, 9999.9999)
+        self.rb_margin_entry.set_precision(self.decimals)
+        self.rb_margin_entry.setSingleStep(0.1)
 
-        self.on_exit()
-        self.app.inform.emit('[success] %s' % _("Generating Pattern Plating Mask done."))
+        grid_lay_1.addWidget(self.rb_margin_label, 2, 0)
+        grid_lay_1.addWidget(self.rb_margin_entry, 2, 1, 1, 2)
 
-    def replot(self, obj):
-        def worker_task():
-            with self.app.proc_container.new('%s...' % _("Plotting")):
-                obj.plot()
+        # THICKNESS #
+        self.rb_thickness_label = QtWidgets.QLabel('%s:' % _("Thickness"))
+        self.rb_thickness_label.setToolTip(
+            _("The robber bar thickness.")
+        )
+        self.rb_thickness_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.rb_thickness_entry.set_range(0.0000, 9999.9999)
+        self.rb_thickness_entry.set_precision(self.decimals)
+        self.rb_thickness_entry.setSingleStep(0.1)
 
-        self.app.worker_task.emit({'fcn': worker_task, 'params': []})
+        grid_lay_1.addWidget(self.rb_thickness_label, 3, 0)
+        grid_lay_1.addWidget(self.rb_thickness_entry, 3, 1, 1, 2)
 
-    def on_exit(self):
-        # plot the objects
-        if self.grb_object:
-            self.replot(obj=self.grb_object)
+        # ## Insert Robber Bar
+        self.rb_button = QtWidgets.QPushButton(_("Insert Robber Bar"))
+        self.rb_button.setToolTip(
+            _("Will add a polygon with a defined thickness\n"
+              "that will surround the actual Gerber object\n"
+              "at a certain distance.\n"
+              "Required when doing holes pattern plating.")
+        )
+        self.rb_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        grid_lay_1.addWidget(self.rb_button, 4, 0, 1, 3)
 
-        if self.sm_object:
-            self.replot(obj=self.sm_object)
+        separator_line_2 = QtWidgets.QFrame()
+        separator_line_2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay_1.addWidget(separator_line_2, 5, 0, 1, 3)
 
-        # update the bounding box values
-        try:
-            a, b, c, d = self.grb_object.bounds()
-            self.grb_object.options['xmin'] = a
-            self.grb_object.options['ymin'] = b
-            self.grb_object.options['xmax'] = c
-            self.grb_object.options['ymax'] = d
-        except Exception as e:
-            log.debug("ToolCopperThieving.on_exit() bounds -> copper thieving Gerber error --> %s" % str(e))
+        self.patern_mask_label = QtWidgets.QLabel('<b>%s</b>' % _('Pattern Plating Mask'))
+        self.patern_mask_label.setToolTip(
+            _("Generate a mask for pattern plating.")
+        )
+        grid_lay_1.addWidget(self.patern_mask_label, 6, 0, 1, 3)
 
-        # update the bounding box values
-        try:
-            a, b, c, d = self.sm_object.bounds()
-            self.sm_object.options['xmin'] = a
-            self.sm_object.options['ymin'] = b
-            self.sm_object.options['xmax'] = c
-            self.sm_object.options['ymax'] = d
-        except Exception as e:
-            log.debug("ToolCopperThieving.on_exit() bounds -> pattern plating mask error --> %s" % str(e))
+        self.sm_obj_label = QtWidgets.QLabel("%s:" % _("Select Soldermask object"))
+        self.sm_obj_label.setToolTip(
+            _("Gerber Object with the soldermask.\n"
+              "It will be used as a base for\n"
+              "the pattern plating mask.")
+        )
 
-        # reset the variables
-        self.grb_object = None
-        self.sm_object = None
-        self.ref_obj = None
-        self.sel_rect = []
+        self.sm_object_combo = FCComboBox()
+        self.sm_object_combo.setModel(self.app.collection)
+        self.sm_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.sm_object_combo.is_last = True
+        self.sm_object_combo.obj_type = 'Gerber'
 
-        # Events ID
-        self.mr = None
-        self.mm = None
+        grid_lay_1.addWidget(self.sm_obj_label, 7, 0, 1, 3)
+        grid_lay_1.addWidget(self.sm_object_combo, 8, 0, 1, 3)
 
-        # Mouse cursor positions
-        self.mouse_is_dragging = False
-        self.cursor_pos = (0, 0)
-        self.first_click = False
+        # Openings CLEARANCE #
+        self.clearance_ppm_label = QtWidgets.QLabel('%s:' % _("Clearance"))
+        self.clearance_ppm_label.setToolTip(
+            _("The distance between the possible copper thieving elements\n"
+              "and/or robber bar and the actual openings in the mask.")
+        )
+        self.clearance_ppm_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.clearance_ppm_entry.set_range(-9999.9999, 9999.9999)
+        self.clearance_ppm_entry.set_precision(self.decimals)
+        self.clearance_ppm_entry.setSingleStep(0.1)
 
-        # if True it means we exited from tool in the middle of area adding therefore disconnect the events
-        if self.area_method is True:
-            self.app.delete_selection_shape()
-            self.area_method = False
+        grid_lay_1.addWidget(self.clearance_ppm_label, 9, 0)
+        grid_lay_1.addWidget(self.clearance_ppm_entry, 9, 1, 1, 2)
 
-            if self.app.is_legacy is False:
-                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
-                self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
-            else:
-                self.app.plotcanvas.graph_event_disconnect(self.mr)
-                self.app.plotcanvas.graph_event_disconnect(self.mm)
+        # Plated area
+        self.plated_area_label = QtWidgets.QLabel('%s:' % _("Plated area"))
+        self.plated_area_label.setToolTip(
+            _("The area to be plated by pattern plating.\n"
+              "Basically is made from the openings in the plating mask.\n\n"
+              "<<WARNING>> - the calculated area is actually a bit larger\n"
+              "due of the fact that the soldermask openings are by design\n"
+              "a bit larger than the copper pads, and this area is\n"
+              "calculated from the soldermask openings.")
+        )
+        self.plated_area_entry = FCEntry()
+        self.plated_area_entry.setDisabled(True)
 
-            self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
-                                                                  self.app.on_mouse_click_over_plot)
-            self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
-                                                                  self.app.on_mouse_move_over_plot)
-            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
-                                                                  self.app.on_mouse_click_release_over_plot)
+        if self.units.upper() == 'MM':
+            self.units_area_label = QtWidgets.QLabel('%s<sup>2</sup>' % _("mm"))
+        else:
+            self.units_area_label = QtWidgets.QLabel('%s<sup>2</sup>' % _("in"))
 
-        self.app.call_source = "app"
-        self.app.inform.emit('[success] %s' % _("Copper Thieving Tool exit."))
+        grid_lay_1.addWidget(self.plated_area_label, 10, 0)
+        grid_lay_1.addWidget(self.plated_area_entry, 10, 1)
+        grid_lay_1.addWidget(self.units_area_label, 10, 2)
 
-    def flatten(self, geometry):
-        """
-        Creates a list of non-iterable linear geometry objects.
-        :param geometry: Shapely type or list or list of list of such.
+        # ## Pattern Plating Mask
+        self.ppm_button = QtWidgets.QPushButton(_("Generate pattern plating mask"))
+        self.ppm_button.setToolTip(
+            _("Will add to the soldermask gerber geometry\n"
+              "the geometries of the copper thieving and/or\n"
+              "the robber bar if those were generated.")
+        )
+        self.ppm_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        grid_lay_1.addWidget(self.ppm_button, 11, 0, 1, 3)
 
-        Results are placed in self.flat_geometry
-        """
+        self.layout.addStretch()
 
-        # ## If iterable, expand recursively.
-        try:
-            for geo in geometry:
-                if geo is not None:
-                    self.flatten(geometry=geo)
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.reset_button)
 
-        # ## Not iterable, do the actual indexing and add.
-        except TypeError:
-            self.flat_geometry.append(geometry)
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
 
-        return self.flat_geometry
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 208 - 173
appTools/ToolCorners.py

@@ -5,7 +5,7 @@
 # MIT Licence                                              #
 # ##########################################################
 
-from PyQt5 import QtWidgets, QtCore
+from PyQt5 import QtWidgets, QtCore, QtGui
 
 from appTool import AppTool
 from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCComboBox, FCButton
@@ -28,8 +28,6 @@ log = logging.getLogger('base')
 
 class ToolCorners(AppTool):
 
-    toolName = _("Corner Markers Tool")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
 
@@ -39,157 +37,11 @@ class ToolCorners(AppTool):
         self.decimals = self.app.decimals
         self.units = ''
 
-        # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-        self.layout.addWidget(QtWidgets.QLabel(''))
-
-        # Gerber object #
-        self.object_label = QtWidgets.QLabel('<b>%s:</b>' % _("GERBER"))
-        self.object_label.setToolTip(
-            _("The Gerber object to which will be added corner markers.")
-        )
-        self.object_combo = FCComboBox()
-        self.object_combo.setModel(self.app.collection)
-        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.object_combo.is_last = True
-        self.object_combo.obj_type = "Gerber"
-
-        self.layout.addWidget(self.object_label)
-        self.layout.addWidget(self.object_combo)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        self.layout.addWidget(separator_line)
-
-        self.points_label = QtWidgets.QLabel('<b>%s:</b>' % _('Locations'))
-        self.points_label.setToolTip(
-            _("Locations where to place corner markers.")
-        )
-        self.layout.addWidget(self.points_label)
-
-        # BOTTOM LEFT
-        self.bl_cb = FCCheckBox(_("Bottom Left"))
-        self.layout.addWidget(self.bl_cb)
-
-        # BOTTOM RIGHT
-        self.br_cb = FCCheckBox(_("Bottom Right"))
-        self.layout.addWidget(self.br_cb)
-
-        # TOP LEFT
-        self.tl_cb = FCCheckBox(_("Top Left"))
-        self.layout.addWidget(self.tl_cb)
-
-        # TOP RIGHT
-        self.tr_cb = FCCheckBox(_("Top Right"))
-        self.layout.addWidget(self.tr_cb)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        self.layout.addWidget(separator_line)
-
-        # Toggle ALL
-        self.toggle_all_cb = FCCheckBox(_("Toggle ALL"))
-        self.layout.addWidget(self.toggle_all_cb)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        self.layout.addWidget(separator_line)
-
-        # ## Grid Layout
-        grid_lay = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid_lay)
-        grid_lay.setColumnStretch(0, 0)
-        grid_lay.setColumnStretch(1, 1)
-
-        self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
-        self.param_label.setToolTip(
-            _("Parameters used for this tool.")
-        )
-        grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
-
-        # Thickness #
-        self.thick_label = QtWidgets.QLabel('%s:' % _("Thickness"))
-        self.thick_label.setToolTip(
-            _("The thickness of the line that makes the corner marker.")
-        )
-        self.thick_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.thick_entry.set_range(0.0000, 9.9999)
-        self.thick_entry.set_precision(self.decimals)
-        self.thick_entry.setWrapping(True)
-        self.thick_entry.setSingleStep(10 ** -self.decimals)
-
-        grid_lay.addWidget(self.thick_label, 1, 0)
-        grid_lay.addWidget(self.thick_entry, 1, 1)
-
-        # Length #
-        self.l_label = QtWidgets.QLabel('%s:' % _("Length"))
-        self.l_label.setToolTip(
-            _("The length of the line that makes the corner marker.")
-        )
-        self.l_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.l_entry.set_range(-9999.9999, 9999.9999)
-        self.l_entry.set_precision(self.decimals)
-        self.l_entry.setSingleStep(10 ** -self.decimals)
-
-        grid_lay.addWidget(self.l_label, 2, 0)
-        grid_lay.addWidget(self.l_entry, 2, 1)
-
-        # Margin #
-        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
-        self.margin_label.setToolTip(
-            _("Bounding box margin.")
-        )
-        self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.margin_entry.set_range(-9999.9999, 9999.9999)
-        self.margin_entry.set_precision(self.decimals)
-        self.margin_entry.setSingleStep(0.1)
-
-        grid_lay.addWidget(self.margin_label, 3, 0)
-        grid_lay.addWidget(self.margin_entry, 3, 1)
-
-        separator_line_2 = QtWidgets.QFrame()
-        separator_line_2.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line_2, 4, 0, 1, 2)
-
-        # ## Insert Corner Marker
-        self.add_marker_button = FCButton(_("Add Marker"))
-        self.add_marker_button.setToolTip(
-            _("Will add corner markers to the selected Gerber file.")
-        )
-        self.add_marker_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        grid_lay.addWidget(self.add_marker_button, 11, 0, 1, 2)
-
-        self.layout.addStretch()
-
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = CornersUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
         # Objects involved in Copper thieving
         self.grb_object = None
@@ -203,8 +55,8 @@ class ToolCorners(AppTool):
         self.grb_steps_per_circle = self.app.defaults["gerber_circle_steps"]
 
         # SIGNALS
-        self.add_marker_button.clicked.connect(self.add_markers)
-        self.toggle_all_cb.toggled.connect(self.on_toggle_all)
+        self.ui.add_marker_button.clicked.connect(self.add_markers)
+        self.ui.toggle_all_cb.toggled.connect(self.on_toggle_all)
 
     def run(self, toggle=True):
         self.app.defaults.report_usage("ToolCorners()")
@@ -239,27 +91,27 @@ class ToolCorners(AppTool):
 
     def set_tool_ui(self):
         self.units = self.app.defaults['units']
-        self.thick_entry.set_value(self.app.defaults["tools_corners_thickness"])
-        self.l_entry.set_value(float(self.app.defaults["tools_corners_length"]))
-        self.margin_entry.set_value(float(self.app.defaults["tools_corners_margin"]))
-        self.toggle_all_cb.set_value(False)
+        self.ui.thick_entry.set_value(self.app.defaults["tools_corners_thickness"])
+        self.ui.l_entry.set_value(float(self.app.defaults["tools_corners_length"]))
+        self.ui.margin_entry.set_value(float(self.app.defaults["tools_corners_margin"]))
+        self.ui.toggle_all_cb.set_value(False)
 
     def on_toggle_all(self, val):
-        self.bl_cb.set_value(val)
-        self.br_cb.set_value(val)
-        self.tl_cb.set_value(val)
-        self.tr_cb.set_value(val)
+        self.ui.bl_cb.set_value(val)
+        self.ui.br_cb.set_value(val)
+        self.ui.tl_cb.set_value(val)
+        self.ui.tr_cb.set_value(val)
 
     def add_markers(self):
         self.app.call_source = "corners_tool"
-        tl_state = self.tl_cb.get_value()
-        tr_state = self.tr_cb.get_value()
-        bl_state = self.bl_cb.get_value()
-        br_state = self.br_cb.get_value()
+        tl_state = self.ui.tl_cb.get_value()
+        tr_state = self.ui.tr_cb.get_value()
+        bl_state = self.ui.bl_cb.get_value()
+        br_state = self.ui.br_cb.get_value()
 
         # get the Gerber object on which the corner marker will be inserted
-        selection_index = self.object_combo.currentIndex()
-        model_index = self.app.collection.index(selection_index, 0, self.object_combo.rootModelIndex())
+        selection_index = self.ui.object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.ui.object_combo.rootModelIndex())
 
         try:
             self.grb_object = model_index.internalPointer().obj
@@ -295,9 +147,9 @@ class ToolCorners(AppTool):
         :return:                None
         """
 
-        line_thickness = self.thick_entry.get_value()
-        line_length = self.l_entry.get_value()
-        margin = self.margin_entry.get_value()
+        line_thickness = self.ui.thick_entry.get_value()
+        line_length = self.ui.l_entry.get_value()
+        margin = self.ui.margin_entry.get_value()
 
         geo_list = []
 
@@ -438,3 +290,186 @@ class ToolCorners(AppTool):
 
         self.app.call_source = "app"
         self.app.inform.emit('[success] %s' % _("Corners Tool exit."))
+
+
+class CornersUI:
+
+    toolName = _("Corner Markers Tool")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(QtWidgets.QLabel(""))
+
+        # Gerber object #
+        self.object_label = QtWidgets.QLabel('<b>%s:</b>' % _("GERBER"))
+        self.object_label.setToolTip(
+            _("The Gerber object to which will be added corner markers.")
+        )
+        self.object_combo = FCComboBox()
+        self.object_combo.setModel(self.app.collection)
+        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.object_combo.is_last = True
+        self.object_combo.obj_type = "Gerber"
+
+        self.layout.addWidget(self.object_label)
+        self.layout.addWidget(self.object_combo)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.layout.addWidget(separator_line)
+
+        self.points_label = QtWidgets.QLabel('<b>%s:</b>' % _('Locations'))
+        self.points_label.setToolTip(
+            _("Locations where to place corner markers.")
+        )
+        self.layout.addWidget(self.points_label)
+
+        # BOTTOM LEFT
+        self.bl_cb = FCCheckBox(_("Bottom Left"))
+        self.layout.addWidget(self.bl_cb)
+
+        # BOTTOM RIGHT
+        self.br_cb = FCCheckBox(_("Bottom Right"))
+        self.layout.addWidget(self.br_cb)
+
+        # TOP LEFT
+        self.tl_cb = FCCheckBox(_("Top Left"))
+        self.layout.addWidget(self.tl_cb)
+
+        # TOP RIGHT
+        self.tr_cb = FCCheckBox(_("Top Right"))
+        self.layout.addWidget(self.tr_cb)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.layout.addWidget(separator_line)
+
+        # Toggle ALL
+        self.toggle_all_cb = FCCheckBox(_("Toggle ALL"))
+        self.layout.addWidget(self.toggle_all_cb)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.layout.addWidget(separator_line)
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+
+        self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
+        self.param_label.setToolTip(
+            _("Parameters used for this tool.")
+        )
+        grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
+
+        # Thickness #
+        self.thick_label = QtWidgets.QLabel('%s:' % _("Thickness"))
+        self.thick_label.setToolTip(
+            _("The thickness of the line that makes the corner marker.")
+        )
+        self.thick_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.thick_entry.set_range(0.0000, 9.9999)
+        self.thick_entry.set_precision(self.decimals)
+        self.thick_entry.setWrapping(True)
+        self.thick_entry.setSingleStep(10 ** -self.decimals)
+
+        grid_lay.addWidget(self.thick_label, 1, 0)
+        grid_lay.addWidget(self.thick_entry, 1, 1)
+
+        # Length #
+        self.l_label = QtWidgets.QLabel('%s:' % _("Length"))
+        self.l_label.setToolTip(
+            _("The length of the line that makes the corner marker.")
+        )
+        self.l_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.l_entry.set_range(-9999.9999, 9999.9999)
+        self.l_entry.set_precision(self.decimals)
+        self.l_entry.setSingleStep(10 ** -self.decimals)
+
+        grid_lay.addWidget(self.l_label, 2, 0)
+        grid_lay.addWidget(self.l_entry, 2, 1)
+
+        # Margin #
+        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
+        self.margin_label.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.margin_entry.set_range(-9999.9999, 9999.9999)
+        self.margin_entry.set_precision(self.decimals)
+        self.margin_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.margin_label, 3, 0)
+        grid_lay.addWidget(self.margin_entry, 3, 1)
+
+        separator_line_2 = QtWidgets.QFrame()
+        separator_line_2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line_2, 4, 0, 1, 2)
+
+        # ## Insert Corner Marker
+        self.add_marker_button = FCButton(_("Add Marker"))
+        self.add_marker_button.setToolTip(
+            _("Will add corner markers to the selected Gerber file.")
+        )
+        self.add_marker_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        grid_lay.addWidget(self.add_marker_button, 11, 0, 1, 2)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.reset_button)
+
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

Разница между файлами не показана из-за своего большого размера
+ 708 - 554
appTools/ToolCutOut.py


Разница между файлами не показана из-за своего большого размера
+ 277 - 692
appTools/ToolDblSided.py


+ 179 - 145
appTools/ToolDistance.py

@@ -9,13 +9,13 @@ from PyQt5 import QtWidgets, QtCore
 
 from appTool import AppTool
 from appGUI.VisPyVisuals import *
-from appGUI.GUIElements import FCEntry, FCButton, FCCheckBox
+from appGUI.GUIElements import FCEntry, FCButton, FCCheckBox, FCLabel
 
 from shapely.geometry import Point, MultiLineString, Polygon
 
 import appTranslation as fcTranslate
 from camlib import FlatCAMRTreeStorage
-from appEditors.FlatCAMGeoEditor import DrawToolShape
+from appEditors.AppGeoEditor import DrawToolShape
 
 from copy import copy
 import math
@@ -32,8 +32,6 @@ log = logging.getLogger('base')
 
 class Distance(AppTool):
 
-    toolName = _("Distance Tool")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
 
@@ -43,107 +41,11 @@ class Distance(AppTool):
         self.canvas = self.app.plotcanvas
         self.units = self.app.defaults['units'].lower()
 
-        # ## Title
-        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
-        self.layout.addWidget(title_label)
-
-        # ## Form Layout
-        grid0 = QtWidgets.QGridLayout()
-        grid0.setColumnStretch(0, 0)
-        grid0.setColumnStretch(1, 1)
-        self.layout.addLayout(grid0)
-
-        self.units_label = QtWidgets.QLabel('%s:' % _("Units"))
-        self.units_label.setToolTip(_("Those are the units in which the distance is measured."))
-        self.units_value = QtWidgets.QLabel("%s" % str({'mm': _("METRIC (mm)"), 'in': _("INCH (in)")}[self.units]))
-        self.units_value.setDisabled(True)
-
-        grid0.addWidget(self.units_label, 0, 0)
-        grid0.addWidget(self.units_value, 0, 1)
-
-        self.snap_center_cb = FCCheckBox(_("Snap to center"))
-        self.snap_center_cb.setToolTip(
-            _("Mouse cursor will snap to the center of the pad/drill\n"
-              "when it is hovering over the geometry of the pad/drill.")
-        )
-        grid0.addWidget(self.snap_center_cb, 1, 0, 1, 2)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 2, 0, 1, 2)
-
-        self.start_label = QtWidgets.QLabel("%s:" % _('Start Coords'))
-        self.start_label.setToolTip(_("This is measuring Start point coordinates."))
-
-        self.start_entry = FCEntry()
-        self.start_entry.setReadOnly(True)
-        self.start_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.start_entry.setToolTip(_("This is measuring Start point coordinates."))
-
-        grid0.addWidget(self.start_label, 3, 0)
-        grid0.addWidget(self.start_entry, 3, 1)
-
-        self.stop_label = QtWidgets.QLabel("%s:" % _('Stop Coords'))
-        self.stop_label.setToolTip(_("This is the measuring Stop point coordinates."))
-
-        self.stop_entry = FCEntry()
-        self.stop_entry.setReadOnly(True)
-        self.stop_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.stop_entry.setToolTip(_("This is the measuring Stop point coordinates."))
-
-        grid0.addWidget(self.stop_label, 4, 0)
-        grid0.addWidget(self.stop_entry, 4, 1)
-
-        self.distance_x_label = QtWidgets.QLabel('%s:' % _("Dx"))
-        self.distance_x_label.setToolTip(_("This is the distance measured over the X axis."))
-
-        self.distance_x_entry = FCEntry()
-        self.distance_x_entry.setReadOnly(True)
-        self.distance_x_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.distance_x_entry.setToolTip(_("This is the distance measured over the X axis."))
-
-        grid0.addWidget(self.distance_x_label, 5, 0)
-        grid0.addWidget(self.distance_x_entry, 5, 1)
-
-        self.distance_y_label = QtWidgets.QLabel('%s:' % _("Dy"))
-        self.distance_y_label.setToolTip(_("This is the distance measured over the Y axis."))
-
-        self.distance_y_entry = FCEntry()
-        self.distance_y_entry.setReadOnly(True)
-        self.distance_y_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.distance_y_entry.setToolTip(_("This is the distance measured over the Y axis."))
-
-        grid0.addWidget(self.distance_y_label, 6, 0)
-        grid0.addWidget(self.distance_y_entry, 6, 1)
-
-        self.angle_label = QtWidgets.QLabel('%s:' % _("Angle"))
-        self.angle_label.setToolTip(_("This is orientation angle of the measuring line."))
-
-        self.angle_entry = FCEntry()
-        self.angle_entry.setReadOnly(True)
-        self.angle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.angle_entry.setToolTip(_("This is orientation angle of the measuring line."))
-
-        grid0.addWidget(self.angle_label, 7, 0)
-        grid0.addWidget(self.angle_entry, 7, 1)
-
-        self.total_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _('DISTANCE'))
-        self.total_distance_label.setToolTip(_("This is the point to point Euclidian distance."))
-
-        self.total_distance_entry = FCEntry()
-        self.total_distance_entry.setReadOnly(True)
-        self.total_distance_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.total_distance_entry.setToolTip(_("This is the point to point Euclidian distance."))
-
-        grid0.addWidget(self.total_distance_label, 8, 0)
-        grid0.addWidget(self.total_distance_entry, 8, 1)
-
-        self.measure_btn = FCButton(_("Measure"))
-        # self.measure_btn.setFixedWidth(70)
-        self.layout.addWidget(self.measure_btn)
-
-        self.layout.addStretch()
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = DistUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
         # store here the first click and second click of the measurement process
         self.points = []
@@ -178,8 +80,9 @@ class Distance(AppTool):
         else:
             from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
             self.sel_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='measurement')
-
-        self.measure_btn.clicked.connect(self.activate_measure_tool)
+        
+        # Signals
+        self.ui.measure_btn.clicked.connect(self.activate_measure_tool)
 
     def run(self, toggle=False):
         self.app.defaults.report_usage("ToolDistance()")
@@ -224,27 +127,27 @@ class Distance(AppTool):
         self.app.command_active = "Distance"
 
         # initial view of the layout
-        self.start_entry.set_value('(0, 0)')
-        self.stop_entry.set_value('(0, 0)')
+        self.ui.start_entry.set_value('(0, 0)')
+        self.ui.stop_entry.set_value('(0, 0)')
 
-        self.distance_x_entry.set_value('0.0')
-        self.distance_y_entry.set_value('0.0')
-        self.angle_entry.set_value('0.0')
-        self.total_distance_entry.set_value('0.0')
+        self.ui.distance_x_entry.set_value('0.0')
+        self.ui.distance_y_entry.set_value('0.0')
+        self.ui.angle_entry.set_value('0.0')
+        self.ui.total_distance_entry.set_value('0.0')
 
-        self.snap_center_cb.set_value(self.app.defaults['tools_dist_snap_center'])
+        self.ui.snap_center_cb.set_value(self.app.defaults['tools_dist_snap_center'])
 
         # snap center works only for Gerber and Execellon Editor's
         if self.original_call_source == 'exc_editor' or self.original_call_source == 'grb_editor':
-            self.snap_center_cb.show()
+            self.ui.snap_center_cb.show()
             snap_center = self.app.defaults['tools_dist_snap_center']
             self.on_snap_toggled(snap_center)
 
-            self.snap_center_cb.toggled.connect(self.on_snap_toggled)
+            self.ui.snap_center_cb.toggled.connect(self.on_snap_toggled)
         else:
-            self.snap_center_cb.hide()
+            self.ui.snap_center_cb.hide()
             try:
-                self.snap_center_cb.toggled.disconnect(self.on_snap_toggled)
+                self.ui.snap_center_cb.toggled.disconnect(self.on_snap_toggled)
             except (TypeError, AttributeError):
                 pass
 
@@ -270,8 +173,8 @@ class Distance(AppTool):
         self.active = True
 
         # disable the measuring button
-        self.measure_btn.setDisabled(True)
-        self.measure_btn.setText('%s...' % _("Working"))
+        self.ui.measure_btn.setDisabled(True)
+        self.ui.measure_btn.setText('%s...' % _("Working"))
 
         self.clicked_meas = 0
         self.original_call_source = copy(self.app.call_source)
@@ -335,8 +238,8 @@ class Distance(AppTool):
         self.points = []
 
         # disable the measuring button
-        self.measure_btn.setDisabled(False)
-        self.measure_btn.setText(_("Measure"))
+        self.ui.measure_btn.setDisabled(False)
+        self.ui.measure_btn.setText(_("Measure"))
 
         self.app.call_source = copy(self.original_call_source)
         if self.original_call_source == 'app':
@@ -406,7 +309,7 @@ class Distance(AppTool):
         if event.button == 1:
             pos_canvas = self.canvas.translate_coords(event_pos)
 
-            if self.snap_center_cb.get_value() is False:
+            if self.ui.snap_center_cb.get_value() is False:
                 # if GRID is active we need to get the snapped positions
                 if self.app.grid_status():
                     pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
@@ -475,9 +378,9 @@ class Distance(AppTool):
 
             # Reset here the relative coordinates so there is a new reference on the click position
             if self.rel_point1 is None:
-                # self.app.ui.rel_position_label.setText("<b>Dx</b>: %.*f&nbsp;&nbsp;  <b>Dy</b>: "
-                #                                        "%.*f&nbsp;&nbsp;&nbsp;&nbsp;" %
-                #                                        (self.decimals, 0.0, self.decimals, 0.0))
+                self.app.ui.rel_position_label.setText("<b>Dx</b>: %.*f&nbsp;&nbsp;  <b>Dy</b>: "
+                                                       "%.*f&nbsp;&nbsp;&nbsp;&nbsp;" %
+                                                       (self.decimals, 0.0, self.decimals, 0.0))
                 self.rel_point1 = pos
             else:
                 self.rel_point2 = copy(self.rel_point1)
@@ -490,14 +393,14 @@ class Distance(AppTool):
 
     def calculate_distance(self, pos):
         if len(self.points) == 1:
-            self.start_entry.set_value("(%.*f, %.*f)" % (self.decimals, pos[0], self.decimals, pos[1]))
+            self.ui.start_entry.set_value("(%.*f, %.*f)" % (self.decimals, pos[0], self.decimals, pos[1]))
             self.app.inform.emit(_("MEASURING: Click on the Destination point ..."))
         elif len(self.points) == 2:
             # self.app.app_cursor.enabled = False
             dx = self.points[1][0] - self.points[0][0]
             dy = self.points[1][1] - self.points[0][1]
             d = math.sqrt(dx ** 2 + dy ** 2)
-            self.stop_entry.set_value("(%.*f, %.*f)" % (self.decimals, pos[0], self.decimals, pos[1]))
+            self.ui.stop_entry.set_value("(%.*f, %.*f)" % (self.decimals, pos[0], self.decimals, pos[1]))
 
             self.app.inform.emit("{tx1}: {tx2} D(x) = {d_x} | D(y) = {d_y} | {tx3} = {d_z}".format(
                 tx1=_("MEASURING"),
@@ -508,23 +411,23 @@ class Distance(AppTool):
                 d_z='%*f' % (self.decimals, abs(d)))
             )
 
-            self.distance_x_entry.set_value('%.*f' % (self.decimals, abs(dx)))
-            self.distance_y_entry.set_value('%.*f' % (self.decimals, abs(dy)))
+            self.ui.distance_x_entry.set_value('%.*f' % (self.decimals, abs(dx)))
+            self.ui.distance_y_entry.set_value('%.*f' % (self.decimals, abs(dy)))
 
             try:
                 angle = math.degrees(math.atan2(dy, dx))
                 if angle < 0:
                     angle += 360
-                self.angle_entry.set_value('%.*f' % (self.decimals, angle))
+                self.ui.angle_entry.set_value('%.*f' % (self.decimals, angle))
             except Exception:
                 pass
 
-            self.total_distance_entry.set_value('%.*f' % (self.decimals, abs(d)))
-            # self.app.ui.rel_position_label.setText(
-            #     "<b>Dx</b>: {}&nbsp;&nbsp;  <b>Dy</b>: {}&nbsp;&nbsp;&nbsp;&nbsp;".format(
-            #         '%.*f' % (self.decimals, pos[0]), '%.*f' % (self.decimals, pos[1])
-            #     )
-            # )
+            self.ui.total_distance_entry.set_value('%.*f' % (self.decimals, abs(d)))
+            self.app.ui.rel_position_label.setText(
+                "<b>Dx</b>: {}&nbsp;&nbsp;  <b>Dy</b>: {}&nbsp;&nbsp;&nbsp;&nbsp;".format(
+                    '%.*f' % (self.decimals, pos[0]), '%.*f' % (self.decimals, pos[1])
+                )
+            )
             self.tool_done = True
             self.deactivate_measure_tool()
 
@@ -573,11 +476,11 @@ class Distance(AppTool):
                 dx = pos[0]
                 dy = pos[1]
 
-            # self.app.ui.rel_position_label.setText(
-            #     "<b>Dx</b>: {}&nbsp;&nbsp;  <b>Dy</b>: {}&nbsp;&nbsp;&nbsp;&nbsp;".format(
-            #         '%.*f' % (self.decimals, dx), '%.*f' % (self.decimals, dy)
-            #     )
-            # )
+            self.app.ui.rel_position_label.setText(
+                "<b>Dx</b>: {}&nbsp;&nbsp;  <b>Dy</b>: {}&nbsp;&nbsp;&nbsp;&nbsp;".format(
+                    '%.*f' % (self.decimals, dx), '%.*f' % (self.decimals, dy)
+                )
+            )
 
             # update utility geometry
             if len(self.points) == 1:
@@ -587,7 +490,7 @@ class Distance(AppTool):
                     angle = math.degrees(math.atan2(dy, dx))
                     if angle < 0:
                         angle += 360
-                    self.angle_entry.set_value('%.*f' % (self.decimals, angle))
+                    self.ui.angle_entry.set_value('%.*f' % (self.decimals, angle))
                 except Exception as e:
                     log.debug("Distance.on_mouse_move_meas() -> update utility geometry -> %s" % str(e))
                     pass
@@ -595,7 +498,7 @@ class Distance(AppTool):
         except Exception as e:
             log.debug("Distance.on_mouse_move_meas() --> %s" % str(e))
             self.app.ui.position_label.setText("")
-            # self.app.ui.rel_position_label.setText("")
+            self.app.ui.rel_position_label.setText("")
 
     def utility_geometry(self, pos):
         # first delete old shape
@@ -635,4 +538,135 @@ class Distance(AppTool):
     # def set_meas_units(self, units):
     #     self.meas.units_label.setText("[" + self.app.options["units"].lower() + "]")
 
-# end of file
+
+class DistUI:
+    
+    toolName = _("Distance Tool")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+        self.units = self.app.defaults['units'].lower()
+
+        # ## Title
+        title_label = FCLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
+        self.layout.addWidget(title_label)
+
+        # ## Form Layout
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.layout.addLayout(grid0)
+
+        self.units_label = FCLabel('%s:' % _("Units"))
+        self.units_label.setToolTip(_("Those are the units in which the distance is measured."))
+        self.units_value = FCLabel("%s" % str({'mm': _("METRIC (mm)"), 'in': _("INCH (in)")}[self.units]))
+        self.units_value.setDisabled(True)
+
+        grid0.addWidget(self.units_label, 0, 0)
+        grid0.addWidget(self.units_value, 0, 1)
+
+        self.snap_center_cb = FCCheckBox(_("Snap to center"))
+        self.snap_center_cb.setToolTip(
+            _("Mouse cursor will snap to the center of the pad/drill\n"
+              "when it is hovering over the geometry of the pad/drill.")
+        )
+        grid0.addWidget(self.snap_center_cb, 1, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 2, 0, 1, 2)
+
+        self.start_label = FCLabel("%s:" % _('Start Coords'))
+        self.start_label.setToolTip(_("This is measuring Start point coordinates."))
+
+        self.start_entry = FCEntry()
+        self.start_entry.setReadOnly(True)
+        self.start_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.start_entry.setToolTip(_("This is measuring Start point coordinates."))
+
+        grid0.addWidget(self.start_label, 3, 0)
+        grid0.addWidget(self.start_entry, 3, 1)
+
+        self.stop_label = FCLabel("%s:" % _('Stop Coords'))
+        self.stop_label.setToolTip(_("This is the measuring Stop point coordinates."))
+
+        self.stop_entry = FCEntry()
+        self.stop_entry.setReadOnly(True)
+        self.stop_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.stop_entry.setToolTip(_("This is the measuring Stop point coordinates."))
+
+        grid0.addWidget(self.stop_label, 4, 0)
+        grid0.addWidget(self.stop_entry, 4, 1)
+
+        self.distance_x_label = FCLabel('%s:' % _("Dx"))
+        self.distance_x_label.setToolTip(_("This is the distance measured over the X axis."))
+
+        self.distance_x_entry = FCEntry()
+        self.distance_x_entry.setReadOnly(True)
+        self.distance_x_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.distance_x_entry.setToolTip(_("This is the distance measured over the X axis."))
+
+        grid0.addWidget(self.distance_x_label, 5, 0)
+        grid0.addWidget(self.distance_x_entry, 5, 1)
+
+        self.distance_y_label = FCLabel('%s:' % _("Dy"))
+        self.distance_y_label.setToolTip(_("This is the distance measured over the Y axis."))
+
+        self.distance_y_entry = FCEntry()
+        self.distance_y_entry.setReadOnly(True)
+        self.distance_y_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.distance_y_entry.setToolTip(_("This is the distance measured over the Y axis."))
+
+        grid0.addWidget(self.distance_y_label, 6, 0)
+        grid0.addWidget(self.distance_y_entry, 6, 1)
+
+        self.angle_label = FCLabel('%s:' % _("Angle"))
+        self.angle_label.setToolTip(_("This is orientation angle of the measuring line."))
+
+        self.angle_entry = FCEntry()
+        self.angle_entry.setReadOnly(True)
+        self.angle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.angle_entry.setToolTip(_("This is orientation angle of the measuring line."))
+
+        grid0.addWidget(self.angle_label, 7, 0)
+        grid0.addWidget(self.angle_entry, 7, 1)
+
+        self.total_distance_label = FCLabel("<b>%s:</b>" % _('DISTANCE'))
+        self.total_distance_label.setToolTip(_("This is the point to point Euclidian distance."))
+
+        self.total_distance_entry = FCEntry()
+        self.total_distance_entry.setReadOnly(True)
+        self.total_distance_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.total_distance_entry.setToolTip(_("This is the point to point Euclidian distance."))
+
+        grid0.addWidget(self.total_distance_label, 8, 0)
+        grid0.addWidget(self.total_distance_entry, 8, 1)
+
+        self.measure_btn = FCButton(_("Measure"))
+        # self.measure_btn.setFixedWidth(70)
+        self.layout.addWidget(self.measure_btn)
+
+        self.layout.addStretch()
+
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 149 - 117
appTools/ToolDistanceMin.py

@@ -28,8 +28,6 @@ log = logging.getLogger('base')
 
 class DistanceMin(AppTool):
 
-    toolName = _("Minimum Distance Tool")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
 
@@ -38,101 +36,16 @@ class DistanceMin(AppTool):
         self.units = self.app.defaults['units'].lower()
         self.decimals = self.app.decimals
 
-        # ## Title
-        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
-        self.layout.addWidget(title_label)
-
-        # ## Form Layout
-        form_layout = QtWidgets.QFormLayout()
-        self.layout.addLayout(form_layout)
-
-        self.units_label = QtWidgets.QLabel('%s:' % _("Units"))
-        self.units_label.setToolTip(_("Those are the units in which the distance is measured."))
-        self.units_value = QtWidgets.QLabel("%s" % str({'mm': _("METRIC (mm)"), 'in': _("INCH (in)")}[self.units]))
-        self.units_value.setDisabled(True)
-
-        self.start_label = QtWidgets.QLabel("%s:" % _('First object point'))
-        self.start_label.setToolTip(_("This is first object point coordinates.\n"
-                                      "This is the start point for measuring distance."))
-
-        self.stop_label = QtWidgets.QLabel("%s:" % _('Second object point'))
-        self.stop_label.setToolTip(_("This is second object point coordinates.\n"
-                                      "This is the end point for measuring distance."))
-
-        self.distance_x_label = QtWidgets.QLabel('%s:' % _("Dx"))
-        self.distance_x_label.setToolTip(_("This is the distance measured over the X axis."))
-
-        self.distance_y_label = QtWidgets.QLabel('%s:' % _("Dy"))
-        self.distance_y_label.setToolTip(_("This is the distance measured over the Y axis."))
-
-        self.angle_label = QtWidgets.QLabel('%s:' % _("Angle"))
-        self.angle_label.setToolTip(_("This is orientation angle of the measuring line."))
-
-        self.total_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _('DISTANCE'))
-        self.total_distance_label.setToolTip(_("This is the point to point Euclidean distance."))
-
-        self.half_point_label = QtWidgets.QLabel("<b>%s:</b>" % _('Half Point'))
-        self.half_point_label.setToolTip(_("This is the middle point of the point to point Euclidean distance."))
-
-        self.start_entry = FCEntry()
-        self.start_entry.setReadOnly(True)
-        self.start_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.start_entry.setToolTip(_("This is first object point coordinates.\n"
-                                      "This is the start point for measuring distance."))
-
-        self.stop_entry = FCEntry()
-        self.stop_entry.setReadOnly(True)
-        self.stop_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.stop_entry.setToolTip(_("This is second object point coordinates.\n"
-                                      "This is the end point for measuring distance."))
-
-        self.distance_x_entry = FCEntry()
-        self.distance_x_entry.setReadOnly(True)
-        self.distance_x_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.distance_x_entry.setToolTip(_("This is the distance measured over the X axis."))
-
-        self.distance_y_entry = FCEntry()
-        self.distance_y_entry.setReadOnly(True)
-        self.distance_y_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.distance_y_entry.setToolTip(_("This is the distance measured over the Y axis."))
-
-        self.angle_entry = FCEntry()
-        self.angle_entry.setReadOnly(True)
-        self.angle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.angle_entry.setToolTip(_("This is orientation angle of the measuring line."))
-
-        self.total_distance_entry = FCEntry()
-        self.total_distance_entry.setReadOnly(True)
-        self.total_distance_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.total_distance_entry.setToolTip(_("This is the point to point Euclidean distance."))
-
-        self.half_point_entry = FCEntry()
-        self.half_point_entry.setReadOnly(True)
-        self.half_point_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.half_point_entry.setToolTip(_("This is the middle point of the point to point Euclidean distance."))
-
-        self.measure_btn = QtWidgets.QPushButton(_("Measure"))
-        self.layout.addWidget(self.measure_btn)
-
-        self.jump_hp_btn = QtWidgets.QPushButton(_("Jump to Half Point"))
-        self.layout.addWidget(self.jump_hp_btn)
-        self.jump_hp_btn.setDisabled(True)
-
-        form_layout.addRow(self.units_label, self.units_value)
-        form_layout.addRow(self.start_label, self.start_entry)
-        form_layout.addRow(self.stop_label, self.stop_entry)
-        form_layout.addRow(self.distance_x_label, self.distance_x_entry)
-        form_layout.addRow(self.distance_y_label, self.distance_y_entry)
-        form_layout.addRow(self.angle_label, self.angle_entry)
-        form_layout.addRow(self.total_distance_label, self.total_distance_entry)
-        form_layout.addRow(self.half_point_label, self.half_point_entry)
-
-        self.layout.addStretch()
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = DistMinUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
         self.h_point = (0, 0)
 
-        self.measure_btn.clicked.connect(self.activate_measure_tool)
-        self.jump_hp_btn.clicked.connect(self.on_jump_to_half_point)
+        self.ui.measure_btn.clicked.connect(self.activate_measure_tool)
+        self.ui.jump_hp_btn.clicked.connect(self.on_jump_to_half_point)
 
     def run(self, toggle=False):
         self.app.defaults.report_usage("ToolDistanceMin()")
@@ -169,22 +82,22 @@ class DistanceMin(AppTool):
         self.units = self.app.defaults['units'].lower()
 
         # initial view of the layout
-        self.start_entry.set_value('(0, 0)')
-        self.stop_entry.set_value('(0, 0)')
+        self.ui.start_entry.set_value('(0, 0)')
+        self.ui.stop_entry.set_value('(0, 0)')
 
-        self.distance_x_entry.set_value('0.0')
-        self.distance_y_entry.set_value('0.0')
-        self.angle_entry.set_value('0.0')
-        self.total_distance_entry.set_value('0.0')
-        self.half_point_entry.set_value('(0, 0)')
+        self.ui.distance_x_entry.set_value('0.0')
+        self.ui.distance_y_entry.set_value('0.0')
+        self.ui.angle_entry.set_value('0.0')
+        self.ui.total_distance_entry.set_value('0.0')
+        self.ui.half_point_entry.set_value('(0, 0)')
 
-        self.jump_hp_btn.setDisabled(True)
+        self.ui.jump_hp_btn.setDisabled(True)
 
         log.debug("Minimum Distance Tool --> tool initialized")
 
     def activate_measure_tool(self):
         # ENABLE the Measuring TOOL
-        self.jump_hp_btn.setDisabled(False)
+        self.ui.jump_hp_btn.setDisabled(False)
 
         self.units = self.app.defaults['units'].lower()
 
@@ -193,7 +106,7 @@ class DistanceMin(AppTool):
             if len(selected_objs) != 2:
                 self.app.inform.emit('[WARNING_NOTCL] %s %s' %
                                      (_("Select two objects and no more. Currently the selection has objects: "),
-                                     str(len(selected_objs))))
+                                      str(len(selected_objs))))
                 return
             else:
                 if isinstance(selected_objs[0].solid_geometry, list):
@@ -214,7 +127,7 @@ class DistanceMin(AppTool):
             if len(selected_objs) != 2:
                 self.app.inform.emit('[WARNING_NOTCL] %s %s' %
                                      (_("Select two objects and no more. Currently the selection has objects: "),
-                                     str(len(selected_objs))))
+                                      str(len(selected_objs))))
                 return
             else:
                 first_pos, last_pos = nearest_points(selected_objs[0].geo, selected_objs[1].geo)
@@ -251,31 +164,31 @@ class DistanceMin(AppTool):
         else:
             first_pos, last_pos = 0, 0
 
-        self.start_entry.set_value("(%.*f, %.*f)" % (self.decimals, first_pos.x, self.decimals, first_pos.y))
-        self.stop_entry.set_value("(%.*f, %.*f)" % (self.decimals, last_pos.x, self.decimals, last_pos.y))
+        self.ui.start_entry.set_value("(%.*f, %.*f)" % (self.decimals, first_pos.x, self.decimals, first_pos.y))
+        self.ui.stop_entry.set_value("(%.*f, %.*f)" % (self.decimals, last_pos.x, self.decimals, last_pos.y))
 
         dx = first_pos.x - last_pos.x
         dy = first_pos.y - last_pos.y
 
-        self.distance_x_entry.set_value('%.*f' % (self.decimals, abs(dx)))
-        self.distance_y_entry.set_value('%.*f' % (self.decimals, abs(dy)))
+        self.ui.distance_x_entry.set_value('%.*f' % (self.decimals, abs(dx)))
+        self.ui.distance_y_entry.set_value('%.*f' % (self.decimals, abs(dy)))
 
         try:
             angle = math.degrees(math.atan(dy / dx))
-            self.angle_entry.set_value('%.*f' % (self.decimals, angle))
-        except Exception as e:
+            self.ui.angle_entry.set_value('%.*f' % (self.decimals, angle))
+        except Exception:
             pass
 
         d = math.sqrt(dx ** 2 + dy ** 2)
-        self.total_distance_entry.set_value('%.*f' % (self.decimals, abs(d)))
+        self.ui.total_distance_entry.set_value('%.*f' % (self.decimals, abs(d)))
 
         self.h_point = (min(first_pos.x, last_pos.x) + (abs(dx) / 2), min(first_pos.y, last_pos.y) + (abs(dy) / 2))
         if d != 0:
-            self.half_point_entry.set_value(
+            self.ui.half_point_entry.set_value(
                 "(%.*f, %.*f)" % (self.decimals, self.h_point[0], self.decimals, self.h_point[1])
             )
         else:
-            self.half_point_entry.set_value(
+            self.ui.half_point_entry.set_value(
                 "(%.*f, %.*f)" % (self.decimals, 0.0, self.decimals, 0.0)
             )
 
@@ -299,7 +212,126 @@ class DistanceMin(AppTool):
                              (_("Jumped to the half point between the two selected objects"),
                               "(%.*f, %.*f)" % (self.decimals, self.h_point[0], self.decimals, self.h_point[1])))
 
-    def set_meas_units(self, units):
-        self.meas.units_label.setText("[" + self.app.options["units"].lower() + "]")
+    # def set_meas_units(self, units):
+    #     self.meas.units_label.setText("[" + self.app.options["units"].lower() + "]")
+
+
+class DistMinUI:
+
+    toolName = _("Minimum Distance Tool")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+        self.units = self.app.defaults['units'].lower()
+
+        # ## Title
+        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
+        self.layout.addWidget(title_label)
+
+        # ## Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout)
+
+        self.units_label = QtWidgets.QLabel('%s:' % _("Units"))
+        self.units_label.setToolTip(_("Those are the units in which the distance is measured."))
+        self.units_value = QtWidgets.QLabel("%s" % str({'mm': _("METRIC (mm)"), 'in': _("INCH (in)")}[self.units]))
+        self.units_value.setDisabled(True)
+
+        self.start_label = QtWidgets.QLabel("%s:" % _('First object point'))
+        self.start_label.setToolTip(_("This is first object point coordinates.\n"
+                                      "This is the start point for measuring distance."))
+
+        self.stop_label = QtWidgets.QLabel("%s:" % _('Second object point'))
+        self.stop_label.setToolTip(_("This is second object point coordinates.\n"
+                                     "This is the end point for measuring distance."))
+
+        self.distance_x_label = QtWidgets.QLabel('%s:' % _("Dx"))
+        self.distance_x_label.setToolTip(_("This is the distance measured over the X axis."))
+
+        self.distance_y_label = QtWidgets.QLabel('%s:' % _("Dy"))
+        self.distance_y_label.setToolTip(_("This is the distance measured over the Y axis."))
+
+        self.angle_label = QtWidgets.QLabel('%s:' % _("Angle"))
+        self.angle_label.setToolTip(_("This is orientation angle of the measuring line."))
+
+        self.total_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _('DISTANCE'))
+        self.total_distance_label.setToolTip(_("This is the point to point Euclidean distance."))
+
+        self.half_point_label = QtWidgets.QLabel("<b>%s:</b>" % _('Half Point'))
+        self.half_point_label.setToolTip(_("This is the middle point of the point to point Euclidean distance."))
+
+        self.start_entry = FCEntry()
+        self.start_entry.setReadOnly(True)
+        self.start_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.start_entry.setToolTip(_("This is first object point coordinates.\n"
+                                      "This is the start point for measuring distance."))
+
+        self.stop_entry = FCEntry()
+        self.stop_entry.setReadOnly(True)
+        self.stop_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.stop_entry.setToolTip(_("This is second object point coordinates.\n"
+                                     "This is the end point for measuring distance."))
+
+        self.distance_x_entry = FCEntry()
+        self.distance_x_entry.setReadOnly(True)
+        self.distance_x_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.distance_x_entry.setToolTip(_("This is the distance measured over the X axis."))
+
+        self.distance_y_entry = FCEntry()
+        self.distance_y_entry.setReadOnly(True)
+        self.distance_y_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.distance_y_entry.setToolTip(_("This is the distance measured over the Y axis."))
+
+        self.angle_entry = FCEntry()
+        self.angle_entry.setReadOnly(True)
+        self.angle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.angle_entry.setToolTip(_("This is orientation angle of the measuring line."))
+
+        self.total_distance_entry = FCEntry()
+        self.total_distance_entry.setReadOnly(True)
+        self.total_distance_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.total_distance_entry.setToolTip(_("This is the point to point Euclidean distance."))
+
+        self.half_point_entry = FCEntry()
+        self.half_point_entry.setReadOnly(True)
+        self.half_point_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.half_point_entry.setToolTip(_("This is the middle point of the point to point Euclidean distance."))
+
+        self.measure_btn = QtWidgets.QPushButton(_("Measure"))
+        self.layout.addWidget(self.measure_btn)
+
+        self.jump_hp_btn = QtWidgets.QPushButton(_("Jump to Half Point"))
+        self.layout.addWidget(self.jump_hp_btn)
+        self.jump_hp_btn.setDisabled(True)
+
+        form_layout.addRow(self.units_label, self.units_value)
+        form_layout.addRow(self.start_label, self.start_entry)
+        form_layout.addRow(self.stop_label, self.stop_entry)
+        form_layout.addRow(self.distance_x_label, self.distance_x_entry)
+        form_layout.addRow(self.distance_y_label, self.distance_y_entry)
+        form_layout.addRow(self.angle_label, self.angle_entry)
+        form_layout.addRow(self.total_distance_label, self.total_distance_entry)
+        form_layout.addRow(self.half_point_label, self.half_point_entry)
+
+        self.layout.addStretch()
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
 
-# end of file
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 2687 - 0
appTools/ToolDrilling.py

@@ -0,0 +1,2687 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File by:  Marius Adrian Stanciu (c)                      #
+# Date:     6/15/2020                                      #
+# License:  MIT Licence                                    #
+# ##########################################################
+
+from PyQt5 import QtWidgets, QtCore, QtGui
+
+from appTool import AppTool
+from appGUI.GUIElements import FCCheckBox, FCDoubleSpinner, RadioSet, FCTable, FCButton, \
+    FCComboBox, OptionalInputSection, FCSpinner, NumericalEvalEntry, OptionalHideInputSection, FCLabel, \
+    NumericalEvalTupleEntry
+from appParsers.ParseExcellon import Excellon
+
+from copy import deepcopy
+
+import numpy as np
+
+from shapely.geometry import LineString
+
+import json
+import sys
+import re
+
+from matplotlib.backend_bases import KeyEvent as mpl_key_event
+
+import logging
+import gettext
+import appTranslation as fcTranslate
+import builtins
+import platform
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+settings = QtCore.QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolDrilling(AppTool, Excellon):
+
+    def __init__(self, app):
+        self.app = app
+        self.dec_format = self.app.dec_format
+
+        AppTool.__init__(self, app)
+        Excellon.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"])
+
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.t_ui = DrillingUI(layout=self.layout, app=self.app)
+        self.toolName = self.t_ui.toolName
+
+        # #############################################################################
+        # ########################## VARIABLES ########################################
+        # #############################################################################
+        self.units = ''
+        self.excellon_tools = {}
+        self.tooluid = 0
+        self.kind = "excellon"
+
+        # dict that holds the object names and the option name
+        # the key is the object name (defines in ObjectUI) for each UI element that is a parameter
+        # particular for a tool and the value is the actual name of the option that the UI element is changing
+        self.name2option = {}
+
+        # store here the default data for Geometry Data
+        self.default_data = {}
+
+        self.obj_name = ""
+        self.excellon_obj = None
+
+        # this holds the resulting GCode
+        self.total_gcode = ''
+
+        # this holds the resulting Parsed Gcode
+        self.total_gcode_parsed = []
+
+        self.first_click = False
+        self.cursor_pos = None
+        self.mouse_is_dragging = False
+
+        # store here the points for the "Polygon" area selection shape
+        self.points = []
+
+        self.mm = None
+        self.mr = None
+        self.kp = None
+
+        # variable to store the total amount of drills per job
+        self.tot_drill_cnt = 0
+        self.tool_row = 0
+
+        # variable to store the total amount of slots per job
+        self.tot_slot_cnt = 0
+        self.tool_row_slots = 0
+
+        # variable to store the distance travelled
+        self.travel_distance = 0.0
+
+        self.grid_status_memory = self.app.ui.grid_snap_btn.isChecked()
+
+        # store here the state of the exclusion checkbox state to be restored after building the UI
+        self.exclusion_area_cb_is_checked = False
+
+        # store here solid_geometry when there are tool with isolation job
+        self.solid_geometry = []
+
+        self.circle_steps = int(self.app.defaults["geometry_circle_steps"])
+
+        self.tooldia = None
+
+        # multiprocessing
+        self.pool = self.app.pool
+        self.results = []
+
+        # disconnect flags
+        self.area_sel_disconnect_flag = False
+        self.poly_sel_disconnect_flag = False
+
+        # Tools Database
+        self.tools_db_dict = None
+
+        self.tool_form_fields = {
+            "tools_drill_cutz":             self.t_ui.cutz_entry,
+            "tools_drill_multidepth":       self.t_ui.mpass_cb,
+            "tools_drill_depthperpass":     self.t_ui.maxdepth_entry,
+            "tools_drill_travelz":          self.t_ui.travelz_entry,
+            "tools_drill_feedrate_z":       self.t_ui.feedrate_z_entry,
+            "tools_drill_feedrate_rapid":   self.t_ui.feedrate_rapid_entry,
+
+            "tools_drill_spindlespeed":     self.t_ui.spindlespeed_entry,
+            "tools_drill_dwell":            self.t_ui.dwell_cb,
+            "tools_drill_dwelltime":        self.t_ui.dwelltime_entry,
+
+            "tools_drill_offset":           self.t_ui.offset_entry,
+
+            "tools_drill_drill_slots":      self.t_ui.drill_slots_cb,
+            "tools_drill_drill_overlap":    self.t_ui.drill_overlap_entry,
+            "tools_drill_last_drill":       self.t_ui.last_drill_cb
+        }
+
+        self.general_form_fields = {
+            "tools_drill_toolchange":       self.t_ui.toolchange_cb,
+            "tools_drill_toolchangez":      self.t_ui.toolchangez_entry,
+            "tools_drill_startz":           self.t_ui.estartz_entry,
+
+            "tools_drill_endz":             self.t_ui.endz_entry,
+            "tools_drill_endxy":            self.t_ui.endxy_entry,
+
+            "tools_drill_z_pdepth":         self.t_ui.pdepth_entry,
+            "tools_drill_feedrate_probe":   self.t_ui.feedrate_probe_entry,
+
+            "tools_drill_ppname_e":         self.t_ui.pp_excellon_name_cb,
+
+            "tools_drill_area_exclusion":   self.t_ui.exclusion_cb,
+            "tools_drill_area_strategy":    self.t_ui.strategy_radio,
+            "tools_drill_area_overz":       self.t_ui.over_z_entry,
+            "tools_drill_area_shape":       self.t_ui.area_shape_radio
+        }
+
+        self.name2option = {
+            "e_cutz":                   "tools_drill_cutz",
+            "e_multidepth":             "tools_drill_multidepth",
+            "e_depthperpass":           "tools_drill_depthperpass",
+            "e_travelz":                "tools_drill_travelz",
+            "e_feedratez":              "tools_drill_feedrate_z",
+            "e_fr_rapid":               "tools_drill_feedrate_rapid",
+
+            "e_spindlespeed":           "tools_drill_spindlespeed",
+            "e_dwell":                  "tools_drill_dwell",
+            "e_dwelltime":              "tools_drill_dwelltime",
+
+            "e_offset":                 "tools_drill_offset",
+
+            "e_drill_slots":            "tools_drill_drill_slots",
+            "e_drill_slots_overlap":    "tools_drill_drill_overlap",
+            "e_drill_last_drill":       "tools_drill_last_drill",
+
+            # General Parameters
+            "e_toolchange":             "tools_drill_toolchange",
+            "e_toolchangez":            "tools_drill_toolchangez",
+            "e_startz":                 "tools_drill_startz",
+
+            "e_endz":                   "tools_drill_endz",
+            "e_endxy":                  "tools_drill_endxy",
+
+            "e_depth_probe":            "tools_drill_z_pdepth",
+            "e_fr_probe":               "tools_drill_feedrate_probe",
+
+            "e_pp":                     "tools_drill_ppname_e",
+
+            "e_area_exclusion":         "tools_drill_area_exclusion",
+            "e_area_strategy":          "tools_drill_area_strategy",
+            "e_area_overz":             "tools_drill_area_overz",
+            "e_area_shape":             "tools_drill_area_shape",
+        }
+
+        self.poly_drawn = False
+        self.connect_signals_at_init()
+
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='Alt+D', **kwargs)
+
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("ToolDrilling()")
+        log.debug("ToolDrilling().run() was launched ...")
+
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+
+        AppTool.run(self)
+
+        self.set_tool_ui()
+        self.on_object_changed()
+        # self.build_tool_ui()
+
+        # all the tools are selected by default
+        self.t_ui.tools_table.selectAll()
+
+        self.app.ui.notebook.setTabText(2, _("Drilling Tool"))
+
+    def connect_signals_at_init(self):
+        # #############################################################################
+        # ############################ SIGNALS ########################################
+        # #############################################################################
+
+        self.t_ui.search_load_db_btn.clicked.connect(self.on_tool_db_load)
+
+        self.t_ui.apply_param_to_all.clicked.connect(self.on_apply_param_to_all_clicked)
+        self.t_ui.generate_cnc_button.clicked.connect(self.on_cnc_button_click)
+        self.t_ui.tools_table.drag_drop_sig.connect(self.rebuild_ui)
+
+        # Exclusion areas signals
+        self.t_ui.exclusion_table.horizontalHeader().sectionClicked.connect(self.exclusion_table_toggle_all)
+        self.t_ui.exclusion_table.lost_focus.connect(self.clear_selection)
+        self.t_ui.exclusion_table.itemClicked.connect(self.draw_sel_shape)
+        self.t_ui.add_area_button.clicked.connect(self.on_add_area_click)
+        self.t_ui.delete_area_button.clicked.connect(self.on_clear_area_click)
+        self.t_ui.delete_sel_area_button.clicked.connect(self.on_delete_sel_areas)
+        self.t_ui.strategy_radio.activated_custom.connect(self.on_strategy)
+
+        self.t_ui.pp_excellon_name_cb.activated.connect(self.on_pp_changed)
+
+        self.t_ui.reset_button.clicked.connect(self.set_tool_ui)
+        # Cleanup on Graceful exit (CTRL+ALT+X combo key)
+        self.app.cleanup.connect(self.set_tool_ui)
+
+    def set_tool_ui(self):
+        self.units = self.app.defaults['units'].upper()
+
+        # try to select in the Excellon combobox the active object
+        try:
+            selected_obj = self.app.collection.get_active()
+            if selected_obj.kind == 'excellon':
+                current_name = selected_obj.options['name']
+                self.t_ui.object_combo.set_value(current_name)
+        except Exception:
+            pass
+
+        # reset the Excellon preprocessor combo
+        self.t_ui.pp_excellon_name_cb.clear()
+        # populate Excellon preprocessor combobox list
+        for name in list(self.app.preprocessors.keys()):
+            # the HPGL preprocessor is only for Geometry not for Excellon job therefore don't add it
+            if name == 'hpgl':
+                continue
+            self.t_ui.pp_excellon_name_cb.addItem(name)
+        # add tooltips
+        for it in range(self.t_ui.pp_excellon_name_cb.count()):
+            self.t_ui.pp_excellon_name_cb.setItemData(
+                it, self.t_ui.pp_excellon_name_cb.itemText(it), QtCore.Qt.ToolTipRole)
+
+        # update the changes in UI depending on the selected preprocessor in Preferences
+        # after this moment all the changes in the Posprocessor combo will be handled by the activated signal of the
+        # self.t_ui.pp_excellon_name_cb combobox
+        self.on_pp_changed()
+
+        app_mode = self.app.defaults["global_app_level"]
+        # Show/Hide Advanced Options
+        if app_mode == 'b':
+            self.t_ui.level.setText('<span style="color:green;"><b>%s</b></span>' % _('Basic'))
+            self.t_ui.estartz_label.hide()
+            self.t_ui.estartz_entry.hide()
+        else:
+            self.t_ui.level.setText('<span style="color:red;"><b>%s</b></span>' % _('Advanced'))
+            self.t_ui.estartz_label.show()
+            self.t_ui.estartz_entry.show()
+
+        self.t_ui.tools_frame.show()
+
+        self.t_ui.order_radio.set_value(self.app.defaults["tools_drill_tool_order"])
+
+        loaded_obj = self.app.collection.get_by_name(self.t_ui.object_combo.get_value())
+        if loaded_obj:
+            outname = loaded_obj.options['name']
+        else:
+            outname = ''
+
+        # init the working variables
+        self.default_data.clear()
+        self.default_data = {
+            "name":                         outname + '_drill',
+            "plot":                         self.app.defaults["excellon_plot"],
+            "solid":                        self.app.defaults["excellon_solid"],
+            "multicolored":                 self.app.defaults["excellon_multicolored"],
+            "merge_fuse_tools":             self.app.defaults["excellon_merge_fuse_tools"],
+            "format_upper_in":              self.app.defaults["excellon_format_upper_in"],
+            "format_lower_in":              self.app.defaults["excellon_format_lower_in"],
+            "format_upper_mm":              self.app.defaults["excellon_format_upper_mm"],
+            "lower_mm":                     self.app.defaults["excellon_format_lower_mm"],
+            "zeros":                        self.app.defaults["excellon_zeros"],
+
+            "tools_drill_tool_order":       self.app.defaults["tools_drill_tool_order"],
+            "tools_drill_cutz":             self.app.defaults["tools_drill_cutz"],
+            "tools_drill_multidepth":       self.app.defaults["tools_drill_multidepth"],
+            "tools_drill_depthperpass":     self.app.defaults["tools_drill_depthperpass"],
+
+            "tools_drill_travelz":          self.app.defaults["tools_drill_travelz"],
+            "tools_drill_endz":             self.app.defaults["tools_drill_endz"],
+            "tools_drill_endxy":            self.app.defaults["tools_drill_endxy"],
+            "tools_drill_feedrate_z":       self.app.defaults["tools_drill_feedrate_z"],
+
+            "tools_drill_spindlespeed":     self.app.defaults["tools_drill_spindlespeed"],
+            "tools_drill_dwell":            self.app.defaults["tools_drill_dwell"],
+            "tools_drill_dwelltime":        self.app.defaults["tools_drill_dwelltime"],
+
+            "tools_drill_toolchange":       self.app.defaults["tools_drill_toolchange"],
+            "tools_drill_toolchangez":      self.app.defaults["tools_drill_toolchangez"],
+            "tools_drill_ppname_e":         self.app.defaults["tools_drill_ppname_e"],
+
+            # Drill Slots
+            "tools_drill_drill_slots":      self.app.defaults["tools_drill_drill_slots"],
+            "tools_drill_drill_overlap":    self.app.defaults["tools_drill_drill_overlap"],
+            "tools_drill_last_drill":       self.app.defaults["tools_drill_last_drill"],
+
+            # Advanced Options
+            "tools_drill_offset":           self.app.defaults["tools_drill_offset"],
+            "tools_drill_toolchangexy":     self.app.defaults["tools_drill_toolchangexy"],
+            "tools_drill_startz":           self.app.defaults["tools_drill_startz"],
+            "tools_drill_feedrate_rapid":   self.app.defaults["tools_drill_feedrate_rapid"],
+            "tools_drill_z_pdepth":         self.app.defaults["tools_drill_z_pdepth"],
+            "tools_drill_feedrate_probe":   self.app.defaults["tools_drill_feedrate_probe"],
+            "tools_drill_spindledir":       self.app.defaults["tools_drill_spindledir"],
+            "tools_drill_f_plunge":         self.app.defaults["tools_drill_f_plunge"],
+            "tools_drill_f_retract":        self.app.defaults["tools_drill_f_retract"],
+
+            "tools_drill_area_exclusion":   self.app.defaults["tools_drill_area_exclusion"],
+            "tools_drill_area_shape":       self.app.defaults["tools_drill_area_shape"],
+            "tools_drill_area_strategy":    self.app.defaults["tools_drill_area_strategy"],
+            "tools_drill_area_overz":       self.app.defaults["tools_drill_area_overz"],
+        }
+
+        # fill in self.default_data values from self.options
+        for opt_key, opt_val in self.app.options.items():
+            if opt_key.find('excellon_') == 0 or opt_key.find('tools_drill_') == 0:
+                self.default_data[opt_key] = deepcopy(opt_val)
+
+        self.first_click = False
+        self.cursor_pos = None
+        self.mouse_is_dragging = False
+
+        self.units = self.app.defaults['units'].upper()
+
+        # ########################################
+        # #######3 TEMP SETTINGS #################
+        # ########################################
+
+        self.t_ui.tools_table.setRowCount(2)
+        self.t_ui.tools_table.setMinimumHeight(self.t_ui.tools_table.getHeight())
+        self.t_ui.tools_table.setMaximumHeight(self.t_ui.tools_table.getHeight())
+
+        # make sure to update the UI on init
+        try:
+            self.excellon_tools = self.excellon_obj.tools
+        except AttributeError:
+            # no object loaded
+            pass
+        self.build_tool_ui()
+
+        # ########################################
+        # ########################################
+        # ####### Fill in the parameters #########
+        # ########################################
+        # ########################################
+        self.t_ui.cutz_entry.set_value(self.app.defaults["tools_drill_cutz"])
+        self.t_ui.mpass_cb.set_value(self.app.defaults["tools_drill_multidepth"])
+        self.t_ui.maxdepth_entry.set_value(self.app.defaults["tools_drill_depthperpass"])
+        self.t_ui.travelz_entry.set_value(self.app.defaults["tools_drill_travelz"])
+        self.t_ui.feedrate_z_entry.set_value(self.app.defaults["tools_drill_feedrate_z"])
+        self.t_ui.feedrate_rapid_entry.set_value(self.app.defaults["tools_drill_feedrate_rapid"])
+        self.t_ui.spindlespeed_entry.set_value(self.app.defaults["tools_drill_spindlespeed"])
+        self.t_ui.dwell_cb.set_value(self.app.defaults["tools_drill_dwell"])
+        self.t_ui.dwelltime_entry.set_value(self.app.defaults["tools_drill_dwelltime"])
+        self.t_ui.offset_entry.set_value(self.app.defaults["tools_drill_offset"])
+        self.t_ui.toolchange_cb.set_value(self.app.defaults["tools_drill_toolchange"])
+        self.t_ui.toolchangez_entry.set_value(self.app.defaults["tools_drill_toolchangez"])
+        self.t_ui.estartz_entry.set_value(self.app.defaults["tools_drill_startz"])
+        self.t_ui.endz_entry.set_value(self.app.defaults["tools_drill_endz"])
+        self.t_ui.endxy_entry.set_value(self.app.defaults["tools_drill_endxy"])
+        self.t_ui.pdepth_entry.set_value(self.app.defaults["tools_drill_z_pdepth"])
+        self.t_ui.feedrate_probe_entry.set_value(self.app.defaults["tools_drill_feedrate_probe"])
+
+        self.t_ui.exclusion_cb.set_value(self.app.defaults["tools_drill_area_exclusion"])
+        self.t_ui.strategy_radio.set_value(self.app.defaults["tools_drill_area_strategy"])
+        self.t_ui.over_z_entry.set_value(self.app.defaults["tools_drill_area_overz"])
+        self.t_ui.area_shape_radio.set_value(self.app.defaults["tools_drill_area_shape"])
+
+        # Drill slots - part of the Advanced Excellon params
+        self.t_ui.drill_overlap_entry.set_value(self.app.defaults["tools_drill_drill_overlap"])
+        self.t_ui.last_drill_cb.set_value(self.app.defaults["tools_drill_last_drill"])
+        self.t_ui.drill_overlap_label.hide()
+        self.t_ui.drill_overlap_entry.hide()
+        self.t_ui.last_drill_cb.hide()
+
+        # if the app mode is Basic then disable this feature
+        if app_mode == 'b':
+            self.t_ui.drill_slots_cb.set_value(False)
+            self.t_ui.drill_slots_cb.hide()
+        else:
+            self.t_ui.drill_slots_cb.show()
+            self.t_ui.drill_slots_cb.set_value(self.app.defaults["tools_drill_drill_slots"])
+
+        try:
+            self.t_ui.object_combo.currentTextChanged.disconnect()
+        except (AttributeError, TypeError):
+            pass
+        self.t_ui.object_combo.currentTextChanged.connect(self.on_object_changed)
+
+    def rebuild_ui(self):
+        # read the table tools uid
+        current_uid_list = []
+        for row in range(self.t_ui.tools_table.rowCount()):
+            try:
+                uid = int(self.t_ui.tools_table.item(row, 3).text())
+                current_uid_list.append(uid)
+            except AttributeError:
+                continue
+
+        new_tools = {}
+        new_uid = 1
+
+        for current_uid in current_uid_list:
+            new_tools[new_uid] = deepcopy(self.excellon_tools[current_uid])
+            new_uid += 1
+
+        self.excellon_tools = new_tools
+
+        # the tools table changed therefore we need to rebuild it
+        QtCore.QTimer.singleShot(20, self.build_tool_ui)
+
+    def build_tool_ui(self):
+        log.debug("ToolDrilling.build_tool_ui()")
+        self.ui_disconnect()
+
+        # order the tools by tool diameter if it's the case
+        sorted_tools = []
+        for k, v in self.excellon_tools.items():
+            sorted_tools.append(self.dec_format(float(v['tooldia'])))
+
+        order = self.t_ui.order_radio.get_value()
+        if order == 'fwd':
+            sorted_tools.sort(reverse=False)
+        elif order == 'rev':
+            sorted_tools.sort(reverse=True)
+        else:
+            pass
+
+        # remake the excellon_tools dict in the order above
+        new_id = 1
+        new_tools = {}
+        for tooldia in sorted_tools:
+            for old_tool in self.excellon_tools:
+                if self.dec_format(float(self.excellon_tools[old_tool]['tooldia'])) == tooldia:
+                    new_tools[new_id] = deepcopy(self.excellon_tools[old_tool])
+                    new_id += 1
+
+        self.excellon_tools = new_tools
+
+        if self.excellon_obj and self.excellon_tools:
+            self.t_ui.exc_param_frame.setDisabled(False)
+            tools = [k for k in self.excellon_tools]
+        else:
+            self.t_ui.exc_param_frame.setDisabled(True)
+            self.t_ui.tools_table.setRowCount(2)
+            tools = []
+
+        n = len(tools)
+        # we have (n+2) rows because there are 'n' tools, each a row, plus the last 2 rows for totals.
+        self.t_ui.tools_table.setRowCount(n + 2)
+        self.tool_row = 0
+
+        tot_drill_cnt = 0
+        tot_slot_cnt = 0
+
+        for tool_no in tools:
+
+            # Find no of drills for the current tool
+            try:
+                drill_cnt = len(self.excellon_tools[tool_no]["drills"])  # variable to store the nr of drills per tool
+            except KeyError:
+                drill_cnt = 0
+            tot_drill_cnt += drill_cnt
+
+            # Find no of slots for the current tool
+            try:
+                slot_cnt = len(self.excellon_tools[tool_no]["slots"])   # variable to store the nr of slots per tool
+            except KeyError:
+                slot_cnt = 0
+            tot_slot_cnt += slot_cnt
+
+            # Tool name/id
+            exc_id_item = QtWidgets.QTableWidgetItem('%d' % int(tool_no))
+            exc_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDragEnabled)
+            self.t_ui.tools_table.setItem(self.tool_row, 0, exc_id_item)
+
+            # Tool Diameter
+            dia_item = QtWidgets.QTableWidgetItem(str(self.dec_format(self.excellon_tools[tool_no]['tooldia'])))
+            dia_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDragEnabled)
+            self.t_ui.tools_table.setItem(self.tool_row, 1, dia_item)
+
+            # Number of drills per tool
+            drill_count_item = QtWidgets.QTableWidgetItem('%d' % drill_cnt)
+            drill_count_item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDragEnabled)
+            self.t_ui.tools_table.setItem(self.tool_row, 2, drill_count_item)
+
+            # Tool unique ID
+            tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tool_no)))
+            # ## REMEMBER: THIS COLUMN IS HIDDEN in UI
+            self.t_ui.tools_table.setItem(self.tool_row, 3, tool_uid_item)
+
+            # Number of slots per tool
+            # if the slot number is zero is better to not clutter the GUI with zero's so we print a space
+            slot_count_str = '%d' % slot_cnt if slot_cnt > 0 else ''
+            slot_count_item = QtWidgets.QTableWidgetItem(slot_count_str)
+            slot_count_item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDragEnabled)
+            self.t_ui.tools_table.setItem(self.tool_row, 4, slot_count_item)
+
+            self.tool_row += 1
+
+        # add a last row with the Total number of drills
+        empty_1 = QtWidgets.QTableWidgetItem('')
+        empty_1.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        empty_1_1 = QtWidgets.QTableWidgetItem('')
+        empty_1_1.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+        label_tot_drill_count = QtWidgets.QTableWidgetItem(_('Total Drills'))
+        label_tot_drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+        tot_drill_count = QtWidgets.QTableWidgetItem('%d' % tot_drill_cnt)
+        tot_drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+        self.t_ui.tools_table.setItem(self.tool_row, 0, empty_1)
+        self.t_ui.tools_table.setItem(self.tool_row, 1, label_tot_drill_count)
+        self.t_ui.tools_table.setItem(self.tool_row, 2, tot_drill_count)  # Total number of drills
+        self.t_ui.tools_table.setItem(self.tool_row, 4, empty_1_1)
+
+        font = QtGui.QFont()
+        font.setBold(True)
+        font.setWeight(75)
+
+        for k in [1, 2]:
+            self.t_ui.tools_table.item(self.tool_row, k).setForeground(QtGui.QColor(127, 0, 255))
+            self.t_ui.tools_table.item(self.tool_row, k).setFont(font)
+
+        self.tool_row += 1
+
+        # add a last row with the Total number of slots
+        empty_2 = QtWidgets.QTableWidgetItem('')
+        empty_2.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        empty_2_1 = QtWidgets.QTableWidgetItem('')
+        empty_2_1.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+        label_tot_slot_count = QtWidgets.QTableWidgetItem(_('Total Slots'))
+        tot_slot_count = QtWidgets.QTableWidgetItem('%d' % tot_slot_cnt)
+        label_tot_slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
+        tot_slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+        self.t_ui.tools_table.setItem(self.tool_row, 0, empty_2)
+        self.t_ui.tools_table.setItem(self.tool_row, 1, label_tot_slot_count)
+        self.t_ui.tools_table.setItem(self.tool_row, 2, empty_2_1)
+        self.t_ui.tools_table.setItem(self.tool_row, 4, tot_slot_count)  # Total number of slots
+
+        for kl in [1, 2, 4]:
+            self.t_ui.tools_table.item(self.tool_row, kl).setFont(font)
+            self.t_ui.tools_table.item(self.tool_row, kl).setForeground(QtGui.QColor(0, 70, 255))
+
+        # make the diameter column editable
+        # for row in range(self.t_ui.tools_table.rowCount() - 2):
+        #     self.t_ui.tools_table.item(row, 1).setFlags(
+        #         QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+        self.t_ui.tools_table.resizeColumnsToContents()
+        self.t_ui.tools_table.resizeRowsToContents()
+
+        vertical_header = self.t_ui.tools_table.verticalHeader()
+        vertical_header.hide()
+        self.t_ui.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.t_ui.tools_table.horizontalHeader()
+        self.t_ui.tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
+
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.ResizeToContents)
+
+        self.t_ui.tools_table.setSortingEnabled(False)
+
+        self.t_ui.tools_table.setMinimumHeight(self.t_ui.tools_table.getHeight())
+        self.t_ui.tools_table.setMaximumHeight(self.t_ui.tools_table.getHeight())
+
+        # all the tools are selected by default
+        self.t_ui.tools_table.selectAll()
+
+        # Build Exclusion Areas section
+        e_len = len(self.app.exc_areas.exclusion_areas_storage)
+        self.t_ui.exclusion_table.setRowCount(e_len)
+
+        area_id = 0
+
+        for area in range(e_len):
+            area_id += 1
+
+            area_dict = self.app.exc_areas.exclusion_areas_storage[area]
+
+            area_id_item = QtWidgets.QTableWidgetItem('%d' % int(area_id))
+            area_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.t_ui.exclusion_table.setItem(area, 0, area_id_item)  # Area id
+
+            object_item = QtWidgets.QTableWidgetItem('%s' % area_dict["obj_type"])
+            object_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.t_ui.exclusion_table.setItem(area, 1, object_item)  # Origin Object
+
+            strategy_item = QtWidgets.QTableWidgetItem('%s' % area_dict["strategy"])
+            strategy_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.t_ui.exclusion_table.setItem(area, 2, strategy_item)  # Strategy
+
+            overz_item = QtWidgets.QTableWidgetItem('%s' % area_dict["overz"])
+            overz_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.t_ui.exclusion_table.setItem(area, 3, overz_item)  # Over Z
+
+        self.t_ui.exclusion_table.resizeColumnsToContents()
+        self.t_ui.exclusion_table.resizeRowsToContents()
+
+        area_vheader = self.t_ui.exclusion_table.verticalHeader()
+        area_vheader.hide()
+        self.t_ui.exclusion_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        area_hheader = self.t_ui.exclusion_table.horizontalHeader()
+        area_hheader.setMinimumSectionSize(10)
+        area_hheader.setDefaultSectionSize(70)
+
+        area_hheader.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        area_hheader.resizeSection(0, 20)
+        area_hheader.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        area_hheader.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        area_hheader.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+
+        # area_hheader.setStretchLastSection(True)
+        self.t_ui.exclusion_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        self.t_ui.exclusion_table.setColumnWidth(0, 20)
+
+        self.t_ui.exclusion_table.setMinimumHeight(self.t_ui.exclusion_table.getHeight())
+        self.t_ui.exclusion_table.setMaximumHeight(self.t_ui.exclusion_table.getHeight())
+
+        self.ui_connect()
+
+        # set the text on tool_data_label after loading the object
+        sel_rows = set()
+        sel_items = self.t_ui.tools_table.selectedItems()
+        for it in sel_items:
+            sel_rows.add(it.row())
+        if len(sel_rows) > 1:
+            self.t_ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
+            )
+        elif len(sel_rows) == 1:
+            # update the QLabel that shows for which Tool we have the parameters in the UI form
+            toolnr = int(self.t_ui.tools_table.item(list(sel_rows)[0], 0).text())
+            self.t_ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s %d</font></b>" % (_('Parameters for'), _("Tool"), toolnr)
+            )
+
+    def on_object_changed(self):
+        log.debug("ToolDrilling.on_object_changed()")
+        # updated units
+        self.units = self.app.defaults['units'].upper()
+
+        # load the Excellon object
+        self.obj_name = self.t_ui.object_combo.currentText()
+
+        # Get source object.
+        try:
+            self.excellon_obj = self.app.collection.get_by_name(self.obj_name)
+        except Exception:
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(self.obj_name)))
+            return
+
+        if self.excellon_obj is None:
+            self.excellon_tools = {}
+            self.t_ui.exc_param_frame.setDisabled(True)
+            self.set_tool_ui()
+        else:
+            self.app.collection.set_active(self.obj_name)
+            self.t_ui.exc_param_frame.setDisabled(False)
+
+            if self.app.defaults["excellon_autoload_db"]:
+                self.excellon_tools = self.excellon_obj.tools
+                self.on_tool_db_load()
+            else:
+                # self.on_tool_db_load() already build once the tool UI, no need to do it twice
+                self.excellon_tools = self.excellon_obj.tools
+                self.build_tool_ui()
+
+        sel_rows = set()
+        table_items = self.t_ui.tools_table.selectedItems()
+        if table_items:
+            for it in table_items:
+                sel_rows.add(it.row())
+
+        if not sel_rows or len(sel_rows) == 0:
+            self.t_ui.generate_cnc_button.setDisabled(True)
+            self.t_ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("No Tool Selected"))
+            )
+        else:
+            self.t_ui.generate_cnc_button.setDisabled(False)
+
+    def ui_connect(self):
+
+        # Area Exception - exclusion shape added signal
+        # first disconnect it from any other object
+        try:
+            self.app.exc_areas.e_shape_modified.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        # then connect it to the current build_tool_ui() method
+        self.app.exc_areas.e_shape_modified.connect(self.update_exclusion_table)
+
+        # rows selected
+        self.t_ui.tools_table.clicked.connect(self.on_row_selection_change)
+        self.t_ui.tools_table.horizontalHeader().sectionClicked.connect(self.on_toggle_all_rows)
+
+        # Tool Parameters
+        for opt in self.tool_form_fields:
+            current_widget = self.tool_form_fields[opt]
+            if isinstance(current_widget, FCCheckBox):
+                current_widget.stateChanged.connect(self.form_to_storage)
+            if isinstance(current_widget, RadioSet):
+                current_widget.activated_custom.connect(self.form_to_storage)
+            elif isinstance(current_widget, FCDoubleSpinner) or isinstance(current_widget, FCSpinner):
+                current_widget.returnPressed.connect(self.form_to_storage)
+            elif isinstance(current_widget, FCComboBox):
+                current_widget.currentIndexChanged.connect(self.form_to_storage)
+
+        # General Parameters
+        for opt in self.general_form_fields:
+            current_widget2 = self.general_form_fields[opt]
+            if isinstance(current_widget2, FCCheckBox):
+                current_widget2.stateChanged.connect(self.form_to_storage)
+            if isinstance(current_widget2, RadioSet):
+                current_widget2.activated_custom.connect(self.form_to_storage)
+            elif isinstance(current_widget2, FCDoubleSpinner) or isinstance(current_widget2, FCSpinner):
+                current_widget2.returnPressed.connect(self.form_to_storage)
+            elif isinstance(current_widget2, FCComboBox):
+                current_widget2.currentIndexChanged.connect(self.form_to_storage)
+            elif isinstance(current_widget2, NumericalEvalEntry):
+                current_widget2.editingFinished.connect(self.form_to_storage)
+            elif isinstance(current_widget2, NumericalEvalTupleEntry):
+                current_widget2.editingFinished.connect(self.form_to_storage)
+
+        self.t_ui.order_radio.activated_custom[str].connect(self.on_order_changed)
+
+    def ui_disconnect(self):
+        # rows selected
+        try:
+            self.t_ui.tools_table.clicked.disconnect(self.on_row_selection_change)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.t_ui.tools_table.horizontalHeader().sectionClicked.disconnect(self.on_toggle_all_rows)
+        except (TypeError, AttributeError):
+            pass
+
+        # tool table widgets
+        for row in range(self.t_ui.tools_table.rowCount()):
+
+            try:
+                self.t_ui.tools_table.cellWidget(row, 2).currentIndexChanged.disconnect()
+            except (TypeError, AttributeError):
+                pass
+
+        # Tool Parameters
+        for opt in self.tool_form_fields:
+            current_widget = self.tool_form_fields[opt]
+            if isinstance(current_widget, FCCheckBox):
+                try:
+                    current_widget.stateChanged.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+            if isinstance(current_widget, RadioSet):
+                try:
+                    current_widget.activated_custom.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+            elif isinstance(current_widget, FCDoubleSpinner) or isinstance(current_widget, FCSpinner):
+                try:
+                    current_widget.returnPressed.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+            elif isinstance(current_widget, FCComboBox):
+                try:
+                    current_widget.currentIndexChanged.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+
+        # General Parameters
+        for opt in self.general_form_fields:
+            current_widget2 = self.general_form_fields[opt]
+            if isinstance(current_widget2, FCCheckBox):
+                try:
+                    current_widget2.stateChanged.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+            if isinstance(current_widget2, RadioSet):
+                try:
+                    current_widget2.activated_custom.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+            elif isinstance(current_widget2, FCDoubleSpinner) or isinstance(current_widget2, FCSpinner):
+                try:
+                    current_widget2.returnPressed.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+            elif isinstance(current_widget2, FCComboBox):
+                try:
+                    current_widget2.currentIndexChanged.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+            elif isinstance(current_widget2, NumericalEvalEntry):
+                try:
+                    current_widget2.editingFinished.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+            elif isinstance(current_widget2, NumericalEvalTupleEntry):
+                try:
+                    current_widget2.editingFinished.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+        try:
+            self.t_ui.order_radio.activated_custom[str].disconnect()
+        except (TypeError, ValueError):
+            pass
+
+    def on_tool_db_load(self):
+
+        filename = self.app.data_path + '\\tools_db.FlatDB'
+
+        # load the database tools from the file
+        try:
+            with open(filename) as f:
+                tools = f.read()
+        except IOError:
+            self.app.log.error("Could not load tools DB file.")
+            self.app.inform.emit('[ERROR] %s' % _("Could not load Tools DB file."))
+            return
+
+        try:
+            self.tools_db_dict = json.loads(tools)
+        except Exception:
+            e = sys.exc_info()[0]
+            self.app.log.error(str(e))
+            self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
+            return
+
+        if not self.tools_db_dict:
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Tools DB empty."))
+            return
+
+        self.replace_tools()
+
+    def replace_tools(self):
+        log.debug("ToolDrilling.replace_tools()")
+
+        if self.excellon_obj:
+            new_tools_dict = deepcopy(self.excellon_tools)
+
+            for orig_tool, orig_tool_val in self.excellon_tools.items():
+                orig_tooldia = orig_tool_val['tooldia']
+
+                tool_found = 0
+
+                # look in database tools
+                for db_tool, db_tool_val in self.tools_db_dict.items():
+                    db_tooldia = db_tool_val['tooldia']
+                    low_limit = float(db_tool_val['data']['tol_min'])
+                    high_limit = float(db_tool_val['data']['tol_max'])
+
+                    # if we find a tool with the same diameter in the Tools DB just update it's data
+                    if orig_tooldia == db_tooldia:
+                        tool_found += 1
+                        for d in db_tool_val['data']:
+                            if d.find('tools_drill') == 0:
+                                new_tools_dict[orig_tool]['data'][d] = db_tool_val['data'][d]
+                            elif d.find('tools_') == 0:
+                                # don't need data for other App Tools; this tests after 'tools_drill_'
+                                continue
+                            else:
+                                new_tools_dict[orig_tool]['data'][d] = db_tool_val['data'][d]
+                    # search for a tool that has a tolerance that the tool fits in
+                    elif high_limit >= orig_tooldia >= low_limit:
+                        tool_found += 1
+                        new_tools_dict[orig_tool]['tooldia'] = db_tooldia
+                        for d in db_tool_val['data']:
+                            if d.find('tools_drill') == 0:
+                                new_tools_dict[orig_tool]['data'][d] = db_tool_val['data'][d]
+                            elif d.find('tools_') == 0:
+                                # don't need data for other App Tools; this tests after 'tools_drill_'
+                                continue
+                            else:
+                                new_tools_dict[orig_tool]['data'][d] = db_tool_val['data'][d]
+
+                if tool_found > 1:
+                    self.app.inform.emit(
+                        '[WARNING_NOTCL] %s' % _("Cancelled.\n"
+                                                 "Multiple tools for one tool diameter found in Tools Database."))
+                    self.blockSignals(False)
+                    return
+
+            self.excellon_tools = new_tools_dict
+            self.build_tool_ui()
+
+    def on_toggle_all_rows(self):
+        """
+        will toggle the selection of all rows in Tools table
+
+        :return:
+        """
+        sel_model = self.t_ui.tools_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        if len(sel_rows) == self.t_ui.tools_table.rowCount():
+            self.t_ui.tools_table.clearSelection()
+            self.t_ui.exc_param_frame.setDisabled(True)
+
+            self.t_ui.generate_cnc_button.setDisabled(True)
+            self.t_ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("No Tool Selected"))
+            )
+        else:
+            self.t_ui.tools_table.selectAll()
+            self.t_ui.exc_param_frame.setDisabled(False)
+            self.t_ui.generate_cnc_button.setDisabled(False)
+            self.t_ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
+            )
+
+    def on_row_selection_change(self):
+        sel_model = self.t_ui.tools_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        # update UI only if only one row is selected otherwise having multiple rows selected will deform information
+        # for the rows other that the current one (first selected)
+        if len(sel_rows) == 1:
+            self.update_ui()
+
+    def update_ui(self):
+        self.blockSignals(True)
+
+        sel_rows = set()
+        table_items = self.t_ui.tools_table.selectedItems()
+        if table_items:
+            for it in table_items:
+                sel_rows.add(it.row())
+            # sel_rows = sorted(set(index.row() for index in self.t_ui.tools_table.selectedIndexes()))
+
+        if not sel_rows or len(sel_rows) == 0:
+            self.t_ui.generate_cnc_button.setDisabled(True)
+            self.t_ui.exc_param_frame.setDisabled(True)
+            self.t_ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("No Tool Selected"))
+            )
+            self.blockSignals(False)
+            return
+        else:
+            self.t_ui.generate_cnc_button.setDisabled(False)
+            self.t_ui.exc_param_frame.setDisabled(False)
+
+        if len(sel_rows) == 1:
+            # update the QLabel that shows for which Tool we have the parameters in the UI form
+            tooluid = int(self.t_ui.tools_table.item(list(sel_rows)[0], 0).text())
+            self.t_ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s %d</font></b>" % (_('Parameters for'), _("Tool"), tooluid)
+            )
+        else:
+            self.t_ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
+            )
+
+        for c_row in sel_rows:
+            # populate the form with the data from the tool associated with the row parameter
+            try:
+                item = self.t_ui.tools_table.item(c_row, 3)
+                if type(item) is not None:
+                    tooluid = int(item.text())
+                    self.storage_to_form(self.excellon_tools[tooluid]['data'])
+                else:
+                    self.blockSignals(False)
+                    return
+            except Exception as e:
+                log.debug("Tool missing. Add a tool in the Tool Table. %s" % str(e))
+                self.blockSignals(False)
+                return
+        self.blockSignals(False)
+
+    def storage_to_form(self, dict_storage):
+        """
+        Will update the GUI with data from the "storage" in this case the dict self.tools
+
+        :param dict_storage:    A dictionary holding the data relevant for gnerating Gcode from Excellon
+        :type dict_storage:     dict
+        :return:                None
+        :rtype:
+        """
+        for form_key in self.tool_form_fields:
+            for storage_key in dict_storage:
+                if form_key == storage_key and form_key not in \
+                        ["tools_drill_toolchange", "tools_drill_toolchangez", "startz", "endz", "tools_drill_ppname_e"]:
+                    try:
+                        self.tool_form_fields[form_key].set_value(dict_storage[form_key])
+                    except Exception as e:
+                        log.debug("ToolDrilling.storage_to_form() --> %s" % str(e))
+                        pass
+
+    def form_to_storage(self):
+        """
+        Will update the 'storage' attribute which is the dict self.tools with data collected from GUI
+
+        :return:    None
+        :rtype:
+        """
+        if self.t_ui.tools_table.rowCount() == 2:
+            # there is no tool in tool table so we can't save the GUI elements values to storage
+            # Excellon Tool Table has 2 rows by default
+            return
+
+        self.blockSignals(True)
+
+        widget_changed = self.sender()
+        wdg_objname = widget_changed.objectName()
+        option_changed = self.name2option[wdg_objname]
+
+        # row = self.t_ui.tools_table.currentRow()
+        rows = sorted(list(set(index.row() for index in self.t_ui.tools_table.selectedIndexes())))
+        for row in rows:
+            if row < 0:
+                row = 0
+            tooluid_item = int(self.t_ui.tools_table.item(row, 3).text())
+
+            # update tool parameters
+            for tooluid_key, tooluid_val in self.excellon_tools.items():
+                if int(tooluid_key) == tooluid_item:
+                    if option_changed in self.tool_form_fields:
+                        new_option_value = self.tool_form_fields[option_changed].get_value()
+                        if option_changed in tooluid_val:
+                            tooluid_val[option_changed] = new_option_value
+                        if option_changed in tooluid_val['data']:
+                            tooluid_val['data'][option_changed] = new_option_value
+
+        # update general parameters
+        # they are updated for all tools
+        for tooluid_key, tooluid_val in self.excellon_tools.items():
+            if option_changed in self.general_form_fields:
+                new_option_value = self.general_form_fields[option_changed].get_value()
+                if option_changed in tooluid_val:
+                    tooluid_val[option_changed] = new_option_value
+                if option_changed in tooluid_val['data']:
+                    tooluid_val['data'][option_changed] = new_option_value
+
+        self.blockSignals(False)
+
+    def get_selected_tools_list(self):
+        """
+        Returns the keys to the self.tools dictionary corresponding
+        to the selections on the tool list in the appGUI.
+
+        :return:    List of tools.
+        :rtype:     list
+        """
+
+        return [str(x.text()) for x in self.t_ui.tools_table.selectedItems()]
+
+    def get_selected_tools_table_items(self):
+        """
+        Returns a list of lists, each list in the list is made out of row elements
+
+        :return:    List of table_tools items.
+        :rtype:     list
+        """
+        table_tools_items = []
+
+        rows = set()
+        for x in self.t_ui.tools_table.selectedItems():
+            rows.add(x.row())
+
+        for row in rows:
+            txt = ''
+            elem = []
+
+            for column in range(self.t_ui.tools_table.columnCount()):
+                if column == 3:
+                    # disregard this column since it's the toolID
+                    continue
+
+                try:
+                    txt = self.t_ui.tools_table.item(row, column).text()
+                except AttributeError:
+                    try:
+                        txt = self.t_ui.tools_table.cellWidget(row, column).currentText()
+                    except AttributeError:
+                        pass
+                elem.append(txt)
+            table_tools_items.append(deepcopy(elem))
+            # table_tools_items.append([self.t_ui.tools_table.item(x.row(), column).text()
+            #                           for column in range(0, self.t_ui.tools_table.columnCount() - 1)])
+        for item in table_tools_items:
+            item[0] = str(item[0])
+        return table_tools_items
+
+    def on_apply_param_to_all_clicked(self):
+        if self.t_ui.tools_table.rowCount() == 0:
+            # there is no tool in tool table so we can't save the GUI elements values to storage
+            log.debug("ToolDrilling.on_apply_param_to_all_clicked() --> no tool in Tools Table, aborting.")
+            return
+
+        self.blockSignals(True)
+
+        row = self.t_ui.tools_table.currentRow()
+        if row < 0:
+            row = 0
+
+        tooluid_item = int(self.t_ui.tools_table.item(row, 3).text())
+        temp_tool_data = {}
+
+        for tooluid_key, tooluid_val in self.excellon_tools.items():
+            if int(tooluid_key) == tooluid_item:
+                # this will hold the 'data' key of the self.tools[tool] dictionary that corresponds to
+                # the current row in the tool table
+                temp_tool_data = tooluid_val['data']
+                break
+
+        for tooluid_key, tooluid_val in self.excellon_tools.items():
+            tooluid_val['data'] = deepcopy(temp_tool_data)
+
+        self.app.inform.emit('[success] %s' % _("Current Tool parameters were applied to all tools."))
+        self.blockSignals(False)
+
+    def on_order_changed(self, order):
+        if order != 'no':
+            self.build_tool_ui()
+
+    def on_tooltable_cellwidget_change(self):
+        cw = self.sender()
+        assert isinstance(cw, QtWidgets.QComboBox), \
+            "Expected a QtWidgets.QComboBox, got %s" % isinstance(cw, QtWidgets.QComboBox)
+
+        cw_index = self.t_ui.tools_table.indexAt(cw.pos())
+        cw_row = cw_index.row()
+        cw_col = cw_index.column()
+
+        current_uid = int(self.t_ui.tools_table.item(cw_row, 3).text())
+
+        # if the sender is in the column with index 2 then we update the tool_type key
+        if cw_col == 2:
+            tt = cw.currentText()
+            typ = 'Iso' if tt == 'V' else "Rough"
+
+            self.excellon_tools[current_uid].update({
+                'type': typ,
+                'tool_type': tt,
+            })
+
+    def on_pp_changed(self):
+        current_pp = self.t_ui.pp_excellon_name_cb.get_value()
+
+        if "toolchange_probe" in current_pp.lower():
+            self.t_ui.pdepth_entry.setVisible(True)
+            self.t_ui.pdepth_label.show()
+
+            self.t_ui.feedrate_probe_entry.setVisible(True)
+            self.t_ui.feedrate_probe_label.show()
+        else:
+            self.t_ui.pdepth_entry.setVisible(False)
+            self.t_ui.pdepth_label.hide()
+
+            self.t_ui.feedrate_probe_entry.setVisible(False)
+            self.t_ui.feedrate_probe_label.hide()
+
+        if 'marlin' in current_pp.lower():
+            self.t_ui.feedrate_rapid_label.show()
+            self.t_ui.feedrate_rapid_entry.show()
+        else:
+            self.t_ui.feedrate_rapid_label.hide()
+            self.t_ui.feedrate_rapid_entry.hide()
+
+        if 'laser' in current_pp.lower():
+            self.t_ui.cutzlabel.hide()
+            self.t_ui.cutz_entry.hide()
+            try:
+                self.t_ui.mpass_cb.hide()
+                self.t_ui.maxdepth_entry.hide()
+            except AttributeError:
+                pass
+
+            if 'marlin' in current_pp.lower():
+                self.t_ui.travelzlabel.setText('%s:' % _("Focus Z"))
+                self.t_ui.travelzlabel.show()
+                self.t_ui.travelz_entry.show()
+
+                self.t_ui.endz_label.show()
+                self.t_ui.endz_entry.show()
+            else:
+                self.t_ui.travelzlabel.hide()
+                self.t_ui.travelz_entry.hide()
+
+                self.t_ui.endz_label.hide()
+                self.t_ui.endz_entry.hide()
+
+            try:
+                self.t_ui.frzlabel.hide()
+                self.t_ui.feedrate_z_entry.hide()
+            except AttributeError:
+                pass
+
+            self.t_ui.dwell_cb.hide()
+            self.t_ui.dwelltime_entry.hide()
+
+            self.t_ui.spindle_label.setText('%s:' % _("Laser Power"))
+
+            try:
+                self.t_ui.tool_offset_label.hide()
+                self.t_ui.offset_entry.hide()
+            except AttributeError:
+                pass
+        else:
+            self.t_ui.cutzlabel.show()
+            self.t_ui.cutz_entry.show()
+            try:
+                self.t_ui.mpass_cb.show()
+                self.t_ui.maxdepth_entry.show()
+            except AttributeError:
+                pass
+
+            self.t_ui.travelzlabel.setText('%s:' % _('Travel Z'))
+
+            self.t_ui.travelzlabel.show()
+            self.t_ui.travelz_entry.show()
+
+            self.t_ui.endz_label.show()
+            self.t_ui.endz_entry.show()
+
+            try:
+                self.t_ui.frzlabel.show()
+                self.t_ui.feedrate_z_entry.show()
+            except AttributeError:
+                pass
+            self.t_ui.dwell_cb.show()
+            self.t_ui.dwelltime_entry.show()
+
+            self.t_ui.spindle_label.setText('%s:' % _('Spindle speed'))
+
+            try:
+                self.t_ui.tool_offset_label.show()
+                self.t_ui.offset_entry.show()
+            except AttributeError:
+                pass
+
+    def on_key_press(self, event):
+        # modifiers = QtWidgets.QApplication.keyboardModifiers()
+        # matplotlib_key_flag = False
+
+        # events out of the self.app.collection view (it's about Project Tab) are of type int
+        if type(event) is int:
+            key = event
+        # events from the GUI are of type QKeyEvent
+        elif type(event) == QtGui.QKeyEvent:
+            key = event.key()
+        elif isinstance(event, mpl_key_event):  # MatPlotLib key events are trickier to interpret than the rest
+            # matplotlib_key_flag = True
+
+            key = event.key
+            key = QtGui.QKeySequence(key)
+
+            # check for modifiers
+            key_string = key.toString().lower()
+            if '+' in key_string:
+                mod, __, key_text = key_string.rpartition('+')
+                if mod.lower() == 'ctrl':
+                    # modifiers = QtCore.Qt.ControlModifier
+                    pass
+                elif mod.lower() == 'alt':
+                    # modifiers = QtCore.Qt.AltModifier
+                    pass
+                elif mod.lower() == 'shift':
+                    # modifiers = QtCore.Qt.ShiftModifier
+                    pass
+                else:
+                    # modifiers = QtCore.Qt.NoModifier
+                    pass
+                key = QtGui.QKeySequence(key_text)
+
+        # events from Vispy are of type KeyEvent
+        else:
+            key = event.key
+
+        if key == QtCore.Qt.Key_Escape or key == 'Escape':
+            self.points = []
+            self.poly_drawn = False
+            self.delete_moving_selection_shape()
+            self.delete_tool_selection_shape()
+
+    def on_add_area_click(self):
+        shape_button = self.t_ui.area_shape_radio
+        overz_button = self.t_ui.over_z_entry
+        strategy_radio = self.t_ui.strategy_radio
+        cnc_button = self.t_ui.generate_cnc_button
+        solid_geo = self.excellon_obj.solid_geometry
+        obj_type = self.excellon_obj.kind
+
+        self.app.exc_areas.on_add_area_click(
+            shape_button=shape_button, overz_button=overz_button, cnc_button=cnc_button, strategy_radio=strategy_radio,
+            solid_geo=solid_geo, obj_type=obj_type)
+
+    def on_clear_area_click(self):
+        if not self.app.exc_areas.exclusion_areas_storage:
+            self.app.inform.emit("[WARNING_NOTCL] %s" % _("Delete failed. There are no exclusion areas to delete."))
+            return
+
+        self.app.exc_areas.on_clear_area_click()
+        self.app.exc_areas.e_shape_modified.emit()
+
+    def on_delete_sel_areas(self):
+        sel_model = self.t_ui.exclusion_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        # so the duplicate rows will not be added
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        if not sel_rows:
+            self.app.inform.emit("[WARNING_NOTCL] %s" % _("Delete failed. Nothing is selected."))
+            return
+
+        self.app.exc_areas.delete_sel_shapes(idxs=list(sel_rows))
+        self.app.exc_areas.e_shape_modified.emit()
+
+    def draw_sel_shape(self):
+        sel_model = self.t_ui.exclusion_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        self.delete_sel_shape()
+
+        if self.app.is_legacy is False:
+            face = self.app.defaults['global_sel_fill'][:-2] + str(hex(int(0.2 * 255)))[2:]
+            outline = self.app.defaults['global_sel_line'][:-2] + str(hex(int(0.8 * 255)))[2:]
+        else:
+            face = self.app.defaults['global_sel_fill'][:-2] + str(hex(int(0.4 * 255)))[2:]
+            outline = self.app.defaults['global_sel_line'][:-2] + str(hex(int(1.0 * 255)))[2:]
+
+        for row in sel_rows:
+            sel_rect = self.app.exc_areas.exclusion_areas_storage[row]['shape']
+            self.app.move_tool.sel_shapes.add(sel_rect, color=outline, face_color=face, update=True, layer=0,
+                                              tolerance=None)
+        if self.app.is_legacy is True:
+            self.app.move_tool.sel_shapes.redraw()
+
+    def clear_selection(self):
+        self.app.delete_selection_shape()
+        # self.t_ui.exclusion_table.clearSelection()
+
+    def delete_sel_shape(self):
+        self.app.delete_selection_shape()
+
+    def update_exclusion_table(self):
+        self.exclusion_area_cb_is_checked = True if self.t_ui.exclusion_cb.isChecked() else False
+
+        self.build_tool_ui()
+        self.t_ui.exclusion_cb.set_value(self.exclusion_area_cb_is_checked)
+
+    def on_strategy(self, val):
+        if val == 'around':
+            self.t_ui.over_z_label.setDisabled(True)
+            self.t_ui.over_z_entry.setDisabled(True)
+        else:
+            self.t_ui.over_z_label.setDisabled(False)
+            self.t_ui.over_z_entry.setDisabled(False)
+
+    def exclusion_table_toggle_all(self):
+        """
+        will toggle the selection of all rows in Exclusion Areas table
+
+        :return:
+        """
+        sel_model = self.t_ui.exclusion_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        if sel_rows:
+            self.t_ui.exclusion_table.clearSelection()
+            self.delete_sel_shape()
+        else:
+            self.t_ui.exclusion_table.selectAll()
+            self.draw_sel_shape()
+
+    @staticmethod
+    def process_slot_as_drills(slot, overlap, add_last_pt=False):
+
+        drills_list = []
+        start_pt = slot[0]
+        stop_pt = slot[1]
+        slot_line = LineString([start_pt, stop_pt])
+        drills_list.append(start_pt)
+
+        ii = 0
+        while True:
+            ii += 1
+            target = overlap * ii
+            new_pt = slot_line.interpolate(target)
+            if new_pt.within(slot_line) is False:
+                break
+            drills_list.append(new_pt)
+
+        if add_last_pt and stop_pt.distance(drills_list[-1]) >= overlap/10:
+            drills_list.append(stop_pt)
+        return drills_list
+
+    def is_valid_excellon(self):
+        slots_as_drills = self.t_ui.drill_slots_cb.get_value()
+
+        has_drills = None
+        for tool_key, tool_dict in self.excellon_tools.items():
+            if 'drills' in tool_dict and tool_dict['drills']:
+                has_drills = True
+                break
+        has_slots = None
+        for tool_key, tool_dict in self.excellon_tools.items():
+            if 'slots' in tool_dict and tool_dict['slots']:
+                has_slots = True
+                break
+
+        if not has_drills:
+            if slots_as_drills and has_slots:
+                return True
+            else:
+                return False
+
+    def get_selected_tools_uid(self):
+        """
+        Return a list of the selected tools UID from the Tool Table
+        """
+        selected_uid = set()
+        for sel_it in self.t_ui.tools_table.selectedItems():
+            uid = int(self.t_ui.tools_table.item(sel_it.row(), 3).text())
+            selected_uid.add(uid)
+        return list(selected_uid)
+
+    def create_drill_points(self, selected_tools, selected_sorted_tools):
+        points = {}
+
+        # create drill points out of the drills locations
+        for tool_key, tl_dict in self.excellon_tools.items():
+            if tool_key in selected_tools:
+                if 'drills' in tl_dict and tl_dict['drills']:
+                    for drill_pt in tl_dict['drills']:
+                        try:
+                            points[tool_key].append(drill_pt)
+                        except KeyError:
+                            points[tool_key] = [drill_pt]
+        log.debug("Found %d TOOLS with drills." % len(points))
+
+        # #############################################################################################################
+        # ############ SLOTS TO DRILLS CONVERSION SECTION #############################################################
+        # #############################################################################################################
+
+        # convert slots to a sequence of drills and add them to drill points
+        should_add_last_pt = self.t_ui.last_drill_cb.get_value()
+
+        for tool_key, tl_dict in self.excellon_tools.items():
+            convert_slots = tl_dict['data']['tools_drill_drill_slots']
+            if convert_slots:
+                if tool_key in selected_tools:
+                    overlap = 1 - (self.t_ui.drill_overlap_entry.get_value() / 100.0)
+                    drill_overlap = 0.0
+                    for i in selected_sorted_tools:
+                        if i[0] == tool_key:
+                            slot_tool_dia = i[1]
+                            drill_overlap = overlap * slot_tool_dia
+                            break
+
+                    new_drills = []
+                    if 'slots' in tl_dict and tl_dict['slots']:
+                        for slot in tl_dict['slots']:
+                            new_drills += self.process_slot_as_drills(slot=slot, overlap=drill_overlap,
+                                                                      add_last_pt=should_add_last_pt)
+                        if new_drills:
+                            try:
+                                points[tool_key] += new_drills
+                            except Exception:
+                                points[tool_key] = new_drills
+        log.debug("Found %d TOOLS with drills after converting slots to drills." % len(points))
+
+        return points
+
+    def check_intersection(self, points):
+        for tool_key in points:
+            for pt in points[tool_key]:
+                for area in self.app.exc_areas.exclusion_areas_storage:
+                    pt_buf = pt.buffer(self.excellon_tools[tool_key]['tooldia'] / 2.0)
+                    if pt_buf.within(area['shape']) or pt_buf.intersects(area['shape']):
+                        return True
+        return False
+
+    def on_cnc_button_click(self):
+        obj_name = self.t_ui.object_combo.currentText()
+        toolchange = self.t_ui.toolchange_cb.get_value()
+
+        # Get source object.
+        try:
+            obj = self.app.collection.get_by_name(obj_name)
+        except Exception:
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(obj_name)))
+            return
+
+        if obj is None:
+            self.app.inform.emit('[ERROR_NOTCL] %s.' % _("Object not found"))
+            return
+
+        xmin = obj.options['xmin']
+        ymin = obj.options['ymin']
+        xmax = obj.options['xmax']
+        ymax = obj.options['ymax']
+
+        job_name = obj.options["name"] + "_cnc"
+        obj.pp_excellon_name = self.t_ui.pp_excellon_name_cb.get_value()
+
+        if self.is_valid_excellon() is False:
+            log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
+                      "The loaded Excellon file has no drills ...")
+            self.app.inform.emit('[ERROR_NOTCL] %s...' % _('The loaded Excellon file has no drills'))
+            return
+
+        # Get the tools from the Tool Table
+        selected_tools_id = self.get_selected_tools_uid()
+        if not selected_tools_id:
+            # if there is a single tool in the table (remember that the last 2 rows are for totals and do not count in
+            # tool number) it means that there are 3 rows (1 tool and 2 totals).
+            # in this case regardless of the selection status of that tool, use it.
+            if self.t_ui.tools_table.rowCount() >= 3:
+                selected_tools_id.append(int(self.t_ui.tools_table.item(0, 3).text()))
+            else:
+                msg = '[ERROR_NOTCL] %s' % _("Please select one or more tools from the list and try again.")
+                self.app.inform.emit(msg)
+                return
+
+        # #############################################################################################################
+        # #############################################################################################################
+        # 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
+        # #############################################################################################################
+        # #############################################################################################################
+        all_tools = []
+        for tool_as_key, v in list(self.excellon_tools.items()):
+            all_tools.append((int(tool_as_key), float(v['tooldia'])))
+
+        order = self.t_ui.order_radio.get_value()
+        if order == 'fwd':
+            sorted_tools = sorted(all_tools, key=lambda t1: t1[1])
+        elif order == 'rev':
+            sorted_tools = sorted(all_tools, key=lambda t1: t1[1], reverse=True)
+        else:
+            sorted_tools = all_tools
+
+        # Create a sorted list of selected sel_tools from the sorted_tools list
+        sel_tools = [i for i, j in sorted_tools for k in selected_tools_id if i == k]
+
+        log.debug("Tools sorted are: %s" % str(sel_tools))
+
+        # #############################################################################################################
+        # #############################################################################################################
+        # #### Create Points (Group by tool): a dictionary of shapely Point geo elements grouped by tool number #######
+        # #############################################################################################################
+        # #############################################################################################################
+        self.app.inform.emit(_("Creating a list of points to drill..."))
+
+        # points is a dictionary: keys are tools ad values are lists of Shapely Points
+        points = self.create_drill_points(selected_tools=sel_tools, selected_sorted_tools=sorted_tools)
+
+        # check if there are drill points in the exclusion areas (if any areas)
+        if self.app.exc_areas.exclusion_areas_storage and self.check_intersection(points) is True:
+            self.app.inform.emit("[ERROR_NOTCL] %s" % _("Failed. Drill points inside the exclusion zones."))
+            return 'fail'
+
+        # #############################################################################################################
+        # General Parameters
+        # #############################################################################################################
+        used_excellon_optimization_type = self.app.defaults["excellon_optimization_type"]
+        current_platform = platform.architecture()[0]
+        if current_platform != '64bit':
+            used_excellon_optimization_type = 'T'
+
+        # #############################################################################################################
+        # #############################################################################################################
+        # GCODE creation
+        # #############################################################################################################
+        # #############################################################################################################
+        self.app.inform.emit('%s...' % _("Starting G-Code"))
+
+        # Object initialization function for app.app_obj.new_object()
+        def job_init(job_obj, app_obj):
+            assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
+            app_obj.inform.emit(_("Generating Excellon CNCJob..."))
+
+            # #########################################################################################################
+            # #########################################################################################################
+            # build a self.options['Tools_in_use'] list from scratch if we don't have one like in the case of
+            # running this method from a Tcl Command
+            # #########################################################################################################
+            # #########################################################################################################
+            build_tools_in_use_list = False
+            if 'Tools_in_use' not in job_obj.options:
+                job_obj.options['Tools_in_use'] = []
+
+            # if the list is empty (either we just added the key or it was already there but empty) signal to build it
+            if not job_obj.options['Tools_in_use']:
+                build_tools_in_use_list = True
+
+            # #########################################################################################################
+            # #########################################################################################################
+            # fill the data into the self.exc_cnc_tools dictionary
+            # #########################################################################################################
+            # #########################################################################################################
+            for it in all_tools:
+                for to_ol in sel_tools:
+                    if to_ol == it[0]:
+                        sol_geo = []
+
+                        # solid geometry addition; we look into points because we may have slots converted to drills
+                        # therefore more drills than there were originally in
+                        # the self.excellon_tools[to_ol]['drills'] list
+                        drill_no = 0
+                        if to_ol in points and points[to_ol]:
+                            drill_no = len(points[to_ol])
+                            for drill in points[to_ol]:
+                                sol_geo.append(drill.buffer((it[1] / 2.0), resolution=job_obj.geo_steps_per_circle))
+
+                        slot_no = 0
+                        convert_slots = self.excellon_tools[to_ol]['data']['tools_drill_drill_slots']
+                        if 'slots' in self.excellon_tools[to_ol] and convert_slots is False:
+                            slot_no = len(self.excellon_tools[to_ol]['slots'])
+                            for eslot in self.excellon_tools[to_ol]['slots']:
+                                start = (eslot[0].x, eslot[0].y)
+                                stop = (eslot[1].x, eslot[1].y)
+                                sol_geo.append(
+                                    LineString([start, stop]).buffer((it[1] / 2.0),
+                                                                     resolution=job_obj.geo_steps_per_circle)
+                                )
+
+                        # adjust Offset for current tool
+                        try:
+                            z_off = float(self.excellon_tools[it[0]]['data']['offset']) * (-1)
+                        except KeyError:
+                            z_off = 0
+
+                        # default tool data
+                        default_data = {}
+                        for kk, vv in list(obj.options.items()):
+                            default_data[kk] = deepcopy(vv)
+                        default_data['tools_drill_cutz'] = float(self.excellon_tools[it[0]]['data']['tools_drill_cutz'])
+
+                        # populate the Excellon CNC tools storage
+                        job_obj.exc_cnc_tools[it[1]] = {}
+                        job_obj.exc_cnc_tools[it[1]]['tool'] = it[0]
+                        job_obj.exc_cnc_tools[it[1]]['nr_drills'] = drill_no
+                        job_obj.exc_cnc_tools[it[1]]['nr_slots'] = slot_no
+                        job_obj.exc_cnc_tools[it[1]]['offset'] = z_off
+                        job_obj.exc_cnc_tools[it[1]]['data'] = default_data
+                        job_obj.exc_cnc_tools[it[1]]['gcode'] = ''
+                        job_obj.exc_cnc_tools[it[1]]['gcode_parsed'] = []
+                        job_obj.exc_cnc_tools[it[1]]['solid_geometry'] = deepcopy(sol_geo)
+
+                        # build a self.options['Tools_in_use'] list from scratch if we don't have one like in the case
+                        # of running this method from a Tcl Command
+                        if build_tools_in_use_list is True:
+                            job_obj.options['Tools_in_use'].append(
+                                [it[0], it[1], drill_no, slot_no]
+                            )
+
+            # #########################################################################################################
+            # #########################################################################################################
+            # Initialization
+            # #########################################################################################################
+            # #########################################################################################################
+            # Preprocessor
+            job_obj.pp_excellon_name = self.t_ui.pp_excellon_name_cb.get_value()
+            job_obj.pp_excellon = self.app.preprocessors[job_obj.pp_excellon_name]
+
+            # get the tool_table items in a list of row items
+            tool_table_items = self.get_selected_tools_table_items()
+            # insert an information only element in the front
+            tool_table_items.insert(0, [_("Tool_nr"), _("Diameter"), _("Drills_Nr"), _("Slots_Nr")])
+
+            # ## Add properties to the object
+            job_obj.options['Tools_in_use'] = tool_table_items
+            job_obj.options['type'] = 'Excellon'
+            job_obj.options['ppname_e'] = obj.pp_excellon_name
+
+            job_obj.options['xmin'] = xmin
+            job_obj.options['ymin'] = ymin
+            job_obj.options['xmax'] = xmax
+            job_obj.options['ymax'] = ymax
+
+            job_obj.origin_kind = 'excellon'
+            job_obj.use_ui = True
+            job_obj.toolchange_xy_type = "excellon"
+            job_obj.coords_decimals = int(self.app.defaults["cncjob_coords_decimals"])
+            job_obj.fr_decimals = int(self.app.defaults["cncjob_fr_decimals"])
+            job_obj.multitool = True
+
+            # first drill point
+            job_obj.xy_toolchange = self.app.defaults["tools_drill_toolchangexy"]
+            x_tc, y_tc = [0, 0]
+            try:
+                if job_obj.xy_toolchange != '':
+                    tcxy_temp = re.sub('[()\[\]]', '', str(job_obj.xy_toolchange))
+                    if tcxy_temp:
+                        x_tc, y_tc = [float(eval(a)) for a in tcxy_temp.split(",")]
+            except Exception:
+                x_tc, y_tc = [0, 0]
+                self.app.inform.emit('[ERROR]%s' % _("The Toolchange X,Y format has to be (x, y)."))
+
+            job_obj.oldx = x_tc
+            job_obj.oldy = y_tc
+            first_drill_point = (job_obj.oldx, job_obj.oldy)
+
+            # #########################################################################################################
+            # ####################### NO TOOLCHANGE ###################################################################
+            # #########################################################################################################
+            if toolchange is False:
+                tool_points = []
+                for tool in sel_tools:
+                    tool_points += points[tool]
+
+                # use the first tool in the selection as the tool that we are going to use
+                used_tool = sel_tools[0]
+                used_tooldia = self.excellon_tools[used_tool]['tooldia']
+
+                # those are used by the preprocessors to display data on the toolchange line
+                job_obj.tool = str(used_tool)
+                job_obj.postdata['toolC'] = used_tooldia
+
+                # reconstitute the tool_table_items to hold the total number of drills and slots since we are going to
+                # process all in one go with no toolchange and with only one tool
+                nr_drills = 0
+                nr_slots = 0
+                total_solid_geo = []
+
+                # calculate the total number of drills and of slots
+                for e_tool_dia in job_obj.exc_cnc_tools:
+                    nr_drills += int(job_obj.exc_cnc_tools[e_tool_dia]['nr_drills'])
+                    nr_slots += int(job_obj.exc_cnc_tools[e_tool_dia]['nr_slots'])
+                    total_solid_geo += job_obj.exc_cnc_tools[e_tool_dia]['solid_geometry']
+
+                tool_table_items.clear()
+                tool_table_items = [[str(used_tool), str(used_tooldia), str(nr_drills), str(nr_slots)]]
+                tool_table_items.insert(0, [_("Tool_nr"), _("Diameter"), _("Drills_Nr"), _("Slots_Nr")])
+                job_obj.options['Tools_in_use'] = tool_table_items
+
+                # generate GCode
+                tool_gcode, __, start_gcode = job_obj.excellon_tool_gcode_gen(used_tool, tool_points,
+                                                                              self.excellon_tools,
+                                                                              first_pt=first_drill_point,
+                                                                              is_first=True,
+                                                                              is_last=True,
+                                                                              opt_type=used_excellon_optimization_type,
+                                                                              toolchange=True)
+
+                # parse the Gcode
+                tool_gcode_parsed = job_obj.excellon_tool_gcode_parse(used_tooldia, gcode=tool_gcode,
+                                                                      start_pt=first_drill_point)
+
+                # store the results in Excellon CNC tools storage
+                job_obj.exc_cnc_tools[used_tooldia]['nr_drills'] = nr_drills
+                job_obj.exc_cnc_tools[used_tooldia]['nr_slots'] = nr_slots
+                job_obj.exc_cnc_tools[used_tooldia]['gcode'] = tool_gcode
+                job_obj.exc_cnc_tools[used_tooldia]['gcode_parsed'] = tool_gcode_parsed
+                job_obj.exc_cnc_tools[used_tooldia]['solid_geometry'] = total_solid_geo
+
+                # delete all tools from the Excellon CNC tools storage except the used one
+                for e_tool_dia in list(job_obj.exc_cnc_tools.keys()):
+                    if e_tool_dia != used_tooldia:
+                        job_obj.exc_cnc_tools.pop(e_tool_dia, None)
+
+                if start_gcode != '':
+                    job_obj.gc_start = start_gcode
+
+                self.total_gcode = tool_gcode
+                self.total_gcode_parsed = tool_gcode_parsed
+
+            # ####################### TOOLCHANGE ACTIVE ######################################################
+            else:
+                for tool in sel_tools:
+                    tool_points = points[tool]
+                    used_tooldia = self.excellon_tools[tool]['tooldia']
+
+                    # if slots are converted to drill for this tool, update the number of drills and make slots nr zero
+                    convert_slots = self.excellon_tools[tool]['data']['tools_drill_drill_slots']
+                    if convert_slots is True:
+                        nr_drills = len(points[tool])
+                        nr_slots = 0
+                        job_obj.exc_cnc_tools[used_tooldia]['nr_drills'] = nr_drills
+                        job_obj.exc_cnc_tools[used_tooldia]['nr_slots'] = nr_slots
+
+                        for line in range(1, len(job_obj.options['Tools_in_use'])):
+                            if self.dec_format(float(job_obj.options['Tools_in_use'][line][1])) == \
+                                    self.dec_format(used_tooldia):
+                                job_obj.options['Tools_in_use'][line][2] = str(nr_drills)
+                                job_obj.options['Tools_in_use'][line][3] = str(nr_slots)
+
+                    # calculate if the current tool is the first one or if it is the last one
+                    # for the first tool we add some extra GCode (start Gcode, header etc)
+                    # for the last tool we add other GCode (the end code, what is happening at the end of the job)
+                    is_last_tool = True if tool == sel_tools[-1] else False
+                    is_first_tool = True if tool == sel_tools[0] else False
+
+                    # Generate Gcode for the current tool
+                    tool_gcode, last_pt, start_gcode = job_obj.excellon_tool_gcode_gen(
+                        tool, tool_points, self.excellon_tools,
+                        first_pt=first_drill_point,
+                        is_first=is_first_tool,
+                        is_last=is_last_tool,
+                        opt_type=used_excellon_optimization_type,
+                        toolchange=True)
+
+                    # parse Gcode for the current tool
+                    tool_gcode_parsed = job_obj.excellon_tool_gcode_parse(used_tooldia, gcode=tool_gcode,
+                                                                          start_pt=first_drill_point)
+                    first_drill_point = last_pt
+
+                    # store the results of GCode generation and parsing
+                    job_obj.exc_cnc_tools[used_tooldia]['gcode'] = tool_gcode
+                    job_obj.exc_cnc_tools[used_tooldia]['gcode_parsed'] = tool_gcode_parsed
+
+                    if start_gcode != '':
+                        job_obj.gc_start = start_gcode
+
+                    self.total_gcode += tool_gcode
+                    self.total_gcode_parsed += tool_gcode_parsed
+
+            job_obj.gcode = self.total_gcode
+            job_obj.source_file = self.total_gcode
+            job_obj.gcode_parsed = self.total_gcode_parsed
+            if job_obj.gcode == 'fail':
+                return 'fail'
+
+            # create Geometry for plotting
+            # FIXME is it necessary? didn't we do it previously when filling data in self.exc_cnc_tools dictionary?
+            job_obj.create_geometry()
+
+            if used_excellon_optimization_type == 'M':
+                log.debug("The total travel distance with OR-TOOLS Metaheuristics is: %s" %
+                          str(job_obj.measured_distance))
+            elif used_excellon_optimization_type == 'B':
+                log.debug("The total travel distance with OR-TOOLS Basic Algorithm is: %s" %
+                          str(job_obj.measured_distance))
+            elif used_excellon_optimization_type == 'T':
+                log.debug(
+                    "The total travel distance with Travelling Salesman Algorithm is: %s" %
+                    str(job_obj.measured_distance))
+            else:
+                log.debug("The total travel distance with with no optimization is: %s" %
+                          str(job_obj.measured_distance))
+
+            # #########################################################################################################
+            # ############################# Calculate DISTANCE and ESTIMATED TIME #####################################
+            # #########################################################################################################
+            if job_obj.xy_end is None:
+                job_obj.xy_end = [job_obj.oldx, job_obj.oldy]
+
+            job_obj.measured_distance += abs(distance_euclidian(
+                job_obj.oldx, job_obj.oldy, job_obj.xy_end[0], job_obj.xy_end[1])
+            )
+            log.debug("The total travel distance including travel to end position is: %s" %
+                      str(job_obj.measured_distance) + '\n')
+            job_obj.travel_distance = job_obj.measured_distance
+
+            # I use the value of self.feedrate_rapid for the feadrate in case of the measure_lift_distance and for
+            # traveled_time because it is not always possible to determine the feedrate that the CNC machine uses
+            # for G0 move (the fastest speed available to the CNC router). Although self.feedrate_rapids is used only
+            # with Marlin preprocessor and derivatives.
+            job_obj.routing_time = \
+                (job_obj.measured_down_distance + job_obj.measured_up_to_zero_distance) / job_obj.z_feedrate
+            lift_time = job_obj.measured_lift_distance / job_obj.feedrate_rapid
+            traveled_time = job_obj.measured_distance / job_obj.feedrate_rapid
+            job_obj.routing_time += lift_time + traveled_time
+
+        # To be run in separate thread
+        def job_thread(a_obj):
+            with self.app.proc_container.new(_("Generating CNC Code")):
+                a_obj.app_obj.new_object("cncjob", job_name, job_init)
+
+            # Switch notebook to Properties page
+            self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
+
+        # Create promise for the new name.
+        self.app.collection.promise(job_name)
+
+        # Send to worker
+        # self.app.worker.add_task(job_thread, [self.app])
+        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+
+    def reset_fields(self):
+        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+
+
+class DrillingUI:
+
+    toolName = _("Drilling Tool")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        self.tools_frame = QtWidgets.QFrame()
+        self.tools_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.tools_frame)
+        self.tools_box = QtWidgets.QVBoxLayout()
+        self.tools_box.setContentsMargins(0, 0, 0, 0)
+        self.tools_frame.setLayout(self.tools_box)
+
+        self.title_box = QtWidgets.QHBoxLayout()
+        self.tools_box.addLayout(self.title_box)
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        title_label.setToolTip(
+            _("Create CNCJob with toolpaths for drilling or milling holes.")
+        )
+
+        self.title_box.addWidget(title_label)
+
+        # App Level label
+        self.level = QtWidgets.QLabel("")
+        self.level.setToolTip(
+            _(
+                "BASIC is suitable for a beginner. Many parameters\n"
+                "are hidden from the user in this mode.\n"
+                "ADVANCED mode will make available all parameters.\n\n"
+                "To change the application LEVEL, go to:\n"
+                "Edit -> Preferences -> General and check:\n"
+                "'APP. LEVEL' radio button."
+            )
+        )
+        self.level.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.title_box.addWidget(self.level)
+
+        # Grid Layout
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.tools_box.addLayout(grid0)
+
+        self.obj_combo_label = QtWidgets.QLabel('<b>%s</b>:' % _("EXCELLON"))
+        self.obj_combo_label.setToolTip(
+            _("Excellon object for drilling/milling operation.")
+        )
+
+        grid0.addWidget(self.obj_combo_label, 0, 0, 1, 2)
+
+        # ################################################
+        # ##### The object to be drilled #################
+        # ################################################
+        self.object_combo = FCComboBox()
+        self.object_combo.setModel(self.app.collection)
+        self.object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        # self.object_combo.setCurrentIndex(1)
+        self.object_combo.is_last = True
+
+        grid0.addWidget(self.object_combo, 1, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 2, 0, 1, 2)
+
+        # ################################################
+        # ########## Excellon Tool Table #################
+        # ################################################
+        self.tools_table = FCTable(drag_drop=True)
+        grid0.addWidget(self.tools_table, 3, 0, 1, 2)
+
+        self.tools_table.setColumnCount(5)
+        self.tools_table.setColumnHidden(3, True)
+        self.tools_table.setSortingEnabled(False)
+
+        self.tools_table.setHorizontalHeaderLabels(['#', _('Diameter'), _('Drills'), '', _('Slots')])
+        self.tools_table.horizontalHeaderItem(0).setToolTip(
+            _("This is the Tool Number.\n"
+              "When ToolChange is checked, on toolchange event this value\n"
+              "will be showed as a T1, T2 ... Tn in the Machine Code.\n\n"
+              "Here the tools are selected for G-code generation."))
+        self.tools_table.horizontalHeaderItem(1).setToolTip(
+            _("Tool Diameter. It's value (in current FlatCAM units) \n"
+              "is the cut width into the material."))
+        self.tools_table.horizontalHeaderItem(2).setToolTip(
+            _("The number of Drill holes. Holes that are drilled with\n"
+              "a drill bit."))
+        self.tools_table.horizontalHeaderItem(4).setToolTip(
+            _("The number of Slot holes. Holes that are created by\n"
+              "milling them with an endmill bit."))
+
+        # Tool order
+        self.order_label = QtWidgets.QLabel('%s:' % _('Tool order'))
+        self.order_label.setToolTip(_("This set the way that the tools in the tools table are used.\n"
+                                      "'No' --> means that the used order is the one in the tool table\n"
+                                      "'Forward' --> means that the tools will be ordered from small to big\n"
+                                      "'Reverse' --> means that the tools will ordered from big to small\n\n"
+                                      "WARNING: using rest machining will automatically set the order\n"
+                                      "in reverse and disable this control."))
+
+        self.order_radio = RadioSet([{'label': _('No'), 'value': 'no'},
+                                     {'label': _('Forward'), 'value': 'fwd'},
+                                     {'label': _('Reverse'), 'value': 'rev'}])
+
+        grid0.addWidget(self.order_label, 4, 0)
+        grid0.addWidget(self.order_radio, 4, 1)
+
+        # Manual Load of Tools from DB
+        self.search_load_db_btn = FCButton(_("Search DB"))
+        self.search_load_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png'))
+        self.search_load_db_btn.setToolTip(
+            _("Will search and try to replace the tools from Tools Table\n"
+              "with tools from DB that have a close diameter value.")
+        )
+
+        grid0.addWidget(self.search_load_db_btn, 5, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 6, 0, 1, 2)
+
+        # ###########################################################
+        # ############# Create CNC Job ##############################
+        # ###########################################################
+        self.tool_data_label = QtWidgets.QLabel(
+            "<b>%s: <font color='#0000FF'>%s %d</font></b>" % (_('Parameters for'), _("Tool"), int(1)))
+        self.tool_data_label.setToolTip(
+            _(
+                "The data used for creating GCode.\n"
+                "Each tool store it's own set of such data."
+            )
+        )
+        grid0.addWidget(self.tool_data_label, 8, 0, 1, 2)
+
+        self.exc_param_frame = QtWidgets.QFrame()
+        self.exc_param_frame.setContentsMargins(0, 0, 0, 0)
+        grid0.addWidget(self.exc_param_frame, 10, 0, 1, 2)
+
+        self.exc_tools_box = QtWidgets.QVBoxLayout()
+        self.exc_tools_box.setContentsMargins(0, 0, 0, 0)
+        self.exc_param_frame.setLayout(self.exc_tools_box)
+
+        # #################################################################
+        # ################# GRID LAYOUT 3   ###############################
+        # #################################################################
+
+        self.grid1 = QtWidgets.QGridLayout()
+        self.grid1.setColumnStretch(0, 0)
+        self.grid1.setColumnStretch(1, 1)
+        self.exc_tools_box.addLayout(self.grid1)
+
+        # Cut Z
+        self.cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
+        self.cutzlabel.setToolTip(
+            _("Drill depth (negative)\n"
+              "below the copper surface.")
+        )
+
+        self.cutz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.cutz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.cutz_entry.set_range(-9999.9999, 0.0000)
+        else:
+            self.cutz_entry.set_range(-9999.9999, 9999.9999)
+
+        self.cutz_entry.setSingleStep(0.1)
+        self.cutz_entry.setObjectName("e_cutz")
+
+        self.grid1.addWidget(self.cutzlabel, 4, 0)
+        self.grid1.addWidget(self.cutz_entry, 4, 1)
+
+        # Multi-Depth
+        self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth"))
+        self.mpass_cb.setToolTip(
+            _(
+                "Use multiple passes to limit\n"
+                "the cut depth in each pass. Will\n"
+                "cut multiple times until Cut Z is\n"
+                "reached."
+            )
+        )
+        self.mpass_cb.setObjectName("e_multidepth")
+
+        self.maxdepth_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.maxdepth_entry.set_precision(self.decimals)
+        self.maxdepth_entry.set_range(0, 9999.9999)
+        self.maxdepth_entry.setSingleStep(0.1)
+
+        self.maxdepth_entry.setToolTip(_("Depth of each pass (positive)."))
+        self.maxdepth_entry.setObjectName("e_depthperpass")
+
+        self.mis_mpass_geo = OptionalInputSection(self.mpass_cb, [self.maxdepth_entry])
+
+        self.grid1.addWidget(self.mpass_cb, 5, 0)
+        self.grid1.addWidget(self.maxdepth_entry, 5, 1)
+
+        # Travel Z (z_move)
+        self.travelzlabel = QtWidgets.QLabel('%s:' % _('Travel Z'))
+        self.travelzlabel.setToolTip(
+            _("Tool height when travelling\n"
+              "across the XY plane.")
+        )
+
+        self.travelz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.travelz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.travelz_entry.set_range(0.00001, 9999.9999)
+        else:
+            self.travelz_entry.set_range(-9999.9999, 9999.9999)
+
+        self.travelz_entry.setSingleStep(0.1)
+        self.travelz_entry.setObjectName("e_travelz")
+
+        self.grid1.addWidget(self.travelzlabel, 6, 0)
+        self.grid1.addWidget(self.travelz_entry, 6, 1)
+
+        # Excellon Feedrate Z
+        self.frzlabel = QtWidgets.QLabel('%s:' % _('Feedrate Z'))
+        self.frzlabel.setToolTip(
+            _("Tool speed while drilling\n"
+              "(in units per minute).\n"
+              "So called 'Plunge' feedrate.\n"
+              "This is for linear move G01.")
+        )
+        self.feedrate_z_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.feedrate_z_entry.set_precision(self.decimals)
+        self.feedrate_z_entry.set_range(0.0, 99999.9999)
+        self.feedrate_z_entry.setSingleStep(0.1)
+        self.feedrate_z_entry.setObjectName("e_feedratez")
+
+        self.grid1.addWidget(self.frzlabel, 14, 0)
+        self.grid1.addWidget(self.feedrate_z_entry, 14, 1)
+
+        # Excellon Rapid Feedrate
+        self.feedrate_rapid_label = QtWidgets.QLabel('%s:' % _('Feedrate Rapids'))
+        self.feedrate_rapid_label.setToolTip(
+            _("Tool speed while drilling\n"
+              "(in units per minute).\n"
+              "This is for the rapid move G00.\n"
+              "It is useful only for Marlin,\n"
+              "ignore for any other cases.")
+        )
+        self.feedrate_rapid_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.feedrate_rapid_entry.set_precision(self.decimals)
+        self.feedrate_rapid_entry.set_range(0.0, 99999.9999)
+        self.feedrate_rapid_entry.setSingleStep(0.1)
+        self.feedrate_rapid_entry.setObjectName("e_fr_rapid")
+
+        self.grid1.addWidget(self.feedrate_rapid_label, 16, 0)
+        self.grid1.addWidget(self.feedrate_rapid_entry, 16, 1)
+
+        # default values is to hide
+        self.feedrate_rapid_label.hide()
+        self.feedrate_rapid_entry.hide()
+
+        # Spindlespeed
+        self.spindle_label = QtWidgets.QLabel('%s:' % _('Spindle speed'))
+        self.spindle_label.setToolTip(
+            _("Speed of the spindle\n"
+              "in RPM (optional)")
+        )
+
+        self.spindlespeed_entry = FCSpinner(callback=self.confirmation_message_int)
+        self.spindlespeed_entry.set_range(0, 1000000)
+        self.spindlespeed_entry.set_step(100)
+        self.spindlespeed_entry.setObjectName("e_spindlespeed")
+
+        self.grid1.addWidget(self.spindle_label, 19, 0)
+        self.grid1.addWidget(self.spindlespeed_entry, 19, 1)
+
+        # Dwell
+        self.dwell_cb = FCCheckBox('%s:' % _('Dwell'))
+        self.dwell_cb.setToolTip(
+            _("Pause to allow the spindle to reach its\n"
+              "speed before cutting.")
+        )
+        self.dwell_cb.setObjectName("e_dwell")
+
+        # Dwelltime
+        self.dwelltime_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.dwelltime_entry.set_precision(self.decimals)
+        self.dwelltime_entry.set_range(0.0, 9999.9999)
+        self.dwelltime_entry.setSingleStep(0.1)
+
+        self.dwelltime_entry.setToolTip(
+            _("Number of time units for spindle to dwell.")
+        )
+        self.dwelltime_entry.setObjectName("e_dwelltime")
+
+        self.grid1.addWidget(self.dwell_cb, 20, 0)
+        self.grid1.addWidget(self.dwelltime_entry, 20, 1)
+
+        self.ois_dwell = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
+
+        # Tool Offset
+        self.tool_offset_label = QtWidgets.QLabel('%s:' % _('Offset Z'))
+        self.tool_offset_label.setToolTip(
+            _("Some drill bits (the larger ones) need to drill deeper\n"
+              "to create the desired exit hole diameter due of the tip shape.\n"
+              "The value here can compensate the Cut Z parameter.")
+        )
+
+        self.offset_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.offset_entry.set_precision(self.decimals)
+        self.offset_entry.set_range(-9999.9999, 9999.9999)
+        self.offset_entry.setObjectName("e_offset")
+
+        self.grid1.addWidget(self.tool_offset_label, 25, 0)
+        self.grid1.addWidget(self.offset_entry, 25, 1)
+
+        # Drill slots
+        self.drill_slots_cb = FCCheckBox('%s' % _('Drill slots'))
+        self.drill_slots_cb.setToolTip(
+            _("If the selected tool has slots then they will be drilled.")
+        )
+        self.drill_slots_cb.setObjectName("e_drill_slots")
+        self.grid1.addWidget(self.drill_slots_cb, 27, 0, 1, 2)
+
+        # Drill Overlap
+        self.drill_overlap_label = QtWidgets.QLabel('%s:' % _('Overlap'))
+        self.drill_overlap_label.setToolTip(
+            _("How much (percentage) of the tool diameter to overlap previous drill hole.")
+        )
+
+        self.drill_overlap_entry = FCDoubleSpinner(suffix='%', callback=self.confirmation_message)
+        self.drill_overlap_entry.set_precision(self.decimals)
+        self.drill_overlap_entry.set_range(0.0, 100.0000)
+        self.drill_overlap_entry.setSingleStep(0.1)
+
+        self.drill_overlap_entry.setObjectName("e_drill_slots_overlap")
+
+        self.grid1.addWidget(self.drill_overlap_label, 28, 0)
+        self.grid1.addWidget(self.drill_overlap_entry, 28, 1)
+
+        # Last drill in slot
+        self.last_drill_cb = FCCheckBox('%s' % _('Last drill'))
+        self.last_drill_cb.setToolTip(
+            _("If the slot length is not completely covered by drill holes,\n"
+              "add a drill hole on the slot end point.")
+        )
+        self.last_drill_cb.setObjectName("e_drill_last_drill")
+        self.grid1.addWidget(self.last_drill_cb, 30, 0, 1, 2)
+
+        self.drill_overlap_label.hide()
+        self.drill_overlap_entry.hide()
+        self.last_drill_cb.hide()
+
+        self.ois_drill_overlap = OptionalHideInputSection(
+            self.drill_slots_cb,
+            [
+                self.drill_overlap_label,
+                self.drill_overlap_entry,
+                self.last_drill_cb
+            ]
+        )
+
+        # #################################################################
+        # ################# GRID LAYOUT 5   ###############################
+        # #################################################################
+        # ################# COMMON PARAMETERS #############################
+
+        self.grid3 = QtWidgets.QGridLayout()
+        self.grid3.setColumnStretch(0, 0)
+        self.grid3.setColumnStretch(1, 1)
+        self.exc_tools_box.addLayout(self.grid3)
+
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid3.addWidget(separator_line2, 0, 0, 1, 2)
+
+        self.apply_param_to_all = FCButton(_("Apply parameters to all tools"))
+        self.apply_param_to_all.setIcon(QtGui.QIcon(self.app.resource_location + '/param_all32.png'))
+        self.apply_param_to_all.setToolTip(
+            _("The parameters in the current form will be applied\n"
+              "on all the tools from the Tool Table.")
+        )
+        self.grid3.addWidget(self.apply_param_to_all, 1, 0, 1, 2)
+
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid3.addWidget(separator_line2, 2, 0, 1, 2)
+
+        # General Parameters
+        self.gen_param_label = QtWidgets.QLabel('<b>%s</b>' % _("Common Parameters"))
+        self.gen_param_label.setToolTip(
+            _("Parameters that are common for all tools.")
+        )
+        self.grid3.addWidget(self.gen_param_label, 3, 0, 1, 2)
+
+        # Tool change
+        self.toolchange_cb = FCCheckBox('%s' % _("Tool change"))
+        self.toolchange_cb.setToolTip(
+            _("Include tool-change sequence\n"
+              "in G-Code (Pause for tool change).")
+        )
+        self.toolchange_cb.setObjectName("e_toolchange")
+        self.grid3.addWidget(self.toolchange_cb, 5, 0, 1, 2)
+
+        # Toolchange Z
+        self.toolchangez_label = QtWidgets.QLabel('%s:' % _("Tool change Z"))
+        self.toolchangez_label.setToolTip(
+            _("Z-axis position (height) for\n"
+              "tool change.")
+        )
+
+        self.toolchangez_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.toolchangez_entry.set_precision(self.decimals)
+        self.toolchangez_entry.setObjectName("e_toolchangez")
+
+        if machinist_setting == 0:
+            self.toolchangez_entry.set_range(0.0, 9999.9999)
+        else:
+            self.toolchangez_entry.set_range(-9999.9999, 9999.9999)
+
+        self.toolchangez_entry.setSingleStep(0.1)
+
+        self.grid3.addWidget(self.toolchangez_label, 7, 0)
+        self.grid3.addWidget(self.toolchangez_entry, 7, 1)
+
+        # Start move Z:
+        self.estartz_label = QtWidgets.QLabel('%s:' % _("Start Z"))
+        self.estartz_label.setToolTip(
+            _("Height of the tool just after start.\n"
+              "Delete the value if you don't need this feature.")
+        )
+        self.estartz_entry = NumericalEvalEntry(border_color='#0069A9')
+        self.estartz_entry.setObjectName("e_startz")
+
+        self.grid3.addWidget(self.estartz_label, 9, 0)
+        self.grid3.addWidget(self.estartz_entry, 9, 1)
+
+        # End move Z:
+        self.endz_label = QtWidgets.QLabel('%s:' % _("End move Z"))
+        self.endz_label.setToolTip(
+            _("Height of the tool after\n"
+              "the last move at the end of the job.")
+        )
+        self.endz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.endz_entry.set_precision(self.decimals)
+        self.endz_entry.setObjectName("e_endz")
+
+        if machinist_setting == 0:
+            self.endz_entry.set_range(0.0, 9999.9999)
+        else:
+            self.endz_entry.set_range(-9999.9999, 9999.9999)
+
+        self.endz_entry.setSingleStep(0.1)
+
+        self.grid3.addWidget(self.endz_label, 11, 0)
+        self.grid3.addWidget(self.endz_entry, 11, 1)
+
+        # End Move X,Y
+        endmove_xy_label = QtWidgets.QLabel('%s:' % _('End move X,Y'))
+        endmove_xy_label.setToolTip(
+            _("End move X,Y position. In format (x,y).\n"
+              "If no value is entered then there is no move\n"
+              "on X,Y plane at the end of the job.")
+        )
+        self.endxy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+        self.endxy_entry.setPlaceholderText(_("X,Y coordinates"))
+        self.endxy_entry.setObjectName("e_endxy")
+
+        self.grid3.addWidget(endmove_xy_label, 12, 0)
+        self.grid3.addWidget(self.endxy_entry, 12, 1)
+
+        # Probe depth
+        self.pdepth_label = QtWidgets.QLabel('%s:' % _("Probe Z depth"))
+        self.pdepth_label.setToolTip(
+            _("The maximum depth that the probe is allowed\n"
+              "to probe. Negative value, in current units.")
+        )
+
+        self.pdepth_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.pdepth_entry.set_precision(self.decimals)
+        self.pdepth_entry.set_range(-9999.9999, 9999.9999)
+        self.pdepth_entry.setSingleStep(0.1)
+        self.pdepth_entry.setObjectName("e_depth_probe")
+
+        self.grid3.addWidget(self.pdepth_label, 13, 0)
+        self.grid3.addWidget(self.pdepth_entry, 13, 1)
+
+        self.pdepth_label.hide()
+        self.pdepth_entry.setVisible(False)
+
+        # Probe feedrate
+        self.feedrate_probe_label = QtWidgets.QLabel('%s:' % _("Feedrate Probe"))
+        self.feedrate_probe_label.setToolTip(
+            _("The feedrate used while the probe is probing.")
+        )
+
+        self.feedrate_probe_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.feedrate_probe_entry.set_precision(self.decimals)
+        self.feedrate_probe_entry.set_range(0.0, 9999.9999)
+        self.feedrate_probe_entry.setSingleStep(0.1)
+        self.feedrate_probe_entry.setObjectName("e_fr_probe")
+
+        self.grid3.addWidget(self.feedrate_probe_label, 14, 0)
+        self.grid3.addWidget(self.feedrate_probe_entry, 14, 1)
+
+        self.feedrate_probe_label.hide()
+        self.feedrate_probe_entry.setVisible(False)
+
+        # Preprocessor Excellon selection
+        pp_excellon_label = QtWidgets.QLabel('%s:' % _("Preprocessor"))
+        pp_excellon_label.setToolTip(
+            _("The preprocessor JSON file that dictates\n"
+              "Gcode output for Excellon Objects.")
+        )
+        self.pp_excellon_name_cb = FCComboBox()
+        self.pp_excellon_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus)
+        self.pp_excellon_name_cb.setObjectName("e_pp")
+
+        self.grid3.addWidget(pp_excellon_label, 15, 0)
+        self.grid3.addWidget(self.pp_excellon_name_cb, 15, 1)
+
+        # ------------------------------------------------------------------------------------------------------------
+        # ------------------------- EXCLUSION AREAS ------------------------------------------------------------------
+        # ------------------------------------------------------------------------------------------------------------
+
+        # Exclusion Areas
+        self.exclusion_cb = FCCheckBox('%s' % _("Add exclusion areas"))
+        self.exclusion_cb.setToolTip(
+            _(
+                "Include exclusion areas.\n"
+                "In those areas the travel of the tools\n"
+                "is forbidden."
+            ))
+        self.exclusion_cb.setObjectName("e_area_exclusion")
+
+        self.grid3.addWidget(self.exclusion_cb, 20, 0, 1, 2)
+
+        self.exclusion_frame = QtWidgets.QFrame()
+        self.exclusion_frame.setContentsMargins(0, 0, 0, 0)
+        self.grid3.addWidget(self.exclusion_frame, 22, 0, 1, 2)
+
+        self.exclusion_box = QtWidgets.QVBoxLayout()
+        self.exclusion_box.setContentsMargins(0, 0, 0, 0)
+        self.exclusion_frame.setLayout(self.exclusion_box)
+
+        self.exclusion_table = FCTable()
+        self.exclusion_box.addWidget(self.exclusion_table)
+        self.exclusion_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+
+        self.exclusion_table.setColumnCount(4)
+        self.exclusion_table.setColumnWidth(0, 20)
+        self.exclusion_table.setHorizontalHeaderLabels(['#', _('Object'), _('Strategy'), _('Over Z')])
+
+        self.exclusion_table.horizontalHeaderItem(0).setToolTip(_("This is the Area ID."))
+        self.exclusion_table.horizontalHeaderItem(1).setToolTip(
+            _("Type of the object where the exclusion area was added."))
+        self.exclusion_table.horizontalHeaderItem(2).setToolTip(
+            _("The strategy used for exclusion area. Go around the exclusion areas or over it."))
+        self.exclusion_table.horizontalHeaderItem(3).setToolTip(
+            _("If the strategy is to go over the area then this is the height at which the tool will go to avoid the "
+              "exclusion area."))
+
+        self.exclusion_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+        grid_a1 = QtWidgets.QGridLayout()
+        grid_a1.setColumnStretch(0, 0)
+        grid_a1.setColumnStretch(1, 1)
+        self.exclusion_box.addLayout(grid_a1)
+
+        # Chose Strategy
+        self.strategy_label = FCLabel('%s:' % _("Strategy"))
+        self.strategy_label.setToolTip(_("The strategy followed when encountering an exclusion area.\n"
+                                         "Can be:\n"
+                                         "- Over -> when encountering the area, the tool will go to a set height\n"
+                                         "- Around -> will avoid the exclusion area by going around the area"))
+        self.strategy_radio = RadioSet([{'label': _('Over'), 'value': 'over'},
+                                        {'label': _('Around'), 'value': 'around'}])
+        self.strategy_radio.setObjectName("e_area_strategy")
+
+        grid_a1.addWidget(self.strategy_label, 1, 0)
+        grid_a1.addWidget(self.strategy_radio, 1, 1)
+
+        # Over Z
+        self.over_z_label = FCLabel('%s:' % _("Over Z"))
+        self.over_z_label.setToolTip(_("The height Z to which the tool will rise in order to avoid\n"
+                                       "an interdiction area."))
+        self.over_z_entry = FCDoubleSpinner()
+        self.over_z_entry.set_range(0.000, 9999.9999)
+        self.over_z_entry.set_precision(self.decimals)
+        self.over_z_entry.setObjectName("e_area_overz")
+
+        grid_a1.addWidget(self.over_z_label, 2, 0)
+        grid_a1.addWidget(self.over_z_entry, 2, 1)
+
+        # Button Add Area
+        self.add_area_button = QtWidgets.QPushButton(_('Add area:'))
+        self.add_area_button.setToolTip(_("Add an Exclusion Area."))
+
+        # Area Selection shape
+        self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
+                                          {'label': _("Polygon"), 'value': 'polygon'}])
+        self.area_shape_radio.setToolTip(
+            _("The kind of selection shape used for area selection.")
+        )
+        self.area_shape_radio.setObjectName("e_area_shape")
+
+        grid_a1.addWidget(self.add_area_button, 4, 0)
+        grid_a1.addWidget(self.area_shape_radio, 4, 1)
+
+        h_lay_1 = QtWidgets.QHBoxLayout()
+        self.exclusion_box.addLayout(h_lay_1)
+
+        # Button Delete All Areas
+        self.delete_area_button = QtWidgets.QPushButton(_('Delete All'))
+        self.delete_area_button.setToolTip(_("Delete all exclusion areas."))
+
+        # Button Delete Selected Areas
+        self.delete_sel_area_button = QtWidgets.QPushButton(_('Delete Selected'))
+        self.delete_sel_area_button.setToolTip(_("Delete all exclusion areas that are selected in the table."))
+
+        h_lay_1.addWidget(self.delete_area_button)
+        h_lay_1.addWidget(self.delete_sel_area_button)
+
+        self.ois_exclusion_exc = OptionalHideInputSection(self.exclusion_cb, [self.exclusion_frame])
+        # -------------------------- EXCLUSION AREAS END -------------------------------------------------------------
+        # ------------------------------------------------------------------------------------------------------------
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid3.addWidget(separator_line, 25, 0, 1, 2)
+
+        # #################################################################
+        # ################# GRID LAYOUT 6   ###############################
+        # #################################################################
+        self.grid4 = QtWidgets.QGridLayout()
+        self.grid4.setColumnStretch(0, 0)
+        self.grid4.setColumnStretch(1, 1)
+        self.tools_box.addLayout(self.grid4)
+
+        self.generate_cnc_button = QtWidgets.QPushButton(_('Generate CNCJob object'))
+        self.generate_cnc_button.setIcon(QtGui.QIcon(self.app.resource_location + '/cnc16.png'))
+        self.generate_cnc_button.setToolTip(
+            _("Generate the CNC Job.\n"
+              "If milling then an additional Geometry object will be created.\n"
+              "Add / Select at least one tool in the tool-table.\n"
+              "Click the # header to select all, or Ctrl + LMB\n"
+              "for custom selection of tools.")
+        )
+        self.generate_cnc_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.grid4.addWidget(self.generate_cnc_button, 3, 0, 1, 3)
+
+        self.tools_box.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.tools_box.addWidget(self.reset_button)
+        # ############################ FINSIHED GUI ###################################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+
+def distance(pt1, pt2):
+    return np.sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
+
+
+def distance_euclidian(x1, y1, x2, y2):
+    return np.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)

+ 253 - 217
appTools/ToolEtchCompensation.py

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

+ 552 - 501
appTools/ToolExtractDrills.py

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

+ 449 - 412
appTools/ToolFiducials.py

@@ -5,7 +5,7 @@
 # MIT Licence                                              #
 # ##########################################################
 
-from PyQt5 import QtWidgets, QtCore
+from PyQt5 import QtWidgets, QtCore, QtGui
 
 from appTool import AppTool
 from appGUI.GUIElements import FCDoubleSpinner, RadioSet, EvalEntry, FCTable, FCComboBox
@@ -30,8 +30,6 @@ log = logging.getLogger('base')
 
 class ToolFiducials(AppTool):
 
-    toolName = _("Fiducials Tool")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
 
@@ -41,434 +39,153 @@ class ToolFiducials(AppTool):
         self.decimals = self.app.decimals
         self.units = ''
 
-        # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-        self.layout.addWidget(QtWidgets.QLabel(''))
-
-        self.points_label = QtWidgets.QLabel('<b>%s:</b>' % _('Fiducials Coordinates'))
-        self.points_label.setToolTip(
-            _("A table with the fiducial points coordinates,\n"
-              "in the format (x, y).")
-        )
-        self.layout.addWidget(self.points_label)
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = FidoUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
-        self.points_table = FCTable()
-        self.points_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        # Objects involved in Copper thieving
+        self.grb_object = None
+        self.sm_object = None
 
-        self.points_table.setColumnCount(3)
-        self.points_table.setHorizontalHeaderLabels(
-            [
-                '#',
-                _("Name"),
-                _("Coordinates"),
-            ]
-        )
-        self.points_table.setRowCount(3)
-        row = 0
-        flags = QtCore.Qt.ItemIsEnabled
+        self.copper_obj_set = set()
+        self.sm_obj_set = set()
 
-        # BOTTOM LEFT
-        id_item_1 = QtWidgets.QTableWidgetItem('%d' % 1)
-        id_item_1.setFlags(flags)
-        self.points_table.setItem(row, 0, id_item_1)  # Tool name/id
+        # store the flattened geometry here:
+        self.flat_geometry = []
 
-        self.bottom_left_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Bottom Left'))
-        self.bottom_left_coords_lbl.setFlags(flags)
-        self.points_table.setItem(row, 1, self.bottom_left_coords_lbl)
-        self.bottom_left_coords_entry = EvalEntry()
-        self.points_table.setCellWidget(row, 2, self.bottom_left_coords_entry)
-        row += 1
+        # Events ID
+        self.mr = None
+        self.mm = None
 
-        # TOP RIGHT
-        id_item_2 = QtWidgets.QTableWidgetItem('%d' % 2)
-        id_item_2.setFlags(flags)
-        self.points_table.setItem(row, 0, id_item_2)  # Tool name/id
+        # Mouse cursor positions
+        self.cursor_pos = (0, 0)
+        self.first_click = False
 
-        self.top_right_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Top Right'))
-        self.top_right_coords_lbl.setFlags(flags)
-        self.points_table.setItem(row, 1, self.top_right_coords_lbl)
-        self.top_right_coords_entry = EvalEntry()
-        self.points_table.setCellWidget(row, 2, self.top_right_coords_entry)
-        row += 1
+        self.mode_method = False
 
-        # Second Point
-        self.id_item_3 = QtWidgets.QTableWidgetItem('%d' % 3)
-        self.id_item_3.setFlags(flags)
-        self.points_table.setItem(row, 0, self.id_item_3)  # Tool name/id
+        # Tool properties
+        self.fid_dia = None
+        self.sm_opening_dia = None
 
-        self.sec_point_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Second Point'))
-        self.sec_point_coords_lbl.setFlags(flags)
-        self.points_table.setItem(row, 1, self.sec_point_coords_lbl)
-        self.sec_points_coords_entry = EvalEntry()
-        self.points_table.setCellWidget(row, 2, self.sec_points_coords_entry)
+        self.margin_val = None
+        self.sec_position = None
 
-        vertical_header = self.points_table.verticalHeader()
-        vertical_header.hide()
-        self.points_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+        self.grb_steps_per_circle = self.app.defaults["gerber_circle_steps"]
 
-        horizontal_header = self.points_table.horizontalHeader()
-        horizontal_header.setMinimumSectionSize(10)
-        horizontal_header.setDefaultSectionSize(70)
+        self.click_points = []
 
-        self.points_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
-        # for x in range(4):
-        #     self.points_table.resizeColumnToContents(x)
-        self.points_table.resizeColumnsToContents()
-        self.points_table.resizeRowsToContents()
+        # SIGNALS
+        self.ui.add_cfid_button.clicked.connect(self.add_fiducials)
+        self.ui.add_sm_opening_button.clicked.connect(self.add_soldermask_opening)
 
-        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(0, 20)
-        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
+        self.ui.fid_type_radio.activated_custom.connect(self.on_fiducial_type)
+        self.ui.pos_radio.activated_custom.connect(self.on_second_point)
+        self.ui.mode_radio.activated_custom.connect(self.on_method_change)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
 
-        self.points_table.setMinimumHeight(self.points_table.getHeight() + 2)
-        self.points_table.setMaximumHeight(self.points_table.getHeight() + 2)
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("ToolFiducials()")
 
-        # remove the frame on the QLineEdit childrens of the table
-        for row in range(self.points_table.rowCount()):
-            self.points_table.cellWidget(row, 2).setFrame(False)
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
 
-        self.layout.addWidget(self.points_table)
+        AppTool.run(self)
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        self.layout.addWidget(separator_line)
+        self.set_tool_ui()
 
-        # ## Grid Layout
-        grid_lay = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid_lay)
-        grid_lay.setColumnStretch(0, 0)
-        grid_lay.setColumnStretch(1, 1)
+        self.app.ui.notebook.setTabText(2, _("Fiducials Tool"))
 
-        self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
-        self.param_label.setToolTip(
-            _("Parameters used for this tool.")
-        )
-        grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='Alt+F', **kwargs)
 
-        # DIAMETER #
-        self.size_label = QtWidgets.QLabel('%s:' % _("Size"))
-        self.size_label.setToolTip(
-            _("This set the fiducial diameter if fiducial type is circular,\n"
-              "otherwise is the size of the fiducial.\n"
-              "The soldermask opening is double than that.")
-        )
-        self.fid_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.fid_size_entry.set_range(1.0000, 3.0000)
-        self.fid_size_entry.set_precision(self.decimals)
-        self.fid_size_entry.setWrapping(True)
-        self.fid_size_entry.setSingleStep(0.1)
+    def set_tool_ui(self):
+        self.units = self.app.defaults['units']
 
-        grid_lay.addWidget(self.size_label, 1, 0)
-        grid_lay.addWidget(self.fid_size_entry, 1, 1)
+        self.ui.fid_size_entry.set_value(self.app.defaults["tools_fiducials_dia"])
+        self.ui.margin_entry.set_value(float(self.app.defaults["tools_fiducials_margin"]))
+        self.ui.mode_radio.set_value(self.app.defaults["tools_fiducials_mode"])
+        self.ui.pos_radio.set_value(self.app.defaults["tools_fiducials_second_pos"])
+        self.ui.fid_type_radio.set_value(self.app.defaults["tools_fiducials_type"])
+        self.ui.line_thickness_entry.set_value(float(self.app.defaults["tools_fiducials_line_thickness"]))
 
-        # MARGIN #
-        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
-        self.margin_label.setToolTip(
-            _("Bounding box margin.")
-        )
-        self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.margin_entry.set_range(-9999.9999, 9999.9999)
-        self.margin_entry.set_precision(self.decimals)
-        self.margin_entry.setSingleStep(0.1)
+        self.click_points = []
+        self.ui.bottom_left_coords_entry.set_value('')
+        self.ui.top_right_coords_entry.set_value('')
+        self.ui.sec_points_coords_entry.set_value('')
 
-        grid_lay.addWidget(self.margin_label, 2, 0)
-        grid_lay.addWidget(self.margin_entry, 2, 1)
+        self.copper_obj_set = set()
+        self.sm_obj_set = set()
 
-        # Mode #
-        self.mode_radio = RadioSet([
-            {'label': _('Auto'), 'value': 'auto'},
-            {"label": _("Manual"), "value": "manual"}
-        ], stretch=False)
-        self.mode_label = QtWidgets.QLabel(_("Mode:"))
-        self.mode_label.setToolTip(
-            _("- 'Auto' - automatic placement of fiducials in the corners of the bounding box.\n "
-              "- 'Manual' - manual placement of fiducials.")
-        )
-        grid_lay.addWidget(self.mode_label, 3, 0)
-        grid_lay.addWidget(self.mode_radio, 3, 1)
+    def on_second_point(self, val):
+        if val == 'no':
+            self.ui.id_item_3.setFlags(QtCore.Qt.NoItemFlags)
+            self.ui.sec_point_coords_lbl.setFlags(QtCore.Qt.NoItemFlags)
+            self.ui.sec_points_coords_entry.setDisabled(True)
+        else:
+            self.ui.id_item_3.setFlags(QtCore.Qt.ItemIsEnabled)
+            self.ui.sec_point_coords_lbl.setFlags(QtCore.Qt.ItemIsEnabled)
+            self.ui.sec_points_coords_entry.setDisabled(False)
 
-        # Position for second fiducial #
-        self.pos_radio = RadioSet([
-            {'label': _('Up'), 'value': 'up'},
-            {"label": _("Down"), "value": "down"},
-            {"label": _("None"), "value": "no"}
-        ], stretch=False)
-        self.pos_label = QtWidgets.QLabel('%s:' % _("Second fiducial"))
-        self.pos_label.setToolTip(
-            _("The position for the second fiducial.\n"
-              "- 'Up' - the order is: bottom-left, top-left, top-right.\n"
-              "- 'Down' - the order is: bottom-left, bottom-right, top-right.\n"
-              "- 'None' - there is no second fiducial. The order is: bottom-left, top-right.")
-        )
-        grid_lay.addWidget(self.pos_label, 4, 0)
-        grid_lay.addWidget(self.pos_radio, 4, 1)
+    def on_method_change(self, val):
+        """
+        Make sure that on method change we disconnect the event handlers and reset the points storage
+        :param val: value of the Radio button which trigger this method
+        :return: None
+        """
+        if val == 'auto':
+            self.click_points = []
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line, 5, 0, 1, 2)
+            try:
+                self.disconnect_event_handlers()
+            except TypeError:
+                pass
 
-        # Fiducial type #
-        self.fid_type_radio = RadioSet([
-            {'label': _('Circular'), 'value': 'circular'},
-            {"label": _("Cross"), "value": "cross"},
-            {"label": _("Chess"), "value": "chess"}
-        ], stretch=False)
-        self.fid_type_label = QtWidgets.QLabel('%s:' % _("Fiducial Type"))
-        self.fid_type_label.setToolTip(
-            _("The type of fiducial.\n"
-              "- 'Circular' - this is the regular fiducial.\n"
-              "- 'Cross' - cross lines fiducial.\n"
-              "- 'Chess' - chess pattern fiducial.")
-        )
-        grid_lay.addWidget(self.fid_type_label, 6, 0)
-        grid_lay.addWidget(self.fid_type_radio, 6, 1)
+    def on_fiducial_type(self, val):
+        if val == 'cross':
+            self.ui.line_thickness_label.setDisabled(False)
+            self.ui.line_thickness_entry.setDisabled(False)
+        else:
+            self.ui.line_thickness_label.setDisabled(True)
+            self.ui.line_thickness_entry.setDisabled(True)
 
-        # Line Thickness #
-        self.line_thickness_label = QtWidgets.QLabel('%s:' % _("Line thickness"))
-        self.line_thickness_label.setToolTip(
-            _("Thickness of the line that makes the fiducial.")
-        )
-        self.line_thickness_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.line_thickness_entry.set_range(0.00001, 9999.9999)
-        self.line_thickness_entry.set_precision(self.decimals)
-        self.line_thickness_entry.setSingleStep(0.1)
+    def add_fiducials(self):
+        self.app.call_source = "fiducials_tool"
 
-        grid_lay.addWidget(self.line_thickness_label, 7, 0)
-        grid_lay.addWidget(self.line_thickness_entry, 7, 1)
+        self.mode_method = self.ui.mode_radio.get_value()
+        self.margin_val = self.ui.margin_entry.get_value()
+        self.sec_position = self.ui.pos_radio.get_value()
+        fid_type = self.ui.fid_type_radio.get_value()
 
-        separator_line_1 = QtWidgets.QFrame()
-        separator_line_1.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line_1.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line_1, 8, 0, 1, 2)
+        self.click_points = []
 
-        # Copper Gerber object
-        self.grb_object_combo = FCComboBox()
-        self.grb_object_combo.setModel(self.app.collection)
-        self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.grb_object_combo.is_last = True
-        self.grb_object_combo.obj_type = "Gerber"
-
-        self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
-        self.grbobj_label.setToolTip(
-            _("Gerber Object to which will be added a copper thieving.")
-        )
-
-        grid_lay.addWidget(self.grbobj_label, 9, 0, 1, 2)
-        grid_lay.addWidget(self.grb_object_combo, 10, 0, 1, 2)
-
-        # ## Insert Copper Fiducial
-        self.add_cfid_button = QtWidgets.QPushButton(_("Add Fiducial"))
-        self.add_cfid_button.setToolTip(
-            _("Will add a polygon on the copper layer to serve as fiducial.")
-        )
-        self.add_cfid_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        grid_lay.addWidget(self.add_cfid_button, 11, 0, 1, 2)
-
-        separator_line_2 = QtWidgets.QFrame()
-        separator_line_2.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid_lay.addWidget(separator_line_2, 12, 0, 1, 2)
-
-        # Soldermask Gerber object #
-        self.sm_object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Soldermask Gerber"))
-        self.sm_object_label.setToolTip(
-            _("The Soldermask Gerber object.")
-        )
-        self.sm_object_combo = FCComboBox()
-        self.sm_object_combo.setModel(self.app.collection)
-        self.sm_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.sm_object_combo.is_last = True
-        self.sm_object_combo.obj_type = "Gerber"
-
-        grid_lay.addWidget(self.sm_object_label, 13, 0, 1, 2)
-        grid_lay.addWidget(self.sm_object_combo, 14, 0, 1, 2)
-
-        # ## Insert Soldermask opening for Fiducial
-        self.add_sm_opening_button = QtWidgets.QPushButton(_("Add Soldermask Opening"))
-        self.add_sm_opening_button.setToolTip(
-            _("Will add a polygon on the soldermask layer\n"
-              "to serve as fiducial opening.\n"
-              "The diameter is always double of the diameter\n"
-              "for the copper fiducial.")
-        )
-        self.add_sm_opening_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        grid_lay.addWidget(self.add_sm_opening_button, 15, 0, 1, 2)
-
-        self.layout.addStretch()
-
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
-
-        # Objects involved in Copper thieving
-        self.grb_object = None
-        self.sm_object = None
-
-        self.copper_obj_set = set()
-        self.sm_obj_set = set()
-
-        # store the flattened geometry here:
-        self.flat_geometry = []
-
-        # Events ID
-        self.mr = None
-        self.mm = None
-
-        # Mouse cursor positions
-        self.cursor_pos = (0, 0)
-        self.first_click = False
-
-        self.mode_method = False
-
-        # Tool properties
-        self.fid_dia = None
-        self.sm_opening_dia = None
-
-        self.margin_val = None
-        self.sec_position = None
-
-        self.grb_steps_per_circle = self.app.defaults["gerber_circle_steps"]
-
-        self.click_points = []
-
-        # SIGNALS
-        self.add_cfid_button.clicked.connect(self.add_fiducials)
-        self.add_sm_opening_button.clicked.connect(self.add_soldermask_opening)
-
-        self.fid_type_radio.activated_custom.connect(self.on_fiducial_type)
-        self.pos_radio.activated_custom.connect(self.on_second_point)
-        self.mode_radio.activated_custom.connect(self.on_method_change)
-        self.reset_button.clicked.connect(self.set_tool_ui)
-
-    def run(self, toggle=True):
-        self.app.defaults.report_usage("ToolFiducials()")
-
-        if toggle:
-            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-            else:
-                try:
-                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        # if tab is populated with the tool but it does not have the focus, focus on it
-                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
-                            # focus on Tool Tab
-                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-                        else:
-                            self.app.ui.splitter.setSizes([0, 1])
-                except AttributeError:
-                    pass
-        else:
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-
-        AppTool.run(self)
-
-        self.set_tool_ui()
-
-        self.app.ui.notebook.setTabText(2, _("Fiducials Tool"))
-
-    def install(self, icon=None, separator=None, **kwargs):
-        AppTool.install(self, icon, separator, shortcut='Alt+F', **kwargs)
-
-    def set_tool_ui(self):
-        self.units = self.app.defaults['units']
-        self.fid_size_entry.set_value(self.app.defaults["tools_fiducials_dia"])
-        self.margin_entry.set_value(float(self.app.defaults["tools_fiducials_margin"]))
-        self.mode_radio.set_value(self.app.defaults["tools_fiducials_mode"])
-        self.pos_radio.set_value(self.app.defaults["tools_fiducials_second_pos"])
-        self.fid_type_radio.set_value(self.app.defaults["tools_fiducials_type"])
-        self.line_thickness_entry.set_value(float(self.app.defaults["tools_fiducials_line_thickness"]))
-
-        self.click_points = []
-        self.bottom_left_coords_entry.set_value('')
-        self.top_right_coords_entry.set_value('')
-        self.sec_points_coords_entry.set_value('')
-
-        self.copper_obj_set = set()
-        self.sm_obj_set = set()
-
-    def on_second_point(self, val):
-        if val == 'no':
-            self.id_item_3.setFlags(QtCore.Qt.NoItemFlags)
-            self.sec_point_coords_lbl.setFlags(QtCore.Qt.NoItemFlags)
-            self.sec_points_coords_entry.setDisabled(True)
-        else:
-            self.id_item_3.setFlags(QtCore.Qt.ItemIsEnabled)
-            self.sec_point_coords_lbl.setFlags(QtCore.Qt.ItemIsEnabled)
-            self.sec_points_coords_entry.setDisabled(False)
-
-    def on_method_change(self, val):
-        """
-        Make sure that on method change we disconnect the event handlers and reset the points storage
-        :param val: value of the Radio button which trigger this method
-        :return: None
-        """
-        if val == 'auto':
-            self.click_points = []
-
-            try:
-                self.disconnect_event_handlers()
-            except TypeError:
-                pass
-
-    def on_fiducial_type(self, val):
-        if val == 'cross':
-            self.line_thickness_label.setDisabled(False)
-            self.line_thickness_entry.setDisabled(False)
-        else:
-            self.line_thickness_label.setDisabled(True)
-            self.line_thickness_entry.setDisabled(True)
-
-    def add_fiducials(self):
-        self.app.call_source = "fiducials_tool"
-        self.mode_method = self.mode_radio.get_value()
-        self.margin_val = self.margin_entry.get_value()
-        self.sec_position = self.pos_radio.get_value()
-        fid_type = self.fid_type_radio.get_value()
-
-        self.click_points = []
-
-        # get the Gerber object on which the Fiducial will be inserted
-        selection_index = self.grb_object_combo.currentIndex()
-        model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
+        # get the Gerber object on which the Fiducial will be inserted
+        selection_index = self.ui.grb_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.ui.grb_object_combo.rootModelIndex())
 
         try:
             self.grb_object = model_index.internalPointer().obj
         except Exception as e:
             log.debug("ToolFiducials.execute() --> %s" % str(e))
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
-            return 'fail'
+            return
 
         self.copper_obj_set.add(self.grb_object.options['name'])
 
@@ -484,7 +201,7 @@ class ToolFiducials(AppTool):
                     float('%.*f' % (self.decimals, y0))
                 )
             )
-            self.bottom_left_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x0, self.decimals, y0))
+            self.ui.bottom_left_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x0, self.decimals, y0))
 
             self.click_points.append(
                 (
@@ -492,7 +209,7 @@ class ToolFiducials(AppTool):
                     float('%.*f' % (self.decimals, y1))
                 )
             )
-            self.top_right_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x1, self.decimals, y1))
+            self.ui.top_right_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x1, self.decimals, y1))
 
             if self.sec_position == 'up':
                 self.click_points.append(
@@ -501,7 +218,7 @@ class ToolFiducials(AppTool):
                         float('%.*f' % (self.decimals, y1))
                     )
                 )
-                self.sec_points_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x0, self.decimals, y1))
+                self.ui.sec_points_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x0, self.decimals, y1))
             elif self.sec_position == 'down':
                 self.click_points.append(
                     (
@@ -509,7 +226,7 @@ class ToolFiducials(AppTool):
                         float('%.*f' % (self.decimals, y0))
                     )
                 )
-                self.sec_points_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x1, self.decimals, y0))
+                self.ui.sec_points_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x1, self.decimals, y0))
 
             self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type)
             self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'],
@@ -518,9 +235,9 @@ class ToolFiducials(AppTool):
             self.on_exit()
         else:
             self.app.inform.emit(_("Click to add first Fiducial. Bottom Left..."))
-            self.bottom_left_coords_entry.set_value('')
-            self.top_right_coords_entry.set_value('')
-            self.sec_points_coords_entry.set_value('')
+            self.ui.bottom_left_coords_entry.set_value('')
+            self.ui.top_right_coords_entry.set_value('')
+            self.ui.sec_points_coords_entry.set_value('')
 
             self.connect_event_handlers()
 
@@ -536,9 +253,9 @@ class ToolFiducials(AppTool):
         :param line_size: the line thickenss when the fiducial type is cross
         :return:
         """
-        fid_size = self.fid_size_entry.get_value() if fid_size is None else fid_size
+        fid_size = self.ui.fid_size_entry.get_value() if fid_size is None else fid_size
         fid_type = 'circular' if fid_type is None else fid_type
-        line_thickness = self.line_thickness_entry.get_value() if line_size is None else line_size
+        line_thickness = self.ui.line_thickness_entry.get_value() if line_size is None else line_size
 
         radius = fid_size / 2.0
 
@@ -734,18 +451,18 @@ class ToolFiducials(AppTool):
             g_obj.solid_geometry = MultiPolygon(s_list)
 
     def add_soldermask_opening(self):
-        sm_opening_dia = self.fid_size_entry.get_value() * 2.0
+        sm_opening_dia = self.ui.fid_size_entry.get_value() * 2.0
 
         # get the Gerber object on which the Fiducial will be inserted
-        selection_index = self.sm_object_combo.currentIndex()
-        model_index = self.app.collection.index(selection_index, 0, self.sm_object_combo.rootModelIndex())
+        selection_index = self.ui.sm_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.ui.sm_object_combo.rootModelIndex())
 
         try:
             self.sm_object = model_index.internalPointer().obj
         except Exception as e:
             log.debug("ToolFiducials.add_soldermask_opening() --> %s" % str(e))
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
-            return 'fail'
+            return
 
         self.sm_obj_set.add(self.sm_object.options['name'])
         self.add_fiducials_geo(self.click_points, g_obj=self.sm_object, fid_size=sm_opening_dia, fid_type='circular')
@@ -780,15 +497,15 @@ class ToolFiducials(AppTool):
         fid_type = self.fid_type_radio.get_value()
 
         if len(self.click_points) == 1:
-            self.bottom_left_coords_entry.set_value(self.click_points[0])
+            self.ui.bottom_left_coords_entry.set_value(self.click_points[0])
             self.app.inform.emit(_("Click to add the last fiducial. Top Right..."))
 
         if self.sec_position != 'no':
             if len(self.click_points) == 2:
-                self.top_right_coords_entry.set_value(self.click_points[1])
+                self.ui.top_right_coords_entry.set_value(self.click_points[1])
                 self.app.inform.emit(_("Click to add the second fiducial. Top Left or Bottom Right..."))
             elif len(self.click_points) == 3:
-                self.sec_points_coords_entry.set_value(self.click_points[2])
+                self.ui.sec_points_coords_entry.set_value(self.click_points[2])
                 self.app.inform.emit('[success] %s' % _("Done. All fiducials have been added."))
                 self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type)
                 self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'],
@@ -797,7 +514,7 @@ class ToolFiducials(AppTool):
                 self.on_exit()
         else:
             if len(self.click_points) == 2:
-                self.top_right_coords_entry.set_value(self.click_points[1])
+                self.ui.top_right_coords_entry.set_value(self.click_points[1])
                 self.app.inform.emit('[success] %s' % _("Done. All fiducials have been added."))
                 self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type)
                 self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'],
@@ -924,3 +641,323 @@ class ToolFiducials(AppTool):
             self.flat_geometry.append(geometry)
 
         return self.flat_geometry
+
+
+class FidoUI:
+
+    toolName = _("Fiducials Tool")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(QtWidgets.QLabel(""))
+
+        self.points_label = QtWidgets.QLabel('<b>%s:</b>' % _('Fiducials Coordinates'))
+        self.points_label.setToolTip(
+            _("A table with the fiducial points coordinates,\n"
+              "in the format (x, y).")
+        )
+        self.layout.addWidget(self.points_label)
+
+        self.points_table = FCTable()
+        self.points_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+        self.points_table.setColumnCount(3)
+        self.points_table.setHorizontalHeaderLabels(
+            [
+                '#',
+                _("Name"),
+                _("Coordinates"),
+            ]
+        )
+        self.points_table.setRowCount(3)
+        row = 0
+        flags = QtCore.Qt.ItemIsEnabled
+
+        # BOTTOM LEFT
+        id_item_1 = QtWidgets.QTableWidgetItem('%d' % 1)
+        id_item_1.setFlags(flags)
+        self.points_table.setItem(row, 0, id_item_1)  # Tool name/id
+
+        self.bottom_left_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Bottom Left'))
+        self.bottom_left_coords_lbl.setFlags(flags)
+        self.points_table.setItem(row, 1, self.bottom_left_coords_lbl)
+        self.bottom_left_coords_entry = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.bottom_left_coords_entry)
+        row += 1
+
+        # TOP RIGHT
+        id_item_2 = QtWidgets.QTableWidgetItem('%d' % 2)
+        id_item_2.setFlags(flags)
+        self.points_table.setItem(row, 0, id_item_2)  # Tool name/id
+
+        self.top_right_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Top Right'))
+        self.top_right_coords_lbl.setFlags(flags)
+        self.points_table.setItem(row, 1, self.top_right_coords_lbl)
+        self.top_right_coords_entry = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.top_right_coords_entry)
+        row += 1
+
+        # Second Point
+        self.id_item_3 = QtWidgets.QTableWidgetItem('%d' % 3)
+        self.id_item_3.setFlags(flags)
+        self.points_table.setItem(row, 0, self.id_item_3)  # Tool name/id
+
+        self.sec_point_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Second Point'))
+        self.sec_point_coords_lbl.setFlags(flags)
+        self.points_table.setItem(row, 1, self.sec_point_coords_lbl)
+        self.sec_points_coords_entry = EvalEntry()
+        self.points_table.setCellWidget(row, 2, self.sec_points_coords_entry)
+
+        vertical_header = self.points_table.verticalHeader()
+        vertical_header.hide()
+        self.points_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.points_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
+
+        self.points_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+        # for x in range(4):
+        #     self.points_table.resizeColumnToContents(x)
+        self.points_table.resizeColumnsToContents()
+        self.points_table.resizeRowsToContents()
+
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
+
+        self.points_table.setMinimumHeight(self.points_table.getHeight() + 2)
+        self.points_table.setMaximumHeight(self.points_table.getHeight() + 2)
+
+        # remove the frame on the QLineEdit childrens of the table
+        for row in range(self.points_table.rowCount()):
+            self.points_table.cellWidget(row, 2).setFrame(False)
+
+        self.layout.addWidget(self.points_table)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.layout.addWidget(separator_line)
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+
+        self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
+        self.param_label.setToolTip(
+            _("Parameters used for this tool.")
+        )
+        grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
+
+        # DIAMETER #
+        self.size_label = QtWidgets.QLabel('%s:' % _("Size"))
+        self.size_label.setToolTip(
+            _("This set the fiducial diameter if fiducial type is circular,\n"
+              "otherwise is the size of the fiducial.\n"
+              "The soldermask opening is double than that.")
+        )
+        self.fid_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.fid_size_entry.set_range(1.0000, 3.0000)
+        self.fid_size_entry.set_precision(self.decimals)
+        self.fid_size_entry.setWrapping(True)
+        self.fid_size_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.size_label, 1, 0)
+        grid_lay.addWidget(self.fid_size_entry, 1, 1)
+
+        # MARGIN #
+        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
+        self.margin_label.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.margin_entry.set_range(-9999.9999, 9999.9999)
+        self.margin_entry.set_precision(self.decimals)
+        self.margin_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.margin_label, 2, 0)
+        grid_lay.addWidget(self.margin_entry, 2, 1)
+
+        # Mode #
+        self.mode_radio = RadioSet([
+            {'label': _('Auto'), 'value': 'auto'},
+            {"label": _("Manual"), "value": "manual"}
+        ], stretch=False)
+        self.mode_label = QtWidgets.QLabel(_("Mode:"))
+        self.mode_label.setToolTip(
+            _("- 'Auto' - automatic placement of fiducials in the corners of the bounding box.\n "
+              "- 'Manual' - manual placement of fiducials.")
+        )
+        grid_lay.addWidget(self.mode_label, 3, 0)
+        grid_lay.addWidget(self.mode_radio, 3, 1)
+
+        # Position for second fiducial #
+        self.pos_radio = RadioSet([
+            {'label': _('Up'), 'value': 'up'},
+            {"label": _("Down"), "value": "down"},
+            {"label": _("None"), "value": "no"}
+        ], stretch=False)
+        self.pos_label = QtWidgets.QLabel('%s:' % _("Second fiducial"))
+        self.pos_label.setToolTip(
+            _("The position for the second fiducial.\n"
+              "- 'Up' - the order is: bottom-left, top-left, top-right.\n"
+              "- 'Down' - the order is: bottom-left, bottom-right, top-right.\n"
+              "- 'None' - there is no second fiducial. The order is: bottom-left, top-right.")
+        )
+        grid_lay.addWidget(self.pos_label, 4, 0)
+        grid_lay.addWidget(self.pos_radio, 4, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 5, 0, 1, 2)
+
+        # Fiducial type #
+        self.fid_type_radio = RadioSet([
+            {'label': _('Circular'), 'value': 'circular'},
+            {"label": _("Cross"), "value": "cross"},
+            {"label": _("Chess"), "value": "chess"}
+        ], stretch=False)
+        self.fid_type_label = QtWidgets.QLabel('%s:' % _("Fiducial Type"))
+        self.fid_type_label.setToolTip(
+            _("The type of fiducial.\n"
+              "- 'Circular' - this is the regular fiducial.\n"
+              "- 'Cross' - cross lines fiducial.\n"
+              "- 'Chess' - chess pattern fiducial.")
+        )
+        grid_lay.addWidget(self.fid_type_label, 6, 0)
+        grid_lay.addWidget(self.fid_type_radio, 6, 1)
+
+        # Line Thickness #
+        self.line_thickness_label = QtWidgets.QLabel('%s:' % _("Line thickness"))
+        self.line_thickness_label.setToolTip(
+            _("Thickness of the line that makes the fiducial.")
+        )
+        self.line_thickness_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.line_thickness_entry.set_range(0.00001, 9999.9999)
+        self.line_thickness_entry.set_precision(self.decimals)
+        self.line_thickness_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.line_thickness_label, 7, 0)
+        grid_lay.addWidget(self.line_thickness_entry, 7, 1)
+
+        separator_line_1 = QtWidgets.QFrame()
+        separator_line_1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line_1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line_1, 8, 0, 1, 2)
+
+        # Copper Gerber object
+        self.grb_object_combo = FCComboBox()
+        self.grb_object_combo.setModel(self.app.collection)
+        self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.grb_object_combo.is_last = True
+        self.grb_object_combo.obj_type = "Gerber"
+
+        self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
+        self.grbobj_label.setToolTip(
+            _("Gerber Object to which will be added a copper thieving.")
+        )
+
+        grid_lay.addWidget(self.grbobj_label, 9, 0, 1, 2)
+        grid_lay.addWidget(self.grb_object_combo, 10, 0, 1, 2)
+
+        # ## Insert Copper Fiducial
+        self.add_cfid_button = QtWidgets.QPushButton(_("Add Fiducial"))
+        self.add_cfid_button.setToolTip(
+            _("Will add a polygon on the copper layer to serve as fiducial.")
+        )
+        self.add_cfid_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        grid_lay.addWidget(self.add_cfid_button, 11, 0, 1, 2)
+
+        separator_line_2 = QtWidgets.QFrame()
+        separator_line_2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line_2, 12, 0, 1, 2)
+
+        # Soldermask Gerber object #
+        self.sm_object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Soldermask Gerber"))
+        self.sm_object_label.setToolTip(
+            _("The Soldermask Gerber object.")
+        )
+        self.sm_object_combo = FCComboBox()
+        self.sm_object_combo.setModel(self.app.collection)
+        self.sm_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.sm_object_combo.is_last = True
+        self.sm_object_combo.obj_type = "Gerber"
+
+        grid_lay.addWidget(self.sm_object_label, 13, 0, 1, 2)
+        grid_lay.addWidget(self.sm_object_combo, 14, 0, 1, 2)
+
+        # ## Insert Soldermask opening for Fiducial
+        self.add_sm_opening_button = QtWidgets.QPushButton(_("Add Soldermask Opening"))
+        self.add_sm_opening_button.setToolTip(
+            _("Will add a polygon on the soldermask layer\n"
+              "to serve as fiducial opening.\n"
+              "The diameter is always double of the diameter\n"
+              "for the copper fiducial.")
+        )
+        self.add_sm_opening_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        grid_lay.addWidget(self.add_sm_opening_button, 15, 0, 1, 2)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.reset_button)
+
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 1064 - 1032
appTools/ToolFilm.py

@@ -5,11 +5,11 @@
 # MIT Licence                                              #
 # ##########################################################
 
-from PyQt5 import QtCore, QtWidgets
+from PyQt5 import QtCore, QtWidgets, QtGui
 
 from appTool import AppTool
 from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \
-    OptionalHideInputSection, FCComboBox, FCFileSaveDialog
+    OptionalHideInputSection, FCComboBox, FCFileSaveDialog, FCButton, FCLabel
 
 from copy import deepcopy
 import logging
@@ -39,1225 +39,1257 @@ log = logging.getLogger('base')
 
 class Film(AppTool):
 
-    toolName = _("Film PCB")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
 
         self.decimals = self.app.decimals
+        self.units = self.app.defaults['units']
 
-        # Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-
-        # Form Layout
-        grid0 = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid0)
-
-        grid0.setColumnStretch(0, 0)
-        grid0.setColumnStretch(1, 1)
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = FilmUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
-        # Type of object for which to create the film
-        self.tf_type_obj_combo = RadioSet([{'label': _('Gerber'), 'value': 'grb'},
-                                           {'label': _('Geometry'), 'value': 'geo'}])
+        # ## Signals
+        self.ui.film_object_button.clicked.connect(self.on_film_creation)
+        self.ui.tf_type_obj_combo.activated_custom.connect(self.on_type_obj_index_changed)
+        self.ui.tf_type_box_combo.activated_custom.connect(self.on_type_box_index_changed)
 
-        self.tf_type_obj_combo_label = QtWidgets.QLabel('<b>%s</b>:' % _("Object"))
-        self.tf_type_obj_combo_label.setToolTip(
-            _("Specify the type of object for which to create the film.\n"
-              "The object can be of type: Gerber or Geometry.\n"
-              "The selection here decide the type of objects that will be\n"
-              "in the Film Object combobox.")
-        )
-        grid0.addWidget(self.tf_type_obj_combo_label, 0, 0)
-        grid0.addWidget(self.tf_type_obj_combo, 0, 1)
+        self.ui.film_type.activated_custom.connect(self.ui.on_film_type)
+        self.ui.source_punch.activated_custom.connect(self.ui.on_punch_source)
+        self.ui.file_type_radio.activated_custom.connect(self.ui.on_file_type)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
 
-        # List of objects for which we can create the film
-        self.tf_object_combo = FCComboBox()
-        self.tf_object_combo.setModel(self.app.collection)
-        self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.tf_object_combo.is_last = True
+    def on_type_obj_index_changed(self, val):
+        obj_type = 2 if val == 'geo' else 0
+        self.ui.tf_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.ui.tf_object_combo.setCurrentIndex(0)
+        self.ui.tf_object_combo.obj_type = {
+            "grb": "gerber", "geo": "geometry"
+        }[self.ui.tf_type_obj_combo.get_value()]
 
-        grid0.addWidget(self.tf_object_combo, 1, 0, 1, 2)
+    def on_type_box_index_changed(self, val):
+        obj_type = 2 if val == 'geo' else 0
+        self.ui.tf_box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.ui.tf_box_combo.setCurrentIndex(0)
+        self.ui.tf_box_combo.obj_type = {
+            "grb": "gerber", "geo": "geometry"
+        }[self.ui.tf_type_obj_combo.get_value()]
 
-        # Type of Box Object to be used as an envelope for film creation
-        # Within this we can create negative
-        self.tf_type_box_combo = RadioSet([{'label': _('Gerber'), 'value': 'grb'},
-                                           {'label': _('Geometry'), 'value': 'geo'}])
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("ToolFilm()")
 
-        self.tf_type_box_combo_label = QtWidgets.QLabel(_("Box Type:"))
-        self.tf_type_box_combo_label.setToolTip(
-            _("Specify the type of object to be used as an container for\n"
-              "film creation. It can be: Gerber or Geometry type."
-              "The selection here decide the type of objects that will be\n"
-              "in the Box Object combobox.")
-        )
-        grid0.addWidget(self.tf_type_box_combo_label, 2, 0)
-        grid0.addWidget(self.tf_type_box_combo, 2, 1)
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
 
-        # Box
-        self.tf_box_combo = FCComboBox()
-        self.tf_box_combo.setModel(self.app.collection)
-        self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.tf_box_combo.is_last = True
+        AppTool.run(self)
 
-        grid0.addWidget(self.tf_box_combo, 3, 0, 1, 2)
+        self.set_tool_ui()
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 4, 0, 1, 2)
+        self.app.ui.notebook.setTabText(2, _("Film Tool"))
 
-        self.film_adj_label = QtWidgets.QLabel('<b>%s</b>' % _("Film Adjustments"))
-        self.film_adj_label.setToolTip(
-            _("Sometime the printers will distort the print shape, especially the Laser types.\n"
-              "This section provide the tools to compensate for the print distortions.")
-        )
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='Alt+L', **kwargs)
 
-        grid0.addWidget(self.film_adj_label, 5, 0, 1, 2)
+    def set_tool_ui(self):
+        self.reset_fields()
 
-        # Scale Geometry
-        self.film_scale_cb = FCCheckBox('%s' % _("Scale Film geometry"))
-        self.film_scale_cb.setToolTip(
-            _("A value greater than 1 will stretch the film\n"
-              "while a value less than 1 will jolt it.")
-        )
-        self.film_scale_cb.setStyleSheet(
-            """
-            QCheckBox {font-weight: bold; color: black}
-            """
-        )
-        grid0.addWidget(self.film_scale_cb, 6, 0, 1, 2)
+        f_type = self.app.defaults["tools_film_type"] if self.app.defaults["tools_film_type"] else 'neg'
+        self.ui.film_type.set_value(str(f_type))
+        self.ui.on_film_type(val=f_type)
 
-        self.film_scalex_label = QtWidgets.QLabel('%s:' % _("X factor"))
-        self.film_scalex_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.film_scalex_entry.set_range(-999.9999, 999.9999)
-        self.film_scalex_entry.set_precision(self.decimals)
-        self.film_scalex_entry.setSingleStep(0.01)
+        b_entry = self.app.defaults["tools_film_boundary"] if self.app.defaults["tools_film_boundary"] else 0.0
+        self.ui.boundary_entry.set_value(float(b_entry))
 
-        grid0.addWidget(self.film_scalex_label, 7, 0)
-        grid0.addWidget(self.film_scalex_entry, 7, 1)
+        scale_stroke_width = self.app.defaults["tools_film_scale_stroke"] if \
+            self.app.defaults["tools_film_scale_stroke"] else 0.0
+        self.ui.film_scale_stroke_entry.set_value(int(scale_stroke_width))
+
+        self.ui.punch_cb.set_value(False)
+        self.ui.source_punch.set_value('exc')
+
+        self.ui.film_scale_cb.set_value(self.app.defaults["tools_film_scale_cb"])
+        self.ui.film_scalex_entry.set_value(float(self.app.defaults["tools_film_scale_x_entry"]))
+        self.ui.film_scaley_entry.set_value(float(self.app.defaults["tools_film_scale_y_entry"]))
+        self.ui.film_skew_cb.set_value(self.app.defaults["tools_film_skew_cb"])
+        self.ui.film_skewx_entry.set_value(float(self.app.defaults["tools_film_skew_x_entry"]))
+        self.ui.film_skewy_entry.set_value(float(self.app.defaults["tools_film_skew_y_entry"]))
+        self.ui.film_skew_reference.set_value(self.app.defaults["tools_film_skew_ref_radio"])
+        self.ui.film_mirror_cb.set_value(self.app.defaults["tools_film_mirror_cb"])
+        self.ui.film_mirror_axis.set_value(self.app.defaults["tools_film_mirror_axis_radio"])
+        self.ui.file_type_radio.set_value(self.app.defaults["tools_film_file_type_radio"])
+        self.ui.orientation_radio.set_value(self.app.defaults["tools_film_orientation"])
+        self.ui.pagesize_combo.set_value(self.app.defaults["tools_film_pagesize"])
+
+        self.ui.tf_type_obj_combo.set_value('grb')
+        self.ui.tf_type_box_combo.set_value('grb')
+        # run once to update the obj_type attribute in the FCCombobox so the last object is showed in cb
+        self.on_type_obj_index_changed(val='grb')
+        self.on_type_box_index_changed(val='grb')
 
-        self.film_scaley_label = QtWidgets.QLabel('%s:' % _("Y factor"))
-        self.film_scaley_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.film_scaley_entry.set_range(-999.9999, 999.9999)
-        self.film_scaley_entry.set_precision(self.decimals)
-        self.film_scaley_entry.setSingleStep(0.01)
+    def on_film_creation(self):
+        log.debug("ToolFilm.Film.on_film_creation() started ...")
 
-        grid0.addWidget(self.film_scaley_label, 8, 0)
-        grid0.addWidget(self.film_scaley_entry, 8, 1)
+        try:
+            name = self.ui.tf_object_combo.currentText()
+        except Exception:
+            self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                 _("No FlatCAM object selected. Load an object for Film and retry."))
+            return
 
-        self.ois_scale = OptionalHideInputSection(self.film_scale_cb,
-                                                  [
-                                                      self.film_scalex_label,
-                                                      self.film_scalex_entry,
-                                                      self.film_scaley_label,
-                                                      self.film_scaley_entry
-                                                  ])
+        try:
+            boxname = self.ui.tf_box_combo.currentText()
+        except Exception:
+            self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                 _("No FlatCAM object selected. Load an object for Box and retry."))
+            return
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 9, 0, 1, 2)
+        if name == '' or boxname == '':
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No FlatCAM object selected."))
+            return
 
-        # Skew Geometry
-        self.film_skew_cb = FCCheckBox('%s' % _("Skew Film geometry"))
-        self.film_skew_cb.setToolTip(
-            _("Positive values will skew to the right\n"
-              "while negative values will skew to the left.")
-        )
-        self.film_skew_cb.setStyleSheet(
-            """
-            QCheckBox {font-weight: bold; color: black}
-            """
-        )
-        grid0.addWidget(self.film_skew_cb, 10, 0, 1, 2)
+        scale_stroke_width = float(self.ui.film_scale_stroke_entry.get_value())
+        source = self.ui.source_punch.get_value()
+        file_type = self.ui.file_type_radio.get_value()
 
-        self.film_skewx_label = QtWidgets.QLabel('%s:' % _("X angle"))
-        self.film_skewx_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.film_skewx_entry.set_range(-999.9999, 999.9999)
-        self.film_skewx_entry.set_precision(self.decimals)
-        self.film_skewx_entry.setSingleStep(0.01)
+        # #################################################################
+        # ################ STARTING THE JOB ###############################
+        # #################################################################
 
-        grid0.addWidget(self.film_skewx_label, 11, 0)
-        grid0.addWidget(self.film_skewx_entry, 11, 1)
+        self.app.inform.emit(_("Generating Film ..."))
 
-        self.film_skewy_label = QtWidgets.QLabel('%s:' % _("Y angle"))
-        self.film_skewy_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.film_skewy_entry.set_range(-999.9999, 999.9999)
-        self.film_skewy_entry.set_precision(self.decimals)
-        self.film_skewy_entry.setSingleStep(0.01)
+        if self.ui.film_type.get_value() == "pos":
 
-        grid0.addWidget(self.film_skewy_label, 12, 0)
-        grid0.addWidget(self.film_skewy_entry, 12, 1)
+            if self.ui.punch_cb.get_value() is False:
+                self.generate_positive_normal_film(name, boxname, factor=scale_stroke_width, ftype=file_type)
+            else:
+                self.generate_positive_punched_film(name, boxname, source, factor=scale_stroke_width, ftype=file_type)
+        else:
+            self.generate_negative_film(name, boxname, factor=scale_stroke_width, ftype=file_type)
 
-        self.film_skew_ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
-        self.film_skew_ref_label.setToolTip(
-            _("The reference point to be used as origin for the skew.\n"
-              "It can be one of the four points of the geometry bounding box.")
-        )
-        self.film_skew_reference = RadioSet([{'label': _('Bottom Left'), 'value': 'bottomleft'},
-                                             {'label': _('Top Left'), 'value': 'topleft'},
-                                             {'label': _('Bottom Right'), 'value': 'bottomright'},
-                                             {'label': _('Top right'), 'value': 'topright'}],
-                                            orientation='vertical',
-                                            stretch=False)
+    def generate_positive_normal_film(self, name, boxname, factor, ftype='svg'):
+        log.debug("ToolFilm.Film.generate_positive_normal_film() started ...")
 
-        grid0.addWidget(self.film_skew_ref_label, 13, 0)
-        grid0.addWidget(self.film_skew_reference, 13, 1)
+        scale_factor_x = None
+        scale_factor_y = None
+        skew_factor_x = None
+        skew_factor_y = None
+        mirror = None
+        skew_reference = 'center'
 
-        self.ois_skew = OptionalHideInputSection(self.film_skew_cb,
-                                                 [
-                                                     self.film_skewx_label,
-                                                     self.film_skewx_entry,
-                                                     self.film_skewy_label,
-                                                     self.film_skewy_entry,
-                                                     self.film_skew_ref_label,
-                                                     self.film_skew_reference
-                                                 ])
+        if self.ui.film_scale_cb.get_value():
+            if self.ui.film_scalex_entry.get_value() != 1.0:
+                scale_factor_x = self.ui.film_scalex_entry.get_value()
+            if self.ui.film_scaley_entry.get_value() != 1.0:
+                scale_factor_y = self.ui.film_scaley_entry.get_value()
+        if self.ui.film_skew_cb.get_value():
+            if self.ui.film_skewx_entry.get_value() != 0.0:
+                skew_factor_x = self.ui.film_skewx_entry.get_value()
+            if self.ui.film_skewy_entry.get_value() != 0.0:
+                skew_factor_y = self.ui.film_skewy_entry.get_value()
+
+            skew_reference = self.ui.film_skew_reference.get_value()
+        if self.ui.film_mirror_cb.get_value():
+            if self.ui.film_mirror_axis.get_value() != 'none':
+                mirror = self.ui.film_mirror_axis.get_value()
 
-        separator_line1 = QtWidgets.QFrame()
-        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line1, 14, 0, 1, 2)
+        if ftype == 'svg':
+            filter_ext = "SVG Files (*.SVG);;"\
+                         "All Files (*.*)"
+        elif ftype == 'png':
+            filter_ext = "PNG Files (*.PNG);;" \
+                         "All Files (*.*)"
+        else:
+            filter_ext = "PDF Files (*.PDF);;" \
+                         "All Files (*.*)"
 
-        # Mirror Geometry
-        self.film_mirror_cb = FCCheckBox('%s' % _("Mirror Film geometry"))
-        self.film_mirror_cb.setToolTip(
-            _("Mirror the film geometry on the selected axis or on both.")
-        )
-        self.film_mirror_cb.setStyleSheet(
-            """
-            QCheckBox {font-weight: bold; color: black}
-            """
-        )
-        grid0.addWidget(self.film_mirror_cb, 15, 0, 1, 2)
+        try:
+            filename, _f = FCFileSaveDialog.get_saved_filename(
+                caption=_("Export positive film"),
+                directory=self.app.get_last_save_folder() + '/' + name + '_film',
+                ext_filter=filter_ext)
+        except TypeError:
+            filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export positive film"))
 
-        self.film_mirror_axis = RadioSet([{'label': _('None'), 'value': 'none'},
-                                          {'label': _('X'), 'value': 'x'},
-                                          {'label': _('Y'), 'value': 'y'},
-                                          {'label': _('Both'), 'value': 'both'}],
-                                         stretch=False)
-        self.film_mirror_axis_label = QtWidgets.QLabel('%s:' % _("Mirror axis"))
+        filename = str(filename)
 
-        grid0.addWidget(self.film_mirror_axis_label, 16, 0)
-        grid0.addWidget(self.film_mirror_axis, 16, 1)
+        if str(filename) == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+            return
+        else:
+            pagesize = self.ui.pagesize_combo.get_value()
+            orientation = self.ui.orientation_radio.get_value()
+            color = self.app.defaults['tools_film_color']
 
-        self.ois_mirror = OptionalHideInputSection(self.film_mirror_cb,
-                                                   [
-                                                       self.film_mirror_axis_label,
-                                                       self.film_mirror_axis
-                                                   ])
-
-        separator_line2 = QtWidgets.QFrame()
-        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line2, 17, 0, 1, 2)
-
-        self.film_param_label = QtWidgets.QLabel('<b>%s</b>' % _("Film Parameters"))
-
-        grid0.addWidget(self.film_param_label, 18, 0, 1, 2)
-
-        # Scale Stroke size
-        self.film_scale_stroke_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.film_scale_stroke_entry.set_range(-999.9999, 999.9999)
-        self.film_scale_stroke_entry.setSingleStep(0.01)
-        self.film_scale_stroke_entry.set_precision(self.decimals)
+            self.export_positive(name, boxname, filename,
+                                 scale_stroke_factor=factor,
+                                 scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                 skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                 skew_reference=skew_reference,
+                                 mirror=mirror,
+                                 pagesize_val=pagesize, orientation_val=orientation, color_val=color, opacity_val=1.0,
+                                 ftype=ftype
+                                 )
 
-        self.film_scale_stroke_label = QtWidgets.QLabel('%s:' % _("Scale Stroke"))
-        self.film_scale_stroke_label.setToolTip(
-            _("Scale the line stroke thickness of each feature in the SVG file.\n"
-              "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
-              "therefore the fine features may be more affected by this parameter.")
-        )
-        grid0.addWidget(self.film_scale_stroke_label, 19, 0)
-        grid0.addWidget(self.film_scale_stroke_entry, 19, 1)
+    def generate_positive_punched_film(self, name, boxname, source, factor, ftype='svg'):
 
-        # Film Type
-        self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'},
-                                   {'label': _('Negative'), 'value': 'neg'}],
-                                  stretch=False)
-        self.film_type_label = QtWidgets.QLabel(_("Film Type:"))
-        self.film_type_label.setToolTip(
-            _("Generate a Positive black film or a Negative film.\n"
-              "Positive means that it will print the features\n"
-              "with black on a white canvas.\n"
-              "Negative means that it will print the features\n"
-              "with white on a black canvas.\n"
-              "The Film format is SVG.")
-        )
-        grid0.addWidget(self.film_type_label, 21, 0)
-        grid0.addWidget(self.film_type, 21, 1)
+        film_obj = self.app.collection.get_by_name(name)
 
-        # Boundary for negative film generation
-        self.boundary_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.boundary_entry.set_range(-999.9999, 999.9999)
-        self.boundary_entry.setSingleStep(0.01)
-        self.boundary_entry.set_precision(self.decimals)
+        if source == 'exc':
+            log.debug("ToolFilm.Film.generate_positive_punched_film() with Excellon source started ...")
 
-        self.boundary_label = QtWidgets.QLabel('%s:' % _("Border"))
-        self.boundary_label.setToolTip(
-            _("Specify a border around the object.\n"
-              "Only for negative film.\n"
-              "It helps if we use as a Box Object the same \n"
-              "object as in Film Object. It will create a thick\n"
-              "black bar around the actual print allowing for a\n"
-              "better delimitation of the outline features which are of\n"
-              "white color like the rest and which may confound with the\n"
-              "surroundings if not for this border.")
-        )
-        grid0.addWidget(self.boundary_label, 22, 0)
-        grid0.addWidget(self.boundary_entry, 22, 1)
+            try:
+                exc_name = self.ui.exc_combo.currentText()
+            except Exception:
+                self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                     _("No Excellon object selected. Load an object for punching reference and retry."))
+                return
 
-        self.boundary_label.hide()
-        self.boundary_entry.hide()
+            exc_obj = self.app.collection.get_by_name(exc_name)
+            exc_solid_geometry = MultiPolygon(exc_obj.solid_geometry)
+            punched_solid_geometry = MultiPolygon(film_obj.solid_geometry).difference(exc_solid_geometry)
 
-        # Punch Drill holes
-        self.punch_cb = FCCheckBox(_("Punch drill holes"))
-        self.punch_cb.setToolTip(_("When checked the generated film will have holes in pads when\n"
-                                   "the generated film is positive. This is done to help drilling,\n"
-                                   "when done manually."))
-        grid0.addWidget(self.punch_cb, 23, 0, 1, 2)
+            def init_func(new_obj, app_obj):
+                new_obj.solid_geometry = deepcopy(punched_solid_geometry)
 
-        # this way I can hide/show the frame
-        self.punch_frame = QtWidgets.QFrame()
-        self.punch_frame.setContentsMargins(0, 0, 0, 0)
-        self.layout.addWidget(self.punch_frame)
-        punch_grid = QtWidgets.QGridLayout()
-        punch_grid.setContentsMargins(0, 0, 0, 0)
-        self.punch_frame.setLayout(punch_grid)
+            outname = name + "_punched"
+            self.app.app_obj.new_object('gerber', outname, init_func)
 
-        punch_grid.setColumnStretch(0, 0)
-        punch_grid.setColumnStretch(1, 1)
+            self.generate_positive_normal_film(outname, boxname, factor=factor, ftype=ftype)
+        else:
+            log.debug("ToolFilm.Film.generate_positive_punched_film() with Pad center source started ...")
 
-        self.ois_p = OptionalHideInputSection(self.punch_cb, [self.punch_frame])
+            punch_size = float(self.ui.punch_size_spinner.get_value())
 
-        self.source_label = QtWidgets.QLabel('%s:' % _("Source"))
-        self.source_label.setToolTip(
-            _("The punch hole source can be:\n"
-              "- Excellon -> an Excellon holes center will serve as reference.\n"
-              "- Pad Center -> will try to use the pads center as reference.")
-        )
-        self.source_punch = RadioSet([{'label': _('Excellon'), 'value': 'exc'},
-                                      {'label': _('Pad center'), 'value': 'pad'}],
-                                     stretch=False)
-        punch_grid.addWidget(self.source_label, 0, 0)
-        punch_grid.addWidget(self.source_punch, 0, 1)
+            punching_geo = []
+            for apid in film_obj.apertures:
+                if film_obj.apertures[apid]['type'] == 'C':
+                    if punch_size >= float(film_obj.apertures[apid]['size']):
+                        self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                             _(" Could not generate punched hole film because the punch hole size"
+                                               "is bigger than some of the apertures in the Gerber object."))
+                        return 'fail'
+                    else:
+                        for elem in film_obj.apertures[apid]['geometry']:
+                            if 'follow' in elem:
+                                if isinstance(elem['follow'], Point):
+                                    punching_geo.append(elem['follow'].buffer(punch_size / 2))
+                else:
+                    if punch_size >= float(film_obj.apertures[apid]['width']) or \
+                            punch_size >= float(film_obj.apertures[apid]['height']):
+                        self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                             _("Could not generate punched hole film because the punch hole size"
+                                               "is bigger than some of the apertures in the Gerber object."))
+                        return 'fail'
+                    else:
+                        for elem in film_obj.apertures[apid]['geometry']:
+                            if 'follow' in elem:
+                                if isinstance(elem['follow'], Point):
+                                    punching_geo.append(elem['follow'].buffer(punch_size / 2))
 
-        self.exc_label = QtWidgets.QLabel('%s:' % _("Excellon Obj"))
-        self.exc_label.setToolTip(
-            _("Remove the geometry of Excellon from the Film to create the holes in pads.")
-        )
-        self.exc_combo = FCComboBox()
-        self.exc_combo.setModel(self.app.collection)
-        self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
-        self.exc_combo.is_last = True
-        self.exc_combo.obj_type = "Excellon"
+            punching_geo = MultiPolygon(punching_geo)
+            if not isinstance(film_obj.solid_geometry, Polygon):
+                temp_solid_geometry = MultiPolygon(film_obj.solid_geometry)
+            else:
+                temp_solid_geometry = film_obj.solid_geometry
+            punched_solid_geometry = temp_solid_geometry.difference(punching_geo)
 
-        punch_grid.addWidget(self.exc_label, 1, 0)
-        punch_grid.addWidget(self.exc_combo, 1, 1)
+            if punched_solid_geometry == temp_solid_geometry:
+                self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                     _("Could not generate punched hole film because the newly created object geometry "
+                                       "is the same as the one in the source object geometry..."))
+                return 'fail'
 
-        self.exc_label.hide()
-        self.exc_combo.hide()
+            def init_func(new_obj, app_obj):
+                new_obj.solid_geometry = deepcopy(punched_solid_geometry)
 
-        self.punch_size_label = QtWidgets.QLabel('%s:' % _("Punch Size"))
-        self.punch_size_label.setToolTip(_("The value here will control how big is the punch hole in the pads."))
-        self.punch_size_spinner = FCDoubleSpinner(callback=self.confirmation_message)
-        self.punch_size_spinner.set_range(0, 999.9999)
-        self.punch_size_spinner.setSingleStep(0.1)
-        self.punch_size_spinner.set_precision(self.decimals)
+            outname = name + "_punched"
+            self.app.app_obj.new_object('gerber', outname, init_func)
 
-        punch_grid.addWidget(self.punch_size_label, 2, 0)
-        punch_grid.addWidget(self.punch_size_spinner, 2, 1)
+            self.generate_positive_normal_film(outname, boxname, factor=factor, ftype=ftype)
 
-        self.punch_size_label.hide()
-        self.punch_size_spinner.hide()
+    def generate_negative_film(self, name, boxname, factor, ftype='svg'):
+        log.debug("ToolFilm.Film.generate_negative_film() started ...")
 
-        grid1 = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid1)
-        grid1.setColumnStretch(0, 0)
-        grid1.setColumnStretch(1, 1)
+        scale_factor_x = None
+        scale_factor_y = None
+        skew_factor_x = None
+        skew_factor_y = None
+        mirror = None
+        skew_reference = 'center'
 
-        separator_line3 = QtWidgets.QFrame()
-        separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid1.addWidget(separator_line3, 0, 0, 1, 2)
+        if self.ui.film_scale_cb.get_value():
+            if self.ui.film_scalex_entry.get_value() != 1.0:
+                scale_factor_x = self.ui.film_scalex_entry.get_value()
+            if self.ui.film_scaley_entry.get_value() != 1.0:
+                scale_factor_y = self.ui.film_scaley_entry.get_value()
+        if self.ui.film_skew_cb.get_value():
+            if self.ui.film_skewx_entry.get_value() != 0.0:
+                skew_factor_x = self.ui.film_skewx_entry.get_value()
+            if self.ui.film_skewy_entry.get_value() != 0.0:
+                skew_factor_y = self.ui.film_skewy_entry.get_value()
 
-        # File type
-        self.file_type_radio = RadioSet([{'label': _('SVG'), 'value': 'svg'},
-                                         {'label': _('PNG'), 'value': 'png'},
-                                         {'label': _('PDF'), 'value': 'pdf'}
-                                         ], stretch=False)
+            skew_reference = self.ui.film_skew_reference.get_value()
+        if self.ui.film_mirror_cb.get_value():
+            if self.ui.film_mirror_axis.get_value() != 'none':
+                mirror = self.ui.film_mirror_axis.get_value()
 
-        self.file_type_label = QtWidgets.QLabel(_("Film Type:"))
-        self.file_type_label.setToolTip(
-            _("The file type of the saved film. Can be:\n"
-              "- 'SVG' -> open-source vectorial format\n"
-              "- 'PNG' -> raster image\n"
-              "- 'PDF' -> portable document format")
-        )
-        grid1.addWidget(self.file_type_label, 1, 0)
-        grid1.addWidget(self.file_type_radio, 1, 1)
+        border = float(self.ui.boundary_entry.get_value())
 
-        # Page orientation
-        self.orientation_label = QtWidgets.QLabel('%s:' % _("Page Orientation"))
-        self.orientation_label.setToolTip(_("Can be:\n"
-                                            "- Portrait\n"
-                                            "- Landscape"))
+        if border is None:
+            border = 0
 
-        self.orientation_radio = RadioSet([{'label': _('Portrait'), 'value': 'p'},
-                                           {'label': _('Landscape'), 'value': 'l'},
-                                           ], stretch=False)
+        if ftype == 'svg':
+            filter_ext = "SVG Files (*.SVG);;"\
+                         "All Files (*.*)"
+        elif ftype == 'png':
+            filter_ext = "PNG Files (*.PNG);;" \
+                         "All Files (*.*)"
+        else:
+            filter_ext = "PDF Files (*.PDF);;" \
+                         "All Files (*.*)"
 
-        grid1.addWidget(self.orientation_label, 2, 0)
-        grid1.addWidget(self.orientation_radio, 2, 1)
+        try:
+            filename, _f = FCFileSaveDialog.get_saved_filename(
+                caption=_("Export negative film"),
+                directory=self.app.get_last_save_folder() + '/' + name + '_film',
+                ext_filter=filter_ext)
+        except TypeError:
+            filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export negative film"))
 
-        # Page Size
-        self.pagesize_label = QtWidgets.QLabel('%s:' % _("Page Size"))
-        self.pagesize_label.setToolTip(_("A selection of standard ISO 216 page sizes."))
+        filename = str(filename)
 
-        self.pagesize_combo = FCComboBox()
+        if str(filename) == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+            return
+        else:
+            self.export_negative(name, boxname, filename, border,
+                                 scale_stroke_factor=factor,
+                                 scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                 skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                 skew_reference=skew_reference,
+                                 mirror=mirror, ftype=ftype
+                                 )
 
-        self.pagesize = {}
-        self.pagesize.update(
-            {
-                'Bounds': None,
-                'A0': (841*mm, 1189*mm),
-                'A1': (594*mm, 841*mm),
-                'A2': (420*mm, 594*mm),
-                'A3': (297*mm, 420*mm),
-                'A4': (210*mm, 297*mm),
-                'A5': (148*mm, 210*mm),
-                'A6': (105*mm, 148*mm),
-                'A7': (74*mm, 105*mm),
-                'A8': (52*mm, 74*mm),
-                'A9': (37*mm, 52*mm),
-                'A10': (26*mm, 37*mm),
-
-                'B0': (1000*mm, 1414*mm),
-                'B1': (707*mm, 1000*mm),
-                'B2': (500*mm, 707*mm),
-                'B3': (353*mm, 500*mm),
-                'B4': (250*mm, 353*mm),
-                'B5': (176*mm, 250*mm),
-                'B6': (125*mm, 176*mm),
-                'B7': (88*mm, 125*mm),
-                'B8': (62*mm, 88*mm),
-                'B9': (44*mm, 62*mm),
-                'B10': (31*mm, 44*mm),
-
-                'C0': (917*mm, 1297*mm),
-                'C1': (648*mm, 917*mm),
-                'C2': (458*mm, 648*mm),
-                'C3': (324*mm, 458*mm),
-                'C4': (229*mm, 324*mm),
-                'C5': (162*mm, 229*mm),
-                'C6': (114*mm, 162*mm),
-                'C7': (81*mm, 114*mm),
-                'C8': (57*mm, 81*mm),
-                'C9': (40*mm, 57*mm),
-                'C10': (28*mm, 40*mm),
+    def export_negative(self, obj_name, box_name, filename, boundary,
+                        scale_stroke_factor=0.00,
+                        scale_factor_x=None, scale_factor_y=None,
+                        skew_factor_x=None, skew_factor_y=None, skew_reference='center',
+                        mirror=None,
+                        use_thread=True, ftype='svg'):
+        """
+        Exports a Geometry Object to an SVG file in negative.
 
-                # American paper sizes
-                'LETTER': (8.5*inch, 11*inch),
-                'LEGAL': (8.5*inch, 14*inch),
-                'ELEVENSEVENTEEN': (11*inch, 17*inch),
+        :param obj_name: the name of the FlatCAM object to be saved as SVG
+        :param box_name: the name of the FlatCAM object to be used as delimitation of the content to be saved
+        :param filename: Path to the SVG file to save to.
+        :param boundary: thickness of a black border to surround all the features
+        :param scale_stroke_factor: factor by which to change/scale the thickness of the features
+        :param scale_factor_x: factor to scale the svg geometry on the X axis
+        :param scale_factor_y: factor to scale the svg geometry on the Y axis
+        :param skew_factor_x: factor to skew the svg geometry on the X axis
+        :param skew_factor_y: factor to skew the svg geometry on the Y axis
+        :param skew_reference: reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft', 'topright' and
+        those are the 4 points of the bounding box of the geometry to be skewed.
+        :param mirror: can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
+        :param use_thread: if to be run in a separate thread; boolean
+        :param ftype: the type of file for saving the film: 'svg', 'png' or 'pdf'
+        :return:
+        """
+        self.app.defaults.report_usage("export_negative()")
 
-                # From https://en.wikipedia.org/wiki/Paper_size
-                'JUNIOR_LEGAL': (5*inch, 8*inch),
-                'HALF_LETTER': (5.5*inch, 8*inch),
-                'GOV_LETTER': (8*inch, 10.5*inch),
-                'GOV_LEGAL': (8.5*inch, 13*inch),
-                'LEDGER': (17*inch, 11*inch),
-            }
-        )
+        if filename is None:
+            filename = self.app.defaults["global_last_save_folder"]
 
-        page_size_list = list(self.pagesize.keys())
-        self.pagesize_combo.addItems(page_size_list)
+        self.app.log.debug("export_svg() negative")
 
-        grid1.addWidget(self.pagesize_label, 3, 0)
-        grid1.addWidget(self.pagesize_combo, 3, 1)
+        try:
+            obj = self.app.collection.get_by_name(str(obj_name))
+        except Exception:
+            return "Could not retrieve object: %s" % obj_name
 
-        self.on_film_type(val='hide')
+        try:
+            box = self.app.collection.get_by_name(str(box_name))
+        except Exception:
+            return "Could not retrieve object: %s" % box_name
 
-        # Buttons
-        self.film_object_button = QtWidgets.QPushButton(_("Save Film"))
-        self.film_object_button.setToolTip(
-            _("Create a Film for the selected object, within\n"
-              "the specified box. Does not create a new \n "
-              "FlatCAM object, but directly save it in the\n"
-              "selected format.")
-        )
-        self.film_object_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        grid1.addWidget(self.film_object_button, 4, 0, 1, 2)
+        if box is None:
+            self.app.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
+            box = obj
 
-        self.layout.addStretch()
+        def make_negative_film():
+            exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
+                                          scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                          skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                          mirror=mirror
+                                          )
 
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
+            # Determine bounding area for svg export
+            bounds = box.bounds()
+            size = box.size()
 
-        self.units = self.app.defaults['units']
+            uom = obj.units.lower()
 
-        # ## Signals
-        self.film_object_button.clicked.connect(self.on_film_creation)
-        self.tf_type_obj_combo.activated_custom.connect(self.on_type_obj_index_changed)
-        self.tf_type_box_combo.activated_custom.connect(self.on_type_box_index_changed)
+            # Convert everything to strings for use in the xml doc
+            svgwidth = str(size[0] + (2 * boundary))
+            svgheight = str(size[1] + (2 * boundary))
+            minx = str(bounds[0] - boundary)
+            miny = str(bounds[1] + boundary + size[1])
+            miny_rect = str(bounds[1] - boundary)
 
-        self.film_type.activated_custom.connect(self.on_film_type)
-        self.source_punch.activated_custom.connect(self.on_punch_source)
-        self.file_type_radio.activated_custom.connect(self.on_file_type)
-        self.reset_button.clicked.connect(self.set_tool_ui)
+            # Add a SVG Header and footer to the svg output from shapely
+            # The transform flips the Y Axis so that everything renders
+            # properly within svg apps such as inkscape
+            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
+                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
+            svg_header += 'width="' + svgwidth + uom + '" '
+            svg_header += 'height="' + svgheight + uom + '" '
+            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
+            svg_header += '>'
+            svg_header += '<g transform="scale(1,-1)">'
+            svg_footer = '</g> </svg>'
 
-    def on_type_obj_index_changed(self, val):
-        obj_type = 2 if val == 'geo' else 0
-        self.tf_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        self.tf_object_combo.setCurrentIndex(0)
-        self.tf_object_combo.obj_type = {
-            "grb": "gerber", "geo": "geometry"
-        }[self.tf_type_obj_combo.get_value()]
+            # Change the attributes of the exported SVG
+            # We don't need stroke-width - wrong, we do when we have lines with certain width
+            # We set opacity to maximum
+            # We set the color to WHITE
+            root = ET.fromstring(exported_svg)
+            for child in root:
+                child.set('fill', '#FFFFFF')
+                child.set('opacity', '1.0')
+                child.set('stroke', '#FFFFFF')
 
-    def on_type_box_index_changed(self, val):
-        obj_type = 2 if val == 'geo' else 0
-        self.tf_box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        self.tf_box_combo.setCurrentIndex(0)
-        self.tf_box_combo.obj_type = {
-            "grb": "gerber", "geo": "geometry"
-        }[self.tf_type_obj_combo.get_value()]
+            # first_svg_elem = 'rect x="' + minx + '" ' + 'y="' + miny_rect + '" '
+            # first_svg_elem += 'width="' + svgwidth + '" ' + 'height="' + svgheight + '" '
+            # first_svg_elem += 'fill="#000000" opacity="1.0" stroke-width="0.0"'
 
-    def run(self, toggle=True):
-        self.app.defaults.report_usage("ToolFilm()")
+            first_svg_elem_tag = 'rect'
+            first_svg_elem_attribs = {
+                'x': minx,
+                'y': miny_rect,
+                'width': svgwidth,
+                'height': svgheight,
+                'id': 'neg_rect',
+                'style': 'fill:#000000;opacity:1.0;stroke-width:0.0'
+            }
 
-        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])
+            root.insert(0, ET.Element(first_svg_elem_tag, first_svg_elem_attribs))
+            exported_svg = ET.tostring(root)
+
+            svg_elem = svg_header + str(exported_svg) + svg_footer
+
+            # Parse the xml through a xml parser just to add line feeds
+            # and to make it look more pretty for the output
+            doc = parse_xml_string(svg_elem)
+            doc_final = doc.toprettyxml()
+
+            if ftype == 'svg':
+                try:
+                    with open(filename, 'w') as fp:
+                        fp.write(doc_final)
+                except PermissionError:
+                    self.app.inform.emit('[WARNING] %s' %
+                                         _("Permission denied, saving not possible.\n"
+                                           "Most likely another app is holding the file open and not accessible."))
+                    return 'fail'
+            elif ftype == 'png':
+                try:
+                    doc_final = StringIO(doc_final)
+                    drawing = svg2rlg(doc_final)
+                    renderPM.drawToFile(drawing, filename, 'PNG')
+                except Exception as e:
+                    log.debug("FilmTool.export_negative() --> PNG output --> %s" % str(e))
+                    return 'fail'
             else:
                 try:
-                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        # if tab is populated with the tool but it does not have the focus, focus on it
-                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
-                            # focus on Tool Tab
-                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                    if self.units == 'INCH':
+                        unit = inch
+                    else:
+                        unit = mm
+
+                    doc_final = StringIO(doc_final)
+                    drawing = svg2rlg(doc_final)
+
+                    p_size = self.ui.pagesize_combo.get_value()
+                    if p_size == 'Bounds':
+                        renderPDF.drawToFile(drawing, filename)
+                    else:
+                        if self.ui.orientation_radio.get_value() == 'p':
+                            page_size = portrait(self.ui.pagesize[p_size])
                         else:
-                            self.app.ui.splitter.setSizes([0, 1])
-                except AttributeError:
-                    pass
-        else:
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
+                            page_size = landscape(self.ui.pagesize[p_size])
 
-        AppTool.run(self)
+                        my_canvas = canvas.Canvas(filename, pagesize=page_size)
+                        my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
+                        renderPDF.draw(drawing, my_canvas, 0, 0)
+                        my_canvas.save()
+                except Exception as e:
+                    log.debug("FilmTool.export_negative() --> PDF output --> %s" % str(e))
+                    return 'fail'
 
-        self.set_tool_ui()
+            if self.app.defaults["global_open_style"] is False:
+                self.app.file_opened.emit("SVG", filename)
+            self.app.file_saved.emit("SVG", filename)
+            self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
 
-        self.app.ui.notebook.setTabText(2, _("Film Tool"))
+        if use_thread is True:
+            proc = self.app.proc_container.new(_("Generating Film ... Please wait."))
 
-    def install(self, icon=None, separator=None, **kwargs):
-        AppTool.install(self, icon, separator, shortcut='Alt+L', **kwargs)
+            def job_thread_film(app_obj):
+                try:
+                    make_negative_film()
+                except Exception:
+                    proc.done()
+                    return
+                proc.done()
 
-    def set_tool_ui(self):
-        self.reset_fields()
+            self.app.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
+        else:
+            make_negative_film()
 
-        f_type = self.app.defaults["tools_film_type"] if self.app.defaults["tools_film_type"] else 'neg'
-        self.film_type.set_value(str(f_type))
-        self.on_film_type(val=f_type)
+    def export_positive(self, obj_name, box_name, filename,
+                        scale_stroke_factor=0.00,
+                        scale_factor_x=None, scale_factor_y=None,
+                        skew_factor_x=None, skew_factor_y=None, skew_reference='center',
+                        mirror=None,  orientation_val='p', pagesize_val='A4', color_val='black', opacity_val=1.0,
+                        use_thread=True, ftype='svg'):
 
-        b_entry = self.app.defaults["tools_film_boundary"] if self.app.defaults["tools_film_boundary"] else 0.0
-        self.boundary_entry.set_value(float(b_entry))
+        """
+        Exports a Geometry Object to an SVG file in positive black.
 
-        scale_stroke_width = self.app.defaults["tools_film_scale_stroke"] if \
-            self.app.defaults["tools_film_scale_stroke"] else 0.0
-        self.film_scale_stroke_entry.set_value(int(scale_stroke_width))
-
-        self.punch_cb.set_value(False)
-        self.source_punch.set_value('exc')
-
-        self.film_scale_cb.set_value(self.app.defaults["tools_film_scale_cb"])
-        self.film_scalex_entry.set_value(float(self.app.defaults["tools_film_scale_x_entry"]))
-        self.film_scaley_entry.set_value(float(self.app.defaults["tools_film_scale_y_entry"]))
-        self.film_skew_cb.set_value(self.app.defaults["tools_film_skew_cb"])
-        self.film_skewx_entry.set_value(float(self.app.defaults["tools_film_skew_x_entry"]))
-        self.film_skewy_entry.set_value(float(self.app.defaults["tools_film_skew_y_entry"]))
-        self.film_skew_reference.set_value(self.app.defaults["tools_film_skew_ref_radio"])
-        self.film_mirror_cb.set_value(self.app.defaults["tools_film_mirror_cb"])
-        self.film_mirror_axis.set_value(self.app.defaults["tools_film_mirror_axis_radio"])
-        self.file_type_radio.set_value(self.app.defaults["tools_film_file_type_radio"])
-        self.orientation_radio.set_value(self.app.defaults["tools_film_orientation"])
-        self.pagesize_combo.set_value(self.app.defaults["tools_film_pagesize"])
-
-        self.tf_type_obj_combo.set_value('grb')
-        self.tf_type_box_combo.set_value('grb')
-        # run once to update the obj_type attribute in the FCCombobox so the last object is showed in cb
-        self.on_type_obj_index_changed(val='grb')
-        self.on_type_box_index_changed(val='grb')
+        :param obj_name:            the name of the FlatCAM object to be saved
+        :param box_name:            the name of the FlatCAM object to be used as delimitation of the content to be saved
+        :param filename:            Path to the file to save to.
+        :param scale_stroke_factor: factor by which to change/scale the thickness of the features
+        :param scale_factor_x:      factor to scale the geometry on the X axis
+        :param scale_factor_y:      factor to scale the geometry on the Y axis
+        :param skew_factor_x:       factor to skew the geometry on the X axis
+        :param skew_factor_y:       factor to skew the geometry on the Y axis
+        :param skew_reference:      reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft',
+        'topright' and those are the 4 points of the bounding box of the geometry to be skewed.
+        :param mirror:              can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
+        :param orientation_val:
+        :param pagesize_val:
+        :param color_val:
+        :param opacity_val:
+        :param use_thread:          if to be run in a separate thread; boolean
+        :param ftype:               the type of file for saving the film: 'svg', 'png' or 'pdf'
 
-    def on_film_type(self, val):
-        type_of_film = val
+        :return:
+        """
+        self.app.defaults.report_usage("export_positive()")
 
-        if type_of_film == 'neg':
-            self.boundary_label.show()
-            self.boundary_entry.show()
-            self.punch_cb.set_value(False)  # required so the self.punch_frame it's hidden also by the signal emitted
-            self.punch_cb.hide()
-        else:
-            self.boundary_label.hide()
-            self.boundary_entry.hide()
-            self.punch_cb.show()
+        if filename is None:
+            filename = self.app.defaults["global_last_save_folder"]
 
-    def on_file_type(self, val):
-        if val == 'pdf':
-            self.orientation_label.show()
-            self.orientation_radio.show()
-            self.pagesize_label.show()
-            self.pagesize_combo.show()
-        else:
-            self.orientation_label.hide()
-            self.orientation_radio.hide()
-            self.pagesize_label.hide()
-            self.pagesize_combo.hide()
-
-    def on_punch_source(self, val):
-        if val == 'pad' and self.punch_cb.get_value():
-            self.punch_size_label.show()
-            self.punch_size_spinner.show()
-            self.exc_label.hide()
-            self.exc_combo.hide()
-        else:
-            self.punch_size_label.hide()
-            self.punch_size_spinner.hide()
-            self.exc_label.show()
-            self.exc_combo.show()
-
-        if val == 'pad' and self.tf_type_obj_combo.get_value() == 'geo':
-            self.source_punch.set_value('exc')
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Using the Pad center does not work on Geometry objects. "
-                                                          "Only a Gerber object has pads."))
-
-    def on_film_creation(self):
-        log.debug("ToolFilm.Film.on_film_creation() started ...")
+        self.app.log.debug("export_svg() black")
 
         try:
-            name = self.tf_object_combo.currentText()
+            obj = self.app.collection.get_by_name(str(obj_name))
         except Exception:
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("No FlatCAM object selected. Load an object for Film and retry."))
-            return
+            return "Could not retrieve object: %s" % obj_name
 
         try:
-            boxname = self.tf_box_combo.currentText()
+            box = self.app.collection.get_by_name(str(box_name))
         except Exception:
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("No FlatCAM object selected. Load an object for Box and retry."))
-            return
-
-        if name == '' or boxname == '':
-            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No FlatCAM object selected."))
-            return
+            return "Could not retrieve object: %s" % box_name
 
-        scale_stroke_width = float(self.film_scale_stroke_entry.get_value())
-        source = self.source_punch.get_value()
-        file_type = self.file_type_radio.get_value()
+        if box is None:
+            self.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
+            box = obj
 
-        # #################################################################
-        # ################ STARTING THE JOB ###############################
-        # #################################################################
+        p_size = pagesize_val
+        orientation = orientation_val
+        color = color_val
+        transparency_level = opacity_val
 
-        self.app.inform.emit(_("Generating Film ..."))
+        def make_positive_film(p_size, orientation, color, transparency_level):
+            log.debug("FilmTool.export_positive().make_positive_film()")
 
-        if self.film_type.get_value() == "pos":
+            exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
+                                          scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                          skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                          mirror=mirror
+                                          )
 
-            if self.punch_cb.get_value() is False:
-                self.generate_positive_normal_film(name, boxname, factor=scale_stroke_width, ftype=file_type)
-            else:
-                self.generate_positive_punched_film(name, boxname, source, factor=scale_stroke_width, ftype=file_type)
-        else:
-            self.generate_negative_film(name, boxname, factor=scale_stroke_width, ftype=file_type)
+            # Change the attributes of the exported SVG
+            # We don't need stroke-width
+            # We set opacity to maximum
+            # We set the colour to WHITE
+            root = ET.fromstring(exported_svg)
+            for child in root:
+                child.set('fill', str(color))
+                child.set('opacity', str(transparency_level))
+                child.set('stroke', str(color))
 
-    def generate_positive_normal_film(self, name, boxname, factor, ftype='svg'):
-        log.debug("ToolFilm.Film.generate_positive_normal_film() started ...")
+            exported_svg = ET.tostring(root)
 
-        scale_factor_x = None
-        scale_factor_y = None
-        skew_factor_x = None
-        skew_factor_y = None
-        mirror = None
-        skew_reference = 'center'
+            # Determine bounding area for svg export
+            bounds = box.bounds()
+            size = box.size()
 
-        if self.film_scale_cb.get_value():
-            if self.film_scalex_entry.get_value() != 1.0:
-                scale_factor_x = self.film_scalex_entry.get_value()
-            if self.film_scaley_entry.get_value() != 1.0:
-                scale_factor_y = self.film_scaley_entry.get_value()
-        if self.film_skew_cb.get_value():
-            if self.film_skewx_entry.get_value() != 0.0:
-                skew_factor_x = self.film_skewx_entry.get_value()
-            if self.film_skewy_entry.get_value() != 0.0:
-                skew_factor_y = self.film_skewy_entry.get_value()
-
-            skew_reference = self.film_skew_reference.get_value()
-        if self.film_mirror_cb.get_value():
-            if self.film_mirror_axis.get_value() != 'none':
-                mirror = self.film_mirror_axis.get_value()
+            # This contain the measure units
+            uom = obj.units.lower()
 
-        if ftype == 'svg':
-            filter_ext = "SVG Files (*.SVG);;"\
-                         "All Files (*.*)"
-        elif ftype == 'png':
-            filter_ext = "PNG Files (*.PNG);;" \
-                         "All Files (*.*)"
-        else:
-            filter_ext = "PDF Files (*.PDF);;" \
-                         "All Files (*.*)"
+            # Define a boundary around SVG of about 1.0mm (~39mils)
+            if uom in "mm":
+                boundary = 1.0
+            else:
+                boundary = 0.0393701
 
-        try:
-            filename, _f = FCFileSaveDialog.get_saved_filename(
-                caption=_("Export positive film"),
-                directory=self.app.get_last_save_folder() + '/' + name + '_film',
-                ext_filter=filter_ext)
-        except TypeError:
-            filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export positive film"))
+            # Convert everything to strings for use in the xml doc
+            svgwidth = str(size[0] + (2 * boundary))
+            svgheight = str(size[1] + (2 * boundary))
+            minx = str(bounds[0] - boundary)
+            miny = str(bounds[1] + boundary + size[1])
 
-        filename = str(filename)
+            # Add a SVG Header and footer to the svg output from shapely
+            # The transform flips the Y Axis so that everything renders
+            # properly within svg apps such as inkscape
+            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
+                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
+            svg_header += 'width="' + svgwidth + uom + '" '
+            svg_header += 'height="' + svgheight + uom + '" '
+            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
+            svg_header += '>'
+            svg_header += '<g transform="scale(1,-1)">'
+            svg_footer = '</g> </svg>'
 
-        if str(filename) == "":
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
-            return
-        else:
-            pagesize = self.pagesize_combo.get_value()
-            orientation = self.orientation_radio.get_value()
-            color = self.app.defaults['tools_film_color']
+            svg_elem = str(svg_header) + str(exported_svg) + str(svg_footer)
 
-            self.export_positive(name, boxname, filename,
-                                 scale_stroke_factor=factor,
-                                 scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
-                                 skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
-                                 skew_reference=skew_reference,
-                                 mirror=mirror,
-                                 pagesize_val=pagesize, orientation_val=orientation, color_val=color, opacity_val=1.0,
-                                 ftype=ftype
-                                 )
+            # Parse the xml through a xml parser just to add line feeds
+            # and to make it look more pretty for the output
+            doc = parse_xml_string(svg_elem)
+            doc_final = doc.toprettyxml()
 
-    def generate_positive_punched_film(self, name, boxname, source, factor, ftype='svg'):
+            if ftype == 'svg':
+                try:
+                    with open(filename, 'w') as fp:
+                        fp.write(doc_final)
+                except PermissionError:
+                    self.app.inform.emit('[WARNING] %s' %
+                                         _("Permission denied, saving not possible.\n"
+                                           "Most likely another app is holding the file open and not accessible."))
+                    return 'fail'
+            elif ftype == 'png':
+                try:
+                    doc_final = StringIO(doc_final)
+                    drawing = svg2rlg(doc_final)
+                    renderPM.drawToFile(drawing, filename, 'PNG')
+                except Exception as e:
+                    log.debug("FilmTool.export_positive() --> PNG output --> %s" % str(e))
+                    return 'fail'
+            else:
+                try:
+                    if self.units == 'IN':
+                        unit = inch
+                    else:
+                        unit = mm
 
-        film_obj = self.app.collection.get_by_name(name)
+                    doc_final = StringIO(doc_final)
+                    drawing = svg2rlg(doc_final)
 
-        if source == 'exc':
-            log.debug("ToolFilm.Film.generate_positive_punched_film() with Excellon source started ...")
+                    if p_size == 'Bounds':
+                        renderPDF.drawToFile(drawing, filename)
+                    else:
+                        if orientation == 'p':
+                            page_size = portrait(self.pagesize[p_size])
+                        else:
+                            page_size = landscape(self.pagesize[p_size])
 
-            try:
-                exc_name = self.exc_combo.currentText()
-            except Exception:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("No Excellon object selected. Load an object for punching reference and retry."))
-                return
+                        my_canvas = canvas.Canvas(filename, pagesize=page_size)
+                        my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
+                        renderPDF.draw(drawing, my_canvas, 0, 0)
+                        my_canvas.save()
+                except Exception as e:
+                    log.debug("FilmTool.export_positive() --> PDF output --> %s" % str(e))
+                    return 'fail'
 
-            exc_obj = self.app.collection.get_by_name(exc_name)
-            exc_solid_geometry = MultiPolygon(exc_obj.solid_geometry)
-            punched_solid_geometry = MultiPolygon(film_obj.solid_geometry).difference(exc_solid_geometry)
+            if self.app.defaults["global_open_style"] is False:
+                self.app.file_opened.emit("SVG", filename)
+            self.app.file_saved.emit("SVG", filename)
+            self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
 
-            def init_func(new_obj, app_obj):
-                new_obj.solid_geometry = deepcopy(punched_solid_geometry)
+        if use_thread is True:
+            proc = self.app.proc_container.new(_("Generating Film ... Please wait."))
 
-            outname = name + "_punched"
-            self.app.app_obj.new_object('gerber', outname, init_func)
+            def job_thread_film():
+                try:
+                    make_positive_film(p_size=p_size, orientation=orientation, color=color,
+                                       transparency_level=transparency_level)
+                except Exception:
+                    proc.done()
+                    return
+                proc.done()
 
-            self.generate_positive_normal_film(outname, boxname, factor=factor, ftype=ftype)
+            self.app.worker_task.emit({'fcn': job_thread_film, 'params': []})
         else:
-            log.debug("ToolFilm.Film.generate_positive_punched_film() with Pad center source started ...")
-
-            punch_size = float(self.punch_size_spinner.get_value())
+            make_positive_film(p_size=p_size, orientation=orientation, color=color,
+                               transparency_level=transparency_level)
 
-            punching_geo = []
-            for apid in film_obj.apertures:
-                if film_obj.apertures[apid]['type'] == 'C':
-                    if punch_size >= float(film_obj.apertures[apid]['size']):
-                        self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                             _(" Could not generate punched hole film because the punch hole size"
-                                               "is bigger than some of the apertures in the Gerber object."))
-                        return 'fail'
-                    else:
-                        for elem in film_obj.apertures[apid]['geometry']:
-                            if 'follow' in elem:
-                                if isinstance(elem['follow'], Point):
-                                    punching_geo.append(elem['follow'].buffer(punch_size / 2))
-                else:
-                    if punch_size >= float(film_obj.apertures[apid]['width']) or \
-                            punch_size >= float(film_obj.apertures[apid]['height']):
-                        self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                             _("Could not generate punched hole film because the punch hole size"
-                                               "is bigger than some of the apertures in the Gerber object."))
-                        return 'fail'
-                    else:
-                        for elem in film_obj.apertures[apid]['geometry']:
-                            if 'follow' in elem:
-                                if isinstance(elem['follow'], Point):
-                                    punching_geo.append(elem['follow'].buffer(punch_size / 2))
+    def reset_fields(self):
+        self.ui.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ui.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
 
-            punching_geo = MultiPolygon(punching_geo)
-            if not isinstance(film_obj.solid_geometry, Polygon):
-                temp_solid_geometry = MultiPolygon(film_obj.solid_geometry)
-            else:
-                temp_solid_geometry = film_obj.solid_geometry
-            punched_solid_geometry = temp_solid_geometry.difference(punching_geo)
 
-            if punched_solid_geometry == temp_solid_geometry:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Could not generate punched hole film because the newly created object geometry "
-                                       "is the same as the one in the source object geometry..."))
-                return 'fail'
+class FilmUI:
 
-            def init_func(new_obj, app_obj):
-                new_obj.solid_geometry = deepcopy(punched_solid_geometry)
+    toolName = _("Film PCB")
 
-            outname = name + "_punched"
-            self.app.app_obj.new_object('gerber', outname, init_func)
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
 
-            self.generate_positive_normal_film(outname, boxname, factor=factor, ftype=ftype)
+        # ## Title
+        title_label = FCLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(FCLabel(""))
 
-    def generate_negative_film(self, name, boxname, factor, ftype='svg'):
-        log.debug("ToolFilm.Film.generate_negative_film() started ...")
+        # Form Layout
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
 
-        scale_factor_x = None
-        scale_factor_y = None
-        skew_factor_x = None
-        skew_factor_y = None
-        mirror = None
-        skew_reference = 'center'
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
 
-        if self.film_scale_cb.get_value():
-            if self.film_scalex_entry.get_value() != 1.0:
-                scale_factor_x = self.film_scalex_entry.get_value()
-            if self.film_scaley_entry.get_value() != 1.0:
-                scale_factor_y = self.film_scaley_entry.get_value()
-        if self.film_skew_cb.get_value():
-            if self.film_skewx_entry.get_value() != 0.0:
-                skew_factor_x = self.film_skewx_entry.get_value()
-            if self.film_skewy_entry.get_value() != 0.0:
-                skew_factor_y = self.film_skewy_entry.get_value()
+        # Type of object for which to create the film
+        self.tf_type_obj_combo = RadioSet([{'label': _('Gerber'), 'value': 'grb'},
+                                           {'label': _('Geometry'), 'value': 'geo'}])
 
-            skew_reference = self.film_skew_reference.get_value()
-        if self.film_mirror_cb.get_value():
-            if self.film_mirror_axis.get_value() != 'none':
-                mirror = self.film_mirror_axis.get_value()
+        self.tf_type_obj_combo_label = FCLabel('<b>%s</b>:' % _("Object"))
+        self.tf_type_obj_combo_label.setToolTip(
+            _("Specify the type of object for which to create the film.\n"
+              "The object can be of type: Gerber or Geometry.\n"
+              "The selection here decide the type of objects that will be\n"
+              "in the Film Object combobox.")
+        )
+        grid0.addWidget(self.tf_type_obj_combo_label, 0, 0)
+        grid0.addWidget(self.tf_type_obj_combo, 0, 1)
 
-        border = float(self.boundary_entry.get_value())
+        # List of objects for which we can create the film
+        self.tf_object_combo = FCComboBox()
+        self.tf_object_combo.setModel(self.app.collection)
+        self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.tf_object_combo.is_last = True
 
-        if border is None:
-            border = 0
+        grid0.addWidget(self.tf_object_combo, 1, 0, 1, 2)
 
-        if ftype == 'svg':
-            filter_ext = "SVG Files (*.SVG);;"\
-                         "All Files (*.*)"
-        elif ftype == 'png':
-            filter_ext = "PNG Files (*.PNG);;" \
-                         "All Files (*.*)"
-        else:
-            filter_ext = "PDF Files (*.PDF);;" \
-                         "All Files (*.*)"
+        # Type of Box Object to be used as an envelope for film creation
+        # Within this we can create negative
+        self.tf_type_box_combo = RadioSet([{'label': _('Gerber'), 'value': 'grb'},
+                                           {'label': _('Geometry'), 'value': 'geo'}])
 
-        try:
-            filename, _f = FCFileSaveDialog.get_saved_filename(
-                caption=_("Export negative film"),
-                directory=self.app.get_last_save_folder() + '/' + name + '_film',
-                ext_filter=filter_ext)
-        except TypeError:
-            filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export negative film"))
+        self.tf_type_box_combo_label = FCLabel(_("Box Type:"))
+        self.tf_type_box_combo_label.setToolTip(
+            _("Specify the type of object to be used as an container for\n"
+              "film creation. It can be: Gerber or Geometry type."
+              "The selection here decide the type of objects that will be\n"
+              "in the Box Object combobox.")
+        )
+        grid0.addWidget(self.tf_type_box_combo_label, 2, 0)
+        grid0.addWidget(self.tf_type_box_combo, 2, 1)
 
-        filename = str(filename)
+        # Box
+        self.tf_box_combo = FCComboBox()
+        self.tf_box_combo.setModel(self.app.collection)
+        self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.tf_box_combo.is_last = True
 
-        if str(filename) == "":
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
-            return
-        else:
-            self.export_negative(name, boxname, filename, border,
-                                 scale_stroke_factor=factor,
-                                 scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
-                                 skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
-                                 skew_reference=skew_reference,
-                                 mirror=mirror, ftype=ftype
-                                 )
+        grid0.addWidget(self.tf_box_combo, 3, 0, 1, 2)
 
-    def export_negative(self, obj_name, box_name, filename, boundary,
-                        scale_stroke_factor=0.00,
-                        scale_factor_x=None, scale_factor_y=None,
-                        skew_factor_x=None, skew_factor_y=None, skew_reference='center',
-                        mirror=None,
-                        use_thread=True, ftype='svg'):
-        """
-        Exports a Geometry Object to an SVG file in negative.
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 4, 0, 1, 2)
 
-        :param obj_name: the name of the FlatCAM object to be saved as SVG
-        :param box_name: the name of the FlatCAM object to be used as delimitation of the content to be saved
-        :param filename: Path to the SVG file to save to.
-        :param boundary: thickness of a black border to surround all the features
-        :param scale_stroke_factor: factor by which to change/scale the thickness of the features
-        :param scale_factor_x: factor to scale the svg geometry on the X axis
-        :param scale_factor_y: factor to scale the svg geometry on the Y axis
-        :param skew_factor_x: factor to skew the svg geometry on the X axis
-        :param skew_factor_y: factor to skew the svg geometry on the Y axis
-        :param skew_reference: reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft', 'topright' and
-        those are the 4 points of the bounding box of the geometry to be skewed.
-        :param mirror: can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
-        :param use_thread: if to be run in a separate thread; boolean
-        :param ftype: the type of file for saving the film: 'svg', 'png' or 'pdf'
-        :return:
-        """
-        self.app.defaults.report_usage("export_negative()")
+        self.film_adj_label = FCLabel('<b>%s</b>' % _("Film Adjustments"))
+        self.film_adj_label.setToolTip(
+            _("Sometime the printers will distort the print shape, especially the Laser types.\n"
+              "This section provide the tools to compensate for the print distortions.")
+        )
 
-        if filename is None:
-            filename = self.app.defaults["global_last_save_folder"]
+        grid0.addWidget(self.film_adj_label, 5, 0, 1, 2)
 
-        self.app.log.debug("export_svg() negative")
+        # Scale Geometry
+        self.film_scale_cb = FCCheckBox('%s' % _("Scale Film geometry"))
+        self.film_scale_cb.setToolTip(
+            _("A value greater than 1 will stretch the film\n"
+              "while a value less than 1 will jolt it.")
+        )
+        self.film_scale_cb.setStyleSheet(
+            """
+            QCheckBox {font-weight: bold; color: black}
+            """
+        )
+        grid0.addWidget(self.film_scale_cb, 6, 0, 1, 2)
 
-        try:
-            obj = self.app.collection.get_by_name(str(obj_name))
-        except Exception:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % obj_name
+        self.film_scalex_label = FCLabel('%s:' % _("X factor"))
+        self.film_scalex_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.film_scalex_entry.set_range(-999.9999, 999.9999)
+        self.film_scalex_entry.set_precision(self.decimals)
+        self.film_scalex_entry.setSingleStep(0.01)
 
-        try:
-            box = self.app.collection.get_by_name(str(box_name))
-        except Exception:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % box_name
+        grid0.addWidget(self.film_scalex_label, 7, 0)
+        grid0.addWidget(self.film_scalex_entry, 7, 1)
 
-        if box is None:
-            self.app.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
-            box = obj
+        self.film_scaley_label = FCLabel('%s:' % _("Y factor"))
+        self.film_scaley_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.film_scaley_entry.set_range(-999.9999, 999.9999)
+        self.film_scaley_entry.set_precision(self.decimals)
+        self.film_scaley_entry.setSingleStep(0.01)
 
-        def make_negative_film():
-            exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
-                                          scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
-                                          skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
-                                          mirror=mirror
-                                          )
+        grid0.addWidget(self.film_scaley_label, 8, 0)
+        grid0.addWidget(self.film_scaley_entry, 8, 1)
 
-            # Determine bounding area for svg export
-            bounds = box.bounds()
-            size = box.size()
+        self.ois_scale = OptionalHideInputSection(self.film_scale_cb,
+                                                  [
+                                                      self.film_scalex_label,
+                                                      self.film_scalex_entry,
+                                                      self.film_scaley_label,
+                                                      self.film_scaley_entry
+                                                  ])
 
-            uom = obj.units.lower()
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 9, 0, 1, 2)
 
-            # Convert everything to strings for use in the xml doc
-            svgwidth = str(size[0] + (2 * boundary))
-            svgheight = str(size[1] + (2 * boundary))
-            minx = str(bounds[0] - boundary)
-            miny = str(bounds[1] + boundary + size[1])
-            miny_rect = str(bounds[1] - boundary)
+        # Skew Geometry
+        self.film_skew_cb = FCCheckBox('%s' % _("Skew Film geometry"))
+        self.film_skew_cb.setToolTip(
+            _("Positive values will skew to the right\n"
+              "while negative values will skew to the left.")
+        )
+        self.film_skew_cb.setStyleSheet(
+            """
+            QCheckBox {font-weight: bold; color: black}
+            """
+        )
+        grid0.addWidget(self.film_skew_cb, 10, 0, 1, 2)
 
-            # Add a SVG Header and footer to the svg output from shapely
-            # The transform flips the Y Axis so that everything renders
-            # properly within svg apps such as inkscape
-            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
-                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
-            svg_header += 'width="' + svgwidth + uom + '" '
-            svg_header += 'height="' + svgheight + uom + '" '
-            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
-            svg_header += '>'
-            svg_header += '<g transform="scale(1,-1)">'
-            svg_footer = '</g> </svg>'
+        self.film_skewx_label = FCLabel('%s:' % _("X angle"))
+        self.film_skewx_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.film_skewx_entry.set_range(-999.9999, 999.9999)
+        self.film_skewx_entry.set_precision(self.decimals)
+        self.film_skewx_entry.setSingleStep(0.01)
 
-            # Change the attributes of the exported SVG
-            # We don't need stroke-width - wrong, we do when we have lines with certain width
-            # We set opacity to maximum
-            # We set the color to WHITE
-            root = ET.fromstring(exported_svg)
-            for child in root:
-                child.set('fill', '#FFFFFF')
-                child.set('opacity', '1.0')
-                child.set('stroke', '#FFFFFF')
+        grid0.addWidget(self.film_skewx_label, 11, 0)
+        grid0.addWidget(self.film_skewx_entry, 11, 1)
 
-            # first_svg_elem = 'rect x="' + minx + '" ' + 'y="' + miny_rect + '" '
-            # first_svg_elem += 'width="' + svgwidth + '" ' + 'height="' + svgheight + '" '
-            # first_svg_elem += 'fill="#000000" opacity="1.0" stroke-width="0.0"'
+        self.film_skewy_label = FCLabel('%s:' % _("Y angle"))
+        self.film_skewy_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.film_skewy_entry.set_range(-999.9999, 999.9999)
+        self.film_skewy_entry.set_precision(self.decimals)
+        self.film_skewy_entry.setSingleStep(0.01)
 
-            first_svg_elem_tag = 'rect'
-            first_svg_elem_attribs = {
-                'x': minx,
-                'y': miny_rect,
-                'width': svgwidth,
-                'height': svgheight,
-                'id': 'neg_rect',
-                'style': 'fill:#000000;opacity:1.0;stroke-width:0.0'
-            }
+        grid0.addWidget(self.film_skewy_label, 12, 0)
+        grid0.addWidget(self.film_skewy_entry, 12, 1)
 
-            root.insert(0, ET.Element(first_svg_elem_tag, first_svg_elem_attribs))
-            exported_svg = ET.tostring(root)
+        self.film_skew_ref_label = FCLabel('%s:' % _("Reference"))
+        self.film_skew_ref_label.setToolTip(
+            _("The reference point to be used as origin for the skew.\n"
+              "It can be one of the four points of the geometry bounding box.")
+        )
+        self.film_skew_reference = RadioSet([{'label': _('Bottom Left'), 'value': 'bottomleft'},
+                                             {'label': _('Top Left'), 'value': 'topleft'},
+                                             {'label': _('Bottom Right'), 'value': 'bottomright'},
+                                             {'label': _('Top right'), 'value': 'topright'}],
+                                            orientation='vertical',
+                                            stretch=False)
 
-            svg_elem = svg_header + str(exported_svg) + svg_footer
+        grid0.addWidget(self.film_skew_ref_label, 13, 0)
+        grid0.addWidget(self.film_skew_reference, 13, 1)
 
-            # Parse the xml through a xml parser just to add line feeds
-            # and to make it look more pretty for the output
-            doc = parse_xml_string(svg_elem)
-            doc_final = doc.toprettyxml()
+        self.ois_skew = OptionalHideInputSection(self.film_skew_cb,
+                                                 [
+                                                     self.film_skewx_label,
+                                                     self.film_skewx_entry,
+                                                     self.film_skewy_label,
+                                                     self.film_skewy_entry,
+                                                     self.film_skew_ref_label,
+                                                     self.film_skew_reference
+                                                 ])
 
-            if ftype == 'svg':
-                try:
-                    with open(filename, 'w') as fp:
-                        fp.write(doc_final)
-                except PermissionError:
-                    self.app.inform.emit('[WARNING] %s' %
-                                         _("Permission denied, saving not possible.\n"
-                                           "Most likely another app is holding the file open and not accessible."))
-                    return 'fail'
-            elif ftype == 'png':
-                try:
-                    doc_final = StringIO(doc_final)
-                    drawing = svg2rlg(doc_final)
-                    renderPM.drawToFile(drawing, filename, 'PNG')
-                except Exception as e:
-                    log.debug("FilmTool.export_negative() --> PNG output --> %s" % str(e))
-                    return 'fail'
-            else:
-                try:
-                    if self.units == 'INCH':
-                        unit = inch
-                    else:
-                        unit = mm
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line1, 14, 0, 1, 2)
 
-                    doc_final = StringIO(doc_final)
-                    drawing = svg2rlg(doc_final)
+        # Mirror Geometry
+        self.film_mirror_cb = FCCheckBox('%s' % _("Mirror Film geometry"))
+        self.film_mirror_cb.setToolTip(
+            _("Mirror the film geometry on the selected axis or on both.")
+        )
+        self.film_mirror_cb.setStyleSheet(
+            """
+            QCheckBox {font-weight: bold; color: black}
+            """
+        )
+        grid0.addWidget(self.film_mirror_cb, 15, 0, 1, 2)
 
-                    p_size = self.pagesize_combo.get_value()
-                    if p_size == 'Bounds':
-                        renderPDF.drawToFile(drawing, filename)
-                    else:
-                        if self.orientation_radio.get_value() == 'p':
-                            page_size = portrait(self.pagesize[p_size])
-                        else:
-                            page_size = landscape(self.pagesize[p_size])
+        self.film_mirror_axis = RadioSet([{'label': _('None'), 'value': 'none'},
+                                          {'label': _('X'), 'value': 'x'},
+                                          {'label': _('Y'), 'value': 'y'},
+                                          {'label': _('Both'), 'value': 'both'}],
+                                         stretch=False)
+        self.film_mirror_axis_label = FCLabel('%s:' % _("Mirror axis"))
 
-                        my_canvas = canvas.Canvas(filename, pagesize=page_size)
-                        my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
-                        renderPDF.draw(drawing, my_canvas, 0, 0)
-                        my_canvas.save()
-                except Exception as e:
-                    log.debug("FilmTool.export_negative() --> PDF output --> %s" % str(e))
-                    return 'fail'
+        grid0.addWidget(self.film_mirror_axis_label, 16, 0)
+        grid0.addWidget(self.film_mirror_axis, 16, 1)
 
-            if self.app.defaults["global_open_style"] is False:
-                self.app.file_opened.emit("SVG", filename)
-            self.app.file_saved.emit("SVG", filename)
-            self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
+        self.ois_mirror = OptionalHideInputSection(self.film_mirror_cb,
+                                                   [
+                                                       self.film_mirror_axis_label,
+                                                       self.film_mirror_axis
+                                                   ])
 
-        if use_thread is True:
-            proc = self.app.proc_container.new(_("Generating Film ... Please wait."))
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line2, 17, 0, 1, 2)
 
-            def job_thread_film(app_obj):
-                try:
-                    make_negative_film()
-                except Exception:
-                    proc.done()
-                    return
-                proc.done()
+        self.film_param_label = FCLabel('<b>%s</b>' % _("Film Parameters"))
 
-            self.app.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
-        else:
-            make_negative_film()
+        grid0.addWidget(self.film_param_label, 18, 0, 1, 2)
 
-    def export_positive(self, obj_name, box_name, filename,
-                        scale_stroke_factor=0.00,
-                        scale_factor_x=None, scale_factor_y=None,
-                        skew_factor_x=None, skew_factor_y=None, skew_reference='center',
-                        mirror=None,  orientation_val='p', pagesize_val='A4', color_val='black', opacity_val=1.0,
-                        use_thread=True, ftype='svg'):
+        # Scale Stroke size
+        self.film_scale_stroke_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.film_scale_stroke_entry.set_range(-999.9999, 999.9999)
+        self.film_scale_stroke_entry.setSingleStep(0.01)
+        self.film_scale_stroke_entry.set_precision(self.decimals)
+
+        self.film_scale_stroke_label = FCLabel('%s:' % _("Scale Stroke"))
+        self.film_scale_stroke_label.setToolTip(
+            _("Scale the line stroke thickness of each feature in the SVG file.\n"
+              "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
+              "therefore the fine features may be more affected by this parameter.")
+        )
+        grid0.addWidget(self.film_scale_stroke_label, 19, 0)
+        grid0.addWidget(self.film_scale_stroke_entry, 19, 1)
+
+        # Film Type
+        self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'},
+                                   {'label': _('Negative'), 'value': 'neg'}],
+                                  stretch=False)
+        self.film_type_label = FCLabel(_("Film Type:"))
+        self.film_type_label.setToolTip(
+            _("Generate a Positive black film or a Negative film.\n"
+              "Positive means that it will print the features\n"
+              "with black on a white canvas.\n"
+              "Negative means that it will print the features\n"
+              "with white on a black canvas.\n"
+              "The Film format is SVG.")
+        )
+        grid0.addWidget(self.film_type_label, 21, 0)
+        grid0.addWidget(self.film_type, 21, 1)
+
+        # Boundary for negative film generation
+        self.boundary_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.boundary_entry.set_range(-999.9999, 999.9999)
+        self.boundary_entry.setSingleStep(0.01)
+        self.boundary_entry.set_precision(self.decimals)
+
+        self.boundary_label = FCLabel('%s:' % _("Border"))
+        self.boundary_label.setToolTip(
+            _("Specify a border around the object.\n"
+              "Only for negative film.\n"
+              "It helps if we use as a Box Object the same \n"
+              "object as in Film Object. It will create a thick\n"
+              "black bar around the actual print allowing for a\n"
+              "better delimitation of the outline features which are of\n"
+              "white color like the rest and which may confound with the\n"
+              "surroundings if not for this border.")
+        )
+        grid0.addWidget(self.boundary_label, 22, 0)
+        grid0.addWidget(self.boundary_entry, 22, 1)
+
+        self.boundary_label.hide()
+        self.boundary_entry.hide()
+
+        # Punch Drill holes
+        self.punch_cb = FCCheckBox(_("Punch drill holes"))
+        self.punch_cb.setToolTip(_("When checked the generated film will have holes in pads when\n"
+                                   "the generated film is positive. This is done to help drilling,\n"
+                                   "when done manually."))
+        grid0.addWidget(self.punch_cb, 23, 0, 1, 2)
+
+        # this way I can hide/show the frame
+        self.punch_frame = QtWidgets.QFrame()
+        self.punch_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.punch_frame)
+        punch_grid = QtWidgets.QGridLayout()
+        punch_grid.setContentsMargins(0, 0, 0, 0)
+        self.punch_frame.setLayout(punch_grid)
+
+        punch_grid.setColumnStretch(0, 0)
+        punch_grid.setColumnStretch(1, 1)
+
+        self.ois_p = OptionalHideInputSection(self.punch_cb, [self.punch_frame])
+
+        self.source_label = FCLabel('%s:' % _("Source"))
+        self.source_label.setToolTip(
+            _("The punch hole source can be:\n"
+              "- Excellon -> an Excellon holes center will serve as reference.\n"
+              "- Pad Center -> will try to use the pads center as reference.")
+        )
+        self.source_punch = RadioSet([{'label': _('Excellon'), 'value': 'exc'},
+                                      {'label': _('Pad center'), 'value': 'pad'}],
+                                     stretch=False)
+        punch_grid.addWidget(self.source_label, 0, 0)
+        punch_grid.addWidget(self.source_punch, 0, 1)
+
+        self.exc_label = FCLabel('%s:' % _("Excellon Obj"))
+        self.exc_label.setToolTip(
+            _("Remove the geometry of Excellon from the Film to create the holes in pads.")
+        )
+        self.exc_combo = FCComboBox()
+        self.exc_combo.setModel(self.app.collection)
+        self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.exc_combo.is_last = True
+        self.exc_combo.obj_type = "Excellon"
+
+        punch_grid.addWidget(self.exc_label, 1, 0)
+        punch_grid.addWidget(self.exc_combo, 1, 1)
+
+        self.exc_label.hide()
+        self.exc_combo.hide()
 
-        """
-        Exports a Geometry Object to an SVG file in positive black.
+        self.punch_size_label = FCLabel('%s:' % _("Punch Size"))
+        self.punch_size_label.setToolTip(_("The value here will control how big is the punch hole in the pads."))
+        self.punch_size_spinner = FCDoubleSpinner(callback=self.confirmation_message)
+        self.punch_size_spinner.set_range(0, 999.9999)
+        self.punch_size_spinner.setSingleStep(0.1)
+        self.punch_size_spinner.set_precision(self.decimals)
 
-        :param obj_name:            the name of the FlatCAM object to be saved
-        :param box_name:            the name of the FlatCAM object to be used as delimitation of the content to be saved
-        :param filename:            Path to the file to save to.
-        :param scale_stroke_factor: factor by which to change/scale the thickness of the features
-        :param scale_factor_x:      factor to scale the geometry on the X axis
-        :param scale_factor_y:      factor to scale the geometry on the Y axis
-        :param skew_factor_x:       factor to skew the geometry on the X axis
-        :param skew_factor_y:       factor to skew the geometry on the Y axis
-        :param skew_reference:      reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft',
-        'topright' and those are the 4 points of the bounding box of the geometry to be skewed.
-        :param mirror:              can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
-        :param orientation_val:
-        :param pagesize_val:
-        :param color_val:
-        :param opacity_val:
-        :param use_thread:          if to be run in a separate thread; boolean
-        :param ftype:               the type of file for saving the film: 'svg', 'png' or 'pdf'
+        punch_grid.addWidget(self.punch_size_label, 2, 0)
+        punch_grid.addWidget(self.punch_size_spinner, 2, 1)
 
-        :return:
-        """
-        self.app.defaults.report_usage("export_positive()")
+        self.punch_size_label.hide()
+        self.punch_size_spinner.hide()
 
-        if filename is None:
-            filename = self.app.defaults["global_last_save_folder"]
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
 
-        self.app.log.debug("export_svg() black")
+        separator_line3 = QtWidgets.QFrame()
+        separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line3, 0, 0, 1, 2)
 
-        try:
-            obj = self.app.collection.get_by_name(str(obj_name))
-        except Exception:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % obj_name
+        # File type
+        self.file_type_radio = RadioSet([{'label': _('SVG'), 'value': 'svg'},
+                                         {'label': _('PNG'), 'value': 'png'},
+                                         {'label': _('PDF'), 'value': 'pdf'}
+                                         ], stretch=False)
 
-        try:
-            box = self.app.collection.get_by_name(str(box_name))
-        except Exception:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % box_name
+        self.file_type_label = FCLabel(_("Film Type:"))
+        self.file_type_label.setToolTip(
+            _("The file type of the saved film. Can be:\n"
+              "- 'SVG' -> open-source vectorial format\n"
+              "- 'PNG' -> raster image\n"
+              "- 'PDF' -> portable document format")
+        )
+        grid1.addWidget(self.file_type_label, 1, 0)
+        grid1.addWidget(self.file_type_radio, 1, 1)
 
-        if box is None:
-            self.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
-            box = obj
+        # Page orientation
+        self.orientation_label = FCLabel('%s:' % _("Page Orientation"))
+        self.orientation_label.setToolTip(_("Can be:\n"
+                                            "- Portrait\n"
+                                            "- Landscape"))
 
-        p_size = pagesize_val
-        orientation = orientation_val
-        color = color_val
-        transparency_level = opacity_val
+        self.orientation_radio = RadioSet([{'label': _('Portrait'), 'value': 'p'},
+                                           {'label': _('Landscape'), 'value': 'l'},
+                                           ], stretch=False)
 
-        def make_positive_film(p_size, orientation, color, transparency_level):
-            log.debug("FilmTool.export_positive().make_positive_film()")
+        grid1.addWidget(self.orientation_label, 2, 0)
+        grid1.addWidget(self.orientation_radio, 2, 1)
 
-            exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
-                                          scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
-                                          skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
-                                          mirror=mirror
-                                          )
+        # Page Size
+        self.pagesize_label = FCLabel('%s:' % _("Page Size"))
+        self.pagesize_label.setToolTip(_("A selection of standard ISO 216 page sizes."))
 
-            # Change the attributes of the exported SVG
-            # We don't need stroke-width
-            # We set opacity to maximum
-            # We set the colour to WHITE
-            root = ET.fromstring(exported_svg)
-            for child in root:
-                child.set('fill', str(color))
-                child.set('opacity', str(transparency_level))
-                child.set('stroke', str(color))
+        self.pagesize_combo = FCComboBox()
 
-            exported_svg = ET.tostring(root)
+        self.pagesize = {}
+        self.pagesize.update(
+            {
+                'Bounds': None,
+                'A0': (841 * mm, 1189 * mm),
+                'A1': (594 * mm, 841 * mm),
+                'A2': (420 * mm, 594 * mm),
+                'A3': (297 * mm, 420 * mm),
+                'A4': (210 * mm, 297 * mm),
+                'A5': (148 * mm, 210 * mm),
+                'A6': (105 * mm, 148 * mm),
+                'A7': (74 * mm, 105 * mm),
+                'A8': (52 * mm, 74 * mm),
+                'A9': (37 * mm, 52 * mm),
+                'A10': (26 * mm, 37 * mm),
+
+                'B0': (1000 * mm, 1414 * mm),
+                'B1': (707 * mm, 1000 * mm),
+                'B2': (500 * mm, 707 * mm),
+                'B3': (353 * mm, 500 * mm),
+                'B4': (250 * mm, 353 * mm),
+                'B5': (176 * mm, 250 * mm),
+                'B6': (125 * mm, 176 * mm),
+                'B7': (88 * mm, 125 * mm),
+                'B8': (62 * mm, 88 * mm),
+                'B9': (44 * mm, 62 * mm),
+                'B10': (31 * mm, 44 * mm),
+
+                'C0': (917 * mm, 1297 * mm),
+                'C1': (648 * mm, 917 * mm),
+                'C2': (458 * mm, 648 * mm),
+                'C3': (324 * mm, 458 * mm),
+                'C4': (229 * mm, 324 * mm),
+                'C5': (162 * mm, 229 * mm),
+                'C6': (114 * mm, 162 * mm),
+                'C7': (81 * mm, 114 * mm),
+                'C8': (57 * mm, 81 * mm),
+                'C9': (40 * mm, 57 * mm),
+                'C10': (28 * mm, 40 * mm),
 
-            # Determine bounding area for svg export
-            bounds = box.bounds()
-            size = box.size()
+                # American paper sizes
+                'LETTER': (8.5 * inch, 11 * inch),
+                'LEGAL': (8.5 * inch, 14 * inch),
+                'ELEVENSEVENTEEN': (11 * inch, 17 * inch),
 
-            # This contain the measure units
-            uom = obj.units.lower()
+                # From https://en.wikipedia.org/wiki/Paper_size
+                'JUNIOR_LEGAL': (5 * inch, 8 * inch),
+                'HALF_LETTER': (5.5 * inch, 8 * inch),
+                'GOV_LETTER': (8 * inch, 10.5 * inch),
+                'GOV_LEGAL': (8.5 * inch, 13 * inch),
+                'LEDGER': (17 * inch, 11 * inch),
+            }
+        )
 
-            # Define a boundary around SVG of about 1.0mm (~39mils)
-            if uom in "mm":
-                boundary = 1.0
-            else:
-                boundary = 0.0393701
+        page_size_list = list(self.pagesize.keys())
+        self.pagesize_combo.addItems(page_size_list)
 
-            # Convert everything to strings for use in the xml doc
-            svgwidth = str(size[0] + (2 * boundary))
-            svgheight = str(size[1] + (2 * boundary))
-            minx = str(bounds[0] - boundary)
-            miny = str(bounds[1] + boundary + size[1])
+        grid1.addWidget(self.pagesize_label, 3, 0)
+        grid1.addWidget(self.pagesize_combo, 3, 1)
 
-            # Add a SVG Header and footer to the svg output from shapely
-            # The transform flips the Y Axis so that everything renders
-            # properly within svg apps such as inkscape
-            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
-                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
-            svg_header += 'width="' + svgwidth + uom + '" '
-            svg_header += 'height="' + svgheight + uom + '" '
-            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
-            svg_header += '>'
-            svg_header += '<g transform="scale(1,-1)">'
-            svg_footer = '</g> </svg>'
+        self.on_film_type(val='hide')
 
-            svg_elem = str(svg_header) + str(exported_svg) + str(svg_footer)
+        # Buttons
+        self.film_object_button = FCButton(_("Save Film"))
+        self.film_object_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+        self.film_object_button.setToolTip(
+            _("Create a Film for the selected object, within\n"
+              "the specified box. Does not create a new \n "
+              "FlatCAM object, but directly save it in the\n"
+              "selected format.")
+        )
+        self.film_object_button.setStyleSheet("""
+                               QPushButton
+                               {
+                                   font-weight: bold;
+                               }
+                               """)
+        grid1.addWidget(self.film_object_button, 4, 0, 1, 2)
 
-            # Parse the xml through a xml parser just to add line feeds
-            # and to make it look more pretty for the output
-            doc = parse_xml_string(svg_elem)
-            doc_final = doc.toprettyxml()
+        self.layout.addStretch()
 
-            if ftype == 'svg':
-                try:
-                    with open(filename, 'w') as fp:
-                        fp.write(doc_final)
-                except PermissionError:
-                    self.app.inform.emit('[WARNING] %s' %
-                                         _("Permission denied, saving not possible.\n"
-                                           "Most likely another app is holding the file open and not accessible."))
-                    return 'fail'
-            elif ftype == 'png':
-                try:
-                    doc_final = StringIO(doc_final)
-                    drawing = svg2rlg(doc_final)
-                    renderPM.drawToFile(drawing, filename, 'PNG')
-                except Exception as e:
-                    log.debug("FilmTool.export_positive() --> PNG output --> %s" % str(e))
-                    return 'fail'
-            else:
-                try:
-                    if self.units == 'IN':
-                        unit = inch
-                    else:
-                        unit = mm
+        # ## Reset Tool
+        self.reset_button = FCButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                               QPushButton
+                               {
+                                   font-weight: bold;
+                               }
+                               """)
+        self.layout.addWidget(self.reset_button)
 
-                    doc_final = StringIO(doc_final)
-                    drawing = svg2rlg(doc_final)
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
 
-                    if p_size == 'Bounds':
-                        renderPDF.drawToFile(drawing, filename)
-                    else:
-                        if orientation == 'p':
-                            page_size = portrait(self.pagesize[p_size])
-                        else:
-                            page_size = landscape(self.pagesize[p_size])
+    def on_film_type(self, val):
+        type_of_film = val
 
-                        my_canvas = canvas.Canvas(filename, pagesize=page_size)
-                        my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
-                        renderPDF.draw(drawing, my_canvas, 0, 0)
-                        my_canvas.save()
-                except Exception as e:
-                    log.debug("FilmTool.export_positive() --> PDF output --> %s" % str(e))
-                    return 'fail'
+        if type_of_film == 'neg':
+            self.boundary_label.show()
+            self.boundary_entry.show()
+            self.punch_cb.set_value(False)  # required so the self.punch_frame it's hidden also by the signal emitted
+            self.punch_cb.hide()
+        else:
+            self.boundary_label.hide()
+            self.boundary_entry.hide()
+            self.punch_cb.show()
 
-            if self.app.defaults["global_open_style"] is False:
-                self.app.file_opened.emit("SVG", filename)
-            self.app.file_saved.emit("SVG", filename)
-            self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
+    def on_file_type(self, val):
+        if val == 'pdf':
+            self.orientation_label.show()
+            self.orientation_radio.show()
+            self.pagesize_label.show()
+            self.pagesize_combo.show()
+        else:
+            self.orientation_label.hide()
+            self.orientation_radio.hide()
+            self.pagesize_label.hide()
+            self.pagesize_combo.hide()
 
-        if use_thread is True:
-            proc = self.app.proc_container.new(_("Generating Film ... Please wait."))
+    def on_punch_source(self, val):
+        if val == 'pad' and self.punch_cb.get_value():
+            self.punch_size_label.show()
+            self.punch_size_spinner.show()
+            self.exc_label.hide()
+            self.exc_combo.hide()
+        else:
+            self.punch_size_label.hide()
+            self.punch_size_spinner.hide()
+            self.exc_label.show()
+            self.exc_combo.show()
 
-            def job_thread_film(app_obj):
-                try:
-                    make_positive_film(p_size=p_size, orientation=orientation, color=color,
-                                       transparency_level=transparency_level)
-                except Exception:
-                    proc.done()
-                    return
-                proc.done()
+        if val == 'pad' and self.tf_type_obj_combo.get_value() == 'geo':
+            self.source_punch.set_value('exc')
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Using the Pad center does not work on Geometry objects. "
+                                                          "Only a Gerber object has pads."))
 
-            self.app.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
         else:
-            make_positive_film(p_size=p_size, orientation=orientation, color=color,
-                               transparency_level=transparency_level)
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
 
-    def reset_fields(self):
-        self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 170 - 132
appTools/ToolImage.py

@@ -21,23 +21,165 @@ if '_' not in builtins.__dict__:
 
 class ToolImage(AppTool):
 
-    toolName = _("Image as Object")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
 
         self.app = app
         self.decimals = self.app.decimals
 
-        # Title
-        title_label = QtWidgets.QLabel("%s" % _('Image to PCB'))
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = ImageUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
+
+        # ## Signals
+        self.ui.import_button.clicked.connect(self.on_file_importimage)
+        self.ui.image_type.activated_custom.connect(self.ui.on_image_type)
+
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("ToolImage()")
+
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+
+        AppTool.run(self)
+        self.set_tool_ui()
+
+        self.app.ui.notebook.setTabText(2, _("Image Tool"))
+
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, **kwargs)
+
+    def set_tool_ui(self):
+        # ## Initialize form
+        self.ui.dpi_entry.set_value(96)
+        self.ui.image_type.set_value('black')
+        self.ui.mask_bw_entry.set_value(250)
+        self.ui.mask_r_entry.set_value(250)
+        self.ui.mask_g_entry.set_value(250)
+        self.ui.mask_b_entry.set_value(250)
+
+    def on_file_importimage(self):
+        """
+        Callback for menu item File->Import IMAGE.
+        :param type_of_obj: to import the IMAGE as Geometry or as Gerber
+        :type type_of_obj: str
+        :return: None
+        """
+        mask = []
+        self.app.log.debug("on_file_importimage()")
+
+        _filter = "Image Files(*.BMP *.PNG *.JPG *.JPEG);;" \
+                  "Bitmap File (*.BMP);;" \
+                  "PNG File (*.PNG);;" \
+                  "Jpeg File (*.JPG);;" \
+                  "All Files (*.*)"
+        try:
+            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import IMAGE"),
+                                                                 directory=self.app.get_last_folder(), filter=_filter)
+        except TypeError:
+            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import IMAGE"), filter=filter)
+
+        filename = str(filename)
+        type_obj = self.ui.tf_type_obj_combo.get_value()
+        dpi = self.ui.dpi_entry.get_value()
+        mode = self.ui.image_type.get_value()
+        mask = [
+            self.ui.mask_bw_entry.get_value(),
+            self.ui.mask_r_entry.get_value(),
+            self.ui.mask_g_entry.get_value(),
+            self.ui.mask_b_entry.get_value()
+        ]
+
+        if filename == "":
+            self.app.inform.emit(_("Cancelled."))
+        else:
+            self.app.worker_task.emit({'fcn': self.import_image,
+                                       'params': [filename, type_obj, dpi, mode, mask]})
+
+    def import_image(self, filename, o_type=_("Gerber"), dpi=96, mode='black', mask=None, outname=None):
+        """
+        Adds a new Geometry Object to the projects and populates
+        it with shapes extracted from the SVG file.
+
+        :param filename: Path to the SVG file.
+        :param o_type: type of FlatCAM objeect
+        :param dpi: dot per inch
+        :param mode: black or color
+        :param mask: dictate the level of detail
+        :param outname: name for the resulting file
+        :return:
+        """
+
+        self.app.defaults.report_usage("import_image()")
+
+        if mask is None:
+            mask = [250, 250, 250, 250]
+
+        if o_type is None or o_type == _("Geometry"):
+            obj_type = "geometry"
+        elif o_type == _("Gerber"):
+            obj_type = "gerber"
+        else:
+            self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                 _("Not supported type is picked as parameter. "
+                                   "Only Geometry and Gerber are supported"))
+            return
+
+        def obj_init(geo_obj, app_obj):
+            geo_obj.import_image(filename, units=units, dpi=dpi, mode=mode, mask=mask)
+            geo_obj.multigeo = False
+
+        with self.app.proc_container.new(_("Importing Image")) as proc:
+
+            # Object name
+            name = outname or filename.split('/')[-1].split('\\')[-1]
+            units = self.app.defaults['units']
+
+            self.app.app_obj.new_object(obj_type, name, obj_init)
+
+            # Register recent file
+            self.app.file_opened.emit("image", filename)
+
+            # GUI feedback
+            self.app.inform.emit('[success] %s: %s' % (_("Opened"), filename))
+
+
+class ImageUI:
+
+    toolName = _("Image as Object")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
         self.layout.addWidget(title_label)
 
         # Form Layout
@@ -53,8 +195,8 @@ class ToolImage(AppTool):
 
         self.tf_type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
         self.tf_type_obj_combo_label.setToolTip(
-           _("Specify the type of object to create from the image.\n"
-             "It can be of type: Gerber or Geometry.")
+            _("Specify the type of object to create from the image.\n"
+              "It can be of type: Gerber or Geometry.")
 
         )
         ti_form_layout.addRow(self.tf_type_obj_combo_label, self.tf_type_obj_combo)
@@ -63,7 +205,7 @@ class ToolImage(AppTool):
         self.dpi_entry = FCSpinner(callback=self.confirmation_message_int)
         self.dpi_entry.set_range(0, 99999)
         self.dpi_label = QtWidgets.QLabel('%s:' % _("DPI value"))
-        self.dpi_label.setToolTip(_("Specify a DPI value for the image.") )
+        self.dpi_label.setToolTip(_("Specify a DPI value for the image."))
         ti_form_layout.addRow(self.dpi_label, self.dpi_entry)
 
         self.emty_lbl = QtWidgets.QLabel("")
@@ -150,48 +292,8 @@ class ToolImage(AppTool):
 
         self.on_image_type(val=False)
 
-        # ## Signals
-        self.import_button.clicked.connect(self.on_file_importimage)
-        self.image_type.activated_custom.connect(self.on_image_type)
-
-    def run(self, toggle=True):
-        self.app.defaults.report_usage("ToolImage()")
-
-        if toggle:
-            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-            else:
-                try:
-                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        # if tab is populated with the tool but it does not have the focus, focus on it
-                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
-                            # focus on Tool Tab
-                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-                        else:
-                            self.app.ui.splitter.setSizes([0, 1])
-                except AttributeError:
-                    pass
-        else:
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-
-        AppTool.run(self)
-        self.set_tool_ui()
-
-        self.app.ui.notebook.setTabText(2, _("Image Tool"))
-
-    def install(self, icon=None, separator=None, **kwargs):
-        AppTool.install(self, icon, separator, **kwargs)
-
-    def set_tool_ui(self):
-        # ## Initialize form
-        self.dpi_entry.set_value(96)
-        self.image_type.set_value('black')
-        self.mask_bw_entry.set_value(250)
-        self.mask_r_entry.set_value(250)
-        self.mask_g_entry.set_value(250)
-        self.mask_b_entry.set_value(250)
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
 
     def on_image_type(self, val):
         if val == 'color':
@@ -215,83 +317,19 @@ class ToolImage(AppTool):
             self.mask_bw_label.setDisabled(False)
             self.mask_bw_entry.setDisabled(False)
 
-    def on_file_importimage(self):
-        """
-        Callback for menu item File->Import IMAGE.
-        :param type_of_obj: to import the IMAGE as Geometry or as Gerber
-        :type type_of_obj: str
-        :return: None
-        """
-        mask = []
-        self.app.log.debug("on_file_importimage()")
-
-        _filter = "Image Files(*.BMP *.PNG *.JPG *.JPEG);;" \
-                  "Bitmap File (*.BMP);;" \
-                  "PNG File (*.PNG);;" \
-                  "Jpeg File (*.JPG);;" \
-                  "All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import IMAGE"),
-                                                                 directory=self.app.get_last_folder(), filter=_filter)
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import IMAGE"), filter=filter)
-
-        filename = str(filename)
-        type_obj = self.tf_type_obj_combo.get_value()
-        dpi = self.dpi_entry.get_value()
-        mode = self.image_type.get_value()
-        mask = [self.mask_bw_entry.get_value(), self.mask_r_entry.get_value(), self.mask_g_entry.get_value(),
-                self.mask_b_entry.get_value()]
-
-        if filename == "":
-            self.app.inform.emit(_("Cancelled."))
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
         else:
-            self.app.worker_task.emit({'fcn': self.import_image,
-                                       'params': [filename, type_obj, dpi, mode, mask]})
-
-    def import_image(self, filename, o_type=_("Gerber"), dpi=96, mode='black', mask=None, outname=None):
-        """
-        Adds a new Geometry Object to the projects and populates
-        it with shapes extracted from the SVG file.
-
-        :param filename: Path to the SVG file.
-        :param o_type: type of FlatCAM objeect
-        :param dpi: dot per inch
-        :param mode: black or color
-        :param mask: dictate the level of detail
-        :param outname: name for the resulting file
-        :return:
-        """
-
-        self.app.defaults.report_usage("import_image()")
-
-        if mask is None:
-            mask = [250, 250, 250, 250]
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
 
-        if o_type is None or o_type == _("Geometry"):
-            obj_type = "geometry"
-        elif o_type == _("Gerber"):
-            obj_type = "gerber"
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
         else:
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("Not supported type is picked as parameter. "
-                                   "Only Geometry and Gerber are supported"))
-            return
-
-        def obj_init(geo_obj, app_obj):
-            geo_obj.import_image(filename, units=units, dpi=dpi, mode=mode, mask=mask)
-            geo_obj.multigeo = False
-
-        with self.app.proc_container.new(_("Importing Image")) as proc:
-
-            # Object name
-            name = outname or filename.split('/')[-1].split('\\')[-1]
-            units = self.app.defaults['units']
-
-            self.app.app_obj.new_object(obj_type, name, obj_init)
-
-            # Register recent file
-            self.app.file_opened.emit("image", filename)
-
-            # GUI feedback
-            self.app.inform.emit('[success] %s: %s' % (_("Opened"), filename))
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 162 - 154
appTools/ToolInvertGerber.py

@@ -5,10 +5,10 @@
 # MIT Licence                                              #
 # ##########################################################
 
-from PyQt5 import QtWidgets, QtCore
+from PyQt5 import QtWidgets, QtCore, QtGui
 
 from appTool import AppTool
-from appGUI.GUIElements import FCButton, FCDoubleSpinner, RadioSet, FCComboBox
+from appGUI.GUIElements import FCButton, FCDoubleSpinner, RadioSet, FCComboBox, FCLabel
 
 from shapely.geometry import box
 
@@ -28,132 +28,20 @@ log = logging.getLogger('base')
 
 class ToolInvertGerber(AppTool):
 
-    toolName = _("Invert Gerber Tool")
-
     def __init__(self, app):
         self.app = app
         self.decimals = self.app.decimals
 
         AppTool.__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)
-
-        # Grid Layout
-        grid0 = QtWidgets.QGridLayout()
-        grid0.setColumnStretch(0, 0)
-        grid0.setColumnStretch(1, 1)
-        self.tools_box.addLayout(grid0)
-
-        grid0.addWidget(QtWidgets.QLabel(''), 0, 0, 1, 2)
-
-        # Target Gerber Object
-        self.gerber_combo = FCComboBox()
-        self.gerber_combo.setModel(self.app.collection)
-        self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.gerber_combo.is_last = True
-        self.gerber_combo.obj_type = "Gerber"
-
-        self.gerber_label = QtWidgets.QLabel('<b>%s:</b>' % _("GERBER"))
-        self.gerber_label.setToolTip(
-            _("Gerber object that will be inverted.")
-        )
-
-        grid0.addWidget(self.gerber_label, 1, 0, 1, 2)
-        grid0.addWidget(self.gerber_combo, 2, 0, 1, 2)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 3, 0, 1, 2)
-
-        self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
-        self.param_label.setToolTip('%s.' % _("Parameters for this tool"))
-
-        grid0.addWidget(self.param_label, 4, 0, 1, 2)
-
-        # Margin
-        self.margin_label = QtWidgets.QLabel('%s:' % _('Margin'))
-        self.margin_label.setToolTip(
-            _("Distance by which to avoid\n"
-              "the edges of the Gerber object.")
-        )
-        self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.margin_entry.set_precision(self.decimals)
-        self.margin_entry.set_range(0.0000, 9999.9999)
-        self.margin_entry.setObjectName(_("Margin"))
-
-        grid0.addWidget(self.margin_label, 5, 0, 1, 2)
-        grid0.addWidget(self.margin_entry, 6, 0, 1, 2)
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = InvertUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
-        self.join_label = QtWidgets.QLabel('%s:' % _("Lines Join Style"))
-        self.join_label.setToolTip(
-            _("The way that the lines in the object outline will be joined.\n"
-              "Can be:\n"
-              "- rounded -> an arc is added between two joining lines\n"
-              "- square -> the lines meet in 90 degrees angle\n"
-              "- bevel -> the lines are joined by a third line")
-        )
-        self.join_radio = RadioSet([
-            {'label': 'Rounded', 'value': 'r'},
-            {'label': 'Square', 'value': 's'},
-            {'label': 'Bevel', 'value': 'b'}
-        ], orientation='vertical', stretch=False)
-
-        grid0.addWidget(self.join_label, 7, 0, 1, 2)
-        grid0.addWidget(self.join_radio, 8, 0, 1, 2)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 9, 0, 1, 2)
-
-        self.invert_btn = FCButton(_('Invert Gerber'))
-        self.invert_btn.setToolTip(
-            _("Will invert the Gerber object: areas that have copper\n"
-              "will be empty of copper and previous empty area will be\n"
-              "filled with copper.")
-        )
-        self.invert_btn.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        grid0.addWidget(self.invert_btn, 10, 0, 1, 2)
-
-        self.tools_box.addStretch()
-
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.tools_box.addWidget(self.reset_button)
-
-        self.invert_btn.clicked.connect(self.on_grb_invert)
-        self.reset_button.clicked.connect(self.set_tool_ui)
+        self.ui.invert_btn.clicked.connect(self.on_grb_invert)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
 
     def install(self, icon=None, separator=None, **kwargs):
         AppTool.install(self, icon, separator, shortcut='', **kwargs)
@@ -187,20 +75,20 @@ class ToolInvertGerber(AppTool):
         self.app.ui.notebook.setTabText(2, _("Invert Tool"))
 
     def set_tool_ui(self):
-        self.margin_entry.set_value(float(self.app.defaults["tools_invert_margin"]))
-        self.join_radio.set_value(self.app.defaults["tools_invert_join_style"])
+        self.ui.margin_entry.set_value(float(self.app.defaults["tools_invert_margin"]))
+        self.ui.join_radio.set_value(self.app.defaults["tools_invert_join_style"])
 
     def on_grb_invert(self):
-        margin = self.margin_entry.get_value()
+        margin = self.ui.margin_entry.get_value()
         if round(margin, self.decimals) == 0.0:
             margin = 1E-10
 
-        join_style = {'r': 1, 'b': 3, 's': 2}[self.join_radio.get_value()]
+        join_style = {'r': 1, 'b': 3, 's': 2}[self.ui.join_radio.get_value()]
         if join_style is None:
             join_style = 'r'
 
         grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
-        obj_name = self.gerber_combo.currentText()
+        obj_name = self.ui.gerber_combo.currentText()
 
         outname = obj_name + "_inverted"
 
@@ -242,32 +130,6 @@ class ToolInvertGerber(AppTool):
 
         new_apertures = {}
 
-        # for apid, val in grb_obj.apertures.items():
-        #     new_apertures[apid] = {}
-        #     for key in val:
-        #         if key == 'geometry':
-        #             new_apertures[apid]['geometry'] = []
-        #             for elem in val['geometry']:
-        #                 geo_elem = {}
-        #                 if 'follow' in elem:
-        #                     try:
-        #                         geo_elem['clear'] = elem['follow'].buffer(val['size'] / 2.0).exterior
-        #                     except AttributeError:
-        #                         # TODO should test if width or height is bigger
-        #                         geo_elem['clear'] = elem['follow'].buffer(val['width'] / 2.0).exterior
-        #                 if 'clear' in elem:
-        #                     if isinstance(elem['clear'], Polygon):
-        #                         try:
-        #                             geo_elem['solid'] = elem['clear'].buffer(val['size'] / 2.0, grb_circle_steps)
-        #                         except AttributeError:
-        #                             # TODO should test if width or height is bigger
-        #                             geo_elem['solid'] = elem['clear'].buffer(val['width'] / 2.0, grb_circle_steps)
-        #                     else:
-        #                         geo_elem['follow'] = elem['clear']
-        #                 new_apertures[apid]['geometry'].append(deepcopy(geo_elem))
-        #         else:
-        #             new_apertures[apid][key] = deepcopy(val[key])
-
         if '0' not in new_apertures:
             new_apertures['0'] = {}
             new_apertures['0']['type'] = 'C'
@@ -301,9 +163,155 @@ class ToolInvertGerber(AppTool):
         self.app.app_obj.new_object('gerber', outname, init_func)
 
     def reset_fields(self):
-        self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ui.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
 
     @staticmethod
     def poly2rings(poly):
         return [poly.exterior] + [interior for interior in poly.interiors]
-# end of file
+
+
+class InvertUI:
+    
+    toolName = _("Invert Gerber Tool")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        # ## Title
+        title_label = FCLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(FCLabel(""))
+
+        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)
+
+        # Grid Layout
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.tools_box.addLayout(grid0)
+
+        # Target Gerber Object
+        self.gerber_combo = FCComboBox()
+        self.gerber_combo.setModel(self.app.collection)
+        self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.gerber_combo.is_last = True
+        self.gerber_combo.obj_type = "Gerber"
+
+        self.gerber_label = FCLabel('<b>%s:</b>' % _("GERBER"))
+        self.gerber_label.setToolTip(
+            _("Gerber object that will be inverted.")
+        )
+
+        grid0.addWidget(self.gerber_label, 1, 0, 1, 2)
+        grid0.addWidget(self.gerber_combo, 2, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 3, 0, 1, 2)
+
+        self.param_label = FCLabel("<b>%s:</b>" % _("Parameters"))
+        self.param_label.setToolTip('%s.' % _("Parameters for this tool"))
+
+        grid0.addWidget(self.param_label, 4, 0, 1, 2)
+
+        # Margin
+        self.margin_label = FCLabel('%s:' % _('Margin'))
+        self.margin_label.setToolTip(
+            _("Distance by which to avoid\n"
+              "the edges of the Gerber object.")
+        )
+        self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.margin_entry.set_precision(self.decimals)
+        self.margin_entry.set_range(0.0000, 9999.9999)
+        self.margin_entry.setObjectName(_("Margin"))
+
+        grid0.addWidget(self.margin_label, 5, 0, 1, 2)
+        grid0.addWidget(self.margin_entry, 6, 0, 1, 2)
+
+        self.join_label = FCLabel('%s:' % _("Lines Join Style"))
+        self.join_label.setToolTip(
+            _("The way that the lines in the object outline will be joined.\n"
+              "Can be:\n"
+              "- rounded -> an arc is added between two joining lines\n"
+              "- square -> the lines meet in 90 degrees angle\n"
+              "- bevel -> the lines are joined by a third line")
+        )
+        self.join_radio = RadioSet([
+            {'label': 'Rounded', 'value': 'r'},
+            {'label': 'Square', 'value': 's'},
+            {'label': 'Bevel', 'value': 'b'}
+        ], orientation='vertical', stretch=False)
+
+        grid0.addWidget(self.join_label, 7, 0, 1, 2)
+        grid0.addWidget(self.join_radio, 8, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 9, 0, 1, 2)
+
+        self.invert_btn = FCButton(_('Invert Gerber'))
+        self.invert_btn.setToolTip(
+            _("Will invert the Gerber object: areas that have copper\n"
+              "will be empty of copper and previous empty area will be\n"
+              "filled with copper.")
+        )
+        self.invert_btn.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        grid0.addWidget(self.invert_btn, 10, 0, 1, 2)
+
+        self.tools_box.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = FCButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.tools_box.addWidget(self.reset_button)
+
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

Разница между файлами не показана из-за своего большого размера
+ 502 - 1007
appTools/ToolIsolation.py


+ 2354 - 0
appTools/ToolMilling.py

@@ -0,0 +1,2354 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File by:  Marius Adrian Stanciu (c)                      #
+# Date:     6/15/2020                                      #
+# License:  MIT Licence                                    #
+# ##########################################################
+
+from PyQt5 import QtWidgets, QtCore, QtGui
+
+from appTool import AppTool
+from appGUI.GUIElements import FCCheckBox, FCDoubleSpinner, RadioSet, FCTable, FCButton, \
+    FCComboBox, OptionalInputSection, FCSpinner, NumericalEvalEntry, OptionalHideInputSection, FCLabel
+from appParsers.ParseExcellon import Excellon
+
+from copy import deepcopy
+
+# import numpy as np
+# import math
+
+# from shapely.ops import cascaded_union
+from shapely.geometry import Point, LineString
+
+from matplotlib.backend_bases import KeyEvent as mpl_key_event
+
+import logging
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+settings = QtCore.QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolMilling(AppTool, Excellon):
+
+    def __init__(self, app):
+        self.app = app
+        self.decimals = self.app.decimals
+
+        AppTool.__init__(self, app)
+        Excellon.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"])
+
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = DrillingUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
+
+        # #############################################################################
+        # ########################## VARIABLES ########################################
+        # #############################################################################
+        self.units = ''
+        self.excellon_tools = {}
+        self.tooluid = 0
+
+        # dict that holds the object names and the option name
+        # the key is the object name (defines in ObjectUI) for each UI element that is a parameter
+        # particular for a tool and the value is the actual name of the option that the UI element is changing
+        self.name2option = {}
+
+        # store here the default data for Geometry Data
+        self.default_data = {}
+
+        self.obj_name = ""
+        self.excellon_obj = None
+
+        self.first_click = False
+        self.cursor_pos = None
+        self.mouse_is_dragging = False
+
+        # store here the points for the "Polygon" area selection shape
+        self.points = []
+
+        self.mm = None
+        self.mr = None
+        self.kp = None
+
+        # variable to store the total amount of drills per job
+        self.tot_drill_cnt = 0
+        self.tool_row = 0
+
+        # variable to store the total amount of slots per job
+        self.tot_slot_cnt = 0
+        self.tool_row_slots = 0
+
+        # variable to store the distance travelled
+        self.travel_distance = 0.0
+
+        self.grid_status_memory = self.app.ui.grid_snap_btn.isChecked()
+
+        # store here the state of the exclusion checkbox state to be restored after building the UI
+        # TODO add this in the sel.app.defaults dict and in Preferences
+        self.exclusion_area_cb_is_checked = False
+
+        # store here solid_geometry when there are tool with isolation job
+        self.solid_geometry = []
+
+        self.circle_steps = int(self.app.defaults["geometry_circle_steps"])
+
+        self.tooldia = None
+
+        # multiprocessing
+        self.pool = self.app.pool
+        self.results = []
+
+        # disconnect flags
+        self.area_sel_disconnect_flag = False
+        self.poly_sel_disconnect_flag = False
+
+        self.form_fields = {
+            "excellon_milling_type":   self.ui.milling_type_radio,
+        }
+
+        self.name2option = {
+            "e_milling_type":   "excellon_milling_type",
+        }
+
+        self.old_tool_dia = None
+        self.poly_drawn = False
+        self.connect_signals_at_init()
+
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='Alt+D', **kwargs)
+
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("ToolDrilling()")
+        log.debug("ToolDrilling().run() was launched ...")
+
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+
+        AppTool.run(self)
+        self.set_tool_ui()
+
+        # reset those objects on a new run
+        self.excellon_obj = None
+        self.obj_name = ''
+
+        self.build_ui()
+
+        # all the tools are selected by default
+        self.ui.tools_table.selectAll()
+
+        self.app.ui.notebook.setTabText(2, _("Drilling Tool"))
+
+    def connect_signals_at_init(self):
+        # #############################################################################
+        # ############################ SIGNALS ########################################
+        # #############################################################################
+
+        self.ui.apply_param_to_all.clicked.connect(self.on_apply_param_to_all_clicked)
+        self.ui.generate_cnc_button.clicked.connect(self.on_cnc_button_click)
+        self.ui.tools_table.drag_drop_sig.connect(self.rebuild_ui)
+
+        # Exclusion areas signals
+        self.ui.exclusion_table.horizontalHeader().sectionClicked.connect(self.exclusion_table_toggle_all)
+        self.ui.exclusion_table.lost_focus.connect(self.clear_selection)
+        self.ui.exclusion_table.itemClicked.connect(self.draw_sel_shape)
+        self.ui.add_area_button.clicked.connect(self.on_add_area_click)
+        self.ui.delete_area_button.clicked.connect(self.on_clear_area_click)
+        self.ui.delete_sel_area_button.clicked.connect(self.on_delete_sel_areas)
+        self.ui.strategy_radio.activated_custom.connect(self.on_strategy)
+
+        self.on_operation_type(val='drill')
+        self.ui.operation_radio.activated_custom.connect(self.on_operation_type)
+
+        self.ui.pp_excellon_name_cb.activated.connect(self.on_pp_changed)
+
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
+        # Cleanup on Graceful exit (CTRL+ALT+X combo key)
+        self.app.cleanup.connect(self.set_tool_ui)
+
+    def set_tool_ui(self):
+        self.units = self.app.defaults['units'].upper()
+        self.old_tool_dia = self.app.defaults["tools_iso_newdia"]
+
+        # try to select in the Gerber combobox the active object
+        try:
+            selected_obj = self.app.collection.get_active()
+            if selected_obj.kind == 'excellon':
+                current_name = selected_obj.options['name']
+                self.ui.object_combo.set_value(current_name)
+        except Exception:
+            pass
+
+        self.form_fields.update({
+
+            "operation": self.ui.operation_radio,
+            "milling_type": self.ui.milling_type_radio,
+
+            "milling_dia": self.ui.mill_dia_entry,
+            "cutz": self.ui.cutz_entry,
+            "multidepth": self.ui.mpass_cb,
+            "depthperpass": self.ui.maxdepth_entry,
+            "travelz": self.ui.travelz_entry,
+            "feedrate_z": self.ui.feedrate_z_entry,
+            "feedrate": self.ui.xyfeedrate_entry,
+            "feedrate_rapid": self.ui.feedrate_rapid_entry,
+            # "tooldia": self.ui.tooldia_entry,
+            # "slot_tooldia": self.ui.slot_tooldia_entry,
+            "toolchange": self.ui.toolchange_cb,
+            "toolchangez": self.ui.toolchangez_entry,
+            "extracut": self.ui.extracut_cb,
+            "extracut_length": self.ui.e_cut_entry,
+
+            "spindlespeed": self.ui.spindlespeed_entry,
+            "dwell": self.ui.dwell_cb,
+            "dwelltime": self.ui.dwelltime_entry,
+
+            "startz": self.ui.estartz_entry,
+            "endz": self.ui.endz_entry,
+            "endxy": self.ui.endxy_entry,
+
+            "offset": self.ui.offset_entry,
+
+            "ppname_e": self.ui.pp_excellon_name_cb,
+            "ppname_g": self.ui.pp_geo_name_cb,
+            "z_pdepth": self.ui.pdepth_entry,
+            "feedrate_probe": self.ui.feedrate_probe_entry,
+            # "gcode_type": self.ui.excellon_gcode_type_radio,
+            "area_exclusion": self.ui.exclusion_cb,
+            "area_shape": self.ui.area_shape_radio,
+            "area_strategy": self.ui.strategy_radio,
+            "area_overz": self.ui.over_z_entry,
+        })
+
+        self.name2option = {
+            "e_operation": "operation",
+            "e_milling_type": "milling_type",
+            "e_milling_dia": "milling_dia",
+            "e_cutz": "cutz",
+            "e_multidepth": "multidepth",
+            "e_depthperpass": "depthperpass",
+
+            "e_travelz": "travelz",
+            "e_feedratexy": "feedrate",
+            "e_feedratez": "feedrate_z",
+            "e_fr_rapid": "feedrate_rapid",
+            "e_extracut": "extracut",
+            "e_extracut_length": "extracut_length",
+            "e_spindlespeed": "spindlespeed",
+            "e_dwell": "dwell",
+            "e_dwelltime": "dwelltime",
+            "e_offset": "offset",
+        }
+
+        # populate Excellon preprocessor combobox list
+        for name in list(self.app.preprocessors.keys()):
+            # the HPGL preprocessor is only for Geometry not for Excellon job therefore don't add it
+            if name == 'hpgl':
+                continue
+            self.ui.pp_excellon_name_cb.addItem(name)
+
+        # populate Geometry (milling) preprocessor combobox list
+        for name in list(self.app.preprocessors.keys()):
+            self.ui.pp_geo_name_cb.addItem(name)
+
+        # Fill form fields
+        # self.to_form()
+
+        # update the changes in UI depending on the selected preprocessor in Preferences
+        # after this moment all the changes in the Posprocessor combo will be handled by the activated signal of the
+        # self.ui.pp_excellon_name_cb combobox
+        self.on_pp_changed()
+
+        app_mode = self.app.defaults["global_app_level"]
+        # Show/Hide Advanced Options
+        if app_mode == 'b':
+            self.ui.level.setText('<span style="color:green;"><b>%s</b></span>' % _('Basic'))
+            self.ui.estartz_label.hide()
+            self.ui.estartz_entry.hide()
+            self.ui.feedrate_rapid_label.hide()
+            self.ui.feedrate_rapid_entry.hide()
+            self.ui.pdepth_label.hide()
+            self.ui.pdepth_entry.hide()
+            self.ui.feedrate_probe_label.hide()
+            self.ui.feedrate_probe_entry.hide()
+
+        else:
+            self.ui.level.setText('<span style="color:red;"><b>%s</b></span>' % _('Advanced'))
+
+        self.ui.tools_frame.show()
+
+        self.ui.order_radio.set_value(self.app.defaults["excellon_tool_order"])
+        self.ui.milling_type_radio.set_value(self.app.defaults["excellon_milling_type"])
+
+        loaded_obj = self.app.collection.get_by_name(self.ui.object_combo.get_value())
+        if loaded_obj:
+            outname = loaded_obj.options['name']
+        else:
+            outname = ''
+
+        # init the working variables
+        self.default_data.clear()
+        self.default_data = {
+            "name":                     outname + '_iso',
+            "plot":                     self.app.defaults["excellon_plot"],
+            "solid": False,
+            "multicolored": False,
+
+            "operation": "drill",
+            "milling_type": "drills",
+
+            "milling_dia": 0.04,
+
+            "cutz": -0.1,
+            "multidepth": False,
+            "depthperpass": 0.7,
+            "travelz": 0.1,
+            "feedrate": self.app.defaults["geometry_feedrate"],
+            "feedrate_z": 5.0,
+            "feedrate_rapid": 5.0,
+            "tooldia": 0.1,
+            "slot_tooldia": 0.1,
+            "toolchange": False,
+            "toolchangez": 1.0,
+            "toolchangexy": "0.0, 0.0",
+            "extracut": self.app.defaults["geometry_extracut"],
+            "extracut_length": self.app.defaults["geometry_extracut_length"],
+            "endz": 2.0,
+            "endxy": '',
+
+            "startz": None,
+            "offset": 0.0,
+            "spindlespeed": 0,
+            "dwell": True,
+            "dwelltime": 1000,
+            "ppname_e": 'default',
+            "ppname_g": self.app.defaults["geometry_ppname_g"],
+            "z_pdepth": -0.02,
+            "feedrate_probe": 3.0,
+            "optimization_type": "B",
+        }
+
+        # fill in self.default_data values from self.options
+        for opt_key, opt_val in self.app.options.items():
+            if opt_key.find('excellon_') == 0:
+                self.default_data[opt_key] = deepcopy(opt_val)
+        for opt_key, opt_val in self.app.options.items():
+            if opt_key.find('geometry_') == 0:
+                self.default_data[opt_key] = deepcopy(opt_val)
+
+        self.obj_name = ""
+        self.excellon_obj = None
+
+        self.first_click = False
+        self.cursor_pos = None
+        self.mouse_is_dragging = False
+
+        self.units = self.app.defaults['units'].upper()
+
+        # ########################################
+        # #######3 TEMP SETTINGS #################
+        # ########################################
+        self.ui.operation_radio.set_value("drill")
+        self.ui.operation_radio.setEnabled(False)
+
+        self.on_object_changed()
+        if self.excellon_obj:
+            self.build_ui()
+
+        try:
+            self.ui.object_combo.currentIndexChanged.disconnect()
+        except (AttributeError, TypeError):
+            pass
+        self.ui.object_combo.currentIndexChanged.connect(self.on_object_changed)
+
+    def rebuild_ui(self):
+        # read the table tools uid
+        current_uid_list = []
+        for row in range(self.ui.tools_table.rowCount()):
+            uid = int(self.ui.tools_table.item(row, 3).text())
+            current_uid_list.append(uid)
+
+        new_tools = {}
+        new_uid = 1
+
+        for current_uid in current_uid_list:
+            new_tools[new_uid] = deepcopy(self.iso_tools[current_uid])
+            new_uid += 1
+
+        self.iso_tools = new_tools
+
+        # the tools table changed therefore we need to rebuild it
+        QtCore.QTimer.singleShot(20, self.build_ui)
+
+    def build_ui(self):
+        self.ui_disconnect()
+
+        # updated units
+        self.units = self.app.defaults['units'].upper()
+
+        self.obj_name = self.ui.object_combo.currentText()
+
+        # Get source object.
+        try:
+            self.excellon_obj = self.app.collection.get_by_name(self.obj_name)
+        except Exception as e:
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(self.obj_name)))
+            return "Could not retrieve object: %s with error: %s" % (self.obj_name, str(e))
+
+        if self.excellon_obj:
+            self.ui.exc_param_frame.setDisabled(False)
+
+            tools = [k for k in self.excellon_tools]
+
+        else:
+            tools = []
+
+        n = len(tools)
+        # we have (n+2) rows because there are 'n' tools, each a row, plus the last 2 rows for totals.
+        self.ui.tools_table.setRowCount(n + 2)
+        self.tool_row = 0
+
+        for tool_no in tools:
+
+            drill_cnt = 0  # variable to store the nr of drills per tool
+            slot_cnt = 0  # variable to store the nr of slots per tool
+
+            # Find no of drills for the current tool
+            try:
+                drill_cnt = len(self.excellon_tools[tool_no]["drills"])
+            except KeyError:
+                drill_cnt = 0
+            self.tot_drill_cnt += drill_cnt
+
+            # Find no of slots for the current tool
+            try:
+                slot_cnt = len(self.excellon_tools[tool_no]["slots"])
+            except KeyError:
+                slot_cnt = 0
+            self.tot_slot_cnt += slot_cnt
+
+            # Tool name/id
+            exc_id_item = QtWidgets.QTableWidgetItem('%d' % int(tool_no))
+            exc_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.tools_table.setItem(self.tool_row, 0, exc_id_item)
+
+            # Tool Diameter
+            dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, self.excellon_tools[tool_no]['tooldia']))
+            dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
+            self.ui.tools_table.setItem(self.tool_row, 1, dia_item)
+
+            # Number of drills per tool
+            drill_count_item = QtWidgets.QTableWidgetItem('%d' % drill_cnt)
+            drill_count_item.setFlags(QtCore.Qt.ItemIsEnabled)
+            self.ui.tools_table.setItem(self.tool_row, 2, drill_count_item)
+
+            # Tool unique ID
+            tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tool_no)))
+            # ## REMEMBER: THIS COLUMN IS HIDDEN in UI
+            self.ui.tools_table.setItem(self.tool_row, 3, tool_uid_item)
+
+            # Number of slots per tool
+            # if the slot number is zero is better to not clutter the GUI with zero's so we print a space
+            slot_count_str = '%d' % slot_cnt if slot_cnt > 0 else ''
+            slot_count_item = QtWidgets.QTableWidgetItem(slot_count_str)
+            slot_count_item.setFlags(QtCore.Qt.ItemIsEnabled)
+            self.ui.tools_table.setItem(self.tool_row, 4, slot_count_item)
+
+            self.tool_row += 1
+
+        # add a last row with the Total number of drills
+        empty_1 = QtWidgets.QTableWidgetItem('')
+        empty_1.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        empty_1_1 = QtWidgets.QTableWidgetItem('')
+        empty_1_1.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+        label_tot_drill_count = QtWidgets.QTableWidgetItem(_('Total Drills'))
+        label_tot_drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+        tot_drill_count = QtWidgets.QTableWidgetItem('%d' % self.tot_drill_cnt)
+        tot_drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+        self.ui.tools_table.setItem(self.tool_row, 0, empty_1)
+        self.ui.tools_table.setItem(self.tool_row, 1, label_tot_drill_count)
+        self.ui.tools_table.setItem(self.tool_row, 2, tot_drill_count)  # Total number of drills
+        self.ui.tools_table.setItem(self.tool_row, 4, empty_1_1)
+
+        font = QtGui.QFont()
+        font.setBold(True)
+        font.setWeight(75)
+
+        for k in [1, 2]:
+            self.ui.tools_table.item(self.tool_row, k).setForeground(QtGui.QColor(127, 0, 255))
+            self.ui.tools_table.item(self.tool_row, k).setFont(font)
+
+        self.tool_row += 1
+
+        # add a last row with the Total number of slots
+        empty_2 = QtWidgets.QTableWidgetItem('')
+        empty_2.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        empty_2_1 = QtWidgets.QTableWidgetItem('')
+        empty_2_1.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+        label_tot_slot_count = QtWidgets.QTableWidgetItem(_('Total Slots'))
+        tot_slot_count = QtWidgets.QTableWidgetItem('%d' % self.tot_slot_cnt)
+        label_tot_slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
+        tot_slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+        self.ui.tools_table.setItem(self.tool_row, 0, empty_2)
+        self.ui.tools_table.setItem(self.tool_row, 1, label_tot_slot_count)
+        self.ui.tools_table.setItem(self.tool_row, 2, empty_2_1)
+        self.ui.tools_table.setItem(self.tool_row, 4, tot_slot_count)  # Total number of slots
+
+        for kl in [1, 2, 4]:
+            self.ui.tools_table.item(self.tool_row, kl).setFont(font)
+            self.ui.tools_table.item(self.tool_row, kl).setForeground(QtGui.QColor(0, 70, 255))
+
+        # make the diameter column editable
+        for row in range(self.ui.tools_table.rowCount() - 2):
+            self.ui.tools_table.item(row, 1).setFlags(
+                QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+        self.ui.tools_table.resizeColumnsToContents()
+        self.ui.tools_table.resizeRowsToContents()
+
+        vertical_header = self.ui.tools_table.verticalHeader()
+        vertical_header.hide()
+        self.ui.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.ui.tools_table.horizontalHeader()
+        self.ui.tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
+
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.ResizeToContents)
+
+        self.ui.tools_table.setSortingEnabled(False)
+
+        self.ui.tools_table.setMinimumHeight(self.ui.tools_table.getHeight())
+        self.ui.tools_table.setMaximumHeight(self.ui.tools_table.getHeight())
+
+        # all the tools are selected by default
+        self.ui.tools_table.selectAll()
+
+        # Build Exclusion Areas section
+        e_len = len(self.app.exc_areas.exclusion_areas_storage)
+        self.ui.exclusion_table.setRowCount(e_len)
+
+        area_id = 0
+
+        for area in range(e_len):
+            area_id += 1
+
+            area_dict = self.app.exc_areas.exclusion_areas_storage[area]
+
+            area_id_item = QtWidgets.QTableWidgetItem('%d' % int(area_id))
+            area_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.exclusion_table.setItem(area, 0, area_id_item)  # Area id
+
+            object_item = QtWidgets.QTableWidgetItem('%s' % area_dict["obj_type"])
+            object_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.exclusion_table.setItem(area, 1, object_item)  # Origin Object
+
+            strategy_item = QtWidgets.QTableWidgetItem('%s' % area_dict["strategy"])
+            strategy_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.exclusion_table.setItem(area, 2, strategy_item)  # Strategy
+
+            overz_item = QtWidgets.QTableWidgetItem('%s' % area_dict["overz"])
+            overz_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.exclusion_table.setItem(area, 3, overz_item)  # Over Z
+
+        self.ui.exclusion_table.resizeColumnsToContents()
+        self.ui.exclusion_table.resizeRowsToContents()
+
+        area_vheader = self.ui.exclusion_table.verticalHeader()
+        area_vheader.hide()
+        self.ui.exclusion_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        area_hheader = self.ui.exclusion_table.horizontalHeader()
+        area_hheader.setMinimumSectionSize(10)
+        area_hheader.setDefaultSectionSize(70)
+
+        area_hheader.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        area_hheader.resizeSection(0, 20)
+        area_hheader.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        area_hheader.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        area_hheader.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+
+        # area_hheader.setStretchLastSection(True)
+        self.ui.exclusion_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        self.ui.exclusion_table.setColumnWidth(0, 20)
+
+        self.ui.exclusion_table.setMinimumHeight(self.ui.exclusion_table.getHeight())
+        self.ui.exclusion_table.setMaximumHeight(self.ui.exclusion_table.getHeight())
+
+        self.ui_connect()
+
+        # set the text on tool_data_label after loading the object
+        sel_rows = set()
+        sel_items = self.ui.tools_table.selectedItems()
+        for it in sel_items:
+            sel_rows.add(it.row())
+        if len(sel_rows) > 1:
+            self.ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
+            )
+
+    def on_object_changed(self):
+        # load the Excellon object
+        self.obj_name = self.ui.object_combo.currentText()
+
+        # Get source object.
+        try:
+            self.excellon_obj = self.app.collection.get_by_name(self.obj_name)
+        except Exception as e:
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(self.obj_name)))
+            return
+
+        if self.excellon_obj is None:
+            self.ui.exc_param_frame.setDisabled(True)
+        else:
+            self.ui.exc_param_frame.setDisabled(False)
+            self.excellon_tools = self.excellon_obj.tools
+
+            self.build_ui()
+
+    def ui_connect(self):
+
+        # Area Exception - exclusion shape added signal
+        # first disconnect it from any other object
+        try:
+            self.app.exc_areas.e_shape_modified.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        # then connect it to the current build_ui() method
+        self.app.exc_areas.e_shape_modified.connect(self.update_exclusion_table)
+
+        # rows selected
+        self.ui.tools_table.clicked.connect(self.on_row_selection_change)
+        self.ui.tools_table.horizontalHeader().sectionClicked.connect(self.on_toggle_all_rows)
+
+        # Tool Parameters
+        for opt in self.form_fields:
+            current_widget = self.form_fields[opt]
+            if isinstance(current_widget, FCCheckBox):
+                current_widget.stateChanged.connect(self.form_to_storage)
+            if isinstance(current_widget, RadioSet):
+                current_widget.activated_custom.connect(self.form_to_storage)
+            elif isinstance(current_widget, FCDoubleSpinner) or isinstance(current_widget, FCSpinner):
+                current_widget.returnPressed.connect(self.form_to_storage)
+            elif isinstance(current_widget, FCComboBox):
+                current_widget.currentIndexChanged.connect(self.form_to_storage)
+
+        self.ui.order_radio.activated_custom[str].connect(self.on_order_changed)
+
+    def ui_disconnect(self):
+        # rows selected
+        try:
+            self.ui.tools_table.clicked.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.ui.tools_table.horizontalHeader().sectionClicked.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+        # tool table widgets
+        for row in range(self.ui.tools_table.rowCount()):
+
+            try:
+                self.ui.tools_table.cellWidget(row, 2).currentIndexChanged.disconnect()
+            except (TypeError, AttributeError):
+                pass
+
+        # Tool Parameters
+        for opt in self.form_fields:
+            current_widget = self.form_fields[opt]
+            if isinstance(current_widget, FCCheckBox):
+                try:
+                    current_widget.stateChanged.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+            if isinstance(current_widget, RadioSet):
+                try:
+                    current_widget.activated_custom.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+            elif isinstance(current_widget, FCDoubleSpinner) or isinstance(current_widget, FCSpinner):
+                try:
+                    current_widget.returnPressed.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+            elif isinstance(current_widget, FCComboBox):
+                try:
+                    current_widget.currentIndexChanged.disconnect(self.form_to_storage)
+                except (TypeError, ValueError):
+                    pass
+
+        try:
+            self.ui.order_radio.activated_custom[str].disconnect()
+        except (TypeError, ValueError):
+            pass
+
+    def on_toggle_all_rows(self):
+        """
+        will toggle the selection of all rows in Tools table
+
+        :return:
+        """
+        sel_model = self.ui.tools_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        if len(sel_rows) == self.ui.tools_table.rowCount():
+            self.ui.tools_table.clearSelection()
+            self.ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("No Tool Selected"))
+            )
+        else:
+            self.ui.tools_table.selectAll()
+            self.ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
+            )
+
+    def on_row_selection_change(self):
+        sel_model = self.ui.tools_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        # update UI only if only one row is selected otherwise having multiple rows selected will deform information
+        # for the rows other that the current one (first selected)
+        if len(sel_rows) == 1:
+            self.update_ui()
+
+    def update_ui(self):
+        self.blockSignals(True)
+
+        sel_rows = set()
+        table_items = self.ui.tools_table.selectedItems()
+        if table_items:
+            for it in table_items:
+                sel_rows.add(it.row())
+            # sel_rows = sorted(set(index.row() for index in self.ui.tools_table.selectedIndexes()))
+
+        if not sel_rows or len(sel_rows) == 0:
+            self.ui.generate_cnc_button.setDisabled(True)
+            self.ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("No Tool Selected"))
+            )
+            self.blockSignals(False)
+            return
+        else:
+            self.ui.generate_cnc_button.setDisabled(False)
+
+        if len(sel_rows) == 1:
+            # update the QLabel that shows for which Tool we have the parameters in the UI form
+            tooluid = int(self.ui.tools_table.item(list(sel_rows)[0], 0).text())
+            self.ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s %d</font></b>" % (_('Parameters for'), _("Tool"), tooluid)
+            )
+        else:
+            self.ui.tool_data_label.setText(
+                "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
+            )
+
+        for c_row in sel_rows:
+            # populate the form with the data from the tool associated with the row parameter
+            try:
+                item = self.ui.tools_table.item(c_row, 3)
+                if type(item) is not None:
+                    tooluid = item.text()
+                    self.storage_to_form(self.excellon_tools[str(tooluid)]['data'])
+                else:
+                    self.blockSignals(False)
+                    return
+            except Exception as e:
+                log.debug("Tool missing. Add a tool in the Tool Table. %s" % str(e))
+                self.blockSignals(False)
+                return
+        self.blockSignals(False)
+
+    def storage_to_form(self, dict_storage):
+        """
+        Will update the GUI with data from the "storage" in this case the dict self.tools
+
+        :param dict_storage:    A dictionary holding the data relevant for gnerating Gcode from Excellon
+        :type dict_storage:     dict
+        :return:                None
+        :rtype:
+        """
+        for form_key in self.form_fields:
+            for storage_key in dict_storage:
+                if form_key == storage_key and form_key not in \
+                        ["toolchange", "toolchangez", "startz", "endz", "ppname_e", "ppname_g"]:
+                    try:
+                        self.form_fields[form_key].set_value(dict_storage[form_key])
+                    except Exception as e:
+                        log.debug("ToolDrilling.storage_to_form() --> %s" % str(e))
+                        pass
+
+    def form_to_storage(self):
+        """
+        Will update the 'storage' attribute which is the dict self.tools with data collected from GUI
+
+        :return:    None
+        :rtype:
+        """
+        if self.ui.tools_table.rowCount() == 2:
+            # there is no tool in tool table so we can't save the GUI elements values to storage
+            # Excellon Tool Table has 2 rows by default
+            return
+
+        self.blockSignals(True)
+
+        widget_changed = self.sender()
+        wdg_objname = widget_changed.objectName()
+        option_changed = self.name2option[wdg_objname]
+
+        # row = self.ui.tools_table.currentRow()
+        rows = sorted(set(index.row() for index in self.ui.tools_table.selectedIndexes()))
+        for row in rows:
+            if row < 0:
+                row = 0
+            tooluid_item = int(self.ui.tools_table.item(row, 3).text())
+
+            for tooluid_key, tooluid_val in self.excellon_tools.items():
+                if int(tooluid_key) == tooluid_item:
+                    new_option_value = self.form_fields[option_changed].get_value()
+                    if option_changed in tooluid_val:
+                        tooluid_val[option_changed] = new_option_value
+                    if option_changed in tooluid_val['data']:
+                        tooluid_val['data'][option_changed] = new_option_value
+
+        self.blockSignals(False)
+
+    def on_operation_type(self, val):
+        """
+        Called by a RadioSet activated_custom signal
+
+        :param val:     Parameter passes by the signal that called this method
+        :type val:      str
+        :return:        None
+        :rtype:
+        """
+        if val == 'mill':
+            self.ui.mill_type_label.show()
+            self.ui.milling_type_radio.show()
+            self.ui.mill_dia_label.show()
+            self.ui.mill_dia_entry.show()
+            self.ui.frxylabel.show()
+            self.ui.xyfeedrate_entry.show()
+            self.ui.extracut_cb.show()
+            self.ui.e_cut_entry.show()
+        else:
+            self.ui.mill_type_label.hide()
+            self.ui.milling_type_radio.hide()
+            self.ui.mill_dia_label.hide()
+            self.ui.mill_dia_entry.hide()
+
+            self.ui.frxylabel.hide()
+            self.ui.xyfeedrate_entry.hide()
+            self.ui.extracut_cb.hide()
+            self.ui.e_cut_entry.hide()
+
+    def get_selected_tools_list(self):
+        """
+        Returns the keys to the self.tools dictionary corresponding
+        to the selections on the tool list in the appGUI.
+
+        :return:    List of tools.
+        :rtype:     list
+        """
+
+        return [str(x.text()) for x in self.ui.tools_table.selectedItems()]
+
+    def get_selected_tools_table_items(self):
+        """
+        Returns a list of lists, each list in the list is made out of row elements
+
+        :return:    List of table_tools items.
+        :rtype:     list
+        """
+        table_tools_items = []
+        for x in self.ui.tools_table.selectedItems():
+            # from the columnCount we subtract a value of 1 which represent the last column (plot column)
+            # which does not have text
+            txt = ''
+            elem = []
+
+            for column in range(0, self.ui.tools_table.columnCount() - 1):
+                try:
+                    txt = self.ui.tools_table.item(x.row(), column).text()
+                except AttributeError:
+                    try:
+                        txt = self.ui.tools_table.cellWidget(x.row(), column).currentText()
+                    except AttributeError:
+                        pass
+                elem.append(txt)
+            table_tools_items.append(deepcopy(elem))
+            # table_tools_items.append([self.ui.tools_table.item(x.row(), column).text()
+            #                           for column in range(0, self.ui.tools_table.columnCount() - 1)])
+        for item in table_tools_items:
+            item[0] = str(item[0])
+        return table_tools_items
+
+    def on_apply_param_to_all_clicked(self):
+        if self.ui.tools_table.rowCount() == 0:
+            # there is no tool in tool table so we can't save the GUI elements values to storage
+            log.debug("ToolDrilling.on_apply_param_to_all_clicked() --> no tool in Tools Table, aborting.")
+            return
+
+        self.blockSignals(True)
+
+        row = self.ui.tools_table.currentRow()
+        if row < 0:
+            row = 0
+
+        tooluid_item = int(self.ui.tools_table.item(row, 3).text())
+        temp_tool_data = {}
+
+        for tooluid_key, tooluid_val in self.iso_tools.items():
+            if int(tooluid_key) == tooluid_item:
+                # this will hold the 'data' key of the self.tools[tool] dictionary that corresponds to
+                # the current row in the tool table
+                temp_tool_data = tooluid_val['data']
+                break
+
+        for tooluid_key, tooluid_val in self.iso_tools.items():
+            tooluid_val['data'] = deepcopy(temp_tool_data)
+
+        self.app.inform.emit('[success] %s' % _("Current Tool parameters were applied to all tools."))
+        self.blockSignals(False)
+
+    def on_order_changed(self, order):
+        if order != 'no':
+            self.build_ui()
+
+    def on_tooltable_cellwidget_change(self):
+        cw = self.sender()
+        assert isinstance(cw, QtWidgets.QComboBox), \
+            "Expected a QtWidgets.QComboBox, got %s" % isinstance(cw, QtWidgets.QComboBox)
+
+        cw_index = self.ui.tools_table.indexAt(cw.pos())
+        cw_row = cw_index.row()
+        cw_col = cw_index.column()
+
+        current_uid = int(self.ui.tools_table.item(cw_row, 3).text())
+
+        # if the sender is in the column with index 2 then we update the tool_type key
+        if cw_col == 2:
+            tt = cw.currentText()
+            typ = 'Iso' if tt == 'V' else "Rough"
+
+            self.iso_tools[current_uid].update({
+                'type': typ,
+                'tool_type': tt,
+            })
+
+    def generate_milling_drills(self, tools=None, outname=None, tooldia=None, plot=False, use_thread=False):
+        """
+        Will generate an Geometry Object allowing to cut a drill hole instead of drilling it.
+
+        Note: This method is a good template for generic operations as
+        it takes it's options from parameters or otherwise from the
+        object's options and returns a (success, msg) tuple as feedback
+        for shell operations.
+
+        :param tools:       A list of tools where the drills are to be milled or a string: "all"
+        :type tools:
+        :param outname:     the name of the resulting Geometry object
+        :type outname:      str
+        :param tooldia:     the tool diameter to be used in creation of the milling path (Geometry Object)
+        :type tooldia:      float
+        :param plot:        if to plot the resulting object
+        :type plot:         bool
+        :param use_thread:  if to use threading for creation of the Geometry object
+        :type use_thread:   bool
+        :return:            Success/failure condition tuple (bool, str).
+        :rtype:             tuple
+        """
+
+        # Get the tools from the list. These are keys
+        # to self.tools
+        if tools is None:
+            tools = self.get_selected_tools_list()
+
+        if outname is None:
+            outname = self.options["name"] + "_mill"
+
+        if tooldia is None:
+            tooldia = float(self.options["tooldia"])
+
+        # Sort tools by diameter. items() -> [('name', diameter), ...]
+        # sorted_tools = sorted(list(self.tools.items()), key=lambda tl: tl[1]) # no longer works in Python3
+
+        sort = []
+        for k, v in self.tools.items():
+            sort.append((k, v.get('tooldia')))
+        sorted_tools = sorted(sort, key=lambda t1: t1[1])
+
+        if tools == "all":
+            tools = [i[0] for i in sorted_tools]  # List if ordered tool names.
+            log.debug("Tools 'all' and sorted are: %s" % str(tools))
+
+        if len(tools) == 0:
+            self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                 _("Please select one or more tools from the list and try again."))
+            return False, "Error: No tools."
+
+        for tool in tools:
+            if tooldia > self.tools[tool]["C"]:
+                self.app.inform.emit(
+                    '[ERROR_NOTCL] %s %s: %s' % (
+                        _("Milling tool for DRILLS is larger than hole size. Cancelled."),
+                        _("Tool"),
+                        str(tool)
+                    )
+                )
+                return False, "Error: Milling tool is larger than hole."
+
+        def geo_init(geo_obj, app_obj):
+            """
+
+            :param geo_obj:     New object
+            :type geo_obj:      GeometryObject
+            :param app_obj:     App
+            :type app_obj:      FlatCAMApp.App
+            :return:
+            :rtype:
+            """
+            assert geo_obj.kind == 'geometry', "Initializer expected a GeometryObject, got %s" % type(geo_obj)
+
+            app_obj.inform.emit(_("Generating drills milling geometry..."))
+
+            # ## Add properties to the object
+
+            # get the tool_table items in a list of row items
+            tool_table_items = self.get_selected_tools_table_items()
+            # insert an information only element in the front
+            tool_table_items.insert(0, [_("Tool_nr"), _("Diameter"), _("Drills_Nr"), _("Slots_Nr")])
+
+            geo_obj.options['Tools_in_use'] = tool_table_items
+            geo_obj.options['type'] = 'Excellon Geometry'
+            geo_obj.options["cnctooldia"] = str(tooldia)
+            geo_obj.options["multidepth"] = self.options["multidepth"]
+            geo_obj.solid_geometry = []
+
+            # in case that the tool used has the same diameter with the hole, and since the maximum resolution
+            # for FlatCAM is 6 decimals,
+            # we add a tenth of the minimum value, meaning 0.0000001, which from our point of view is "almost zero"
+            for hole in self.drills:
+                if hole['tool'] in tools:
+                    buffer_value = self.tools[hole['tool']]["C"] / 2 - tooldia / 2
+                    if buffer_value == 0:
+                        geo_obj.solid_geometry.append(
+                            Point(hole['point']).buffer(0.0000001).exterior)
+                    else:
+                        geo_obj.solid_geometry.append(
+                            Point(hole['point']).buffer(buffer_value).exterior)
+
+        if use_thread:
+            def geo_thread(a_obj):
+                a_obj.app_obj.new_object("geometry", outname, geo_init, plot=plot)
+
+            # Create a promise with the new name
+            self.app.collection.promise(outname)
+
+            # Send to worker
+            self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]})
+        else:
+            self.app.app_obj.new_object("geometry", outname, geo_init, plot=plot)
+
+        return True, ""
+
+    def generate_milling_slots(self, tools=None, outname=None, tooldia=None, plot=False, use_thread=False):
+        """
+        Will generate an Geometry Object allowing to cut/mill a slot hole.
+
+        Note: This method is a good template for generic operations as
+        it takes it's options from parameters or otherwise from the
+        object's options and returns a (success, msg) tuple as feedback
+        for shell operations.
+
+        :param tools:       A list of tools where the drills are to be milled or a string: "all"
+        :type tools:
+        :param outname:     the name of the resulting Geometry object
+        :type outname:      str
+        :param tooldia:     the tool diameter to be used in creation of the milling path (Geometry Object)
+        :type tooldia:      float
+        :param plot:        if to plot the resulting object
+        :type plot:         bool
+        :param use_thread:  if to use threading for creation of the Geometry object
+        :type use_thread:   bool
+        :return:            Success/failure condition tuple (bool, str).
+        :rtype:             tuple
+        """
+
+        # Get the tools from the list. These are keys
+        # to self.tools
+        if tools is None:
+            tools = self.get_selected_tools_list()
+
+        if outname is None:
+            outname = self.options["name"] + "_mill"
+
+        if tooldia is None:
+            tooldia = float(self.options["slot_tooldia"])
+
+        # Sort tools by diameter. items() -> [('name', diameter), ...]
+        # sorted_tools = sorted(list(self.tools.items()), key=lambda tl: tl[1]) # no longer works in Python3
+
+        sort = []
+        for k, v in self.tools.items():
+            sort.append((k, v.get('tooldia')))
+        sorted_tools = sorted(sort, key=lambda t1: t1[1])
+
+        if tools == "all":
+            tools = [i[0] for i in sorted_tools]  # List if ordered tool names.
+            log.debug("Tools 'all' and sorted are: %s" % str(tools))
+
+        if len(tools) == 0:
+            self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                 _("Please select one or more tools from the list and try again."))
+            return False, "Error: No tools."
+
+        for tool in tools:
+            # I add the 0.0001 value to account for the rounding error in converting from IN to MM and reverse
+            adj_toolstable_tooldia = float('%.*f' % (self.decimals, float(tooldia)))
+            adj_file_tooldia = float('%.*f' % (self.decimals, float(self.tools[tool]["C"])))
+            if adj_toolstable_tooldia > adj_file_tooldia + 0.0001:
+                self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                     _("Milling tool for SLOTS is larger than hole size. Cancelled."))
+                return False, "Error: Milling tool is larger than hole."
+
+        def geo_init(geo_obj, app_obj):
+            assert geo_obj.kind == 'geometry', "Initializer expected a GeometryObject, got %s" % type(geo_obj)
+
+            app_obj.inform.emit(_("Generating slot milling geometry..."))
+
+            # ## Add properties to the object
+            # get the tool_table items in a list of row items
+            tool_table_items = self.get_selected_tools_table_items()
+            # insert an information only element in the front
+            tool_table_items.insert(0, [_("Tool_nr"), _("Diameter"), _("Drills_Nr"), _("Slots_Nr")])
+
+            geo_obj.options['Tools_in_use'] = tool_table_items
+            geo_obj.options['type'] = 'Excellon Geometry'
+            geo_obj.options["cnctooldia"] = str(tooldia)
+            geo_obj.options["multidepth"] = self.options["multidepth"]
+            geo_obj.solid_geometry = []
+
+            # in case that the tool used has the same diameter with the hole, and since the maximum resolution
+            # for FlatCAM is 6 decimals,
+            # we add a tenth of the minimum value, meaning 0.0000001, which from our point of view is "almost zero"
+            for slot in self.slots:
+                if slot['tool'] in tools:
+                    toolstable_tool = float('%.*f' % (self.decimals, float(tooldia)))
+                    file_tool = float('%.*f' % (self.decimals, float(self.tools[tool]["C"])))
+
+                    # I add the 0.0001 value to account for the rounding error in converting from IN to MM and reverse
+                    # for the file_tool (tooldia actually)
+                    buffer_value = float(file_tool / 2) - float(toolstable_tool / 2) + 0.0001
+                    if buffer_value == 0:
+                        start = slot['start']
+                        stop = slot['stop']
+
+                        lines_string = LineString([start, stop])
+                        poly = lines_string.buffer(0.0000001, int(self.geo_steps_per_circle)).exterior
+                        geo_obj.solid_geometry.append(poly)
+                    else:
+                        start = slot['start']
+                        stop = slot['stop']
+
+                        lines_string = LineString([start, stop])
+                        poly = lines_string.buffer(buffer_value, int(self.geo_steps_per_circle)).exterior
+                        geo_obj.solid_geometry.append(poly)
+
+        if use_thread:
+            def geo_thread(a_obj):
+                a_obj.app_obj.new_object("geometry", outname + '_slot', geo_init, plot=plot)
+
+            # Create a promise with the new name
+            self.app.collection.promise(outname)
+
+            # Send to worker
+            self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]})
+        else:
+            self.app.app_obj.new_object("geometry", outname + '_slot', geo_init, plot=plot)
+
+        return True, ""
+
+    def on_pp_changed(self):
+        current_pp = self.ui.pp_excellon_name_cb.get_value()
+
+        if "toolchange_probe" in current_pp.lower():
+            self.ui.pdepth_entry.setVisible(True)
+            self.ui.pdepth_label.show()
+
+            self.ui.feedrate_probe_entry.setVisible(True)
+            self.ui.feedrate_probe_label.show()
+        else:
+            self.ui.pdepth_entry.setVisible(False)
+            self.ui.pdepth_label.hide()
+
+            self.ui.feedrate_probe_entry.setVisible(False)
+            self.ui.feedrate_probe_label.hide()
+
+        if 'marlin' in current_pp.lower() or 'custom' in current_pp.lower():
+            self.ui.feedrate_rapid_label.show()
+            self.ui.feedrate_rapid_entry.show()
+        else:
+            self.ui.feedrate_rapid_label.hide()
+            self.ui.feedrate_rapid_entry.hide()
+
+        if 'laser' in current_pp.lower():
+            self.ui.cutzlabel.hide()
+            self.ui.cutz_entry.hide()
+            try:
+                self.ui.mpass_cb.hide()
+                self.ui.maxdepth_entry.hide()
+            except AttributeError:
+                pass
+
+            if 'marlin' in current_pp.lower():
+                self.ui.travelzlabel.setText('%s:' % _("Focus Z"))
+                self.ui.endz_label.show()
+                self.ui.endz_entry.show()
+            else:
+                self.ui.travelzlabel.hide()
+                self.ui.travelz_entry.hide()
+
+                self.ui.endz_label.hide()
+                self.ui.endz_entry.hide()
+
+            try:
+                self.ui.frzlabel.hide()
+                self.ui.feedrate_z_entry.hide()
+            except AttributeError:
+                pass
+
+            self.ui.dwell_cb.hide()
+            self.ui.dwelltime_entry.hide()
+
+            self.ui.spindle_label.setText('%s:' % _("Laser Power"))
+
+            try:
+                self.ui.tool_offset_label.hide()
+                self.ui.offset_entry.hide()
+            except AttributeError:
+                pass
+        else:
+            self.ui.cutzlabel.show()
+            self.ui.cutz_entry.show()
+            try:
+                self.ui.mpass_cb.show()
+                self.ui.maxdepth_entry.show()
+            except AttributeError:
+                pass
+
+            self.ui.travelzlabel.setText('%s:' % _('Travel Z'))
+
+            self.ui.travelzlabel.show()
+            self.ui.travelz_entry.show()
+
+            self.ui.endz_label.show()
+            self.ui.endz_entry.show()
+
+            try:
+                self.ui.frzlabel.show()
+                self.ui.feedrate_z_entry.show()
+            except AttributeError:
+                pass
+            self.ui.dwell_cb.show()
+            self.ui.dwelltime_entry.show()
+
+            self.ui.spindle_label.setText('%s:' % _('Spindle speed'))
+
+            try:
+                # self.ui.tool_offset_lbl.show()
+                self.ui.offset_entry.show()
+            except AttributeError:
+                pass
+
+    def on_cnc_button_click(self):
+        self.obj_name = self.ui.object_combo.currentText()
+
+        # Get source object.
+        try:
+            self.excellon_obj = self.app.collection.get_by_name(self.obj_name)
+        except Exception as e:
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(self.obj_name)))
+            return "Could not retrieve object: %s with error: %s" % (self.obj_name, str(e))
+
+        if self.excellon_obj is None:
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(self.obj_name)))
+            return
+
+        # Get the tools from the list
+        tools = self.get_selected_tools_list()
+
+        if len(tools) == 0:
+            # if there is a single tool in the table (remember that the last 2 rows are for totals and do not count in
+            # tool number) it means that there are 3 rows (1 tool and 2 totals).
+            # in this case regardless of the selection status of that tool, use it.
+            if self.ui.tools_table.rowCount() == 3:
+                tools.append(self.ui.tools_table.item(0, 0).text())
+            else:
+                self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                     _("Please select one or more tools from the list and try again."))
+                return
+
+        xmin = self.options['xmin']
+        ymin = self.options['ymin']
+        xmax = self.options['xmax']
+        ymax = self.options['ymax']
+
+        job_name = self.options["name"] + "_cnc"
+        pp_excellon_name = self.options["ppname_e"]
+
+        # Object initialization function for app.app_obj.new_object()
+        def job_init(job_obj, app_obj):
+            assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
+
+            app_obj.inform.emit(_("Generating Excellon CNCJob..."))
+
+            # get the tool_table items in a list of row items
+            tool_table_items = self.get_selected_tools_table_items()
+            # insert an information only element in the front
+            tool_table_items.insert(0, [_("Tool_nr"), _("Diameter"), _("Drills_Nr"), _("Slots_Nr")])
+
+            # ## Add properties to the object
+
+            job_obj.origin_kind = 'excellon'
+
+            job_obj.options['Tools_in_use'] = tool_table_items
+            job_obj.options['type'] = 'Excellon'
+            job_obj.options['ppname_e'] = pp_excellon_name
+
+            job_obj.multidepth = self.options["multidepth"]
+            job_obj.z_depthpercut = self.options["depthperpass"]
+
+            job_obj.z_move = float(self.options["travelz"])
+            job_obj.feedrate = float(self.options["feedrate_z"])
+            job_obj.z_feedrate = float(self.options["feedrate_z"])
+            job_obj.feedrate_rapid = float(self.options["feedrate_rapid"])
+
+            job_obj.spindlespeed = float(self.options["spindlespeed"]) if self.options["spindlespeed"] != 0 else None
+            job_obj.spindledir = self.app.defaults['excellon_spindledir']
+            job_obj.dwell = self.options["dwell"]
+            job_obj.dwelltime = float(self.options["dwelltime"])
+
+            job_obj.pp_excellon_name = pp_excellon_name
+
+            job_obj.toolchange_xy_type = "excellon"
+            job_obj.coords_decimals = int(self.app.defaults["cncjob_coords_decimals"])
+            job_obj.fr_decimals = int(self.app.defaults["cncjob_fr_decimals"])
+
+            job_obj.options['xmin'] = xmin
+            job_obj.options['ymin'] = ymin
+            job_obj.options['xmax'] = xmax
+            job_obj.options['ymax'] = ymax
+
+            job_obj.z_pdepth = float(self.options["z_pdepth"])
+            job_obj.feedrate_probe = float(self.options["feedrate_probe"])
+
+            job_obj.z_cut = float(self.options['cutz'])
+            job_obj.toolchange = self.options["toolchange"]
+            job_obj.xy_toolchange = self.app.defaults["excellon_toolchangexy"]
+            job_obj.z_toolchange = float(self.options["toolchangez"])
+            job_obj.startz = float(self.options["startz"]) if self.options["startz"] else None
+            job_obj.endz = float(self.options["endz"])
+            job_obj.xy_end = self.options["endxy"]
+            job_obj.excellon_optimization_type = self.app.defaults["excellon_optimization_type"]
+
+            tools_csv = ','.join(tools)
+            ret_val = job_obj.generate_from_excellon_by_tool(self, tools_csv, use_ui=True)
+
+            if ret_val == 'fail':
+                return 'fail'
+
+            job_obj.gcode_parse()
+            job_obj.create_geometry()
+
+        # To be run in separate thread
+        def job_thread(a_obj):
+            with self.app.proc_container.new(_("Generating CNC Code")):
+                a_obj.app_obj.new_object("cncjob", job_name, job_init)
+
+        # Create promise for the new name.
+        self.app.collection.promise(job_name)
+
+        # Send to worker
+        # self.app.worker.add_task(job_thread, [self.app])
+        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+
+    def drilling_handler(self, obj):
+        pass
+
+    def on_key_press(self, event):
+        # modifiers = QtWidgets.QApplication.keyboardModifiers()
+        # matplotlib_key_flag = False
+
+        # events out of the self.app.collection view (it's about Project Tab) are of type int
+        if type(event) is int:
+            key = event
+        # events from the GUI are of type QKeyEvent
+        elif type(event) == QtGui.QKeyEvent:
+            key = event.key()
+        elif isinstance(event, mpl_key_event):  # MatPlotLib key events are trickier to interpret than the rest
+            # matplotlib_key_flag = True
+
+            key = event.key
+            key = QtGui.QKeySequence(key)
+
+            # check for modifiers
+            key_string = key.toString().lower()
+            if '+' in key_string:
+                mod, __, key_text = key_string.rpartition('+')
+                if mod.lower() == 'ctrl':
+                    # modifiers = QtCore.Qt.ControlModifier
+                    pass
+                elif mod.lower() == 'alt':
+                    # modifiers = QtCore.Qt.AltModifier
+                    pass
+                elif mod.lower() == 'shift':
+                    # modifiers = QtCore.Qt.ShiftModifier
+                    pass
+                else:
+                    # modifiers = QtCore.Qt.NoModifier
+                    pass
+                key = QtGui.QKeySequence(key_text)
+
+        # events from Vispy are of type KeyEvent
+        else:
+            key = event.key
+
+        if key == QtCore.Qt.Key_Escape or key == 'Escape':
+            self.points = []
+            self.poly_drawn = False
+            self.delete_moving_selection_shape()
+            self.delete_tool_selection_shape()
+
+    def on_add_area_click(self):
+        shape_button = self.ui.area_shape_radio
+        overz_button = self.ui.over_z_entry
+        strategy_radio = self.ui.strategy_radio
+        cnc_button = self.ui.generate_cnc_button
+        solid_geo = self.solid_geometry
+        obj_type = self.kind
+
+        self.app.exc_areas.on_add_area_click(
+            shape_button=shape_button, overz_button=overz_button, cnc_button=cnc_button, strategy_radio=strategy_radio,
+            solid_geo=solid_geo, obj_type=obj_type)
+
+    def on_clear_area_click(self):
+        if not self.app.exc_areas.exclusion_areas_storage:
+            self.app.inform.emit("[WARNING_NOTCL] %s" % _("Delete failed. There are no exclusion areas to delete."))
+            return
+
+        self.app.exc_areas.on_clear_area_click()
+        self.app.exc_areas.e_shape_modified.emit()
+
+    def on_delete_sel_areas(self):
+        sel_model = self.ui.exclusion_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        # so the duplicate rows will not be added
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        if not sel_rows:
+            self.app.inform.emit("[WARNING_NOTCL] %s" % _("Delete failed. Nothing is selected."))
+            return
+
+        self.app.exc_areas.delete_sel_shapes(idxs=list(sel_rows))
+        self.app.exc_areas.e_shape_modified.emit()
+
+    def draw_sel_shape(self):
+        sel_model = self.ui.exclusion_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        self.delete_sel_shape()
+
+        if self.app.is_legacy is False:
+            face = self.app.defaults['global_sel_fill'][:-2] + str(hex(int(0.2 * 255)))[2:]
+            outline = self.app.defaults['global_sel_line'][:-2] + str(hex(int(0.8 * 255)))[2:]
+        else:
+            face = self.app.defaults['global_sel_fill'][:-2] + str(hex(int(0.4 * 255)))[2:]
+            outline = self.app.defaults['global_sel_line'][:-2] + str(hex(int(1.0 * 255)))[2:]
+
+        for row in sel_rows:
+            sel_rect = self.app.exc_areas.exclusion_areas_storage[row]['shape']
+            self.app.move_tool.sel_shapes.add(sel_rect, color=outline, face_color=face, update=True, layer=0,
+                                              tolerance=None)
+        if self.app.is_legacy is True:
+            self.app.move_tool.sel_shapes.redraw()
+
+    def clear_selection(self):
+        self.app.delete_selection_shape()
+        # self.ui.exclusion_table.clearSelection()
+
+    def delete_sel_shape(self):
+        self.app.delete_selection_shape()
+
+    def update_exclusion_table(self):
+        self.exclusion_area_cb_is_checked = True if self.ui.exclusion_cb.isChecked() else False
+
+        self.build_ui()
+        self.ui.exclusion_cb.set_value(self.exclusion_area_cb_is_checked)
+
+    def on_strategy(self, val):
+        if val == 'around':
+            self.ui.over_z_label.setDisabled(True)
+            self.ui.over_z_entry.setDisabled(True)
+        else:
+            self.ui.over_z_label.setDisabled(False)
+            self.ui.over_z_entry.setDisabled(False)
+
+    def exclusion_table_toggle_all(self):
+        """
+        will toggle the selection of all rows in Exclusion Areas table
+
+        :return:
+        """
+        sel_model = self.ui.exclusion_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        if sel_rows:
+            self.ui.exclusion_table.clearSelection()
+            self.delete_sel_shape()
+        else:
+            self.ui.exclusion_table.selectAll()
+            self.draw_sel_shape()
+
+    def reset_fields(self):
+        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+
+
+class MillingUI:
+
+    toolName = _("Milling Holes Tool")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        self.tools_frame = QtWidgets.QFrame()
+        self.tools_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.tools_frame)
+        self.tools_box = QtWidgets.QVBoxLayout()
+        self.tools_box.setContentsMargins(0, 0, 0, 0)
+        self.tools_frame.setLayout(self.tools_box)
+
+        self.title_box = QtWidgets.QHBoxLayout()
+        self.tools_box.addLayout(self.title_box)
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        title_label.setToolTip(
+            _("Create CNCJob with toolpaths for drilling or milling holes.")
+        )
+
+        self.title_box.addWidget(title_label)
+
+        # App Level label
+        self.level = QtWidgets.QLabel("")
+        self.level.setToolTip(
+            _(
+                "BASIC is suitable for a beginner. Many parameters\n"
+                "are hidden from the user in this mode.\n"
+                "ADVANCED mode will make available all parameters.\n\n"
+                "To change the application LEVEL, go to:\n"
+                "Edit -> Preferences -> General and check:\n"
+                "'APP. LEVEL' radio button."
+            )
+        )
+        self.level.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.title_box.addWidget(self.level)
+
+        # Grid Layout
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.tools_box.addLayout(grid0)
+
+        self.obj_combo_label = QtWidgets.QLabel('<b>%s</b>:' % _("EXCELLON"))
+        self.obj_combo_label.setToolTip(
+            _("Excellon object for drilling/milling operation.")
+        )
+
+        grid0.addWidget(self.obj_combo_label, 0, 0, 1, 2)
+
+        # ################################################
+        # ##### The object to be drilled #################
+        # ################################################
+        self.object_combo = FCComboBox()
+        self.object_combo.setModel(self.app.collection)
+        self.object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        # self.object_combo.setCurrentIndex(1)
+        self.object_combo.is_last = True
+
+        grid0.addWidget(self.object_combo, 1, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 2, 0, 1, 2)
+
+        # ################################################
+        # ########## Excellon Tool Table #################
+        # ################################################
+        self.tools_table = FCTable(drag_drop=True)
+        grid0.addWidget(self.tools_table, 3, 0, 1, 2)
+
+        self.tools_table.setColumnCount(5)
+        self.tools_table.setColumnHidden(3, True)
+        self.tools_table.setSortingEnabled(False)
+
+        self.tools_table.setHorizontalHeaderLabels(['#', _('Diameter'), _('Drills'), '', _('Slots')])
+        self.tools_table.horizontalHeaderItem(0).setToolTip(
+            _("This is the Tool Number.\n"
+              "When ToolChange is checked, on toolchange event this value\n"
+              "will be showed as a T1, T2 ... Tn in the Machine Code.\n\n"
+              "Here the tools are selected for G-code generation."))
+        self.tools_table.horizontalHeaderItem(1).setToolTip(
+            _("Tool Diameter. It's value (in current FlatCAM units) \n"
+              "is the cut width into the material."))
+        self.tools_table.horizontalHeaderItem(2).setToolTip(
+            _("The number of Drill holes. Holes that are drilled with\n"
+              "a drill bit."))
+        self.tools_table.horizontalHeaderItem(3).setToolTip(
+            _("The number of Slot holes. Holes that are created by\n"
+              "milling them with an endmill bit."))
+
+        # Tool order
+        self.order_label = QtWidgets.QLabel('%s:' % _('Tool order'))
+        self.order_label.setToolTip(_("This set the way that the tools in the tools table are used.\n"
+                                      "'No' --> means that the used order is the one in the tool table\n"
+                                      "'Forward' --> means that the tools will be ordered from small to big\n"
+                                      "'Reverse' --> means that the tools will ordered from big to small\n\n"
+                                      "WARNING: using rest machining will automatically set the order\n"
+                                      "in reverse and disable this control."))
+
+        self.order_radio = RadioSet([{'label': _('No'), 'value': 'no'},
+                                     {'label': _('Forward'), 'value': 'fwd'},
+                                     {'label': _('Reverse'), 'value': 'rev'}])
+
+        grid0.addWidget(self.order_label, 4, 0)
+        grid0.addWidget(self.order_radio, 4, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 5, 0, 1, 2)
+
+        # ###########################################################
+        # ############# Create CNC Job ##############################
+        # ###########################################################
+        self.tool_data_label = QtWidgets.QLabel(
+            "<b>%s: <font color='#0000FF'>%s %d</font></b>" % (_('Parameters for'), _("Tool"), int(1)))
+        self.tool_data_label.setToolTip(
+            _(
+                "The data used for creating GCode.\n"
+                "Each tool store it's own set of such data."
+            )
+        )
+        grid0.addWidget(self.tool_data_label, 6, 0, 1, 2)
+
+        self.exc_param_frame = QtWidgets.QFrame()
+        self.exc_param_frame.setContentsMargins(0, 0, 0, 0)
+        grid0.addWidget(self.exc_param_frame, 7, 0, 1, 2)
+
+        self.exc_tools_box = QtWidgets.QVBoxLayout()
+        self.exc_tools_box.setContentsMargins(0, 0, 0, 0)
+        self.exc_param_frame.setLayout(self.exc_tools_box)
+
+        # #################################################################
+        # ################# GRID LAYOUT 3   ###############################
+        # #################################################################
+
+        self.grid1 = QtWidgets.QGridLayout()
+        self.grid1.setColumnStretch(0, 0)
+        self.grid1.setColumnStretch(1, 1)
+        self.exc_tools_box.addLayout(self.grid1)
+
+        # Operation Type
+        self.operation_label = QtWidgets.QLabel('<b>%s:</b>' % _('Operation'))
+        self.operation_label.setToolTip(
+            _("Operation type:\n"
+              "- Drilling -> will drill the drills/slots associated with this tool\n"
+              "- Milling -> will mill the drills/slots")
+        )
+        self.operation_radio = RadioSet(
+            [
+                {'label': _('Drilling'), 'value': 'drill'},
+                {'label': _("Milling"), 'value': 'mill'}
+            ]
+        )
+        self.operation_radio.setObjectName("e_operation")
+
+        self.grid1.addWidget(self.operation_label, 0, 0)
+        self.grid1.addWidget(self.operation_radio, 0, 1)
+
+        # separator_line = QtWidgets.QFrame()
+        # separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        # separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        # self.grid3.addWidget(separator_line, 1, 0, 1, 2)
+
+        self.mill_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
+        self.mill_type_label.setToolTip(
+            _("Milling type:\n"
+              "- Drills -> will mill the drills associated with this tool\n"
+              "- Slots -> will mill the slots associated with this tool\n"
+              "- Both -> will mill both drills and mills or whatever is available")
+        )
+        self.milling_type_radio = RadioSet(
+            [
+                {'label': _('Drills'), 'value': 'drills'},
+                {'label': _("Slots"), 'value': 'slots'},
+                {'label': _("Both"), 'value': 'both'},
+            ]
+        )
+        self.milling_type_radio.setObjectName("e_milling_type")
+
+        self.grid1.addWidget(self.mill_type_label, 2, 0)
+        self.grid1.addWidget(self.milling_type_radio, 2, 1)
+
+        self.mill_dia_label = QtWidgets.QLabel('%s:' % _('Milling Diameter'))
+        self.mill_dia_label.setToolTip(
+            _("The diameter of the tool who will do the milling")
+        )
+
+        self.mill_dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.mill_dia_entry.set_precision(self.decimals)
+        self.mill_dia_entry.set_range(0.0000, 9999.9999)
+        self.mill_dia_entry.setObjectName("e_milling_dia")
+
+        self.grid1.addWidget(self.mill_dia_label, 3, 0)
+        self.grid1.addWidget(self.mill_dia_entry, 3, 1)
+
+        # Cut Z
+        self.cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
+        self.cutzlabel.setToolTip(
+            _("Drill depth (negative)\n"
+              "below the copper surface.")
+        )
+
+        self.cutz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.cutz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.cutz_entry.set_range(-9999.9999, 0.0000)
+        else:
+            self.cutz_entry.set_range(-9999.9999, 9999.9999)
+
+        self.cutz_entry.setSingleStep(0.1)
+        self.cutz_entry.setObjectName("e_cutz")
+
+        self.grid1.addWidget(self.cutzlabel, 4, 0)
+        self.grid1.addWidget(self.cutz_entry, 4, 1)
+
+        # Multi-Depth
+        self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth"))
+        self.mpass_cb.setToolTip(
+            _(
+                "Use multiple passes to limit\n"
+                "the cut depth in each pass. Will\n"
+                "cut multiple times until Cut Z is\n"
+                "reached."
+            )
+        )
+        self.mpass_cb.setObjectName("e_multidepth")
+
+        self.maxdepth_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.maxdepth_entry.set_precision(self.decimals)
+        self.maxdepth_entry.set_range(0, 9999.9999)
+        self.maxdepth_entry.setSingleStep(0.1)
+
+        self.maxdepth_entry.setToolTip(_("Depth of each pass (positive)."))
+        self.maxdepth_entry.setObjectName("e_depthperpass")
+
+        self.mis_mpass_geo = OptionalInputSection(self.mpass_cb, [self.maxdepth_entry])
+
+        self.grid1.addWidget(self.mpass_cb, 5, 0)
+        self.grid1.addWidget(self.maxdepth_entry, 5, 1)
+
+        # Travel Z (z_move)
+        self.travelzlabel = QtWidgets.QLabel('%s:' % _('Travel Z'))
+        self.travelzlabel.setToolTip(
+            _("Tool height when travelling\n"
+              "across the XY plane.")
+        )
+
+        self.travelz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.travelz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.travelz_entry.set_range(0.00001, 9999.9999)
+        else:
+            self.travelz_entry.set_range(-9999.9999, 9999.9999)
+
+        self.travelz_entry.setSingleStep(0.1)
+        self.travelz_entry.setObjectName("e_travelz")
+
+        self.grid1.addWidget(self.travelzlabel, 6, 0)
+        self.grid1.addWidget(self.travelz_entry, 6, 1)
+
+        # Feedrate X-Y
+        self.frxylabel = QtWidgets.QLabel('%s:' % _('Feedrate X-Y'))
+        self.frxylabel.setToolTip(
+            _("Cutting speed in the XY\n"
+              "plane in units per minute")
+        )
+        self.xyfeedrate_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.xyfeedrate_entry.set_precision(self.decimals)
+        self.xyfeedrate_entry.set_range(0, 9999.9999)
+        self.xyfeedrate_entry.setSingleStep(0.1)
+        self.xyfeedrate_entry.setObjectName("e_feedratexy")
+
+        self.grid1.addWidget(self.frxylabel, 12, 0)
+        self.grid1.addWidget(self.xyfeedrate_entry, 12, 1)
+
+        # Excellon Feedrate Z
+        self.frzlabel = QtWidgets.QLabel('%s:' % _('Feedrate Z'))
+        self.frzlabel.setToolTip(
+            _("Tool speed while drilling\n"
+              "(in units per minute).\n"
+              "So called 'Plunge' feedrate.\n"
+              "This is for linear move G01.")
+        )
+        self.feedrate_z_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.feedrate_z_entry.set_precision(self.decimals)
+        self.feedrate_z_entry.set_range(0.0, 99999.9999)
+        self.feedrate_z_entry.setSingleStep(0.1)
+        self.feedrate_z_entry.setObjectName("e_feedratez")
+
+        self.grid1.addWidget(self.frzlabel, 14, 0)
+        self.grid1.addWidget(self.feedrate_z_entry, 14, 1)
+
+        # Excellon Rapid Feedrate
+        self.feedrate_rapid_label = QtWidgets.QLabel('%s:' % _('Feedrate Rapids'))
+        self.feedrate_rapid_label.setToolTip(
+            _("Tool speed while drilling\n"
+              "(in units per minute).\n"
+              "This is for the rapid move G00.\n"
+              "It is useful only for Marlin,\n"
+              "ignore for any other cases.")
+        )
+        self.feedrate_rapid_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.feedrate_rapid_entry.set_precision(self.decimals)
+        self.feedrate_rapid_entry.set_range(0.0, 99999.9999)
+        self.feedrate_rapid_entry.setSingleStep(0.1)
+        self.feedrate_rapid_entry.setObjectName("e_fr_rapid")
+
+        self.grid1.addWidget(self.feedrate_rapid_label, 16, 0)
+        self.grid1.addWidget(self.feedrate_rapid_entry, 16, 1)
+
+        # default values is to hide
+        self.feedrate_rapid_label.hide()
+        self.feedrate_rapid_entry.hide()
+
+        # Cut over 1st point in path
+        self.extracut_cb = FCCheckBox('%s:' % _('Re-cut'))
+        self.extracut_cb.setToolTip(
+            _("In order to remove possible\n"
+              "copper leftovers where first cut\n"
+              "meet with last cut, we generate an\n"
+              "extended cut over the first cut section.")
+        )
+        self.extracut_cb.setObjectName("e_extracut")
+
+        self.e_cut_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.e_cut_entry.set_range(0, 99999)
+        self.e_cut_entry.set_precision(self.decimals)
+        self.e_cut_entry.setSingleStep(0.1)
+        self.e_cut_entry.setWrapping(True)
+        self.e_cut_entry.setToolTip(
+            _("In order to remove possible\n"
+              "copper leftovers where first cut\n"
+              "meet with last cut, we generate an\n"
+              "extended cut over the first cut section.")
+        )
+        self.e_cut_entry.setObjectName("e_extracut_length")
+
+        self.ois_recut = OptionalInputSection(self.extracut_cb, [self.e_cut_entry])
+
+        self.grid1.addWidget(self.extracut_cb, 17, 0)
+        self.grid1.addWidget(self.e_cut_entry, 17, 1)
+
+        # Spindlespeed
+        self.spindle_label = QtWidgets.QLabel('%s:' % _('Spindle speed'))
+        self.spindle_label.setToolTip(
+            _("Speed of the spindle\n"
+              "in RPM (optional)")
+        )
+
+        self.spindlespeed_entry = FCSpinner(callback=self.confirmation_message_int)
+        self.spindlespeed_entry.set_range(0, 1000000)
+        self.spindlespeed_entry.set_step(100)
+        self.spindlespeed_entry.setObjectName("e_spindlespeed")
+
+        self.grid1.addWidget(self.spindle_label, 19, 0)
+        self.grid1.addWidget(self.spindlespeed_entry, 19, 1)
+
+        # Dwell
+        self.dwell_cb = FCCheckBox('%s:' % _('Dwell'))
+        self.dwell_cb.setToolTip(
+            _("Pause to allow the spindle to reach its\n"
+              "speed before cutting.")
+        )
+        self.dwell_cb.setObjectName("e_dwell")
+
+        self.dwelltime_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.dwelltime_entry.set_precision(self.decimals)
+        self.dwelltime_entry.set_range(0.0, 9999.9999)
+        self.dwelltime_entry.setSingleStep(0.1)
+
+        self.dwelltime_entry.setToolTip(
+            _("Number of time units for spindle to dwell.")
+        )
+        self.dwelltime_entry.setObjectName("e_dwelltime")
+
+        self.grid1.addWidget(self.dwell_cb, 20, 0)
+        self.grid1.addWidget(self.dwelltime_entry, 20, 1)
+
+        self.ois_dwell = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
+
+        # Tool Offset
+        self.tool_offset_label = QtWidgets.QLabel('%s:' % _('Offset Z'))
+        self.tool_offset_label.setToolTip(
+            _("Some drill bits (the larger ones) need to drill deeper\n"
+              "to create the desired exit hole diameter due of the tip shape.\n"
+              "The value here can compensate the Cut Z parameter.")
+        )
+
+        self.offset_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.offset_entry.set_precision(self.decimals)
+        self.offset_entry.set_range(-9999.9999, 9999.9999)
+        self.offset_entry.setObjectName("e_offset")
+
+        self.grid1.addWidget(self.tool_offset_label, 25, 0)
+        self.grid1.addWidget(self.offset_entry, 25, 1)
+
+        # #################################################################
+        # ################# GRID LAYOUT 4   ###############################
+        # #################################################################
+
+        # self.grid4 = QtWidgets.QGridLayout()
+        # self.exc_tools_box.addLayout(self.grid4)
+        # self.grid4.setColumnStretch(0, 0)
+        # self.grid4.setColumnStretch(1, 1)
+        #
+        # # choose_tools_label = QtWidgets.QLabel(
+        # #     _("Select from the Tools Table above the hole dias to be\n"
+        # #       "drilled. Use the # column to make the selection.")
+        # # )
+        # # grid2.addWidget(choose_tools_label, 0, 0, 1, 3)
+        #
+        # # ### Choose what to use for Gcode creation: Drills, Slots or Both
+        # gcode_type_label = QtWidgets.QLabel('<b>%s</b>' % _('Gcode'))
+        # gcode_type_label.setToolTip(
+        #     _("Choose what to use for GCode generation:\n"
+        #       "'Drills', 'Slots' or 'Both'.\n"
+        #       "When choosing 'Slots' or 'Both', slots will be\n"
+        #       "converted to a series of drills.")
+        # )
+        # self.excellon_gcode_type_radio = RadioSet([{'label': 'Drills', 'value': 'drills'},
+        #                                            {'label': 'Slots', 'value': 'slots'},
+        #                                            {'label': 'Both', 'value': 'both'}])
+        # self.grid4.addWidget(gcode_type_label, 1, 0)
+        # self.grid4.addWidget(self.excellon_gcode_type_radio, 1, 1)
+        # # temporary action until I finish the feature
+        # self.excellon_gcode_type_radio.setVisible(False)
+        # gcode_type_label.hide()
+
+        # #################################################################
+        # ################# GRID LAYOUT 5   ###############################
+        # #################################################################
+        # ################# COMMON PARAMETERS #############################
+
+        self.grid3 = QtWidgets.QGridLayout()
+        self.grid3.setColumnStretch(0, 0)
+        self.grid3.setColumnStretch(1, 1)
+        self.exc_tools_box.addLayout(self.grid3)
+
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid3.addWidget(separator_line2, 0, 0, 1, 2)
+
+        self.apply_param_to_all = FCButton(_("Apply parameters to all tools"))
+        self.apply_param_to_all.setIcon(QtGui.QIcon(self.app.resource_location + '/param_all32.png'))
+        self.apply_param_to_all.setToolTip(
+            _("The parameters in the current form will be applied\n"
+              "on all the tools from the Tool Table.")
+        )
+        self.grid3.addWidget(self.apply_param_to_all, 1, 0, 1, 2)
+
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid3.addWidget(separator_line2, 2, 0, 1, 2)
+
+        # General Parameters
+        self.gen_param_label = QtWidgets.QLabel('<b>%s</b>' % _("Common Parameters"))
+        self.gen_param_label.setToolTip(
+            _("Parameters that are common for all tools.")
+        )
+        self.grid3.addWidget(self.gen_param_label, 3, 0, 1, 2)
+
+        # Tool change Z:
+        self.toolchange_cb = FCCheckBox('%s:' % _("Tool change Z"))
+        self.toolchange_cb.setToolTip(
+            _("Include tool-change sequence\n"
+              "in G-Code (Pause for tool change).")
+        )
+
+        self.toolchangez_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.toolchangez_entry.set_precision(self.decimals)
+        self.toolchangez_entry.setToolTip(
+            _("Z-axis position (height) for\n"
+              "tool change.")
+        )
+        if machinist_setting == 0:
+            self.toolchangez_entry.set_range(0.0, 9999.9999)
+        else:
+            self.toolchangez_entry.set_range(-9999.9999, 9999.9999)
+
+        self.toolchangez_entry.setSingleStep(0.1)
+        self.ois_tcz_e = OptionalInputSection(self.toolchange_cb, [self.toolchangez_entry])
+
+        self.grid3.addWidget(self.toolchange_cb, 8, 0)
+        self.grid3.addWidget(self.toolchangez_entry, 8, 1)
+
+        # Start move Z:
+        self.estartz_label = QtWidgets.QLabel('%s:' % _("Start Z"))
+        self.estartz_label.setToolTip(
+            _("Height of the tool just after start.\n"
+              "Delete the value if you don't need this feature.")
+        )
+        self.estartz_entry = NumericalEvalEntry(border_color='#0069A9')
+
+        self.grid3.addWidget(self.estartz_label, 9, 0)
+        self.grid3.addWidget(self.estartz_entry, 9, 1)
+
+        # End move Z:
+        self.endz_label = QtWidgets.QLabel('%s:' % _("End move Z"))
+        self.endz_label.setToolTip(
+            _("Height of the tool after\n"
+              "the last move at the end of the job.")
+        )
+        self.endz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.endz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.endz_entry.set_range(0.0, 9999.9999)
+        else:
+            self.endz_entry.set_range(-9999.9999, 9999.9999)
+
+        self.endz_entry.setSingleStep(0.1)
+
+        self.grid3.addWidget(self.endz_label, 11, 0)
+        self.grid3.addWidget(self.endz_entry, 11, 1)
+
+        # End Move X,Y
+        endmove_xy_label = QtWidgets.QLabel('%s:' % _('End move X,Y'))
+        endmove_xy_label.setToolTip(
+            _("End move X,Y position. In format (x,y).\n"
+              "If no value is entered then there is no move\n"
+              "on X,Y plane at the end of the job.")
+        )
+        self.endxy_entry = NumericalEvalEntry(border_color='#0069A9')
+        self.endxy_entry.setPlaceholderText(_("X,Y coordinates"))
+        self.grid3.addWidget(endmove_xy_label, 12, 0)
+        self.grid3.addWidget(self.endxy_entry, 12, 1)
+
+        # Probe depth
+        self.pdepth_label = QtWidgets.QLabel('%s:' % _("Probe Z depth"))
+        self.pdepth_label.setToolTip(
+            _("The maximum depth that the probe is allowed\n"
+              "to probe. Negative value, in current units.")
+        )
+
+        self.pdepth_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.pdepth_entry.set_precision(self.decimals)
+        self.pdepth_entry.set_range(-9999.9999, 9999.9999)
+        self.pdepth_entry.setSingleStep(0.1)
+        self.pdepth_entry.setObjectName("e_depth_probe")
+
+        self.grid3.addWidget(self.pdepth_label, 13, 0)
+        self.grid3.addWidget(self.pdepth_entry, 13, 1)
+
+        self.pdepth_label.hide()
+        self.pdepth_entry.setVisible(False)
+
+        # Probe feedrate
+        self.feedrate_probe_label = QtWidgets.QLabel('%s:' % _("Feedrate Probe"))
+        self.feedrate_probe_label.setToolTip(
+            _("The feedrate used while the probe is probing.")
+        )
+
+        self.feedrate_probe_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.feedrate_probe_entry.set_precision(self.decimals)
+        self.feedrate_probe_entry.set_range(0.0, 9999.9999)
+        self.feedrate_probe_entry.setSingleStep(0.1)
+        self.feedrate_probe_entry.setObjectName("e_fr_probe")
+
+        self.grid3.addWidget(self.feedrate_probe_label, 14, 0)
+        self.grid3.addWidget(self.feedrate_probe_entry, 14, 1)
+
+        self.feedrate_probe_label.hide()
+        self.feedrate_probe_entry.setVisible(False)
+
+        # Preprocessor Excellon selection
+        pp_excellon_label = QtWidgets.QLabel('%s:' % _("Preprocessor E"))
+        pp_excellon_label.setToolTip(
+            _("The preprocessor JSON file that dictates\n"
+              "Gcode output for Excellon Objects.")
+        )
+        self.pp_excellon_name_cb = FCComboBox()
+        self.pp_excellon_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus)
+
+        self.grid3.addWidget(pp_excellon_label, 15, 0)
+        self.grid3.addWidget(self.pp_excellon_name_cb, 15, 1)
+
+        # Preprocessor Geometry selection
+        pp_geo_label = QtWidgets.QLabel('%s:' % _("Preprocessor G"))
+        pp_geo_label.setToolTip(
+            _("The preprocessor JSON file that dictates\n"
+              "Gcode output for Geometry (Milling) Objects.")
+        )
+        self.pp_geo_name_cb = FCComboBox()
+        self.pp_geo_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus)
+
+        self.grid3.addWidget(pp_geo_label, 16, 0)
+        self.grid3.addWidget(self.pp_geo_name_cb, 16, 1)
+
+        # ------------------------------------------------------------------------------------------------------------
+        # ------------------------- EXCLUSION AREAS ------------------------------------------------------------------
+        # ------------------------------------------------------------------------------------------------------------
+
+        # Exclusion Areas
+        self.exclusion_cb = FCCheckBox('%s' % _("Add exclusion areas"))
+        self.exclusion_cb.setToolTip(
+            _(
+                "Include exclusion areas.\n"
+                "In those areas the travel of the tools\n"
+                "is forbidden."
+            )
+        )
+        self.grid3.addWidget(self.exclusion_cb, 20, 0, 1, 2)
+
+        self.exclusion_frame = QtWidgets.QFrame()
+        self.exclusion_frame.setContentsMargins(0, 0, 0, 0)
+        self.grid3.addWidget(self.exclusion_frame, 22, 0, 1, 2)
+
+        self.exclusion_box = QtWidgets.QVBoxLayout()
+        self.exclusion_box.setContentsMargins(0, 0, 0, 0)
+        self.exclusion_frame.setLayout(self.exclusion_box)
+
+        self.exclusion_table = FCTable()
+        self.exclusion_box.addWidget(self.exclusion_table)
+        self.exclusion_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+
+        self.exclusion_table.setColumnCount(4)
+        self.exclusion_table.setColumnWidth(0, 20)
+        self.exclusion_table.setHorizontalHeaderLabels(['#', _('Object'), _('Strategy'), _('Over Z')])
+
+        self.exclusion_table.horizontalHeaderItem(0).setToolTip(_("This is the Area ID."))
+        self.exclusion_table.horizontalHeaderItem(1).setToolTip(
+            _("Type of the object where the exclusion area was added."))
+        self.exclusion_table.horizontalHeaderItem(2).setToolTip(
+            _("The strategy used for exclusion area. Go around the exclusion areas or over it."))
+        self.exclusion_table.horizontalHeaderItem(3).setToolTip(
+            _("If the strategy is to go over the area then this is the height at which the tool will go to avoid the "
+              "exclusion area."))
+
+        self.exclusion_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+        grid_a1 = QtWidgets.QGridLayout()
+        grid_a1.setColumnStretch(0, 0)
+        grid_a1.setColumnStretch(1, 1)
+        self.exclusion_box.addLayout(grid_a1)
+
+        # Chose Strategy
+        self.strategy_label = FCLabel('%s:' % _("Strategy"))
+        self.strategy_label.setToolTip(_("The strategy followed when encountering an exclusion area.\n"
+                                         "Can be:\n"
+                                         "- Over -> when encountering the area, the tool will go to a set height\n"
+                                         "- Around -> will avoid the exclusion area by going around the area"))
+        self.strategy_radio = RadioSet([{'label': _('Over'), 'value': 'over'},
+                                        {'label': _('Around'), 'value': 'around'}])
+
+        grid_a1.addWidget(self.strategy_label, 1, 0)
+        grid_a1.addWidget(self.strategy_radio, 1, 1)
+
+        # Over Z
+        self.over_z_label = FCLabel('%s:' % _("Over Z"))
+        self.over_z_label.setToolTip(_("The height Z to which the tool will rise in order to avoid\n"
+                                       "an interdiction area."))
+        self.over_z_entry = FCDoubleSpinner()
+        self.over_z_entry.set_range(0.000, 9999.9999)
+        self.over_z_entry.set_precision(self.decimals)
+
+        grid_a1.addWidget(self.over_z_label, 2, 0)
+        grid_a1.addWidget(self.over_z_entry, 2, 1)
+
+        # Button Add Area
+        self.add_area_button = QtWidgets.QPushButton(_('Add area:'))
+        self.add_area_button.setToolTip(_("Add an Exclusion Area."))
+
+        # Area Selection shape
+        self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
+                                          {'label': _("Polygon"), 'value': 'polygon'}])
+        self.area_shape_radio.setToolTip(
+            _("The kind of selection shape used for area selection.")
+        )
+
+        grid_a1.addWidget(self.add_area_button, 4, 0)
+        grid_a1.addWidget(self.area_shape_radio, 4, 1)
+
+        h_lay_1 = QtWidgets.QHBoxLayout()
+        self.exclusion_box.addLayout(h_lay_1)
+
+        # Button Delete All Areas
+        self.delete_area_button = QtWidgets.QPushButton(_('Delete All'))
+        self.delete_area_button.setToolTip(_("Delete all exclusion areas."))
+
+        # Button Delete Selected Areas
+        self.delete_sel_area_button = QtWidgets.QPushButton(_('Delete Selected'))
+        self.delete_sel_area_button.setToolTip(_("Delete all exclusion areas that are selected in the table."))
+
+        h_lay_1.addWidget(self.delete_area_button)
+        h_lay_1.addWidget(self.delete_sel_area_button)
+
+        self.ois_exclusion_exc = OptionalHideInputSection(self.exclusion_cb, [self.exclusion_frame])
+        # -------------------------- EXCLUSION AREAS END -------------------------------------------------------------
+        # ------------------------------------------------------------------------------------------------------------
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid3.addWidget(separator_line, 25, 0, 1, 2)
+
+        # #################################################################
+        # ################# GRID LAYOUT 6   ###############################
+        # #################################################################
+        self.grid4 = QtWidgets.QGridLayout()
+        self.grid4.setColumnStretch(0, 0)
+        self.grid4.setColumnStretch(1, 1)
+        self.tools_box.addLayout(self.grid4)
+
+        self.generate_cnc_button = QtWidgets.QPushButton(_('Generate CNCJob object'))
+        self.generate_cnc_button.setIcon(QtGui.QIcon(self.app.resource_location + '/cnc16.png'))
+        self.generate_cnc_button.setToolTip(
+            _("Generate the CNC Job.\n"
+              "If milling then an additional Geometry object will be created.\n"
+              "Add / Select at least one tool in the tool-table.\n"
+              "Click the # header to select all, or Ctrl + LMB\n"
+              "for custom selection of tools.")
+        )
+        self.generate_cnc_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.grid4.addWidget(self.generate_cnc_button, 3, 0, 1, 3)
+
+        self.tools_box.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.tools_box.addWidget(self.reset_button)
+        # ############################ FINSIHED GUI ###################################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

Разница между файлами не показана из-за своего большого размера
+ 972 - 1338
appTools/ToolNCC.py


+ 285 - 254
appTools/ToolOptimal.py

@@ -8,7 +8,8 @@
 from PyQt5 import QtWidgets, QtCore, QtGui
 
 from appTool import AppTool
-from appGUI.GUIElements import OptionalHideInputSection, FCTextArea, FCEntry, FCSpinner, FCCheckBox, FCComboBox
+from appGUI.GUIElements import OptionalHideInputSection, FCTextArea, FCEntry, FCSpinner, FCCheckBox, FCComboBox, \
+    FCLabel, FCButton
 from camlib import grace
 
 from shapely.geometry import MultiPolygon
@@ -30,8 +31,6 @@ log = logging.getLogger('base')
 
 class ToolOptimal(AppTool):
 
-    toolName = _("Optimal Tool")
-
     update_text = QtCore.pyqtSignal(list)
     update_sec_distances = QtCore.pyqtSignal(dict)
 
@@ -41,221 +40,11 @@ class ToolOptimal(AppTool):
         self.units = self.app.defaults['units'].upper()
         self.decimals = self.app.decimals
 
-        # ############################################################################
-        # ############################ GUI creation ##################################
-        # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet(
-            """
-            QLabel
-            {
-                font-size: 16px;
-                font-weight: bold;
-            }
-            """)
-        self.layout.addWidget(title_label)
-
-        # ## Form Layout
-        form_lay = QtWidgets.QFormLayout()
-        self.layout.addLayout(form_lay)
-
-        form_lay.addRow(QtWidgets.QLabel(""))
-
-        # ## Gerber Object to mirror
-        self.gerber_object_combo = FCComboBox()
-        self.gerber_object_combo.setModel(self.app.collection)
-        self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.gerber_object_combo.is_last = True
-        self.gerber_object_combo.obj_type = "Gerber"
-
-        self.gerber_object_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
-        self.gerber_object_label.setToolTip(
-            "Gerber object for which to find the minimum distance between copper features."
-        )
-        form_lay.addRow(self.gerber_object_label)
-        form_lay.addRow(self.gerber_object_combo)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        form_lay.addRow(separator_line)
-
-        # Precision = nr of decimals
-        self.precision_label = QtWidgets.QLabel('%s:' % _("Precision"))
-        self.precision_label.setToolTip(_("Number of decimals kept for found distances."))
-
-        self.precision_spinner = FCSpinner(callback=self.confirmation_message_int)
-        self.precision_spinner.set_range(2, 10)
-        self.precision_spinner.setWrapping(True)
-        form_lay.addRow(self.precision_label, self.precision_spinner)
-
-        # Results Title
-        self.title_res_label = QtWidgets.QLabel('<b>%s:</b>' % _("Minimum distance"))
-        self.title_res_label.setToolTip(_("Display minimum distance between copper features."))
-        form_lay.addRow(self.title_res_label)
-
-        # Result value
-        self.result_label = QtWidgets.QLabel('%s:' % _("Determined"))
-        self.result_entry = FCEntry()
-        self.result_entry.setReadOnly(True)
-
-        self.units_lbl = QtWidgets.QLabel(self.units.lower())
-        self.units_lbl.setDisabled(True)
-
-        hlay = QtWidgets.QHBoxLayout()
-        hlay.addWidget(self.result_entry)
-        hlay.addWidget(self.units_lbl)
-
-        form_lay.addRow(self.result_label, hlay)
-
-        # Frequency of minimum encounter
-        self.freq_label = QtWidgets.QLabel('%s:' % _("Occurring"))
-        self.freq_label.setToolTip(_("How many times this minimum is found."))
-        self.freq_entry = FCEntry()
-        self.freq_entry.setReadOnly(True)
-        form_lay.addRow(self.freq_label, self.freq_entry)
-
-        # Control if to display the locations of where the minimum was found
-        self.locations_cb = FCCheckBox(_("Minimum points coordinates"))
-        self.locations_cb.setToolTip(_("Coordinates for points where minimum distance was found."))
-        form_lay.addRow(self.locations_cb)
-
-        # Locations where minimum was found
-        self.locations_textb = FCTextArea(parent=self)
-        self.locations_textb.setPlaceholderText(
-            _("Coordinates for points where minimum distance was found.")
-        )
-        self.locations_textb.setReadOnly(True)
-        stylesheet = """
-                        QTextEdit { selection-background-color:blue;
-                                    selection-color:white;
-                        }
-                     """
-
-        self.locations_textb.setStyleSheet(stylesheet)
-        form_lay.addRow(self.locations_textb)
-
-        # Jump button
-        self.locate_button = QtWidgets.QPushButton(_("Jump to selected position"))
-        self.locate_button.setToolTip(
-            _("Select a position in the Locations text box and then\n"
-              "click this button.")
-        )
-        self.locate_button.setMinimumWidth(60)
-        self.locate_button.setDisabled(True)
-        form_lay.addRow(self.locate_button)
-
-        # Other distances in Gerber
-        self.title_second_res_label = QtWidgets.QLabel('<b>%s:</b>' % _("Other distances"))
-        self.title_second_res_label.setToolTip(_("Will display other distances in the Gerber file ordered from\n"
-                                                 "the minimum to the maximum, not including the absolute minimum."))
-        form_lay.addRow(self.title_second_res_label)
-
-        # Control if to display the locations of where the minimum was found
-        self.sec_locations_cb = FCCheckBox(_("Other distances points coordinates"))
-        self.sec_locations_cb.setToolTip(_("Other distances and the coordinates for points\n"
-                                           "where the distance was found."))
-        form_lay.addRow(self.sec_locations_cb)
-
-        # this way I can hide/show the frame
-        self.sec_locations_frame = QtWidgets.QFrame()
-        self.sec_locations_frame.setContentsMargins(0, 0, 0, 0)
-        self.layout.addWidget(self.sec_locations_frame)
-        self.distances_box = QtWidgets.QVBoxLayout()
-        self.distances_box.setContentsMargins(0, 0, 0, 0)
-        self.sec_locations_frame.setLayout(self.distances_box)
-
-        # Other Distances label
-        self.distances_label = QtWidgets.QLabel('%s' % _("Gerber distances"))
-        self.distances_label.setToolTip(_("Other distances and the coordinates for points\n"
-                                          "where the distance was found."))
-        self.distances_box.addWidget(self.distances_label)
-
-        # Other distances
-        self.distances_textb = FCTextArea(parent=self)
-        self.distances_textb.setPlaceholderText(
-            _("Other distances and the coordinates for points\n"
-              "where the distance was found.")
-        )
-        self.distances_textb.setReadOnly(True)
-        stylesheet = """
-                        QTextEdit { selection-background-color:blue;
-                                    selection-color:white;
-                        }
-                     """
-
-        self.distances_textb.setStyleSheet(stylesheet)
-        self.distances_box.addWidget(self.distances_textb)
-
-        self.distances_box.addWidget(QtWidgets.QLabel(''))
-
-        # Other Locations label
-        self.locations_label = QtWidgets.QLabel('%s' % _("Points coordinates"))
-        self.locations_label.setToolTip(_("Other distances and the coordinates for points\n"
-                                          "where the distance was found."))
-        self.distances_box.addWidget(self.locations_label)
-
-        # Locations where minimum was found
-        self.locations_sec_textb = FCTextArea(parent=self)
-        self.locations_sec_textb.setPlaceholderText(
-            _("Other distances and the coordinates for points\n"
-              "where the distance was found.")
-        )
-        self.locations_sec_textb.setReadOnly(True)
-        stylesheet = """
-                        QTextEdit { selection-background-color:blue;
-                                    selection-color:white;
-                        }
-                     """
-
-        self.locations_sec_textb.setStyleSheet(stylesheet)
-        self.distances_box.addWidget(self.locations_sec_textb)
-
-        # Jump button
-        self.locate_sec_button = QtWidgets.QPushButton(_("Jump to selected position"))
-        self.locate_sec_button.setToolTip(
-            _("Select a position in the Locations text box and then\n"
-              "click this button.")
-        )
-        self.locate_sec_button.setMinimumWidth(60)
-        self.locate_sec_button.setDisabled(True)
-        self.distances_box.addWidget(self.locate_sec_button)
-
-        # GO button
-        self.calculate_button = QtWidgets.QPushButton(_("Find Minimum"))
-        self.calculate_button.setToolTip(
-            _("Calculate the minimum distance between copper features,\n"
-              "this will allow the determination of the right tool to\n"
-              "use for isolation or copper clearing.")
-        )
-        self.calculate_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.calculate_button.setMinimumWidth(60)
-        self.layout.addWidget(self.calculate_button)
-
-        self.layout.addStretch()
-
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
-
-        self.loc_ois = OptionalHideInputSection(self.locations_cb, [self.locations_textb, self.locate_button])
-        self.sec_loc_ois = OptionalHideInputSection(self.sec_locations_cb, [self.sec_locations_frame])
-        # ################## Finished GUI creation ###################################
-        # ############################################################################
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = OptimalUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
         # this is the line selected in the textbox with the locations of the minimum
         self.selected_text = ''
@@ -270,17 +59,18 @@ class ToolOptimal(AppTool):
         # ############################################################################
         # ############################ Signals #######################################
         # ############################################################################
-        self.calculate_button.clicked.connect(self.find_minimum_distance)
-        self.locate_button.clicked.connect(self.on_locate_position)
         self.update_text.connect(self.on_update_text)
-        self.locations_textb.cursorPositionChanged.connect(self.on_textbox_clicked)
-
-        self.locate_sec_button.clicked.connect(self.on_locate_sec_position)
         self.update_sec_distances.connect(self.on_update_sec_distances_txt)
-        self.distances_textb.cursorPositionChanged.connect(self.on_distances_textb_clicked)
-        self.locations_sec_textb.cursorPositionChanged.connect(self.on_locations_sec_clicked)
 
-        self.reset_button.clicked.connect(self.set_tool_ui)
+        self.ui.calculate_button.clicked.connect(self.find_minimum_distance)
+        self.ui.locate_button.clicked.connect(self.on_locate_position)
+        self.ui.locations_textb.cursorPositionChanged.connect(self.on_textbox_clicked)
+
+        self.ui.locate_sec_button.clicked.connect(self.on_locate_sec_position)
+        self.ui.distances_textb.cursorPositionChanged.connect(self.on_distances_textb_clicked)
+        self.ui.locations_sec_textb.cursorPositionChanged.connect(self.on_locations_sec_clicked)
+
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
 
     def install(self, icon=None, separator=None, **kwargs):
         AppTool.install(self, icon, separator, shortcut='Alt+O', **kwargs)
@@ -313,13 +103,13 @@ class ToolOptimal(AppTool):
         self.app.ui.notebook.setTabText(2, _("Optimal Tool"))
 
     def set_tool_ui(self):
-        self.result_entry.set_value(0.0)
-        self.freq_entry.set_value('0')
+        self.ui.result_entry.set_value(0.0)
+        self.ui.freq_entry.set_value('0')
 
-        self.precision_spinner.set_value(int(self.app.defaults["tools_opt_precision"]))
-        self.locations_textb.clear()
+        self.ui.precision_spinner.set_value(int(self.app.defaults["tools_opt_precision"]))
+        self.ui.locations_textb.clear()
         # new cursor - select all document
-        cursor = self.locations_textb.textCursor()
+        cursor = self.ui.locations_textb.textCursor()
         cursor.select(QtGui.QTextCursor.Document)
 
         # clear previous selection highlight
@@ -327,20 +117,20 @@ class ToolOptimal(AppTool):
         tmp.clearBackground()
         cursor.setBlockFormat(tmp)
 
-        self.locations_textb.setVisible(False)
-        self.locate_button.setVisible(False)
+        self.ui.locations_textb.setVisible(False)
+        self.ui.locate_button.setVisible(False)
 
-        self.result_entry.set_value(0.0)
-        self.freq_entry.set_value('0')
+        self.ui.result_entry.set_value(0.0)
+        self.ui.freq_entry.set_value('0')
         self.reset_fields()
 
     def find_minimum_distance(self):
         self.units = self.app.defaults['units'].upper()
-        self.decimals = int(self.precision_spinner.get_value())
+        self.decimals = int(self.ui.precision_spinner.get_value())
 
-        selection_index = self.gerber_object_combo.currentIndex()
+        selection_index = self.ui.gerber_object_combo.currentIndex()
 
-        model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
+        model_index = self.app.collection.index(selection_index, 0, self.ui.gerber_object_combo.rootModelIndex())
         try:
             fcobj = model_index.internalPointer().obj
         except Exception as e:
@@ -425,17 +215,16 @@ class ToolOptimal(AppTool):
                             old_disp_number = disp_number
                     idx += 1
 
-                app_obj.inform.emit(
-                    _("Optimal Tool. Finding the minimum distance."))
+                app_obj.inform.emit(_("Optimal Tool. Finding the minimum distance."))
 
                 min_list = list(self.min_dict.keys())
                 min_dist = min(min_list)
                 min_dist_string = '%.*f' % (self.decimals, float(min_dist))
-                self.result_entry.set_value(min_dist_string)
+                self.ui.result_entry.set_value(min_dist_string)
 
                 freq = len(self.min_dict[min_dist])
                 freq = '%d' % int(freq)
-                self.freq_entry.set_value(freq)
+                self.ui.freq_entry.set_value(freq)
 
                 min_locations = self.min_dict.pop(min_dist)
 
@@ -483,12 +272,12 @@ class ToolOptimal(AppTool):
         for loc in data:
             if loc:
                 txt += '%s, %s\n' % (str(loc[0]), str(loc[1]))
-        self.locations_textb.setPlainText(txt)
-        self.locate_button.setDisabled(False)
+        self.ui.locations_textb.setPlainText(txt)
+        self.ui.locate_button.setDisabled(False)
 
     def on_textbox_clicked(self):
         # new cursor - select all document
-        cursor = self.locations_textb.textCursor()
+        cursor = self.ui.locations_textb.textCursor()
         cursor.select(QtGui.QTextCursor.Document)
 
         # clear previous selection highlight
@@ -497,7 +286,7 @@ class ToolOptimal(AppTool):
         cursor.setBlockFormat(tmp)
 
         # new cursor - select the current line
-        cursor = self.locations_textb.textCursor()
+        cursor = self.ui.locations_textb.textCursor()
         cursor.select(QtGui.QTextCursor.LineUnderCursor)
 
         # highlight the current selected line
@@ -512,12 +301,12 @@ class ToolOptimal(AppTool):
         txt = ''
         for loc in distance_list:
             txt += '%s\n' % str(loc)
-        self.distances_textb.setPlainText(txt)
-        self.locate_sec_button.setDisabled(False)
+        self.ui.distances_textb.setPlainText(txt)
+        self.ui.locate_sec_button.setDisabled(False)
 
     def on_distances_textb_clicked(self):
         # new cursor - select all document
-        cursor = self.distances_textb.textCursor()
+        cursor = self.ui.distances_textb.textCursor()
         cursor.select(QtGui.QTextCursor.Document)
 
         # clear previous selection highlight
@@ -526,7 +315,7 @@ class ToolOptimal(AppTool):
         cursor.setBlockFormat(tmp)
 
         # new cursor - select the current line
-        cursor = self.distances_textb.textCursor()
+        cursor = self.ui.distances_textb.textCursor()
         cursor.select(QtGui.QTextCursor.LineUnderCursor)
 
         # highlight the current selected line
@@ -544,11 +333,11 @@ class ToolOptimal(AppTool):
         for loc in distance_list:
             if loc:
                 txt += '%s, %s\n' % (str(loc[0]), str(loc[1]))
-        self.locations_sec_textb.setPlainText(txt)
+        self.ui.locations_sec_textb.setPlainText(txt)
 
     def on_locations_sec_clicked(self):
         # new cursor - select all document
-        cursor = self.locations_sec_textb.textCursor()
+        cursor = self.ui.locations_sec_textb.textCursor()
         cursor.select(QtGui.QTextCursor.Document)
 
         # clear previous selection highlight
@@ -557,7 +346,7 @@ class ToolOptimal(AppTool):
         cursor.setBlockFormat(tmp)
 
         # new cursor - select the current line
-        cursor = self.locations_sec_textb.textCursor()
+        cursor = self.ui.locations_sec_textb.textCursor()
         cursor.select(QtGui.QTextCursor.LineUnderCursor)
 
         # highlight the current selected line
@@ -592,5 +381,247 @@ class ToolOptimal(AppTool):
             return
 
     def reset_fields(self):
+        self.ui.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ui.gerber_object_combo.setCurrentIndex(0)
+
+
+class OptimalUI:
+
+    toolName = _("Optimal Tool")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+        self.units = self.app.defaults['units'].upper()
+
+        # ## Title
+        title_label = FCLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(FCLabel(""))
+
+        # ## Form Layout
+        form_lay = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_lay)
+
+        # ## Gerber Object to mirror
+        self.gerber_object_combo = FCComboBox()
+        self.gerber_object_combo.setModel(self.app.collection)
         self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.gerber_object_combo.setCurrentIndex(0)
+        self.gerber_object_combo.is_last = True
+        self.gerber_object_combo.obj_type = "Gerber"
+
+        self.gerber_object_label = FCLabel("<b>%s:</b>" % _("GERBER"))
+        self.gerber_object_label.setToolTip(
+            "Gerber object for which to find the minimum distance between copper features."
+        )
+        form_lay.addRow(self.gerber_object_label)
+        form_lay.addRow(self.gerber_object_combo)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        form_lay.addRow(separator_line)
+
+        # Precision = nr of decimals
+        self.precision_label = FCLabel('%s:' % _("Precision"))
+        self.precision_label.setToolTip(_("Number of decimals kept for found distances."))
+
+        self.precision_spinner = FCSpinner(callback=self.confirmation_message_int)
+        self.precision_spinner.set_range(2, 10)
+        self.precision_spinner.setWrapping(True)
+        form_lay.addRow(self.precision_label, self.precision_spinner)
+
+        # Results Title
+        self.title_res_label = FCLabel('<b>%s:</b>' % _("Minimum distance"))
+        self.title_res_label.setToolTip(_("Display minimum distance between copper features."))
+        form_lay.addRow(self.title_res_label)
+
+        # Result value
+        self.result_label = FCLabel('%s:' % _("Determined"))
+        self.result_entry = FCEntry()
+        self.result_entry.setReadOnly(True)
+
+        self.units_lbl = FCLabel(self.units.lower())
+        self.units_lbl.setDisabled(True)
+
+        hlay = QtWidgets.QHBoxLayout()
+        hlay.addWidget(self.result_entry)
+        hlay.addWidget(self.units_lbl)
+
+        form_lay.addRow(self.result_label, hlay)
+
+        # Frequency of minimum encounter
+        self.freq_label = FCLabel('%s:' % _("Occurring"))
+        self.freq_label.setToolTip(_("How many times this minimum is found."))
+        self.freq_entry = FCEntry()
+        self.freq_entry.setReadOnly(True)
+        form_lay.addRow(self.freq_label, self.freq_entry)
+
+        # Control if to display the locations of where the minimum was found
+        self.locations_cb = FCCheckBox(_("Minimum points coordinates"))
+        self.locations_cb.setToolTip(_("Coordinates for points where minimum distance was found."))
+        form_lay.addRow(self.locations_cb)
+
+        # Locations where minimum was found
+        self.locations_textb = FCTextArea()
+        self.locations_textb.setPlaceholderText(
+            _("Coordinates for points where minimum distance was found.")
+        )
+        self.locations_textb.setReadOnly(True)
+        stylesheet = """
+                                QTextEdit { selection-background-color:blue;
+                                            selection-color:white;
+                                }
+                             """
+
+        self.locations_textb.setStyleSheet(stylesheet)
+        form_lay.addRow(self.locations_textb)
+
+        # Jump button
+        self.locate_button = FCButton(_("Jump to selected position"))
+        self.locate_button.setToolTip(
+            _("Select a position in the Locations text box and then\n"
+              "click this button.")
+        )
+        self.locate_button.setMinimumWidth(60)
+        self.locate_button.setDisabled(True)
+        form_lay.addRow(self.locate_button)
+
+        # Other distances in Gerber
+        self.title_second_res_label = FCLabel('<b>%s:</b>' % _("Other distances"))
+        self.title_second_res_label.setToolTip(_("Will display other distances in the Gerber file ordered from\n"
+                                                 "the minimum to the maximum, not including the absolute minimum."))
+        form_lay.addRow(self.title_second_res_label)
+
+        # Control if to display the locations of where the minimum was found
+        self.sec_locations_cb = FCCheckBox(_("Other distances points coordinates"))
+        self.sec_locations_cb.setToolTip(_("Other distances and the coordinates for points\n"
+                                           "where the distance was found."))
+        form_lay.addRow(self.sec_locations_cb)
+
+        # this way I can hide/show the frame
+        self.sec_locations_frame = QtWidgets.QFrame()
+        self.sec_locations_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.sec_locations_frame)
+        self.distances_box = QtWidgets.QVBoxLayout()
+        self.distances_box.setContentsMargins(0, 0, 0, 0)
+        self.sec_locations_frame.setLayout(self.distances_box)
+
+        # Other Distances label
+        self.distances_label = FCLabel('%s' % _("Gerber distances"))
+        self.distances_label.setToolTip(_("Other distances and the coordinates for points\n"
+                                          "where the distance was found."))
+        self.distances_box.addWidget(self.distances_label)
+
+        # Other distances
+        self.distances_textb = FCTextArea()
+        self.distances_textb.setPlaceholderText(
+            _("Other distances and the coordinates for points\n"
+              "where the distance was found.")
+        )
+        self.distances_textb.setReadOnly(True)
+        stylesheet = """
+                                QTextEdit { selection-background-color:blue;
+                                            selection-color:white;
+                                }
+                             """
+
+        self.distances_textb.setStyleSheet(stylesheet)
+        self.distances_box.addWidget(self.distances_textb)
+
+        self.distances_box.addWidget(FCLabel(''))
+
+        # Other Locations label
+        self.locations_label = FCLabel('%s' % _("Points coordinates"))
+        self.locations_label.setToolTip(_("Other distances and the coordinates for points\n"
+                                          "where the distance was found."))
+        self.distances_box.addWidget(self.locations_label)
+
+        # Locations where minimum was found
+        self.locations_sec_textb = FCTextArea()
+        self.locations_sec_textb.setPlaceholderText(
+            _("Other distances and the coordinates for points\n"
+              "where the distance was found.")
+        )
+        self.locations_sec_textb.setReadOnly(True)
+        stylesheet = """
+                                QTextEdit { selection-background-color:blue;
+                                            selection-color:white;
+                                }
+                             """
+
+        self.locations_sec_textb.setStyleSheet(stylesheet)
+        self.distances_box.addWidget(self.locations_sec_textb)
+
+        # Jump button
+        self.locate_sec_button = FCButton(_("Jump to selected position"))
+        self.locate_sec_button.setToolTip(
+            _("Select a position in the Locations text box and then\n"
+              "click this button.")
+        )
+        self.locate_sec_button.setMinimumWidth(60)
+        self.locate_sec_button.setDisabled(True)
+        self.distances_box.addWidget(self.locate_sec_button)
+
+        # GO button
+        self.calculate_button = FCButton(_("Find Minimum"))
+        self.calculate_button.setToolTip(
+            _("Calculate the minimum distance between copper features,\n"
+              "this will allow the determination of the right tool to\n"
+              "use for isolation or copper clearing.")
+        )
+        self.calculate_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.calculate_button.setMinimumWidth(60)
+        self.layout.addWidget(self.calculate_button)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = FCButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.reset_button)
+
+        self.loc_ois = OptionalHideInputSection(self.locations_cb, [self.locations_textb, self.locate_button])
+        self.sec_loc_ois = OptionalHideInputSection(self.sec_locations_cb, [self.sec_locations_frame])
+
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

Разница между файлами не показана из-за своего большого размера
+ 776 - 1190
appTools/ToolPaint.py


+ 478 - 373
appTools/ToolPanelize.py

@@ -15,8 +15,8 @@ from copy import deepcopy
 import numpy as np
 
 import shapely.affinity as affinity
-from shapely.ops import unary_union
-from shapely.geometry import LineString
+from shapely.ops import unary_union, linemerge, snap
+from shapely.geometry import LineString, MultiLineString
 
 import gettext
 import appTranslation as fcTranslate
@@ -35,266 +35,22 @@ class Panelize(AppTool):
     toolName = _("Panelize PCB")
 
     def __init__(self, app):
-        self.decimals = app.decimals
-
         AppTool.__init__(self, app)
+        self.decimals = app.decimals
+        self.app = app
 
-        # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-
-        self.object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Source Object"))
-        self.object_label.setToolTip(
-            _("Specify the type of object to be panelized\n"
-              "It can be of type: Gerber, Excellon or Geometry.\n"
-              "The selection here decide the type of objects that will be\n"
-              "in the Object combobox.")
-        )
-
-        self.layout.addWidget(self.object_label)
-
-        # Form Layout
-        form_layout_0 = QtWidgets.QFormLayout()
-        self.layout.addLayout(form_layout_0)
-
-        # Type of object to be panelized
-        self.type_obj_combo = FCComboBox()
-        self.type_obj_combo.addItem("Gerber")
-        self.type_obj_combo.addItem("Excellon")
-        self.type_obj_combo.addItem("Geometry")
-
-        self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
-        self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
-        self.type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
-
-        self.type_object_label = QtWidgets.QLabel('%s:' % _("Object Type"))
-
-        form_layout_0.addRow(self.type_object_label, self.type_obj_combo)
-
-        # Object to be panelized
-        self.object_combo = FCComboBox()
-        self.object_combo.setModel(self.app.collection)
-        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.object_combo.is_last = True
-
-        self.object_combo.setToolTip(
-            _("Object to be panelized. This means that it will\n"
-              "be duplicated in an array of rows and columns.")
-        )
-        form_layout_0.addRow(self.object_combo)
-
-        # Form Layout
-        form_layout = QtWidgets.QFormLayout()
-        self.layout.addLayout(form_layout)
-
-        # Type of box Panel object
-        self.reference_radio = RadioSet([{'label': _('Object'), 'value': 'object'},
-                                         {'label': _('Bounding Box'), 'value': 'bbox'}])
-        self.box_label = QtWidgets.QLabel("<b>%s:</b>" % _("Penelization Reference"))
-        self.box_label.setToolTip(
-            _("Choose the reference for panelization:\n"
-              "- Object = the bounding box of a different object\n"
-              "- Bounding Box = the bounding box of the object to be panelized\n"
-              "\n"
-              "The reference is useful when doing panelization for more than one\n"
-              "object. The spacings (really offsets) will be applied in reference\n"
-              "to this reference object therefore maintaining the panelized\n"
-              "objects in sync.")
-        )
-        form_layout.addRow(self.box_label)
-        form_layout.addRow(self.reference_radio)
-
-        # Type of Box Object to be used as an envelope for panelization
-        self.type_box_combo = FCComboBox()
-        self.type_box_combo.addItems([_("Gerber"), _("Geometry")])
-
-        # we get rid of item1 ("Excellon") as it is not suitable for use as a "box" for panelizing
-        # self.type_box_combo.view().setRowHidden(1, True)
-        self.type_box_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
-        self.type_box_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
-
-        self.type_box_combo_label = QtWidgets.QLabel('%s:' % _("Box Type"))
-        self.type_box_combo_label.setToolTip(
-            _("Specify the type of object to be used as an container for\n"
-              "panelization. It can be: Gerber or Geometry type.\n"
-              "The selection here decide the type of objects that will be\n"
-              "in the Box Object combobox.")
-        )
-        form_layout.addRow(self.type_box_combo_label, self.type_box_combo)
-
-        # Box
-        self.box_combo = FCComboBox()
-        self.box_combo.setModel(self.app.collection)
-        self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.box_combo.is_last = True
-
-        self.box_combo.setToolTip(
-            _("The actual object that is used as container for the\n "
-              "selected object that is to be panelized.")
-        )
-        form_layout.addRow(self.box_combo)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        form_layout.addRow(separator_line)
-
-        panel_data_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Data"))
-        panel_data_label.setToolTip(
-            _("This informations will shape the resulting panel.\n"
-              "The number of rows and columns will set how many\n"
-              "duplicates of the original geometry will be generated.\n"
-              "\n"
-              "The spacings will set the distance between any two\n"
-              "elements of the panel array.")
-        )
-        form_layout.addRow(panel_data_label)
-
-        # Spacing Columns
-        self.spacing_columns = FCDoubleSpinner(callback=self.confirmation_message)
-        self.spacing_columns.set_range(0, 9999)
-        self.spacing_columns.set_precision(4)
-
-        self.spacing_columns_label = QtWidgets.QLabel('%s:' % _("Spacing cols"))
-        self.spacing_columns_label.setToolTip(
-            _("Spacing between columns of the desired panel.\n"
-              "In current units.")
-        )
-        form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
-
-        # Spacing Rows
-        self.spacing_rows = FCDoubleSpinner(callback=self.confirmation_message)
-        self.spacing_rows.set_range(0, 9999)
-        self.spacing_rows.set_precision(4)
-
-        self.spacing_rows_label = QtWidgets.QLabel('%s:' % _("Spacing rows"))
-        self.spacing_rows_label.setToolTip(
-            _("Spacing between rows of the desired panel.\n"
-              "In current units.")
-        )
-        form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
-
-        # Columns
-        self.columns = FCSpinner(callback=self.confirmation_message_int)
-        self.columns.set_range(0, 9999)
-
-        self.columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
-        self.columns_label.setToolTip(
-            _("Number of columns of the desired panel")
-        )
-        form_layout.addRow(self.columns_label, self.columns)
-
-        # Rows
-        self.rows = FCSpinner(callback=self.confirmation_message_int)
-        self.rows.set_range(0, 9999)
-
-        self.rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
-        self.rows_label.setToolTip(
-            _("Number of rows of the desired panel")
-        )
-        form_layout.addRow(self.rows_label, self.rows)
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        form_layout.addRow(separator_line)
-
-        # Type of resulting Panel object
-        self.panel_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'gerber'},
-                                          {'label': _('Geo'), 'value': 'geometry'}])
-        self.panel_type_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Type"))
-        self.panel_type_label.setToolTip(
-            _("Choose the type of object for the panel object:\n"
-              "- Geometry\n"
-              "- Gerber")
-        )
-        form_layout.addRow(self.panel_type_label)
-        form_layout.addRow(self.panel_type_radio)
-
-        # Constrains
-        self.constrain_cb = FCCheckBox('%s:' % _("Constrain panel within"))
-        self.constrain_cb.setToolTip(
-            _("Area define by DX and DY within to constrain the panel.\n"
-              "DX and DY values are in current units.\n"
-              "Regardless of how many columns and rows are desired,\n"
-              "the final panel will have as many columns and rows as\n"
-              "they fit completely within selected area.")
-        )
-        form_layout.addRow(self.constrain_cb)
-
-        self.x_width_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.x_width_entry.set_precision(4)
-        self.x_width_entry.set_range(0, 9999)
-
-        self.x_width_lbl = QtWidgets.QLabel('%s:' % _("Width (DX)"))
-        self.x_width_lbl.setToolTip(
-            _("The width (DX) within which the panel must fit.\n"
-              "In current units.")
-        )
-        form_layout.addRow(self.x_width_lbl, self.x_width_entry)
-
-        self.y_height_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.y_height_entry.set_range(0, 9999)
-        self.y_height_entry.set_precision(4)
-
-        self.y_height_lbl = QtWidgets.QLabel('%s:' % _("Height (DY)"))
-        self.y_height_lbl.setToolTip(
-            _("The height (DY)within which the panel must fit.\n"
-              "In current units.")
-        )
-        form_layout.addRow(self.y_height_lbl, self.y_height_entry)
-
-        self.constrain_sel = OptionalInputSection(
-            self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
-
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        form_layout.addRow(separator_line)
-
-        # Buttons
-        self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object"))
-        self.panelize_object_button.setToolTip(
-            _("Panelize the specified object around the specified box.\n"
-              "In other words it creates multiple copies of the source object,\n"
-              "arranged in a 2D array of rows and columns.")
-        )
-        self.panelize_object_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.panelize_object_button)
-
-        self.layout.addStretch()
-
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = PanelizeUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
         # Signals
-        self.reference_radio.activated_custom.connect(self.on_reference_radio_changed)
-        self.panelize_object_button.clicked.connect(self.on_panelize)
-        self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
-        self.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
-        self.reset_button.clicked.connect(self.set_tool_ui)
+        self.ui.reference_radio.activated_custom.connect(self.on_reference_radio_changed)
+        self.ui.panelize_object_button.clicked.connect(self.on_panelize)
+        self.ui.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
+        self.ui.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
 
         # list to hold the temporary objects
         self.objs = []
@@ -338,39 +94,45 @@ class Panelize(AppTool):
     def set_tool_ui(self):
         self.reset_fields()
 
-        self.reference_radio.set_value('bbox')
+        self.ui.reference_radio.set_value('bbox')
 
         sp_c = self.app.defaults["tools_panelize_spacing_columns"] if \
             self.app.defaults["tools_panelize_spacing_columns"] else 0.0
-        self.spacing_columns.set_value(float(sp_c))
+        self.ui.spacing_columns.set_value(float(sp_c))
 
         sp_r = self.app.defaults["tools_panelize_spacing_rows"] if \
             self.app.defaults["tools_panelize_spacing_rows"] else 0.0
-        self.spacing_rows.set_value(float(sp_r))
+        self.ui.spacing_rows.set_value(float(sp_r))
 
         rr = self.app.defaults["tools_panelize_rows"] if \
             self.app.defaults["tools_panelize_rows"] else 0.0
-        self.rows.set_value(int(rr))
+        self.ui.rows.set_value(int(rr))
 
         cc = self.app.defaults["tools_panelize_columns"] if \
             self.app.defaults["tools_panelize_columns"] else 0.0
-        self.columns.set_value(int(cc))
+        self.ui.columns.set_value(int(cc))
+
+        optimized_path_cb = self.app.defaults["tools_panelize_optimization"] if \
+            self.app.defaults["tools_panelize_optimization"] else True
+        self.ui.optimization_cb.set_value(optimized_path_cb)
 
         c_cb = self.app.defaults["tools_panelize_constrain"] if \
             self.app.defaults["tools_panelize_constrain"] else False
-        self.constrain_cb.set_value(c_cb)
+        self.ui.constrain_cb.set_value(c_cb)
 
         x_w = self.app.defaults["tools_panelize_constrainx"] if \
             self.app.defaults["tools_panelize_constrainx"] else 0.0
-        self.x_width_entry.set_value(float(x_w))
+        self.ui.x_width_entry.set_value(float(x_w))
 
         y_w = self.app.defaults["tools_panelize_constrainy"] if \
             self.app.defaults["tools_panelize_constrainy"] else 0.0
-        self.y_height_entry.set_value(float(y_w))
+        self.ui.y_height_entry.set_value(float(y_w))
 
         panel_type = self.app.defaults["tools_panelize_panel_type"] if \
             self.app.defaults["tools_panelize_panel_type"] else 'gerber'
-        self.panel_type_radio.set_value(panel_type)
+        self.ui.panel_type_radio.set_value(panel_type)
+
+        self.ui.on_panel_type(val=panel_type)
 
         # run once the following so the obj_type attribute is updated in the FCComboBoxes
         # such that the last loaded object is populated in the combo boxes
@@ -378,43 +140,48 @@ class Panelize(AppTool):
         self.on_type_box_index_changed()
 
     def on_type_obj_index_changed(self):
-        obj_type = self.type_obj_combo.currentIndex()
-        self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        self.object_combo.setCurrentIndex(0)
-        self.object_combo.obj_type = {
+        obj_type = self.ui.type_obj_combo.currentIndex()
+        self.ui.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.ui.object_combo.setCurrentIndex(0)
+        self.ui.object_combo.obj_type = {
             _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
-        }[self.type_obj_combo.get_value()]
+        }[self.ui.type_obj_combo.get_value()]
 
         # hide the panel type for Excellons, the panel can be only of type Geometry
-        if self.type_obj_combo.currentText() != 'Excellon':
-            self.panel_type_label.setDisabled(False)
-            self.panel_type_radio.setDisabled(False)
+        if self.ui.type_obj_combo.currentText() != 'Excellon':
+            self.ui.panel_type_label.setDisabled(False)
+            self.ui.panel_type_radio.setDisabled(False)
+            self.ui.on_panel_type(val=self.ui.panel_type_radio.get_value())
         else:
-            self.panel_type_label.setDisabled(True)
-            self.panel_type_radio.setDisabled(True)
-            self.panel_type_radio.set_value('geometry')
+            self.ui.panel_type_label.setDisabled(True)
+            self.ui.panel_type_radio.setDisabled(True)
+            self.ui.panel_type_radio.set_value('geometry')
+            self.ui.optimization_cb.setDisabled(True)
 
     def on_type_box_index_changed(self):
-        obj_type = self.type_box_combo.currentIndex()
+        obj_type = self.ui.type_box_combo.currentIndex()
         obj_type = 2 if obj_type == 1 else obj_type
-        self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        self.box_combo.setCurrentIndex(0)
-        self.box_combo.obj_type = {
+        self.ui.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.ui.box_combo.setCurrentIndex(0)
+        self.ui.box_combo.obj_type = {
             _("Gerber"): "Gerber", _("Geometry"): "Geometry"
-        }[self.type_box_combo.get_value()]
+        }[self.ui.type_box_combo.get_value()]
 
     def on_reference_radio_changed(self, current_val):
         if current_val == 'object':
-            self.type_box_combo.setDisabled(False)
-            self.type_box_combo_label.setDisabled(False)
-            self.box_combo.setDisabled(False)
+            self.ui.type_box_combo.setDisabled(False)
+            self.ui.type_box_combo_label.setDisabled(False)
+            self.ui.box_combo.setDisabled(False)
         else:
-            self.type_box_combo.setDisabled(True)
-            self.type_box_combo_label.setDisabled(True)
-            self.box_combo.setDisabled(True)
+            self.ui.type_box_combo.setDisabled(True)
+            self.ui.type_box_combo_label.setDisabled(True)
+            self.ui.box_combo.setDisabled(True)
 
     def on_panelize(self):
-        name = self.object_combo.currentText()
+        name = self.ui.object_combo.currentText()
+
+        # delete any selection box
+        self.app.delete_selection_shape()
 
         # Get source object to be panelized.
         try:
@@ -429,7 +196,7 @@ class Panelize(AppTool):
                                  (_("Object not found"), panel_source_obj))
             return
 
-        boxname = self.box_combo.currentText()
+        boxname = self.ui.box_combo.currentText()
 
         try:
             box = self.app.collection.get_by_name(boxname)
@@ -440,29 +207,29 @@ class Panelize(AppTool):
 
         if box is None:
             self.app.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), panel_source_obj))
-            self.reference_radio.set_value('bbox')
+            self.ui.reference_radio.set_value('bbox')
 
-        if self.reference_radio.get_value() == 'bbox':
+        if self.ui.reference_radio.get_value() == 'bbox':
             box = panel_source_obj
 
         self.outname = name + '_panelized'
 
-        spacing_columns = float(self.spacing_columns.get_value())
+        spacing_columns = float(self.ui.spacing_columns.get_value())
         spacing_columns = spacing_columns if spacing_columns is not None else 0
 
-        spacing_rows = float(self.spacing_rows.get_value())
+        spacing_rows = float(self.ui.spacing_rows.get_value())
         spacing_rows = spacing_rows if spacing_rows is not None else 0
 
-        rows = int(self.rows.get_value())
+        rows = int(self.ui.rows.get_value())
         rows = rows if rows is not None else 1
 
-        columns = int(self.columns.get_value())
+        columns = int(self.ui.columns.get_value())
         columns = columns if columns is not None else 1
 
-        constrain_dx = float(self.x_width_entry.get_value())
-        constrain_dy = float(self.y_height_entry.get_value())
+        constrain_dx = float(self.ui.x_width_entry.get_value())
+        constrain_dy = float(self.ui.y_height_entry.get_value())
 
-        panel_type = str(self.panel_type_radio.get_value())
+        panel_type = str(self.ui.panel_type_radio.get_value())
 
         if 0 in {columns, rows}:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
@@ -474,7 +241,7 @@ class Panelize(AppTool):
         lenghty = ymax - ymin + spacing_rows
 
         # check if constrain within an area is desired
-        if self.constrain_cb.isChecked():
+        if self.ui.constrain_cb.isChecked():
             panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
             panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
 
@@ -501,15 +268,19 @@ class Panelize(AppTool):
             for tt, tt_val in list(panel_source_obj.apertures.items()):
                 copied_apertures[tt] = deepcopy(tt_val)
 
+        to_optimize = self.ui.optimization_cb.get_value()
+
         def panelize_worker():
             if panel_source_obj is not None:
                 self.app.inform.emit(_("Generating panel ... "))
 
                 def job_init_excellon(obj_fin, app_obj):
                     currenty = 0.0
+                    # init the storage for drills and for slots
+                    for tool in copied_tools:
+                        copied_tools[tool]['drills'] = []
+                        copied_tools[tool]['slots'] = []
                     obj_fin.tools = copied_tools
-                    obj_fin.drills = []
-                    obj_fin.slots = []
                     obj_fin.solid_geometry = []
 
                     for option in panel_source_obj.options:
@@ -519,9 +290,14 @@ class Panelize(AppTool):
                             except KeyError:
                                 log.warning("Failed to copy option. %s" % str(option))
 
-                    geo_len_drills = len(panel_source_obj.drills) if panel_source_obj.drills else 0
-                    geo_len_slots = len(panel_source_obj.slots) if panel_source_obj.slots else 0
+                    # calculate the total number of drills and slots
+                    geo_len_drills = 0
+                    geo_len_slots = 0
+                    for tool in copied_tools:
+                        geo_len_drills += len(copied_tools[tool]['drills'])
+                        geo_len_slots += len(copied_tools[tool]['slots'])
 
+                    # panelization
                     element = 0
                     for row in range(rows):
                         currentx = 0.0
@@ -529,57 +305,53 @@ class Panelize(AppTool):
                             element += 1
                             old_disp_number = 0
 
-                            if panel_source_obj.drills:
-                                drill_nr = 0
-                                for tool_dict in panel_source_obj.drills:
-                                    if self.app.abort_flag:
+                            for tool in panel_source_obj.tools:
+                                if panel_source_obj.tools[tool]['drills']:
+                                    drill_nr = 0
+                                    for drill in panel_source_obj.tools[tool]['drills']:
                                         # graceful abort requested by the user
-                                        raise grace
+                                        if self.app.abort_flag:
+                                            raise grace
 
-                                    point_offseted = affinity.translate(tool_dict['point'], currentx, currenty)
-                                    obj_fin.drills.append(
-                                        {
-                                            "point": point_offseted,
-                                            "tool": tool_dict['tool']
-                                        }
-                                    )
-
-                                    drill_nr += 1
-                                    disp_number = int(np.interp(drill_nr, [0, geo_len_drills], [0, 100]))
-
-                                    if old_disp_number < disp_number <= 100:
-                                        self.app.proc_container.update_view_text(' %s: %d D:%d%%' %
-                                                                                 (_("Copy"),
-                                                                                  int(element),
-                                                                                  disp_number))
-                                        old_disp_number = disp_number
-
-                            if panel_source_obj.slots:
-                                slot_nr = 0
-                                for tool_dict in panel_source_obj.slots:
-                                    if self.app.abort_flag:
+                                        # offset / panelization
+                                        point_offseted = affinity.translate(drill, currentx, currenty)
+                                        obj_fin.tools[tool]['drills'].append(point_offseted)
+
+                                        # update progress
+                                        drill_nr += 1
+                                        disp_number = int(np.interp(drill_nr, [0, geo_len_drills], [0, 100]))
+                                        if old_disp_number < disp_number <= 100:
+                                            self.app.proc_container.update_view_text(' %s: %d D:%d%%' %
+                                                                                     (_("Copy"),
+                                                                                      int(element),
+                                                                                      disp_number))
+                                            old_disp_number = disp_number
+
+                                if panel_source_obj.tools[tool]['slots']:
+                                    slot_nr = 0
+                                    for slot in panel_source_obj.tools[tool]['slots']:
                                         # graceful abort requested by the user
-                                        raise grace
+                                        if self.app.abort_flag:
+                                            raise grace
 
-                                    start_offseted = affinity.translate(tool_dict['start'], currentx, currenty)
-                                    stop_offseted = affinity.translate(tool_dict['stop'], currentx, currenty)
-                                    obj_fin.slots.append(
-                                        {
-                                            "start": start_offseted,
-                                            "stop": stop_offseted,
-                                            "tool": tool_dict['tool']
-                                        }
-                                    )
-
-                                    slot_nr += 1
-                                    disp_number = int(np.interp(slot_nr, [0, geo_len_slots], [0, 100]))
-
-                                    if old_disp_number < disp_number <= 100:
-                                        self.app.proc_container.update_view_text(' %s: %d S:%d%%' %
-                                                                                 (_("Copy"),
-                                                                                  int(element),
-                                                                                  disp_number))
-                                        old_disp_number = disp_number
+                                        # offset / panelization
+                                        start_offseted = affinity.translate(slot[0], currentx, currenty)
+                                        stop_offseted = affinity.translate(slot[1], currentx, currenty)
+                                        offseted_slot = (
+                                            start_offseted,
+                                            stop_offseted
+                                        )
+                                        obj_fin.tools[tool]['slots'].append(offseted_slot)
+
+                                        # update progress
+                                        slot_nr += 1
+                                        disp_number = int(np.interp(slot_nr, [0, geo_len_slots], [0, 100]))
+                                        if old_disp_number < disp_number <= 100:
+                                            self.app.proc_container.update_view_text(' %s: %d S:%d%%' %
+                                                                                     (_("Copy"),
+                                                                                      int(element),
+                                                                                      disp_number))
+                                            old_disp_number = disp_number
 
                             currentx += lenghtx
                         currenty += lenghty
@@ -587,6 +359,9 @@ class Panelize(AppTool):
                     obj_fin.create_geometry()
                     obj_fin.zeros = panel_source_obj.zeros
                     obj_fin.units = panel_source_obj.units
+                    app_obj.inform.emit('%s' % _("Generating panel ... Adding the source code."))
+                    obj_fin.source_file = self.app.export_excellon(obj_name=self.outname, filename=None,
+                                                                 local_use=obj_fin, use_thread=False)
                     app_obj.proc_container.update_view_text('')
 
                 def job_init_geometry(obj_fin, app_obj):
@@ -614,7 +389,7 @@ class Panelize(AppTool):
                         obj_fin.tools = copied_tools
                         if panel_source_obj.multigeo is True:
                             for tool in panel_source_obj.tools:
-                                obj_fin.tools[tool]['solid_geometry'][:] = []
+                                obj_fin.tools[tool]['solid_geometry'] = []
                     elif panel_source_obj.kind == 'gerber':
                         obj_fin.apertures = copied_apertures
                         for ap in obj_fin.apertures:
@@ -629,11 +404,6 @@ class Panelize(AppTool):
                                     geo_len += len(panel_source_obj.tools[tool]['solid_geometry'])
                                 except TypeError:
                                     geo_len += 1
-                        # else:
-                        #     try:
-                        #         geo_len = len(panel_source_obj.solid_geometry)
-                        #     except TypeError:
-                        #         geo_len = 1
                     elif panel_source_obj.kind == 'gerber':
                         for ap in panel_source_obj.apertures:
                             if 'geometry' in panel_source_obj.apertures[ap]:
@@ -654,17 +424,23 @@ class Panelize(AppTool):
                             if panel_source_obj.kind == 'geometry':
                                 if panel_source_obj.multigeo is True:
                                     for tool in panel_source_obj.tools:
+                                        # graceful abort requested by the user
                                         if app_obj.abort_flag:
-                                            # graceful abort requested by the user
                                             raise grace
 
                                         # calculate the number of polygons
-                                        geo_len = len(panel_source_obj.tools[tool]['solid_geometry'])
+                                        try:
+                                            geo_len = len(panel_source_obj.tools[tool]['solid_geometry'])
+                                        except TypeError:
+                                            geo_len = 1
+
+                                        # panelization
                                         pol_nr = 0
                                         for geo_el in panel_source_obj.tools[tool]['solid_geometry']:
                                             trans_geo = translate_recursion(geo_el)
                                             obj_fin.tools[tool]['solid_geometry'].append(trans_geo)
 
+                                            # update progress
                                             pol_nr += 1
                                             disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
                                             if old_disp_number < disp_number <= 100:
@@ -672,15 +448,17 @@ class Panelize(AppTool):
                                                     ' %s: %d %d%%' % (_("Copy"), int(element), disp_number))
                                                 old_disp_number = disp_number
                                 else:
+                                    # graceful abort requested by the user
                                     if app_obj.abort_flag:
-                                        # graceful abort requested by the user
                                         raise grace
 
+                                    # calculate the number of polygons
                                     try:
-                                        # calculate the number of polygons
                                         geo_len = len(panel_source_obj.solid_geometry)
                                     except TypeError:
                                         geo_len = 1
+
+                                    # panelization
                                     pol_nr = 0
                                     try:
                                         for geo_el in panel_source_obj.solid_geometry:
@@ -691,9 +469,9 @@ class Panelize(AppTool):
                                             trans_geo = translate_recursion(geo_el)
                                             obj_fin.solid_geometry.append(trans_geo)
 
+                                            # update progress
                                             pol_nr += 1
                                             disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
-
                                             if old_disp_number < disp_number <= 100:
                                                 app_obj.proc_container.update_view_text(
                                                     ' %s: %d %d%%' % (_("Copy"), int(element), disp_number))
@@ -704,14 +482,15 @@ class Panelize(AppTool):
                                         obj_fin.solid_geometry.append(trans_geo)
                             # Will panelize a Gerber Object
                             else:
+                                # graceful abort requested by the user
                                 if self.app.abort_flag:
-                                    # graceful abort requested by the user
                                     raise grace
 
+                                # panelization solid_geometry
                                 try:
                                     for geo_el in panel_source_obj.solid_geometry:
+                                        # graceful abort requested by the user
                                         if app_obj.abort_flag:
-                                            # graceful abort requested by the user
                                             raise grace
 
                                         trans_geo = translate_recursion(geo_el)
@@ -721,15 +500,18 @@ class Panelize(AppTool):
                                     obj_fin.solid_geometry.append(trans_geo)
 
                                 for apid in panel_source_obj.apertures:
+                                    # graceful abort requested by the user
                                     if app_obj.abort_flag:
-                                        # graceful abort requested by the user
                                         raise grace
+
                                     if 'geometry' in panel_source_obj.apertures[apid]:
+                                        # calculate the number of polygons
                                         try:
-                                            # calculate the number of polygons
                                             geo_len = len(panel_source_obj.apertures[apid]['geometry'])
                                         except TypeError:
                                             geo_len = 1
+
+                                        # panelization -> tools
                                         pol_nr = 0
                                         for el in panel_source_obj.apertures[apid]['geometry']:
                                             if app_obj.abort_flag:
@@ -740,20 +522,17 @@ class Panelize(AppTool):
                                             if 'solid' in el:
                                                 geo_aper = translate_recursion(el['solid'])
                                                 new_el['solid'] = geo_aper
-
                                             if 'clear' in el:
                                                 geo_aper = translate_recursion(el['clear'])
                                                 new_el['clear'] = geo_aper
-
                                             if 'follow' in el:
                                                 geo_aper = translate_recursion(el['follow'])
                                                 new_el['follow'] = geo_aper
-
                                             obj_fin.apertures[apid]['geometry'].append(deepcopy(new_el))
 
+                                            # update progress
                                             pol_nr += 1
                                             disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
-
                                             if old_disp_number < disp_number <= 100:
                                                 app_obj.proc_container.update_view_text(
                                                     ' %s: %d %d%%' % (_("Copy"), int(element), disp_number))
@@ -766,21 +545,49 @@ class Panelize(AppTool):
                         # I'm going to do this only here as a fix for panelizing cutouts
                         # I'm going to separate linestrings out of the solid geometry from other
                         # possible type of elements and apply unary_union on them to fuse them
+
+                        if to_optimize is True:
+                            app_obj.inform.emit('%s' % _("Optimizing the overlapping paths."))
+
                         for tool in obj_fin.tools:
                             lines = []
                             other_geo = []
                             for geo in obj_fin.tools[tool]['solid_geometry']:
                                 if isinstance(geo, LineString):
                                     lines.append(geo)
+                                elif isinstance(geo, MultiLineString):
+                                    for line in geo:
+                                        lines.append(line)
                                 else:
                                     other_geo.append(geo)
-                            fused_lines = list(unary_union(lines))
+
+                            if to_optimize is True:
+                                for idx, line in enumerate(lines):
+                                    for idx_s in range(idx+1, len(lines)):
+                                        line_mod = lines[idx_s]
+                                        dist = line.distance(line_mod)
+                                        if dist < 1e-8:
+                                            print("Disjoint %d: %d -> %s" % (idx, idx_s, str(dist)))
+                                            print("Distance %f" % dist)
+                                        res = snap(line_mod, line, tolerance=1e-7)
+                                        if res and not res.is_empty:
+                                            lines[idx_s] = res
+
+                            fused_lines = linemerge(lines)
+                            fused_lines = [unary_union(fused_lines)]
+
                             obj_fin.tools[tool]['solid_geometry'] = fused_lines + other_geo
 
+                        if to_optimize is True:
+                            app_obj.inform.emit('%s' % _("Optimization complete."))
+
+                    app_obj.inform.emit('%s' % _("Generating panel ... Adding the source code."))
                     if panel_type == 'gerber':
-                        app_obj.inform.emit('%s' % _("Generating panel ... Adding the Gerber code."))
                         obj_fin.source_file = self.app.export_gerber(obj_name=self.outname, filename=None,
                                                                      local_use=obj_fin, use_thread=False)
+                    if panel_type == 'geometry':
+                        obj_fin.source_file = self.app.export_dxf(obj_name=self.outname, filename=None,
+                                                                     local_use=obj_fin, use_thread=False)
 
                     # obj_fin.solid_geometry = cascaded_union(obj_fin.solid_geometry)
                     # app_obj.log.debug("Finished creating a cascaded union for the panel.")
@@ -818,5 +625,303 @@ class Panelize(AppTool):
         self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
 
     def reset_fields(self):
+        self.ui.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ui.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+
+
+class PanelizeUI:
+
+    toolName = _("Panelize PCB")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+
+        self.object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Source Object"))
+        self.object_label.setToolTip(
+            _("Specify the type of object to be panelized\n"
+              "It can be of type: Gerber, Excellon or Geometry.\n"
+              "The selection here decide the type of objects that will be\n"
+              "in the Object combobox.")
+        )
+
+        self.layout.addWidget(self.object_label)
+
+        # Form Layout
+        form_layout_0 = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout_0)
+
+        # Type of object to be panelized
+        self.type_obj_combo = FCComboBox()
+        self.type_obj_combo.addItem("Gerber")
+        self.type_obj_combo.addItem("Excellon")
+        self.type_obj_combo.addItem("Geometry")
+
+        self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
+        self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
+        self.type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
+
+        self.type_object_label = QtWidgets.QLabel('%s:' % _("Object Type"))
+
+        form_layout_0.addRow(self.type_object_label, self.type_obj_combo)
+
+        # Object to be panelized
+        self.object_combo = FCComboBox()
+        self.object_combo.setModel(self.app.collection)
         self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.object_combo.is_last = True
+
+        self.object_combo.setToolTip(
+            _("Object to be panelized. This means that it will\n"
+              "be duplicated in an array of rows and columns.")
+        )
+        form_layout_0.addRow(self.object_combo)
+
+        # Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout)
+
+        # Type of box Panel object
+        self.reference_radio = RadioSet([{'label': _('Object'), 'value': 'object'},
+                                         {'label': _('Bounding Box'), 'value': 'bbox'}])
+        self.box_label = QtWidgets.QLabel("<b>%s:</b>" % _("Penelization Reference"))
+        self.box_label.setToolTip(
+            _("Choose the reference for panelization:\n"
+              "- Object = the bounding box of a different object\n"
+              "- Bounding Box = the bounding box of the object to be panelized\n"
+              "\n"
+              "The reference is useful when doing panelization for more than one\n"
+              "object. The spacings (really offsets) will be applied in reference\n"
+              "to this reference object therefore maintaining the panelized\n"
+              "objects in sync.")
+        )
+        form_layout.addRow(self.box_label)
+        form_layout.addRow(self.reference_radio)
+
+        # Type of Box Object to be used as an envelope for panelization
+        self.type_box_combo = FCComboBox()
+        self.type_box_combo.addItems([_("Gerber"), _("Geometry")])
+
+        # we get rid of item1 ("Excellon") as it is not suitable for use as a "box" for panelizing
+        # self.type_box_combo.view().setRowHidden(1, True)
+        self.type_box_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
+        self.type_box_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
+
+        self.type_box_combo_label = QtWidgets.QLabel('%s:' % _("Box Type"))
+        self.type_box_combo_label.setToolTip(
+            _("Specify the type of object to be used as an container for\n"
+              "panelization. It can be: Gerber or Geometry type.\n"
+              "The selection here decide the type of objects that will be\n"
+              "in the Box Object combobox.")
+        )
+        form_layout.addRow(self.type_box_combo_label, self.type_box_combo)
+
+        # Box
+        self.box_combo = FCComboBox()
+        self.box_combo.setModel(self.app.collection)
         self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        # self.box_combo.is_last = True
+
+        self.box_combo.setToolTip(
+            _("The actual object that is used as container for the\n "
+              "selected object that is to be panelized.")
+        )
+        form_layout.addRow(self.box_combo)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        form_layout.addRow(separator_line)
+
+        panel_data_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Data"))
+        panel_data_label.setToolTip(
+            _("This informations will shape the resulting panel.\n"
+              "The number of rows and columns will set how many\n"
+              "duplicates of the original geometry will be generated.\n"
+              "\n"
+              "The spacings will set the distance between any two\n"
+              "elements of the panel array.")
+        )
+        form_layout.addRow(panel_data_label)
+
+        # Spacing Columns
+        self.spacing_columns = FCDoubleSpinner(callback=self.confirmation_message)
+        self.spacing_columns.set_range(0, 9999)
+        self.spacing_columns.set_precision(4)
+
+        self.spacing_columns_label = QtWidgets.QLabel('%s:' % _("Spacing cols"))
+        self.spacing_columns_label.setToolTip(
+            _("Spacing between columns of the desired panel.\n"
+              "In current units.")
+        )
+        form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
+
+        # Spacing Rows
+        self.spacing_rows = FCDoubleSpinner(callback=self.confirmation_message)
+        self.spacing_rows.set_range(0, 9999)
+        self.spacing_rows.set_precision(4)
+
+        self.spacing_rows_label = QtWidgets.QLabel('%s:' % _("Spacing rows"))
+        self.spacing_rows_label.setToolTip(
+            _("Spacing between rows of the desired panel.\n"
+              "In current units.")
+        )
+        form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
+
+        # Columns
+        self.columns = FCSpinner(callback=self.confirmation_message_int)
+        self.columns.set_range(0, 9999)
+
+        self.columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
+        self.columns_label.setToolTip(
+            _("Number of columns of the desired panel")
+        )
+        form_layout.addRow(self.columns_label, self.columns)
+
+        # Rows
+        self.rows = FCSpinner(callback=self.confirmation_message_int)
+        self.rows.set_range(0, 9999)
+
+        self.rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
+        self.rows_label.setToolTip(
+            _("Number of rows of the desired panel")
+        )
+        form_layout.addRow(self.rows_label, self.rows)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        form_layout.addRow(separator_line)
+
+        # Type of resulting Panel object
+        self.panel_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'gerber'},
+                                          {'label': _('Geo'), 'value': 'geometry'}])
+        self.panel_type_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Type"))
+        self.panel_type_label.setToolTip(
+            _("Choose the type of object for the panel object:\n"
+              "- Geometry\n"
+              "- Gerber")
+        )
+        form_layout.addRow(self.panel_type_label)
+        form_layout.addRow(self.panel_type_radio)
+
+        # Path optimization
+        self.optimization_cb = FCCheckBox('%s' % _("Path Optimization"))
+        self.optimization_cb.setToolTip(
+            _("Active only for Geometry panel type.\n"
+              "When checked the application will find\n"
+              "any two overlapping Line elements in the panel\n"
+              "and remove the overlapping parts, keeping only one of them.")
+        )
+        form_layout.addRow(self.optimization_cb)
+
+        # Constrains
+        self.constrain_cb = FCCheckBox('%s:' % _("Constrain panel within"))
+        self.constrain_cb.setToolTip(
+            _("Area define by DX and DY within to constrain the panel.\n"
+              "DX and DY values are in current units.\n"
+              "Regardless of how many columns and rows are desired,\n"
+              "the final panel will have as many columns and rows as\n"
+              "they fit completely within selected area.")
+        )
+        form_layout.addRow(self.constrain_cb)
+
+        self.x_width_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.x_width_entry.set_precision(4)
+        self.x_width_entry.set_range(0, 9999)
+
+        self.x_width_lbl = QtWidgets.QLabel('%s:' % _("Width (DX)"))
+        self.x_width_lbl.setToolTip(
+            _("The width (DX) within which the panel must fit.\n"
+              "In current units.")
+        )
+        form_layout.addRow(self.x_width_lbl, self.x_width_entry)
+
+        self.y_height_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.y_height_entry.set_range(0, 9999)
+        self.y_height_entry.set_precision(4)
+
+        self.y_height_lbl = QtWidgets.QLabel('%s:' % _("Height (DY)"))
+        self.y_height_lbl.setToolTip(
+            _("The height (DY)within which the panel must fit.\n"
+              "In current units.")
+        )
+        form_layout.addRow(self.y_height_lbl, self.y_height_entry)
+
+        self.constrain_sel = OptionalInputSection(
+            self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        form_layout.addRow(separator_line)
+
+        # Buttons
+        self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object"))
+        self.panelize_object_button.setToolTip(
+            _("Panelize the specified object around the specified box.\n"
+              "In other words it creates multiple copies of the source object,\n"
+              "arranged in a 2D array of rows and columns.")
+        )
+        self.panelize_object_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.panelize_object_button)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.reset_button)
+
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+        self.panel_type_radio.activated_custom.connect(self.on_panel_type)
+
+    def on_panel_type(self, val):
+        if val == 'geometry':
+            self.optimization_cb.setDisabled(False)
+        else:
+            self.optimization_cb.setDisabled(True)
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 183 - 151
appTools/ToolPcbWizard.py

@@ -8,7 +8,7 @@
 from PyQt5 import QtWidgets, QtCore
 
 from appTool import AppTool
-from appGUI.GUIElements import RadioSet, FCSpinner, FCButton, FCTable
+from appGUI.GUIElements import RadioSet, FCSpinner, FCButton, FCTable, FCLabel
 
 import re
 import os
@@ -28,121 +28,17 @@ class PcbWizard(AppTool):
 
     file_loaded = QtCore.pyqtSignal(str, str)
 
-    toolName = _("PcbWizard Import Tool")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
 
         self.app = app
         self.decimals = self.app.decimals
 
-        # Title
-        title_label = QtWidgets.QLabel("%s" % _('Import 2-file Excellon'))
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-
-        self.layout.addWidget(QtWidgets.QLabel(""))
-        self.layout.addWidget(QtWidgets.QLabel("<b>%s:</b>" % _("Load files")))
-
-        # Form Layout
-        form_layout = QtWidgets.QFormLayout()
-        self.layout.addLayout(form_layout)
-
-        self.excellon_label = QtWidgets.QLabel('%s:' % _("Excellon file"))
-        self.excellon_label.setToolTip(
-           _("Load the Excellon file.\n"
-             "Usually it has a .DRL extension")
-        )
-        self.excellon_brn = FCButton(_("Open"))
-        form_layout.addRow(self.excellon_label, self.excellon_brn)
-
-        self.inf_label = QtWidgets.QLabel('%s:' % _("INF file"))
-        self.inf_label.setToolTip(
-            _("Load the INF file.")
-        )
-        self.inf_btn = FCButton(_("Open"))
-        form_layout.addRow(self.inf_label, self.inf_btn)
-
-        self.tools_table = FCTable()
-        self.layout.addWidget(self.tools_table)
-
-        self.tools_table.setColumnCount(2)
-        self.tools_table.setHorizontalHeaderLabels(['#Tool', _('Diameter')])
-
-        self.tools_table.horizontalHeaderItem(0).setToolTip(
-            _("Tool Number"))
-        self.tools_table.horizontalHeaderItem(1).setToolTip(
-            _("Tool diameter in file units."))
-
-        # start with apertures table hidden
-        self.tools_table.setVisible(False)
-
-        self.layout.addWidget(QtWidgets.QLabel(""))
-        self.layout.addWidget(QtWidgets.QLabel("<b>%s:</b>" % _("Excellon format")))
-        # Form Layout
-        form_layout1 = QtWidgets.QFormLayout()
-        self.layout.addLayout(form_layout1)
-
-        # Integral part of the coordinates
-        self.int_entry = FCSpinner(callback=self.confirmation_message_int)
-        self.int_entry.set_range(1, 10)
-        self.int_label = QtWidgets.QLabel('%s:' % _("Int. digits"))
-        self.int_label.setToolTip(
-           _("The number of digits for the integral part of the coordinates.")
-        )
-        form_layout1.addRow(self.int_label, self.int_entry)
-
-        # Fractional part of the coordinates
-        self.frac_entry = FCSpinner(callback=self.confirmation_message_int)
-        self.frac_entry.set_range(1, 10)
-        self.frac_label = QtWidgets.QLabel('%s:' % _("Frac. digits"))
-        self.frac_label.setToolTip(
-            _("The number of digits for the fractional part of the coordinates.")
-        )
-        form_layout1.addRow(self.frac_label, self.frac_entry)
-
-        # Zeros suppression for coordinates
-        self.zeros_radio = RadioSet([{'label': _('LZ'), 'value': 'LZ'},
-                                     {'label': _('TZ'), 'value': 'TZ'},
-                                     {'label': _('No Suppression'), 'value': 'D'}])
-        self.zeros_label = QtWidgets.QLabel('%s:' % _("Zeros supp."))
-        self.zeros_label.setToolTip(
-            _("The type of zeros suppression used.\n"
-              "Can be of type:\n"
-              "- LZ = leading zeros are kept\n"
-              "- TZ = trailing zeros are kept\n"
-              "- No Suppression = no zero suppression")
-        )
-        form_layout1.addRow(self.zeros_label, self.zeros_radio)
-
-        # Units type
-        self.units_radio = RadioSet([{'label': _('INCH'), 'value': 'INCH'},
-                                    {'label': _('MM'), 'value': 'METRIC'}])
-        self.units_label = QtWidgets.QLabel("<b>%s:</b>" % _('Units'))
-        self.units_label.setToolTip(
-            _("The type of units that the coordinates and tool\n"
-              "diameters are using. Can be INCH or MM.")
-        )
-        form_layout1.addRow(self.units_label, self.units_radio)
-
-        # Buttons
-
-        self.import_button = QtWidgets.QPushButton(_("Import Excellon"))
-        self.import_button.setToolTip(
-            _("Import in FlatCAM an Excellon file\n"
-              "that store it's information's in 2 files.\n"
-              "One usually has .DRL extension while\n"
-              "the other has .INF extension.")
-        )
-        self.layout.addWidget(self.import_button)
-
-        self.layout.addStretch()
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = WizardUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
         self.excellon_loaded = False
         self.inf_loaded = False
@@ -151,13 +47,13 @@ class PcbWizard(AppTool):
         self.modified_excellon_file = ''
 
         # ## Signals
-        self.excellon_brn.clicked.connect(self.on_load_excellon_click)
-        self.inf_btn.clicked.connect(self.on_load_inf_click)
-        self.import_button.clicked.connect(lambda: self.on_import_excellon(
+        self.ui.excellon_brn.clicked.connect(self.on_load_excellon_click)
+        self.ui.inf_btn.clicked.connect(self.on_load_inf_click)
+        self.ui.import_button.clicked.connect(lambda: self.on_import_excellon(
             excellon_fileobj=self.modified_excellon_file))
 
         self.file_loaded.connect(self.on_file_loaded)
-        self.units_radio.activated_custom.connect(self.on_units_change)
+        self.ui.units_radio.activated_custom.connect(self.ui.on_units_change)
 
         self.units = 'INCH'
         self.zeros = 'LZ'
@@ -211,10 +107,10 @@ class PcbWizard(AppTool):
         self.tools_from_inf = {}
 
         # ## Initialize form
-        self.int_entry.set_value(self.integral)
-        self.frac_entry.set_value(self.fractional)
-        self.zeros_radio.set_value(self.zeros)
-        self.units_radio.set_value(self.units)
+        self.ui.int_entry.set_value(self.integral)
+        self.ui.frac_entry.set_value(self.fractional)
+        self.ui.zeros_radio.set_value(self.zeros)
+        self.ui.units_radio.set_value(self.units)
 
         self.excellon_loaded = False
         self.inf_loaded = False
@@ -227,57 +123,49 @@ class PcbWizard(AppTool):
         sorted_tools = []
 
         if not self.tools_from_inf:
-            self.tools_table.setVisible(False)
+            self.ui.tools_table.setVisible(False)
         else:
             sort = []
             for k, v in list(self.tools_from_inf.items()):
                 sort.append(int(k))
             sorted_tools = sorted(sort)
             n = len(sorted_tools)
-            self.tools_table.setRowCount(n)
+            self.ui.tools_table.setRowCount(n)
 
         tool_row = 0
         for tool in sorted_tools:
             tool_id_item = QtWidgets.QTableWidgetItem('%d' % int(tool))
             tool_id_item.setFlags(QtCore.Qt.ItemIsEnabled)
-            self.tools_table.setItem(tool_row, 0, tool_id_item)  # Tool name/id
+            self.ui.tools_table.setItem(tool_row, 0, tool_id_item)  # Tool name/id
 
             tool_dia_item = QtWidgets.QTableWidgetItem(str(self.tools_from_inf[tool]))
             tool_dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
-            self.tools_table.setItem(tool_row, 1, tool_dia_item)
+            self.ui.tools_table.setItem(tool_row, 1, tool_dia_item)
             tool_row += 1
 
-        self.tools_table.resizeColumnsToContents()
-        self.tools_table.resizeRowsToContents()
+        self.ui.tools_table.resizeColumnsToContents()
+        self.ui.tools_table.resizeRowsToContents()
 
-        vertical_header = self.tools_table.verticalHeader()
+        vertical_header = self.ui.tools_table.verticalHeader()
         vertical_header.hide()
-        self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+        self.ui.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
 
-        horizontal_header = self.tools_table.horizontalHeader()
+        horizontal_header = self.ui.tools_table.horizontalHeader()
         # horizontal_header.setMinimumSectionSize(10)
         # horizontal_header.setDefaultSectionSize(70)
         horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
         horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
 
-        self.tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-        self.tools_table.setSortingEnabled(False)
-        self.tools_table.setMinimumHeight(self.tools_table.getHeight())
-        self.tools_table.setMaximumHeight(self.tools_table.getHeight())
+        self.ui.tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+        self.ui.tools_table.setSortingEnabled(False)
+        self.ui.tools_table.setMinimumHeight(self.ui.tools_table.getHeight())
+        self.ui.tools_table.setMaximumHeight(self.ui.tools_table.getHeight())
 
     def update_params(self):
-        self.units = self.units_radio.get_value()
-        self.zeros = self.zeros_radio.get_value()
-        self.integral = self.int_entry.get_value()
-        self.fractional = self.frac_entry.get_value()
-
-    def on_units_change(self, val):
-        if val == 'INCH':
-            self.int_entry.set_value(2)
-            self.frac_entry.set_value(4)
-        else:
-            self.int_entry.set_value(3)
-            self.frac_entry.set_value(3)
+        self.units = self.ui.units_radio.get_value()
+        self.zeros = self.ui.zeros_radio.get_value()
+        self.integral = self.ui.int_entry.get_value()
+        self.fractional = self.ui.frac_entry.get_value()
 
     def on_load_excellon_click(self):
         """
@@ -357,9 +245,9 @@ class PcbWizard(AppTool):
                     self.units = 'INCH'
                 else:
                     self.units = 'METRIC'
-                self.units_radio.set_value(self.units)
-                self.int_entry.set_value(self.integral)
-                self.frac_entry.set_value(self.fractional)
+                self.ui.units_radio.set_value(self.units)
+                self.ui.int_entry.set_value(self.integral)
+                self.ui.frac_entry.set_value(self.fractional)
 
         if not self.tools_from_inf:
             self.app.inform.emit('[ERROR] %s' %
@@ -382,14 +270,12 @@ class PcbWizard(AppTool):
 
         if signal == 'inf':
             self.inf_loaded = True
-            self.tools_table.setVisible(True)
-            self.app.inform.emit('[success] %s' %
-                                 _("PcbWizard .INF file loaded."))
+            self.ui.tools_table.setVisible(True)
+            self.app.inform.emit('[success] %s' % _("PcbWizard .INF file loaded."))
         elif signal == 'excellon':
             self.excellon_loaded = True
             self.outname = os.path.split(str(filename))[1]
-            self.app.inform.emit('[success] %s' %
-                                 _("Main PcbWizard Excellon file loaded."))
+            self.app.inform.emit('[success] %s' % _("Main PcbWizard Excellon file loaded."))
 
         if self.excellon_loaded and self.inf_loaded:
             self.update_params()
@@ -467,3 +353,149 @@ class PcbWizard(AppTool):
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _('Excellon merging is in progress. Please wait...'))
         else:
             self.app.inform.emit('[ERROR_NOTCL] %s' % _('The imported Excellon file is empty.'))
+
+
+class WizardUI:
+    
+    toolName = _("PcbWizard Import Tool")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        # ## Title
+        title_label = FCLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(FCLabel(""))
+
+        self.layout.addWidget(FCLabel("<b>%s:</b>" % _("Load files")))
+
+        # Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout)
+
+        self.excellon_label = FCLabel('%s:' % _("Excellon file"))
+        self.excellon_label.setToolTip(
+            _("Load the Excellon file.\n"
+              "Usually it has a .DRL extension")
+        )
+        self.excellon_brn = FCButton(_("Open"))
+        form_layout.addRow(self.excellon_label, self.excellon_brn)
+
+        self.inf_label = FCLabel('%s:' % _("INF file"))
+        self.inf_label.setToolTip(
+            _("Load the INF file.")
+        )
+        self.inf_btn = FCButton(_("Open"))
+        form_layout.addRow(self.inf_label, self.inf_btn)
+
+        self.tools_table = FCTable()
+        self.layout.addWidget(self.tools_table)
+
+        self.tools_table.setColumnCount(2)
+        self.tools_table.setHorizontalHeaderLabels(['#Tool', _('Diameter')])
+
+        self.tools_table.horizontalHeaderItem(0).setToolTip(
+            _("Tool Number"))
+        self.tools_table.horizontalHeaderItem(1).setToolTip(
+            _("Tool diameter in file units."))
+
+        # start with apertures table hidden
+        self.tools_table.setVisible(False)
+
+        self.layout.addWidget(FCLabel(""))
+        self.layout.addWidget(FCLabel("<b>%s:</b>" % _("Excellon format")))
+        # Form Layout
+        form_layout1 = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout1)
+
+        # Integral part of the coordinates
+        self.int_entry = FCSpinner(callback=self.confirmation_message_int)
+        self.int_entry.set_range(1, 10)
+        self.int_label = FCLabel('%s:' % _("Int. digits"))
+        self.int_label.setToolTip(
+            _("The number of digits for the integral part of the coordinates.")
+        )
+        form_layout1.addRow(self.int_label, self.int_entry)
+
+        # Fractional part of the coordinates
+        self.frac_entry = FCSpinner(callback=self.confirmation_message_int)
+        self.frac_entry.set_range(1, 10)
+        self.frac_label = FCLabel('%s:' % _("Frac. digits"))
+        self.frac_label.setToolTip(
+            _("The number of digits for the fractional part of the coordinates.")
+        )
+        form_layout1.addRow(self.frac_label, self.frac_entry)
+
+        # Zeros suppression for coordinates
+        self.zeros_radio = RadioSet([{'label': _('LZ'), 'value': 'LZ'},
+                                     {'label': _('TZ'), 'value': 'TZ'},
+                                     {'label': _('No Suppression'), 'value': 'D'}])
+        self.zeros_label = FCLabel('%s:' % _("Zeros supp."))
+        self.zeros_label.setToolTip(
+            _("The type of zeros suppression used.\n"
+              "Can be of type:\n"
+              "- LZ = leading zeros are kept\n"
+              "- TZ = trailing zeros are kept\n"
+              "- No Suppression = no zero suppression")
+        )
+        form_layout1.addRow(self.zeros_label, self.zeros_radio)
+
+        # Units type
+        self.units_radio = RadioSet([{'label': _('INCH'), 'value': 'INCH'},
+                                     {'label': _('MM'), 'value': 'METRIC'}])
+        self.units_label = FCLabel("<b>%s:</b>" % _('Units'))
+        self.units_label.setToolTip(
+            _("The type of units that the coordinates and tool\n"
+              "diameters are using. Can be INCH or MM.")
+        )
+        form_layout1.addRow(self.units_label, self.units_radio)
+
+        # Buttons
+
+        self.import_button = QtWidgets.QPushButton(_("Import Excellon"))
+        self.import_button.setToolTip(
+            _("Import in FlatCAM an Excellon file\n"
+              "that store it's information's in 2 files.\n"
+              "One usually has .DRL extension while\n"
+              "the other has .INF extension.")
+        )
+        self.layout.addWidget(self.import_button)
+
+        self.layout.addStretch()
+
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def on_units_change(self, val):
+        if val == 'INCH':
+            self.int_entry.set_value(2)
+            self.frac_entry.set_value(4)
+        else:
+            self.int_entry.set_value(3)
+            self.frac_entry.set_value(3)
+            
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 45 - 29
appTools/ToolProperties.py

@@ -146,30 +146,33 @@ class Properties(AppTool):
         font = QtGui.QFont()
         font.setBold(True)
 
+        p_color = QtGui.QColor("#000000") if self.app.defaults['global_gray_icons'] is False \
+            else QtGui.QColor("#FFFFFF")
+
         # main Items categories
-        obj_type = self.treeWidget.addParent(parent, _('TYPE'), expanded=True, color=QtGui.QColor("#000000"), font=font)
-        obj_name = self.treeWidget.addParent(parent, _('NAME'), expanded=True, color=QtGui.QColor("#000000"), font=font)
+        obj_type = self.treeWidget.addParent(parent, _('TYPE'), expanded=True, color=p_color, font=font)
+        obj_name = self.treeWidget.addParent(parent, _('NAME'), expanded=True, color=p_color, font=font)
         dims = self.treeWidget.addParent(
-            parent, _('Dimensions'), expanded=True, color=QtGui.QColor("#000000"), font=font)
-        units = self.treeWidget.addParent(parent, _('Units'), expanded=True, color=QtGui.QColor("#000000"), font=font)
-        options = self.treeWidget.addParent(parent, _('Options'), color=QtGui.QColor("#000000"), font=font)
+            parent, _('Dimensions'), expanded=True, color=p_color, font=font)
+        units = self.treeWidget.addParent(parent, _('Units'), expanded=True, color=p_color, font=font)
+        options = self.treeWidget.addParent(parent, _('Options'), color=p_color, font=font)
 
         if obj.kind.lower() == 'gerber':
             apertures = self.treeWidget.addParent(
-                parent, _('Apertures'), expanded=True, color=QtGui.QColor("#000000"), font=font)
+                parent, _('Apertures'), expanded=True, color=p_color, font=font)
         else:
             tools = self.treeWidget.addParent(
-                parent, _('Tools'), expanded=True, color=QtGui.QColor("#000000"), font=font)
+                parent, _('Tools'), expanded=True, color=p_color, font=font)
 
         if obj.kind.lower() == 'excellon':
             drills = self.treeWidget.addParent(
-                parent, _('Drills'), expanded=True, color=QtGui.QColor("#000000"), font=font)
+                parent, _('Drills'), expanded=True, color=p_color, font=font)
             slots = self.treeWidget.addParent(
-                parent, _('Slots'), expanded=True, color=QtGui.QColor("#000000"), font=font)
+                parent, _('Slots'), expanded=True, color=p_color, font=font)
 
         if obj.kind.lower() == 'cncjob':
             others = self.treeWidget.addParent(
-                parent, _('Others'), expanded=True, color=QtGui.QColor("#000000"), font=font)
+                parent, _('Others'), expanded=True, color=p_color, font=font)
 
         separator = self.treeWidget.addParent(parent, '')
 
@@ -193,7 +196,7 @@ class Properties(AppTool):
         self.treeWidget.addChild(obj_name, [obj.options['name']])
 
         def job_thread(obj_prop):
-            proc = self.app.proc_container.new(_("Calculating dimensions ... Please wait."))
+            self.app.proc_container.new(_("Calculating dimensions ... Please wait."))
 
             length = 0.0
             width = 0.0
@@ -382,7 +385,7 @@ class Properties(AppTool):
                 temp_ap['Clear_Geo'] = '%s Polygons' % str(clear_nr)
 
                 apid = self.treeWidget.addParent(
-                    apertures, str(ap), expanded=False, color=QtGui.QColor("#000000"), font=font)
+                    apertures, str(ap), expanded=False, color=p_color, font=font)
                 for key in temp_ap:
                     self.treeWidget.addChild(apid, [str(key), str(temp_ap[key])], True)
         elif obj.kind.lower() == 'excellon':
@@ -391,22 +394,20 @@ class Properties(AppTool):
 
             for tool, value in obj.tools.items():
                 toolid = self.treeWidget.addParent(
-                    tools, str(tool), expanded=False, color=QtGui.QColor("#000000"), font=font)
+                    tools, str(tool), expanded=False, color=p_color, font=font)
 
                 drill_cnt = 0  # variable to store the nr of drills per tool
                 slot_cnt = 0  # variable to store the nr of slots per tool
 
                 # Find no of drills for the current tool
-                for drill in obj.drills:
-                    if drill['tool'] == tool:
-                        drill_cnt += 1
+                if 'drills' in value and value['drills']:
+                    drill_cnt = len(value['drills'])
 
                 tot_drill_cnt += drill_cnt
 
                 # Find no of slots for the current tool
-                for slot in obj.slots:
-                    if slot['tool'] == tool:
-                        slot_cnt += 1
+                if 'slots' in value and value['slots']:
+                    slot_cnt = len(value['slots'])
 
                 tot_slot_cnt += slot_cnt
 
@@ -414,7 +415,7 @@ class Properties(AppTool):
                     toolid,
                     [
                         _('Diameter'),
-                        '%.*f %s' % (self.decimals, value['C'], self.app.defaults['units'].lower())
+                        '%.*f %s' % (self.decimals, value['tooldia'], self.app.defaults['units'].lower())
                     ],
                     True
                 )
@@ -426,7 +427,7 @@ class Properties(AppTool):
         elif obj.kind.lower() == 'geometry':
             for tool, value in obj.tools.items():
                 geo_tool = self.treeWidget.addParent(
-                    tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
+                    tools, str(tool), expanded=True, color=p_color, font=font)
                 for k, v in value.items():
                     if k == 'solid_geometry':
                         # printed_value = _('Present') if v else _('None')
@@ -437,7 +438,7 @@ class Properties(AppTool):
                         self.treeWidget.addChild(geo_tool, [str(k), printed_value], True)
                     elif k == 'data':
                         tool_data = self.treeWidget.addParent(
-                            geo_tool, str(k).capitalize(), color=QtGui.QColor("#000000"), font=font)
+                            geo_tool, str(k).capitalize(), color=p_color, font=font)
                         for data_k, data_v in v.items():
                             self.treeWidget.addChild(tool_data, [str(data_k), str(data_v)], True)
                     else:
@@ -446,7 +447,7 @@ class Properties(AppTool):
             # for cncjob objects made from gerber or geometry
             for tool, value in obj.cnc_tools.items():
                 geo_tool = self.treeWidget.addParent(
-                    tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
+                    tools, str(tool), expanded=True, color=p_color, font=font)
                 for k, v in value.items():
                     if k == 'solid_geometry':
                         printed_value = _('Present') if v else _('None')
@@ -458,17 +459,20 @@ class Properties(AppTool):
                         printed_value = _('Present') if v else _('None')
                         self.treeWidget.addChild(geo_tool, [_("GCode Geometry"), printed_value], True)
                     elif k == 'data':
-                        tool_data = self.treeWidget.addParent(
-                            geo_tool, _("Data"), color=QtGui.QColor("#000000"), font=font)
-                        for data_k, data_v in v.items():
-                            self.treeWidget.addChild(tool_data, [str(data_k).capitalize(), str(data_v)], True)
+                        pass
                     else:
                         self.treeWidget.addChild(geo_tool, [str(k), str(v)], True)
 
+                v = value['data']
+                tool_data = self.treeWidget.addParent(
+                    geo_tool, _("Tool Data"), color=p_color, font=font)
+                for data_k, data_v in v.items():
+                    self.treeWidget.addChild(tool_data, [str(data_k).capitalize(), str(data_v)], True)
+
             # for cncjob objects made from excellon
             for tool_dia, value in obj.exc_cnc_tools.items():
                 exc_tool = self.treeWidget.addParent(
-                    tools, str(value['tool']), expanded=False, color=QtGui.QColor("#000000"), font=font
+                    tools, str(value['tool']), expanded=False, color=p_color, font=font
                 )
                 self.treeWidget.addChild(
                     exc_tool,
@@ -486,6 +490,12 @@ class Properties(AppTool):
                         self.treeWidget.addChild(exc_tool, [_("Drills number"), str(v)], True)
                     elif k == 'nr_slots':
                         self.treeWidget.addChild(exc_tool, [_("Slots number"), str(v)], True)
+                    elif k == 'gcode':
+                        printed_value = _('Present') if v != '' else _('None')
+                        self.treeWidget.addChild(exc_tool, [_("GCode Text"), printed_value], True)
+                    elif k == 'gcode_parsed':
+                        printed_value = _('Present') if v else _('None')
+                        self.treeWidget.addChild(exc_tool, [_("GCode Geometry"), printed_value], True)
                     else:
                         pass
 
@@ -495,7 +505,7 @@ class Properties(AppTool):
                         _("Depth of Cut"),
                         '%.*f %s' % (
                             self.decimals,
-                            (obj.z_cut - abs(obj.tool_offset[tool_dia])),
+                            (obj.z_cut - abs(value['data']['tools_drill_offset'])),
                             self.app.defaults['units'].lower()
                         )
                     ],
@@ -526,6 +536,12 @@ class Properties(AppTool):
                     True
                 )
 
+                v = value['data']
+                tool_data = self.treeWidget.addParent(
+                    exc_tool, _("Tool Data"), color=p_color, font=font)
+                for data_k, data_v in v.items():
+                    self.treeWidget.addChild(tool_data, [str(data_k).capitalize(), str(data_v)], True)
+
             r_time = obj.routing_time
             if r_time > 1:
                 units_lbl = 'min'

+ 536 - 504
appTools/ToolPunchGerber.py

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

+ 394 - 355
appTools/ToolQRCode.py

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

+ 1362 - 1327
appTools/ToolRulesCheck.py

@@ -5,10 +5,10 @@
 # MIT Licence                                              #
 # ##########################################################
 
-from PyQt5 import QtWidgets
+from PyQt5 import QtWidgets, QtGui
 
 from appTool import AppTool
-from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, OptionalInputSection, FCComboBox
+from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, OptionalInputSection, FCComboBox, FCLabel, FCButton
 from copy import deepcopy
 
 from appPool import *
@@ -30,8 +30,6 @@ log = logging.getLogger('base')
 
 class RulesCheck(AppTool):
 
-    toolName = _("Check Rules")
-
     tool_finished = QtCore.pyqtSignal(list)
 
     def __init__(self, app):
@@ -39,1595 +37,1632 @@ class RulesCheck(AppTool):
 
         AppTool.__init__(self, app)
 
-        # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-
-        # Form Layout
-        self.grid_layout = QtWidgets.QGridLayout()
-        self.layout.addLayout(self.grid_layout)
-
-        self.grid_layout.setColumnStretch(0, 0)
-        self.grid_layout.setColumnStretch(1, 3)
-        self.grid_layout.setColumnStretch(2, 0)
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = RulesUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
-        self.gerber_title_lbl = QtWidgets.QLabel('<b>%s</b>:' % _("GERBER"))
-        self.gerber_title_lbl.setToolTip(
-            _("Gerber objects for which to check rules.")
-        )
+        # #######################################################
+        # ################ SIGNALS ##############################
+        # #######################################################
+        self.ui.copper_t_cb.stateChanged.connect(lambda st: self.ui.copper_t_object.setDisabled(not st))
+        self.ui.copper_b_cb.stateChanged.connect(lambda st: self.ui.copper_b_object.setDisabled(not st))
 
-        self.all_obj_cb = FCCheckBox()
+        self.ui.sm_t_cb.stateChanged.connect(lambda st: self.ui.sm_t_object.setDisabled(not st))
+        self.ui.sm_b_cb.stateChanged.connect(lambda st: self.ui.sm_b_object.setDisabled(not st))
 
-        self.grid_layout.addWidget(self.gerber_title_lbl, 0, 0, 1, 2)
-        self.grid_layout.addWidget(self.all_obj_cb, 0, 2)
+        self.ui.ss_t_cb.stateChanged.connect(lambda st: self.ui.ss_t_object.setDisabled(not st))
+        self.ui.ss_b_cb.stateChanged.connect(lambda st: self.ui.ss_b_object.setDisabled(not st))
 
-        # Copper Top object
-        self.copper_t_object = FCComboBox()
-        self.copper_t_object.setModel(self.app.collection)
-        self.copper_t_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.copper_t_object.is_last = True
-        self.copper_t_object.obj_type = "Gerber"
+        self.ui.out_cb.stateChanged.connect(lambda st: self.ui.outline_object.setDisabled(not st))
 
-        self.copper_t_object_lbl = QtWidgets.QLabel('%s:' % _("Top"))
-        self.copper_t_object_lbl.setToolTip(
-            _("The Top Gerber Copper object for which rules are checked.")
-        )
+        self.ui.e1_cb.stateChanged.connect(lambda st: self.ui.e1_object.setDisabled(not st))
+        self.ui.e2_cb.stateChanged.connect(lambda st: self.ui.e2_object.setDisabled(not st))
 
-        self.copper_t_cb = FCCheckBox()
+        self.ui.all_obj_cb.stateChanged.connect(self.ui.on_all_objects_cb_changed)
+        self.ui.all_cb.stateChanged.connect(self.ui.on_all_cb_changed)
+        self.ui.run_button.clicked.connect(self.execute)
 
-        self.grid_layout.addWidget(self.copper_t_object_lbl, 1, 0)
-        self.grid_layout.addWidget(self.copper_t_object, 1, 1)
-        self.grid_layout.addWidget(self.copper_t_cb, 1, 2)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
+        
+        # Custom Signals
+        self.tool_finished.connect(self.on_tool_finished)
 
-        # Copper Bottom object
-        self.copper_b_object = FCComboBox()
-        self.copper_b_object.setModel(self.app.collection)
-        self.copper_b_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.copper_b_object.is_last = True
-        self.copper_b_object.obj_type = "Gerber"
+        # list to hold the temporary objects
+        self.objs = []
 
-        self.copper_b_object_lbl = QtWidgets.QLabel('%s:' % _("Bottom"))
-        self.copper_b_object_lbl.setToolTip(
-            _("The Bottom Gerber Copper object for which rules are checked.")
-        )
+        # final name for the panel object
+        self.outname = ""
 
-        self.copper_b_cb = FCCheckBox()
+        # flag to signal the constrain was activated
+        self.constrain_flag = False
 
-        self.grid_layout.addWidget(self.copper_b_object_lbl, 2, 0)
-        self.grid_layout.addWidget(self.copper_b_object, 2, 1)
-        self.grid_layout.addWidget(self.copper_b_cb, 2, 2)
+        # Multiprocessing Process Pool
+        self.pool = self.app.pool
+        self.results = None
 
-        # SolderMask Top object
-        self.sm_t_object = FCComboBox()
-        self.sm_t_object.setModel(self.app.collection)
-        self.sm_t_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.sm_t_object.is_last = True
-        self.sm_t_object.obj_type = "Gerber"
+        self.decimals = 4
 
-        self.sm_t_object_lbl = QtWidgets.QLabel('%s:' % _("SM Top"))
-        self.sm_t_object_lbl.setToolTip(
-            _("The Top Gerber Solder Mask object for which rules are checked.")
-        )
+    # def on_object_loaded(self, index, row):
+    #     print(index.internalPointer().child_items[row].obj.options['name'], index.data())
 
-        self.sm_t_cb = FCCheckBox()
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("ToolRulesCheck()")
 
-        self.grid_layout.addWidget(self.sm_t_object_lbl, 3, 0)
-        self.grid_layout.addWidget(self.sm_t_object, 3, 1)
-        self.grid_layout.addWidget(self.sm_t_cb, 3, 2)
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
 
-        # SolderMask Bottom object
-        self.sm_b_object = FCComboBox()
-        self.sm_b_object.setModel(self.app.collection)
-        self.sm_b_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.sm_b_object.is_last = True
-        self.sm_b_object.obj_type = "Gerber"
+        AppTool.run(self)
+        self.set_tool_ui()
 
-        self.sm_b_object_lbl = QtWidgets.QLabel('%s:' % _("SM Bottom"))
-        self.sm_b_object_lbl.setToolTip(
-            _("The Bottom Gerber Solder Mask object for which rules are checked.")
-        )
+        self.app.ui.notebook.setTabText(2, _("Rules Tool"))
 
-        self.sm_b_cb = FCCheckBox()
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='Alt+R', **kwargs)
 
-        self.grid_layout.addWidget(self.sm_b_object_lbl, 4, 0)
-        self.grid_layout.addWidget(self.sm_b_object, 4, 1)
-        self.grid_layout.addWidget(self.sm_b_cb, 4, 2)
+    def set_tool_ui(self):
 
-        # SilkScreen Top object
-        self.ss_t_object = FCComboBox()
-        self.ss_t_object.setModel(self.app.collection)
-        self.ss_t_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.ss_t_object.is_last = True
-        self.ss_t_object.obj_type = "Gerber"
+        # all object combobox default as disabled
+        self.ui.copper_t_object.setDisabled(True)
+        self.ui.copper_b_object.setDisabled(True)
+
+        self.ui.sm_t_object.setDisabled(True)
+        self.ui.sm_b_object.setDisabled(True)
+
+        self.ui.ss_t_object.setDisabled(True)
+        self.ui.ss_b_object.setDisabled(True)
+
+        self.ui.outline_object.setDisabled(True)
+
+        self.ui.e1_object.setDisabled(True)
+        self.ui.e2_object.setDisabled(True)
+
+        self.ui.trace_size_cb.set_value(self.app.defaults["tools_cr_trace_size"])
+        self.ui.trace_size_entry.set_value(float(self.app.defaults["tools_cr_trace_size_val"]))
+        self.ui.clearance_copper2copper_cb.set_value(self.app.defaults["tools_cr_c2c"])
+        self.ui.clearance_copper2copper_entry.set_value(float(self.app.defaults["tools_cr_c2c_val"]))
+        self.ui.clearance_copper2ol_cb.set_value(self.app.defaults["tools_cr_c2o"])
+        self.ui.clearance_copper2ol_entry.set_value(float(self.app.defaults["tools_cr_c2o_val"]))
+        self.ui.clearance_silk2silk_cb.set_value(self.app.defaults["tools_cr_s2s"])
+        self.ui.clearance_silk2silk_entry.set_value(float(self.app.defaults["tools_cr_s2s_val"]))
+        self.ui.clearance_silk2sm_cb.set_value(self.app.defaults["tools_cr_s2sm"])
+        self.ui.clearance_silk2sm_entry.set_value(float(self.app.defaults["tools_cr_s2sm_val"]))
+        self.ui.clearance_silk2ol_cb.set_value(self.app.defaults["tools_cr_s2o"])
+        self.ui.clearance_silk2ol_entry.set_value(float(self.app.defaults["tools_cr_s2o_val"]))
+        self.ui.clearance_sm2sm_cb.set_value(self.app.defaults["tools_cr_sm2sm"])
+        self.ui.clearance_sm2sm_entry.set_value(float(self.app.defaults["tools_cr_sm2sm_val"]))
+        self.ui.ring_integrity_cb.set_value(self.app.defaults["tools_cr_ri"])
+        self.ui.ring_integrity_entry.set_value(float(self.app.defaults["tools_cr_ri_val"]))
+        self.ui.clearance_d2d_cb.set_value(self.app.defaults["tools_cr_h2h"])
+        self.ui.clearance_d2d_entry.set_value(float(self.app.defaults["tools_cr_h2h_val"]))
+        self.ui.drill_size_cb.set_value(self.app.defaults["tools_cr_dh"])
+        self.ui.drill_size_entry.set_value(float(self.app.defaults["tools_cr_dh_val"]))
 
-        self.ss_t_object_lbl = QtWidgets.QLabel('%s:' % _("Silk Top"))
-        self.ss_t_object_lbl.setToolTip(
-            _("The Top Gerber Silkscreen object for which rules are checked.")
-        )
+        self.reset_fields()
 
-        self.ss_t_cb = FCCheckBox()
+    @staticmethod
+    def check_inside_gerber_clearance(gerber_obj, size, rule):
+        log.debug("RulesCheck.check_inside_gerber_clearance()")
 
-        self.grid_layout.addWidget(self.ss_t_object_lbl, 5, 0)
-        self.grid_layout.addWidget(self.ss_t_object, 5, 1)
-        self.grid_layout.addWidget(self.ss_t_cb, 5, 2)
+        rule_title = rule
 
-        # SilkScreen Bottom object
-        self.ss_b_object = FCComboBox()
-        self.ss_b_object.setModel(self.app.collection)
-        self.ss_b_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.ss_b_object.is_last = True
-        self.ss_b_object.obj_type = "Gerber"
+        violations = []
+        obj_violations = {}
+        obj_violations.update({
+            'name': '',
+            'points': list()
+        })
 
-        self.ss_b_object_lbl = QtWidgets.QLabel('%s:' % _("Silk Bottom"))
-        self.ss_b_object_lbl.setToolTip(
-            _("The Bottom Gerber Silkscreen object for which rules are checked.")
-        )
+        if not gerber_obj:
+            return 'Fail. Not enough Gerber objects to check Gerber 2 Gerber clearance'
 
-        self.ss_b_cb = FCCheckBox()
+        obj_violations['name'] = gerber_obj['name']
 
-        self.grid_layout.addWidget(self.ss_b_object_lbl, 6, 0)
-        self.grid_layout.addWidget(self.ss_b_object, 6, 1)
-        self.grid_layout.addWidget(self.ss_b_cb, 6, 2)
+        solid_geo = []
+        clear_geo = []
+        for apid in gerber_obj['apertures']:
+            if 'geometry' in gerber_obj['apertures'][apid]:
+                geometry = gerber_obj['apertures'][apid]['geometry']
+                for geo_el in geometry:
+                    if 'solid' in geo_el and geo_el['solid'] is not None:
+                        solid_geo.append(geo_el['solid'])
+                    if 'clear' in geo_el and geo_el['clear'] is not None:
+                        clear_geo.append(geo_el['clear'])
 
-        # Outline object
-        self.outline_object = FCComboBox()
-        self.outline_object.setModel(self.app.collection)
-        self.outline_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.outline_object.is_last = True
-        self.outline_object.obj_type = "Gerber"
+        if clear_geo:
+            total_geo = []
+            for geo_c in clear_geo:
+                for geo_s in solid_geo:
+                    if geo_c.within(geo_s):
+                        total_geo.append(geo_s.difference(geo_c))
+        else:
+            total_geo = MultiPolygon(solid_geo)
+            total_geo = total_geo.buffer(0.000001)
 
-        self.outline_object_lbl = QtWidgets.QLabel('%s:' % _("Outline"))
-        self.outline_object_lbl.setToolTip(
-            _("The Gerber Outline (Cutout) object for which rules are checked.")
-        )
+        if isinstance(total_geo, Polygon):
+            obj_violations['points'] = ['Failed. Only one polygon.']
+            return rule_title, [obj_violations]
+        else:
+            iterations = len(total_geo)
+            iterations = (iterations * (iterations - 1)) / 2
+        log.debug("RulesCheck.check_gerber_clearance(). Iterations: %s" % str(iterations))
 
-        self.out_cb = FCCheckBox()
+        min_dict = {}
+        idx = 1
+        for geo in total_geo:
+            for s_geo in total_geo[idx:]:
+                # minimize the number of distances by not taking into considerations those that are too small
+                dist = geo.distance(s_geo)
+                if float(dist) < float(size):
+                    loc_1, loc_2 = nearest_points(geo, s_geo)
 
-        self.grid_layout.addWidget(self.outline_object_lbl, 7, 0)
-        self.grid_layout.addWidget(self.outline_object, 7, 1)
-        self.grid_layout.addWidget(self.out_cb, 7, 2)
+                    dx = loc_1.x - loc_2.x
+                    dy = loc_1.y - loc_2.y
+                    loc = min(loc_1.x, loc_2.x) + (abs(dx) / 2), min(loc_1.y, loc_2.y) + (abs(dy) / 2)
 
-        self.grid_layout.addWidget(QtWidgets.QLabel(""), 8, 0, 1, 3)
+                    if dist in min_dict:
+                        min_dict[dist].append(loc)
+                    else:
+                        min_dict[dist] = [loc]
+            idx += 1
+        points_list = set()
+        for dist in min_dict.keys():
+            for location in min_dict[dist]:
+                points_list.add(location)
 
-        self.excellon_title_lbl = QtWidgets.QLabel('<b>%s</b>:' % _("EXCELLON"))
-        self.excellon_title_lbl.setToolTip(
-            _("Excellon objects for which to check rules.")
-        )
+        obj_violations['points'] = list(points_list)
+        violations.append(deepcopy(obj_violations))
 
-        self.grid_layout.addWidget(self.excellon_title_lbl, 9, 0, 1, 3)
+        return rule_title, violations
 
-        # Excellon 1 object
-        self.e1_object = FCComboBox()
-        self.e1_object.setModel(self.app.collection)
-        self.e1_object.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
-        self.e1_object.is_last = True
-        self.e1_object.obj_type = "Excellon"
+    @staticmethod
+    def check_gerber_clearance(gerber_list, size, rule):
+        log.debug("RulesCheck.check_gerber_clearance()")
+        rule_title = rule
 
-        self.e1_object_lbl = QtWidgets.QLabel('%s:' % _("Excellon 1"))
-        self.e1_object_lbl.setToolTip(
-            _("Excellon object for which to check rules.\n"
-              "Holds the plated holes or a general Excellon file content.")
-        )
+        violations = []
+        obj_violations = {}
+        obj_violations.update({
+            'name': '',
+            'points': list()
+        })
 
-        self.e1_cb = FCCheckBox()
+        if len(gerber_list) == 2:
+            gerber_1 = gerber_list[0]
+            # added it so I won't have errors of using before declaring
+            gerber_2 = {}
 
-        self.grid_layout.addWidget(self.e1_object_lbl, 10, 0)
-        self.grid_layout.addWidget(self.e1_object, 10, 1)
-        self.grid_layout.addWidget(self.e1_cb, 10, 2)
+            gerber_3 = gerber_list[1]
+        elif len(gerber_list) == 3:
+            gerber_1 = gerber_list[0]
+            gerber_2 = gerber_list[1]
+            gerber_3 = gerber_list[2]
+        else:
+            return 'Fail. Not enough Gerber objects to check Gerber 2 Gerber clearance'
 
-        # Excellon 2 object
-        self.e2_object = FCComboBox()
-        self.e2_object.setModel(self.app.collection)
-        self.e2_object.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
-        self.e2_object.is_last = True
-        self.e2_object.obj_type = "Excellon"
+        total_geo_grb_1 = []
+        for apid in gerber_1['apertures']:
+            if 'geometry' in gerber_1['apertures'][apid]:
+                geometry = gerber_1['apertures'][apid]['geometry']
+                for geo_el in geometry:
+                    if 'solid' in geo_el and geo_el['solid'] is not None:
+                        total_geo_grb_1.append(geo_el['solid'])
 
-        self.e2_object_lbl = QtWidgets.QLabel('%s:' % _("Excellon 2"))
-        self.e2_object_lbl.setToolTip(
-            _("Excellon object for which to check rules.\n"
-              "Holds the non-plated holes.")
-        )
+        if len(gerber_list) == 3:
+            # add the second Gerber geometry to the first one if it exists
+            for apid in gerber_2['apertures']:
+                if 'geometry' in gerber_2['apertures'][apid]:
+                    geometry = gerber_2['apertures'][apid]['geometry']
+                    for geo_el in geometry:
+                        if 'solid' in geo_el and geo_el['solid'] is not None:
+                            total_geo_grb_1.append(geo_el['solid'])
 
-        self.e2_cb = FCCheckBox()
+        total_geo_grb_3 = []
+        for apid in gerber_3['apertures']:
+            if 'geometry' in gerber_3['apertures'][apid]:
+                geometry = gerber_3['apertures'][apid]['geometry']
+                for geo_el in geometry:
+                    if 'solid' in geo_el and geo_el['solid'] is not None:
+                        total_geo_grb_3.append(geo_el['solid'])
 
-        self.grid_layout.addWidget(self.e2_object_lbl, 11, 0)
-        self.grid_layout.addWidget(self.e2_object, 11, 1)
-        self.grid_layout.addWidget(self.e2_cb, 11, 2)
+        total_geo_grb_1 = MultiPolygon(total_geo_grb_1)
+        total_geo_grb_1 = total_geo_grb_1.buffer(0)
 
-        self.grid_layout.addWidget(QtWidgets.QLabel(""), 12, 0, 1, 3)
+        total_geo_grb_3 = MultiPolygon(total_geo_grb_3)
+        total_geo_grb_3 = total_geo_grb_3.buffer(0)
 
-        # Control All
-        self.all_cb = FCCheckBox('%s' % _("All Rules"))
-        self.all_cb.setToolTip(
-            _("This check/uncheck all the rules below.")
-        )
-        self.all_cb.setStyleSheet(
-            """
-            QCheckBox {font-weight: bold; color: green}
-            """
-        )
-        self.layout.addWidget(self.all_cb)
+        if isinstance(total_geo_grb_1, Polygon):
+            len_1 = 1
+            total_geo_grb_1 = [total_geo_grb_1]
+        else:
+            len_1 = len(total_geo_grb_1)
 
-        # Form Layout
-        self.form_layout_1 = QtWidgets.QFormLayout()
-        self.layout.addLayout(self.form_layout_1)
+        if isinstance(total_geo_grb_3, Polygon):
+            len_3 = 1
+            total_geo_grb_3 = [total_geo_grb_3]
+        else:
+            len_3 = len(total_geo_grb_3)
 
-        self.form_layout_1.addRow(QtWidgets.QLabel(""))
+        iterations = len_1 * len_3
+        log.debug("RulesCheck.check_gerber_clearance(). Iterations: %s" % str(iterations))
 
-        # Trace size
-        self.trace_size_cb = FCCheckBox('%s:' % _("Trace Size"))
-        self.trace_size_cb.setToolTip(
-            _("This checks if the minimum size for traces is met.")
-        )
-        self.form_layout_1.addRow(self.trace_size_cb)
+        min_dict = {}
+        for geo in total_geo_grb_1:
+            for s_geo in total_geo_grb_3:
+                # minimize the number of distances by not taking into considerations those that are too small
+                dist = geo.distance(s_geo)
+                if float(dist) < float(size):
+                    loc_1, loc_2 = nearest_points(geo, s_geo)
 
-        # Trace size value
-        self.trace_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.trace_size_entry.set_range(0.00001, 999.99999)
-        self.trace_size_entry.set_precision(self.decimals)
-        self.trace_size_entry.setSingleStep(0.1)
+                    dx = loc_1.x - loc_2.x
+                    dy = loc_1.y - loc_2.y
+                    loc = min(loc_1.x, loc_2.x) + (abs(dx) / 2), min(loc_1.y, loc_2.y) + (abs(dy) / 2)
 
-        self.trace_size_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
-        self.trace_size_lbl.setToolTip(
-            _("Minimum acceptable trace size.")
-        )
-        self.form_layout_1.addRow(self.trace_size_lbl, self.trace_size_entry)
+                    if dist in min_dict:
+                        min_dict[dist].append(loc)
+                    else:
+                        min_dict[dist] = [loc]
 
-        self.ts = OptionalInputSection(self.trace_size_cb, [self.trace_size_lbl, self.trace_size_entry])
+        points_list = set()
+        for dist in min_dict.keys():
+            for location in min_dict[dist]:
+                points_list.add(location)
 
-        # Copper2copper clearance
-        self.clearance_copper2copper_cb = FCCheckBox('%s:' % _("Copper to Copper clearance"))
-        self.clearance_copper2copper_cb.setToolTip(
-            _("This checks if the minimum clearance between copper\n"
-              "features is met.")
-        )
-        self.form_layout_1.addRow(self.clearance_copper2copper_cb)
+        name_list = []
+        if gerber_1:
+            name_list.append(gerber_1['name'])
+        if gerber_2:
+            name_list.append(gerber_2['name'])
+        if gerber_3:
+            name_list.append(gerber_3['name'])
 
-        # Copper2copper clearance value
-        self.clearance_copper2copper_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.clearance_copper2copper_entry.set_range(0.00001, 999.99999)
-        self.clearance_copper2copper_entry.set_precision(self.decimals)
-        self.clearance_copper2copper_entry.setSingleStep(0.1)
+        obj_violations['name'] = name_list
+        obj_violations['points'] = list(points_list)
+        violations.append(deepcopy(obj_violations))
 
-        self.clearance_copper2copper_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
-        self.clearance_copper2copper_lbl.setToolTip(
-            _("Minimum acceptable clearance value.")
-        )
-        self.form_layout_1.addRow(self.clearance_copper2copper_lbl, self.clearance_copper2copper_entry)
+        return rule_title, violations
 
-        self.c2c = OptionalInputSection(
-            self.clearance_copper2copper_cb, [self.clearance_copper2copper_lbl, self.clearance_copper2copper_entry])
+    @staticmethod
+    def check_holes_size(elements, size):
+        log.debug("RulesCheck.check_holes_size()")
 
-        # Copper2outline clearance
-        self.clearance_copper2ol_cb = FCCheckBox('%s:' % _("Copper to Outline clearance"))
-        self.clearance_copper2ol_cb.setToolTip(
-            _("This checks if the minimum clearance between copper\n"
-              "features and the outline is met.")
-        )
-        self.form_layout_1.addRow(self.clearance_copper2ol_cb)
+        rule = _("Hole Size")
 
-        # Copper2outline clearance value
-        self.clearance_copper2ol_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.clearance_copper2ol_entry.set_range(0.00001, 999.99999)
-        self.clearance_copper2ol_entry.set_precision(self.decimals)
-        self.clearance_copper2ol_entry.setSingleStep(0.1)
+        violations = []
+        obj_violations = {}
+        obj_violations.update({
+            'name': '',
+            'dia': list()
+        })
 
-        self.clearance_copper2ol_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
-        self.clearance_copper2ol_lbl.setToolTip(
-            _("Minimum acceptable clearance value.")
-        )
-        self.form_layout_1.addRow(self.clearance_copper2ol_lbl, self.clearance_copper2ol_entry)
+        for elem in elements:
+            dia_list = []
 
-        self.c2ol = OptionalInputSection(
-            self.clearance_copper2ol_cb, [self.clearance_copper2ol_lbl, self.clearance_copper2ol_entry])
+            name = elem['name']
+            for tool in elem['tools']:
+                tool_dia = float('%.*f' % (4, float(elem['tools'][tool]['C'])))
+                if tool_dia < float(size):
+                    dia_list.append(tool_dia)
+            obj_violations['name'] = name
+            obj_violations['dia'] = dia_list
+            violations.append(deepcopy(obj_violations))
 
-        # Silkscreen2silkscreen clearance
-        self.clearance_silk2silk_cb = FCCheckBox('%s:' % _("Silk to Silk Clearance"))
-        self.clearance_silk2silk_cb.setToolTip(
-            _("This checks if the minimum clearance between silkscreen\n"
-              "features and silkscreen features is met.")
-        )
-        self.form_layout_1.addRow(self.clearance_silk2silk_cb)
+        return rule, violations
 
-        # Copper2silkscreen clearance value
-        self.clearance_silk2silk_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.clearance_silk2silk_entry.set_range(0.00001, 999.99999)
-        self.clearance_silk2silk_entry.set_precision(self.decimals)
-        self.clearance_silk2silk_entry.setSingleStep(0.1)
+    @staticmethod
+    def check_holes_clearance(elements, size):
+        log.debug("RulesCheck.check_holes_clearance()")
+        rule = _("Hole to Hole Clearance")
 
-        self.clearance_silk2silk_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
-        self.clearance_silk2silk_lbl.setToolTip(
-            _("Minimum acceptable clearance value.")
-        )
-        self.form_layout_1.addRow(self.clearance_silk2silk_lbl, self.clearance_silk2silk_entry)
+        violations = []
+        obj_violations = {}
+        obj_violations.update({
+            'name': '',
+            'points': list()
+        })
 
-        self.s2s = OptionalInputSection(
-            self.clearance_silk2silk_cb, [self.clearance_silk2silk_lbl, self.clearance_silk2silk_entry])
+        total_geo = []
+        for elem in elements:
+            for tool in elem['tools']:
+                if 'solid_geometry' in elem['tools'][tool]:
+                    geometry = elem['tools'][tool]['solid_geometry']
+                    for geo in geometry:
+                        total_geo.append(geo)
 
-        # Silkscreen2soldermask clearance
-        self.clearance_silk2sm_cb = FCCheckBox('%s:' % _("Silk to Solder Mask Clearance"))
-        self.clearance_silk2sm_cb.setToolTip(
-            _("This checks if the minimum clearance between silkscreen\n"
-              "features and soldermask features is met.")
-        )
-        self.form_layout_1.addRow(self.clearance_silk2sm_cb)
+        min_dict = {}
+        idx = 1
+        for geo in total_geo:
+            for s_geo in total_geo[idx:]:
 
-        # Silkscreen2soldermask clearance value
-        self.clearance_silk2sm_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.clearance_silk2sm_entry.set_range(0.00001, 999.99999)
-        self.clearance_silk2sm_entry.set_precision(self.decimals)
-        self.clearance_silk2sm_entry.setSingleStep(0.1)
+                # minimize the number of distances by not taking into considerations those that are too small
+                dist = geo.distance(s_geo)
+                loc_1, loc_2 = nearest_points(geo, s_geo)
 
-        self.clearance_silk2sm_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
-        self.clearance_silk2sm_lbl.setToolTip(
-            _("Minimum acceptable clearance value.")
-        )
-        self.form_layout_1.addRow(self.clearance_silk2sm_lbl, self.clearance_silk2sm_entry)
+                dx = loc_1.x - loc_2.x
+                dy = loc_1.y - loc_2.y
+                loc = min(loc_1.x, loc_2.x) + (abs(dx) / 2), min(loc_1.y, loc_2.y) + (abs(dy) / 2)
 
-        self.s2sm = OptionalInputSection(
-            self.clearance_silk2sm_cb, [self.clearance_silk2sm_lbl, self.clearance_silk2sm_entry])
+                if dist in min_dict:
+                    min_dict[dist].append(loc)
+                else:
+                    min_dict[dist] = [loc]
+            idx += 1
 
-        # Silk2outline clearance
-        self.clearance_silk2ol_cb = FCCheckBox('%s:' % _("Silk to Outline Clearance"))
-        self.clearance_silk2ol_cb.setToolTip(
-            _("This checks if the minimum clearance between silk\n"
-              "features and the outline is met.")
-        )
-        self.form_layout_1.addRow(self.clearance_silk2ol_cb)
+        points_list = set()
+        for dist in min_dict.keys():
+            if float(dist) < size:
+                for location in min_dict[dist]:
+                    points_list.add(location)
 
-        # Silk2outline clearance value
-        self.clearance_silk2ol_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.clearance_silk2ol_entry.set_range(0.00001, 999.99999)
-        self.clearance_silk2ol_entry.set_precision(self.decimals)
-        self.clearance_silk2ol_entry.setSingleStep(0.1)
+        name_list = []
+        for elem in elements:
+            name_list.append(elem['name'])
 
-        self.clearance_silk2ol_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
-        self.clearance_silk2ol_lbl.setToolTip(
-            _("Minimum acceptable clearance value.")
-        )
-        self.form_layout_1.addRow(self.clearance_silk2ol_lbl, self.clearance_silk2ol_entry)
+        obj_violations['name'] = name_list
+        obj_violations['points'] = list(points_list)
+        violations.append(deepcopy(obj_violations))
 
-        self.s2ol = OptionalInputSection(
-            self.clearance_silk2ol_cb, [self.clearance_silk2ol_lbl, self.clearance_silk2ol_entry])
+        return rule, violations
 
-        # Soldermask2soldermask clearance
-        self.clearance_sm2sm_cb = FCCheckBox('%s:' % _("Minimum Solder Mask Sliver"))
-        self.clearance_sm2sm_cb.setToolTip(
-            _("This checks if the minimum clearance between soldermask\n"
-              "features and soldermask features is met.")
-        )
-        self.form_layout_1.addRow(self.clearance_sm2sm_cb)
+    @staticmethod
+    def check_traces_size(elements, size):
+        log.debug("RulesCheck.check_traces_size()")
 
-        # Soldermask2soldermask clearance value
-        self.clearance_sm2sm_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.clearance_sm2sm_entry.set_range(0.00001, 999.99999)
-        self.clearance_sm2sm_entry.set_precision(self.decimals)
-        self.clearance_sm2sm_entry.setSingleStep(0.1)
+        rule = _("Trace Size")
 
-        self.clearance_sm2sm_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
-        self.clearance_sm2sm_lbl.setToolTip(
-            _("Minimum acceptable clearance value.")
-        )
-        self.form_layout_1.addRow(self.clearance_sm2sm_lbl, self.clearance_sm2sm_entry)
+        violations = []
+        obj_violations = {}
+        obj_violations.update({
+            'name': '',
+            'size': list(),
+            'points': list()
+        })
 
-        self.sm2sm = OptionalInputSection(
-            self.clearance_sm2sm_cb, [self.clearance_sm2sm_lbl, self.clearance_sm2sm_entry])
+        for elem in elements:
+            dia_list = []
+            points_list = []
+            name = elem['name']
 
-        # Ring integrity check
-        self.ring_integrity_cb = FCCheckBox('%s:' % _("Minimum Annular Ring"))
-        self.ring_integrity_cb.setToolTip(
-            _("This checks if the minimum copper ring left by drilling\n"
-              "a hole into a pad is met.")
-        )
-        self.form_layout_1.addRow(self.ring_integrity_cb)
+            for apid in elem['apertures']:
+                try:
+                    tool_dia = float(elem['apertures'][apid]['size'])
+                    if tool_dia < float(size) and tool_dia != 0.0:
+                        dia_list.append(tool_dia)
+                        for geo_el in elem['apertures'][apid]['geometry']:
+                            if 'solid' in geo_el.keys():
+                                geo = geo_el['solid']
+                                pt = geo.representative_point()
+                                points_list.append((pt.x, pt.y))
+                except Exception:
+                    # An exception  will be raised for the 'size' key in case of apertures of type AM (macro) which does
+                    # not have the size key
+                    pass
 
-        # Ring integrity value
-        self.ring_integrity_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.ring_integrity_entry.set_range(0.00001, 999.99999)
-        self.ring_integrity_entry.set_precision(self.decimals)
-        self.ring_integrity_entry.setSingleStep(0.1)
+            obj_violations['name'] = name
+            obj_violations['size'] = dia_list
+            obj_violations['points'] = points_list
+            violations.append(deepcopy(obj_violations))
+        return rule, violations
 
-        self.ring_integrity_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
-        self.ring_integrity_lbl.setToolTip(
-            _("Minimum acceptable ring value.")
-        )
-        self.form_layout_1.addRow(self.ring_integrity_lbl, self.ring_integrity_entry)
+    @staticmethod
+    def check_gerber_annular_ring(obj_list, size, rule):
+        rule_title = rule
 
-        self.anr = OptionalInputSection(
-            self.ring_integrity_cb, [self.ring_integrity_lbl, self.ring_integrity_entry])
+        violations = []
+        obj_violations = {}
+        obj_violations.update({
+            'name': '',
+            'points': list()
+        })
 
-        self.form_layout_1.addRow(QtWidgets.QLabel(""))
+        # added it so I won't have errors of using before declaring
+        gerber_obj = {}
+        gerber_extra_obj = {}
+        exc_obj = {}
+        exc_extra_obj = {}
 
-        # Hole2Hole clearance
-        self.clearance_d2d_cb = FCCheckBox('%s:' % _("Hole to Hole Clearance"))
-        self.clearance_d2d_cb.setToolTip(
-            _("This checks if the minimum clearance between a drill hole\n"
-              "and another drill hole is met.")
-        )
-        self.form_layout_1.addRow(self.clearance_d2d_cb)
+        if len(obj_list) == 2:
+            gerber_obj = obj_list[0]
+            exc_obj = obj_list[1]
+            if 'apertures' in gerber_obj and 'tools' in exc_obj:
+                pass
+            else:
+                return 'Fail. At least one Gerber and one Excellon object is required to check Minimum Annular Ring'
+        elif len(obj_list) == 3:
+            o1 = obj_list[0]
+            o2 = obj_list[1]
+            o3 = obj_list[2]
+            if 'apertures' in o1 and 'apertures' in o2:
+                gerber_obj = o1
+                gerber_extra_obj = o2
+                exc_obj = o3
+            elif 'tools' in o2 and 'tools' in o3:
+                gerber_obj = o1
+                exc_obj = o2
+                exc_extra_obj = o3
+        elif len(obj_list) == 4:
+            gerber_obj = obj_list[0]
+            gerber_extra_obj = obj_list[1]
+            exc_obj = obj_list[2]
+            exc_extra_obj = obj_list[3]
+        else:
+            return 'Fail. Not enough objects to check Minimum Annular Ring'
 
-        # Hole2Hole clearance value
-        self.clearance_d2d_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.clearance_d2d_entry.set_range(0.00001, 999.99999)
-        self.clearance_d2d_entry.set_precision(self.decimals)
-        self.clearance_d2d_entry.setSingleStep(0.1)
+        total_geo_grb = []
+        for apid in gerber_obj['apertures']:
+            if 'geometry' in gerber_obj['apertures'][apid]:
+                geometry = gerber_obj['apertures'][apid]['geometry']
+                for geo_el in geometry:
+                    if 'solid' in geo_el and geo_el['solid'] is not None:
+                        total_geo_grb.append(geo_el['solid'])
 
-        self.clearance_d2d_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
-        self.clearance_d2d_lbl.setToolTip(
-            _("Minimum acceptable clearance value.")
-        )
-        self.form_layout_1.addRow(self.clearance_d2d_lbl, self.clearance_d2d_entry)
+        if len(obj_list) == 3 and gerber_extra_obj:
+            # add the second Gerber geometry to the first one if it exists
+            for apid in gerber_extra_obj['apertures']:
+                if 'geometry' in gerber_extra_obj['apertures'][apid]:
+                    geometry = gerber_extra_obj['apertures'][apid]['geometry']
+                    for geo_el in geometry:
+                        if 'solid' in geo_el and geo_el['solid'] is not None:
+                            total_geo_grb.append(geo_el['solid'])
 
-        self.d2d = OptionalInputSection(
-            self.clearance_d2d_cb, [self.clearance_d2d_lbl, self.clearance_d2d_entry])
+        total_geo_grb = MultiPolygon(total_geo_grb)
+        total_geo_grb = total_geo_grb.buffer(0)
 
-        # Drill holes size check
-        self.drill_size_cb = FCCheckBox('%s:' % _("Hole Size"))
-        self.drill_size_cb.setToolTip(
-            _("This checks if the drill holes\n"
-              "sizes are above the threshold.")
-        )
-        self.form_layout_1.addRow(self.drill_size_cb)
+        total_geo_exc = []
+        for tool in exc_obj['tools']:
+            if 'solid_geometry' in exc_obj['tools'][tool]:
+                geometry = exc_obj['tools'][tool]['solid_geometry']
+                for geo in geometry:
+                    total_geo_exc.append(geo)
 
-        # Drile holes value
-        self.drill_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.drill_size_entry.set_range(0.00001, 999.99999)
-        self.drill_size_entry.set_precision(self.decimals)
-        self.drill_size_entry.setSingleStep(0.1)
+        if len(obj_list) == 3 and exc_extra_obj:
+            # add the second Excellon geometry to the first one if it exists
+            for tool in exc_extra_obj['tools']:
+                if 'solid_geometry' in exc_extra_obj['tools'][tool]:
+                    geometry = exc_extra_obj['tools'][tool]['solid_geometry']
+                    for geo in geometry:
+                        total_geo_exc.append(geo)
 
-        self.drill_size_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
-        self.drill_size_lbl.setToolTip(
-            _("Minimum acceptable drill size.")
-        )
-        self.form_layout_1.addRow(self.drill_size_lbl, self.drill_size_entry)
+        if isinstance(total_geo_grb, Polygon):
+            len_1 = 1
+            total_geo_grb = [total_geo_grb]
+        else:
+            len_1 = len(total_geo_grb)
 
-        self.ds = OptionalInputSection(
-            self.drill_size_cb, [self.drill_size_lbl, self.drill_size_entry])
+        if isinstance(total_geo_exc, Polygon):
+            len_2 = 1
+            total_geo_exc = [total_geo_exc]
+        else:
+            len_2 = len(total_geo_exc)
 
-        # Buttons
-        hlay_2 = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay_2)
+        iterations = len_1 * len_2
+        log.debug("RulesCheck.check_gerber_annular_ring(). Iterations: %s" % str(iterations))
 
-        # hlay_2.addStretch()
-        self.run_button = QtWidgets.QPushButton(_("Run Rules Check"))
-        self.run_button.setToolTip(
-            _("Panelize the specified object around the specified box.\n"
-              "In other words it creates multiple copies of the source object,\n"
-              "arranged in a 2D array of rows and columns.")
-        )
-        self.run_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        hlay_2.addWidget(self.run_button)
+        min_dict = {}
+        dist = None
+        for geo in total_geo_grb:
+            for s_geo in total_geo_exc:
+                try:
+                    # minimize the number of distances by not taking into considerations those that are too small
+                    dist = abs(geo.exterior.distance(s_geo))
+                except Exception as e:
+                    log.debug("RulesCheck.check_gerber_annular_ring() --> %s" % str(e))
 
-        self.layout.addStretch()
+                if dist > 0:
+                    if float(dist) < float(size):
+                        loc_1, loc_2 = nearest_points(geo.exterior, s_geo)
 
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
+                        dx = loc_1.x - loc_2.x
+                        dy = loc_1.y - loc_2.y
+                        loc = min(loc_1.x, loc_2.x) + (abs(dx) / 2), min(loc_1.y, loc_2.y) + (abs(dy) / 2)
+
+                        if dist in min_dict:
+                            min_dict[dist].append(loc)
+                        else:
+                            min_dict[dist] = [loc]
+                else:
+                    if dist in min_dict:
+                        min_dict[dist].append(s_geo.representative_point())
+                    else:
+                        min_dict[dist] = [s_geo.representative_point()]
+
+        points_list = []
+        for dist in min_dict.keys():
+            for location in min_dict[dist]:
+                points_list.append(location)
+
+        name_list = []
+        try:
+            if gerber_obj:
+                name_list.append(gerber_obj['name'])
+        except KeyError:
+            pass
+        try:
+            if gerber_extra_obj:
+                name_list.append(gerber_extra_obj['name'])
+        except KeyError:
+            pass
+
+        try:
+            if exc_obj:
+                name_list.append(exc_obj['name'])
+        except KeyError:
+            pass
+
+        try:
+            if exc_extra_obj:
+                name_list.append(exc_extra_obj['name'])
+        except KeyError:
+            pass
+
+        obj_violations['name'] = name_list
+        obj_violations['points'] = points_list
+        violations.append(deepcopy(obj_violations))
+        return rule_title, violations
+
+    def execute(self):
+        self.results = []
+
+        log.debug("RuleCheck() executing")
 
-        # #######################################################
-        # ################ SIGNALS ##############################
-        # #######################################################
-        self.copper_t_cb.stateChanged.connect(lambda st: self.copper_t_object.setDisabled(not st))
-        self.copper_b_cb.stateChanged.connect(lambda st: self.copper_b_object.setDisabled(not st))
+        def worker_job(app_obj):
+            self.app.proc_container.new(_("Working..."))
 
-        self.sm_t_cb.stateChanged.connect(lambda st: self.sm_t_object.setDisabled(not st))
-        self.sm_b_cb.stateChanged.connect(lambda st: self.sm_b_object.setDisabled(not st))
+            # RULE: Check Trace Size
+            if self.ui.trace_size_cb.get_value():
+                copper_list = []
+                copper_name_1 = self.ui.copper_t_object.currentText()
+                if copper_name_1 != '' and self.ui.copper_t_cb.get_value():
+                    elem_dict = {}
+                    elem_dict['name'] = deepcopy(copper_name_1)
+                    elem_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_name_1).apertures)
+                    copper_list.append(elem_dict)
 
-        self.ss_t_cb.stateChanged.connect(lambda st: self.ss_t_object.setDisabled(not st))
-        self.ss_b_cb.stateChanged.connect(lambda st: self.ss_b_object.setDisabled(not st))
+                copper_name_2 = self.ui.copper_b_object.currentText()
+                if copper_name_2 != '' and self.ui.copper_b_cb.get_value():
+                    elem_dict = {}
+                    elem_dict['name'] = deepcopy(copper_name_2)
+                    elem_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_name_2).apertures)
+                    copper_list.append(elem_dict)
 
-        self.out_cb.stateChanged.connect(lambda st: self.outline_object.setDisabled(not st))
+                trace_size = float(self.ui.trace_size_entry.get_value())
+                self.results.append(self.pool.apply_async(self.check_traces_size, args=(copper_list, trace_size)))
 
-        self.e1_cb.stateChanged.connect(lambda st: self.e1_object.setDisabled(not st))
-        self.e2_cb.stateChanged.connect(lambda st: self.e2_object.setDisabled(not st))
+            # RULE: Check Copper to Copper Clearance
+            if self.ui.clearance_copper2copper_cb.get_value():
 
-        self.all_obj_cb.stateChanged.connect(self.on_all_objects_cb_changed)
-        self.all_cb.stateChanged.connect(self.on_all_cb_changed)
-        self.run_button.clicked.connect(self.execute)
-        # self.app.collection.rowsInserted.connect(self.on_object_loaded)
+                try:
+                    copper_copper_clearance = float(self.ui.clearance_copper2copper_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Copper to Copper clearance"),
+                        _("Value is not valid.")))
+                    return
 
-        self.tool_finished.connect(self.on_tool_finished)
-        self.reset_button.clicked.connect(self.set_tool_ui)
+                if self.copper_t_cb.get_value():
+                    copper_t_obj = self.ui.copper_t_object.currentText()
+                    copper_t_dict = {}
 
-        # list to hold the temporary objects
-        self.objs = []
+                    if copper_t_obj != '':
+                        copper_t_dict['name'] = deepcopy(copper_t_obj)
+                        copper_t_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_t_obj).apertures)
 
-        # final name for the panel object
-        self.outname = ""
+                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
+                                                                  args=(copper_t_dict,
+                                                                        copper_copper_clearance,
+                                                                        _("TOP -> Copper to Copper clearance"))))
+                if self.ui.copper_b_cb.get_value():
+                    copper_b_obj = self.ui.copper_b_object.currentText()
+                    copper_b_dict = {}
+                    if copper_b_obj != '':
+                        copper_b_dict['name'] = deepcopy(copper_b_obj)
+                        copper_b_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_b_obj).apertures)
 
-        # flag to signal the constrain was activated
-        self.constrain_flag = False
+                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
+                                                                  args=(copper_b_dict,
+                                                                        copper_copper_clearance,
+                                                                        _("BOTTOM -> Copper to Copper clearance"))))
 
-        # Multiprocessing Process Pool
-        self.pool = self.app.pool
-        self.results = None
+                if self.ui.copper_t_cb.get_value() is False and self.ui.copper_b_cb.get_value() is False:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Copper to Copper clearance"),
+                        _("At least one Gerber object has to be selected for this rule but none is selected.")))
+                    return
 
-        self.decimals = 4
+            # RULE: Check Copper to Outline Clearance
+            if self.ui.clearance_copper2ol_cb.get_value() and self.ui.out_cb.get_value():
+                top_dict = {}
+                bottom_dict = {}
+                outline_dict = {}
 
-    # def on_object_loaded(self, index, row):
-    #     print(index.internalPointer().child_items[row].obj.options['name'], index.data())
+                copper_top = self.ui.copper_t_object.currentText()
+                if copper_top != '' and self.ui.copper_t_cb.get_value():
+                    top_dict['name'] = deepcopy(copper_top)
+                    top_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_top).apertures)
 
-    def on_all_cb_changed(self, state):
-        cb_items = [self.form_layout_1.itemAt(i).widget() for i in range(self.form_layout_1.count())
-                    if isinstance(self.form_layout_1.itemAt(i).widget(), FCCheckBox)]
+                copper_bottom = self.ui.copper_b_object.currentText()
+                if copper_bottom != '' and self.ui.copper_b_cb.get_value():
+                    bottom_dict['name'] = deepcopy(copper_bottom)
+                    bottom_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_bottom).apertures)
 
-        for cb in cb_items:
-            if state:
-                cb.setChecked(True)
-            else:
-                cb.setChecked(False)
+                copper_outline = self.ui.outline_object.currentText()
+                if copper_outline != '' and self.ui.out_cb.get_value():
+                    outline_dict['name'] = deepcopy(copper_outline)
+                    outline_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_outline).apertures)
 
-    def on_all_objects_cb_changed(self, state):
-        cb_items = [self.grid_layout.itemAt(i).widget() for i in range(self.grid_layout.count())
-                    if isinstance(self.grid_layout.itemAt(i).widget(), FCCheckBox)]
+                try:
+                    copper_outline_clearance = float(self.ui.clearance_copper2ol_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Copper to Outline clearance"),
+                        _("Value is not valid.")))
+                    return
 
-        for cb in cb_items:
-            if state:
-                cb.setChecked(True)
-            else:
-                cb.setChecked(False)
+                if not top_dict and not bottom_dict or not outline_dict:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Copper to Outline clearance"),
+                        _("One of the copper Gerber objects or the Outline Gerber object is not valid.")))
+                    return
+                objs = []
+                if top_dict:
+                    objs.append(top_dict)
+                if bottom_dict:
+                    objs.append(bottom_dict)
 
-    def run(self, toggle=True):
-        self.app.defaults.report_usage("ToolRulesCheck()")
+                if outline_dict:
+                    objs.append(outline_dict)
+                else:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Copper to Outline clearance"),
+                        _("Outline Gerber object presence is mandatory for this rule but it is not selected.")))
+                    return
 
-        if toggle:
-            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-            else:
-                try:
-                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        # if tab is populated with the tool but it does not have the focus, focus on it
-                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
-                            # focus on Tool Tab
-                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-                        else:
-                            self.app.ui.splitter.setSizes([0, 1])
-                except AttributeError:
-                    pass
-        else:
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
+                self.results.append(self.pool.apply_async(self.check_gerber_clearance,
+                                                          args=(objs,
+                                                                copper_outline_clearance,
+                                                                _("Copper to Outline clearance"))))
 
-        AppTool.run(self)
-        self.set_tool_ui()
+            # RULE: Check Silk to Silk Clearance
+            if self.ui.clearance_silk2silk_cb.get_value():
+                silk_dict = {}
 
-        self.app.ui.notebook.setTabText(2, _("Rules Tool"))
+                try:
+                    silk_silk_clearance = float(self.ui.clearance_silk2silk_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Silk clearance"),
+                        _("Value is not valid.")))
+                    return
 
-    def install(self, icon=None, separator=None, **kwargs):
-        AppTool.install(self, icon, separator, shortcut='Alt+R', **kwargs)
+                if self.ss_t_cb.get_value():
+                    silk_obj = self.ui.ss_t_object.currentText()
+                    if silk_obj != '':
+                        silk_dict['name'] = deepcopy(silk_obj)
+                        silk_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_obj).apertures)
 
-    def set_tool_ui(self):
+                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
+                                                                  args=(silk_dict,
+                                                                        silk_silk_clearance,
+                                                                        _("TOP -> Silk to Silk clearance"))))
+                if self.ui.ss_b_cb.get_value():
+                    silk_obj = self.ui.ss_b_object.currentText()
+                    if silk_obj != '':
+                        silk_dict['name'] = deepcopy(silk_obj)
+                        silk_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_obj).apertures)
 
-        # all object combobox default as disabled
-        self.copper_t_object.setDisabled(True)
-        self.copper_b_object.setDisabled(True)
-
-        self.sm_t_object.setDisabled(True)
-        self.sm_b_object.setDisabled(True)
-
-        self.ss_t_object.setDisabled(True)
-        self.ss_b_object.setDisabled(True)
-
-        self.outline_object.setDisabled(True)
-
-        self.e1_object.setDisabled(True)
-        self.e2_object.setDisabled(True)
-
-        self.trace_size_cb.set_value(self.app.defaults["tools_cr_trace_size"])
-        self.trace_size_entry.set_value(float(self.app.defaults["tools_cr_trace_size_val"]))
-        self.clearance_copper2copper_cb.set_value(self.app.defaults["tools_cr_c2c"])
-        self.clearance_copper2copper_entry.set_value(float(self.app.defaults["tools_cr_c2c_val"]))
-        self.clearance_copper2ol_cb.set_value(self.app.defaults["tools_cr_c2o"])
-        self.clearance_copper2ol_entry.set_value(float(self.app.defaults["tools_cr_c2o_val"]))
-        self.clearance_silk2silk_cb.set_value(self.app.defaults["tools_cr_s2s"])
-        self.clearance_silk2silk_entry.set_value(float(self.app.defaults["tools_cr_s2s_val"]))
-        self.clearance_silk2sm_cb.set_value(self.app.defaults["tools_cr_s2sm"])
-        self.clearance_silk2sm_entry.set_value(float(self.app.defaults["tools_cr_s2sm_val"]))
-        self.clearance_silk2ol_cb.set_value(self.app.defaults["tools_cr_s2o"])
-        self.clearance_silk2ol_entry.set_value(float(self.app.defaults["tools_cr_s2o_val"]))
-        self.clearance_sm2sm_cb.set_value(self.app.defaults["tools_cr_sm2sm"])
-        self.clearance_sm2sm_entry.set_value(float(self.app.defaults["tools_cr_sm2sm_val"]))
-        self.ring_integrity_cb.set_value(self.app.defaults["tools_cr_ri"])
-        self.ring_integrity_entry.set_value(float(self.app.defaults["tools_cr_ri_val"]))
-        self.clearance_d2d_cb.set_value(self.app.defaults["tools_cr_h2h"])
-        self.clearance_d2d_entry.set_value(float(self.app.defaults["tools_cr_h2h_val"]))
-        self.drill_size_cb.set_value(self.app.defaults["tools_cr_dh"])
-        self.drill_size_entry.set_value(float(self.app.defaults["tools_cr_dh_val"]))
+                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
+                                                                  args=(silk_dict,
+                                                                        silk_silk_clearance,
+                                                                        _("BOTTOM -> Silk to Silk clearance"))))
 
-        self.reset_fields()
+                if self.ui.ss_t_cb.get_value() is False and self.ui.ss_b_cb.get_value() is False:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Silk clearance"),
+                        _("At least one Gerber object has to be selected for this rule but none is selected.")))
+                    return
 
-    @staticmethod
-    def check_inside_gerber_clearance(gerber_obj, size, rule):
-        log.debug("RulesCheck.check_inside_gerber_clearance()")
+            # RULE: Check Silk to Solder Mask Clearance
+            if self.ui.clearance_silk2sm_cb.get_value():
+                silk_t_dict = {}
+                sm_t_dict = {}
+                silk_b_dict = {}
+                sm_b_dict = {}
 
-        rule_title = rule
+                top_ss = False
+                bottom_ss = False
+                top_sm = False
+                bottom_sm = False
 
-        violations = []
-        obj_violations = {}
-        obj_violations.update({
-            'name': '',
-            'points': list()
-        })
+                silk_top = self.ui.ss_t_object.currentText()
+                if silk_top != '' and self.ui.ss_t_cb.get_value():
+                    silk_t_dict['name'] = deepcopy(silk_top)
+                    silk_t_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_top).apertures)
+                    top_ss = True
 
-        if not gerber_obj:
-            return 'Fail. Not enough Gerber objects to check Gerber 2 Gerber clearance'
+                silk_bottom = self.ui.ss_b_object.currentText()
+                if silk_bottom != '' and self.ui.ss_b_cb.get_value():
+                    silk_b_dict['name'] = deepcopy(silk_bottom)
+                    silk_b_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_bottom).apertures)
+                    bottom_ss = True
 
-        obj_violations['name'] = gerber_obj['name']
+                sm_top = self.ui.sm_t_object.currentText()
+                if sm_top != '' and self.ui.sm_t_cb.get_value():
+                    sm_t_dict['name'] = deepcopy(sm_top)
+                    sm_t_dict['apertures'] = deepcopy(self.app.collection.get_by_name(sm_top).apertures)
+                    top_sm = True
 
-        solid_geo = []
-        clear_geo = []
-        for apid in gerber_obj['apertures']:
-            if 'geometry' in gerber_obj['apertures'][apid]:
-                geometry = gerber_obj['apertures'][apid]['geometry']
-                for geo_el in geometry:
-                    if 'solid' in geo_el and geo_el['solid'] is not None:
-                        solid_geo.append(geo_el['solid'])
-                    if 'clear' in geo_el and geo_el['clear'] is not None:
-                        clear_geo.append(geo_el['clear'])
+                sm_bottom = self.ui.sm_b_object.currentText()
+                if sm_bottom != '' and self.ui.sm_b_cb.get_value():
+                    sm_b_dict['name'] = deepcopy(sm_bottom)
+                    sm_b_dict['apertures'] = deepcopy(self.app.collection.get_by_name(sm_bottom).apertures)
+                    bottom_sm = True
 
-        if clear_geo:
-            total_geo = []
-            for geo_c in clear_geo:
-                for geo_s in solid_geo:
-                    if geo_c.within(geo_s):
-                        total_geo.append(geo_s.difference(geo_c))
-        else:
-            total_geo = MultiPolygon(solid_geo)
-            total_geo = total_geo.buffer(0.000001)
+                try:
+                    silk_sm_clearance = float(self.ui.clearance_silk2sm_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Solder Mask Clearance"),
+                        _("Value is not valid.")))
+                    return
 
-        if isinstance(total_geo, Polygon):
-            obj_violations['points'] = ['Failed. Only one polygon.']
-            return rule_title, [obj_violations]
-        else:
-            iterations = len(total_geo)
-            iterations = (iterations * (iterations - 1)) / 2
-        log.debug("RulesCheck.check_gerber_clearance(). Iterations: %s" % str(iterations))
+                if (not silk_t_dict and not silk_b_dict) or (not sm_t_dict and not sm_b_dict):
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Solder Mask Clearance"),
+                        _("One or more of the Gerber objects is not valid.")))
+                    return
 
-        min_dict = {}
-        idx = 1
-        for geo in total_geo:
-            for s_geo in total_geo[idx:]:
-                # minimize the number of distances by not taking into considerations those that are too small
-                dist = geo.distance(s_geo)
-                if float(dist) < float(size):
-                    loc_1, loc_2 = nearest_points(geo, s_geo)
+                if top_ss is True and top_sm is True:
+                    objs = [silk_t_dict, sm_t_dict]
+                    self.results.append(self.pool.apply_async(self.check_gerber_clearance,
+                                                              args=(objs,
+                                                                    silk_sm_clearance,
+                                                                    _("TOP -> Silk to Solder Mask Clearance"))))
+                elif bottom_ss is True and bottom_sm is True:
+                    objs = [silk_b_dict, sm_b_dict]
+                    self.results.append(self.pool.apply_async(self.check_gerber_clearance,
+                                                              args=(objs,
+                                                                    silk_sm_clearance,
+                                                                    _("BOTTOM -> Silk to Solder Mask Clearance"))))
+                else:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Solder Mask Clearance"),
+                        _("Both Silk and Solder Mask Gerber objects has to be either both Top or both Bottom.")))
+                    return
 
-                    dx = loc_1.x - loc_2.x
-                    dy = loc_1.y - loc_2.y
-                    loc = min(loc_1.x, loc_2.x) + (abs(dx) / 2), min(loc_1.y, loc_2.y) + (abs(dy) / 2)
+            # RULE: Check Silk to Outline Clearance
+            if self.ui.clearance_silk2ol_cb.get_value():
+                top_dict = {}
+                bottom_dict = {}
+                outline_dict = {}
 
-                    if dist in min_dict:
-                        min_dict[dist].append(loc)
-                    else:
-                        min_dict[dist] = [loc]
-            idx += 1
-        points_list = set()
-        for dist in min_dict.keys():
-            for location in min_dict[dist]:
-                points_list.add(location)
+                silk_top = self.ui.ss_t_object.currentText()
+                if silk_top != '' and self.ui.ss_t_cb.get_value():
+                    top_dict['name'] = deepcopy(silk_top)
+                    top_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_top).apertures)
 
-        obj_violations['points'] = list(points_list)
-        violations.append(deepcopy(obj_violations))
+                silk_bottom = self.ui.ss_b_object.currentText()
+                if silk_bottom != '' and self.ui.ss_b_cb.get_value():
+                    bottom_dict['name'] = deepcopy(silk_bottom)
+                    bottom_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_bottom).apertures)
 
-        return rule_title, violations
+                copper_outline = self.ui.outline_object.currentText()
+                if copper_outline != '' and self.ui.out_cb.get_value():
+                    outline_dict['name'] = deepcopy(copper_outline)
+                    outline_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_outline).apertures)
 
-    @staticmethod
-    def check_gerber_clearance(gerber_list, size, rule):
-        log.debug("RulesCheck.check_gerber_clearance()")
-        rule_title = rule
+                try:
+                    copper_outline_clearance = float(self.ui.clearance_copper2ol_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Outline Clearance"),
+                        _("Value is not valid.")))
+                    return
 
-        violations = []
-        obj_violations = {}
-        obj_violations.update({
-            'name': '',
-            'points': list()
-        })
+                if not top_dict and not bottom_dict or not outline_dict:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Outline Clearance"),
+                        _("One of the Silk Gerber objects or the Outline Gerber object is not valid.")))
+                    return
 
-        if len(gerber_list) == 2:
-            gerber_1 = gerber_list[0]
-            # added it so I won't have errors of using before declaring
-            gerber_2 = {}
+                objs = []
+                if top_dict:
+                    objs.append(top_dict)
+                if bottom_dict:
+                    objs.append(bottom_dict)
 
-            gerber_3 = gerber_list[1]
-        elif len(gerber_list) == 3:
-            gerber_1 = gerber_list[0]
-            gerber_2 = gerber_list[1]
-            gerber_3 = gerber_list[2]
-        else:
-            return 'Fail. Not enough Gerber objects to check Gerber 2 Gerber clearance'
+                if outline_dict:
+                    objs.append(outline_dict)
+                else:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Outline Clearance"),
+                        _("Outline Gerber object presence is mandatory for this rule but it is not selected.")))
+                    return
 
-        total_geo_grb_1 = []
-        for apid in gerber_1['apertures']:
-            if 'geometry' in gerber_1['apertures'][apid]:
-                geometry = gerber_1['apertures'][apid]['geometry']
-                for geo_el in geometry:
-                    if 'solid' in geo_el and geo_el['solid'] is not None:
-                        total_geo_grb_1.append(geo_el['solid'])
+                self.results.append(self.pool.apply_async(self.check_gerber_clearance,
+                                                          args=(objs,
+                                                                copper_outline_clearance,
+                                                                _("Silk to Outline Clearance"))))
 
-        if len(gerber_list) == 3:
-            # add the second Gerber geometry to the first one if it exists
-            for apid in gerber_2['apertures']:
-                if 'geometry' in gerber_2['apertures'][apid]:
-                    geometry = gerber_2['apertures'][apid]['geometry']
-                    for geo_el in geometry:
-                        if 'solid' in geo_el and geo_el['solid'] is not None:
-                            total_geo_grb_1.append(geo_el['solid'])
+            # RULE: Check Minimum Solder Mask Sliver
+            if self.ui.clearance_silk2silk_cb.get_value():
+                sm_dict = {}
 
-        total_geo_grb_3 = []
-        for apid in gerber_3['apertures']:
-            if 'geometry' in gerber_3['apertures'][apid]:
-                geometry = gerber_3['apertures'][apid]['geometry']
-                for geo_el in geometry:
-                    if 'solid' in geo_el and geo_el['solid'] is not None:
-                        total_geo_grb_3.append(geo_el['solid'])
+                try:
+                    sm_sm_clearance = float(self.ui.clearance_sm2sm_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Minimum Solder Mask Sliver"),
+                        _("Value is not valid.")))
+                    return
 
-        total_geo_grb_1 = MultiPolygon(total_geo_grb_1)
-        total_geo_grb_1 = total_geo_grb_1.buffer(0)
+                if self.ui.sm_t_cb.get_value():
+                    solder_obj = self.ui.sm_t_object.currentText()
+                    if solder_obj != '':
+                        sm_dict['name'] = deepcopy(solder_obj)
+                        sm_dict['apertures'] = deepcopy(self.app.collection.get_by_name(solder_obj).apertures)
 
-        total_geo_grb_3 = MultiPolygon(total_geo_grb_3)
-        total_geo_grb_3 = total_geo_grb_3.buffer(0)
+                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
+                                                                  args=(sm_dict,
+                                                                        sm_sm_clearance,
+                                                                        _("TOP -> Minimum Solder Mask Sliver"))))
+                if self.ui.sm_b_cb.get_value():
+                    solder_obj = self.ui.sm_b_object.currentText()
+                    if solder_obj != '':
+                        sm_dict['name'] = deepcopy(solder_obj)
+                        sm_dict['apertures'] = deepcopy(self.app.collection.get_by_name(solder_obj).apertures)
 
-        if isinstance(total_geo_grb_1, Polygon):
-            len_1 = 1
-            total_geo_grb_1 = [total_geo_grb_1]
-        else:
-            len_1 = len(total_geo_grb_1)
+                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
+                                                                  args=(sm_dict,
+                                                                        sm_sm_clearance,
+                                                                        _("BOTTOM -> Minimum Solder Mask Sliver"))))
 
-        if isinstance(total_geo_grb_3, Polygon):
-            len_3 = 1
-            total_geo_grb_3 = [total_geo_grb_3]
-        else:
-            len_3 = len(total_geo_grb_3)
+                if self.ui.sm_t_cb.get_value() is False and self.ui.sm_b_cb.get_value() is False:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Minimum Solder Mask Sliver"),
+                        _("At least one Gerber object has to be selected for this rule but none is selected.")))
+                    return
 
-        iterations = len_1 * len_3
-        log.debug("RulesCheck.check_gerber_clearance(). Iterations: %s" % str(iterations))
+            # RULE: Check Minimum Annular Ring
+            if self.ui.ring_integrity_cb.get_value():
+                top_dict = {}
+                bottom_dict = {}
+                exc_1_dict = {}
+                exc_2_dict = {}
 
-        min_dict = {}
-        for geo in total_geo_grb_1:
-            for s_geo in total_geo_grb_3:
-                # minimize the number of distances by not taking into considerations those that are too small
-                dist = geo.distance(s_geo)
-                if float(dist) < float(size):
-                    loc_1, loc_2 = nearest_points(geo, s_geo)
+                copper_top = self.ui.copper_t_object.currentText()
+                if copper_top != '' and self.ui.copper_t_cb.get_value():
+                    top_dict['name'] = deepcopy(copper_top)
+                    top_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_top).apertures)
 
-                    dx = loc_1.x - loc_2.x
-                    dy = loc_1.y - loc_2.y
-                    loc = min(loc_1.x, loc_2.x) + (abs(dx) / 2), min(loc_1.y, loc_2.y) + (abs(dy) / 2)
+                copper_bottom = self.ui.copper_b_object.currentText()
+                if copper_bottom != '' and self.ui.copper_b_cb.get_value():
+                    bottom_dict['name'] = deepcopy(copper_bottom)
+                    bottom_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_bottom).apertures)
 
-                    if dist in min_dict:
-                        min_dict[dist].append(loc)
-                    else:
-                        min_dict[dist] = [loc]
+                excellon_1 = self.ui.e1_object.currentText()
+                if excellon_1 != '' and self.ui.e1_cb.get_value():
+                    exc_1_dict['name'] = deepcopy(excellon_1)
+                    exc_1_dict['tools'] = deepcopy(
+                        self.app.collection.get_by_name(excellon_1).tools)
 
-        points_list = set()
-        for dist in min_dict.keys():
-            for location in min_dict[dist]:
-                points_list.add(location)
+                excellon_2 = self.ui.e2_object.currentText()
+                if excellon_2 != '' and self.ui.e2_cb.get_value():
+                    exc_2_dict['name'] = deepcopy(excellon_2)
+                    exc_2_dict['tools'] = deepcopy(
+                        self.app.collection.get_by_name(excellon_2).tools)
 
-        name_list = []
-        if gerber_1:
-            name_list.append(gerber_1['name'])
-        if gerber_2:
-            name_list.append(gerber_2['name'])
-        if gerber_3:
-            name_list.append(gerber_3['name'])
+                try:
+                    ring_val = float(self.ui.ring_integrity_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Minimum Annular Ring"),
+                        _("Value is not valid.")))
+                    return
 
-        obj_violations['name'] = name_list
-        obj_violations['points'] = list(points_list)
-        violations.append(deepcopy(obj_violations))
+                if (not top_dict and not bottom_dict) or (not exc_1_dict and not exc_2_dict):
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Minimum Annular Ring"),
+                        _("One of the Copper Gerber objects or the Excellon objects is not valid.")))
+                    return
 
-        return rule_title, violations
+                objs = []
+                if top_dict:
+                    objs.append(top_dict)
+                elif bottom_dict:
+                    objs.append(bottom_dict)
 
-    @staticmethod
-    def check_holes_size(elements, size):
-        log.debug("RulesCheck.check_holes_size()")
+                if exc_1_dict:
+                    objs.append(exc_1_dict)
+                elif exc_2_dict:
+                    objs.append(exc_2_dict)
+                else:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Minimum Annular Ring"),
+                        _("Excellon object presence is mandatory for this rule but none is selected.")))
+                    return
 
-        rule = _("Hole Size")
+                self.results.append(self.pool.apply_async(self.check_gerber_annular_ring,
+                                                          args=(objs,
+                                                                ring_val,
+                                                                _("Minimum Annular Ring"))))
 
-        violations = []
-        obj_violations = {}
-        obj_violations.update({
-            'name': '',
-            'dia': list()
-        })
+            # RULE: Check Hole to Hole Clearance
+            if self.ui.clearance_d2d_cb.get_value():
+                exc_list = []
+                exc_name_1 = self.ui.e1_object.currentText()
+                if exc_name_1 != '' and self.ui.e1_cb.get_value():
+                    elem_dict = {}
+                    elem_dict['name'] = deepcopy(exc_name_1)
+                    elem_dict['tools'] = deepcopy(self.app.collection.get_by_name(exc_name_1).tools)
+                    exc_list.append(elem_dict)
 
-        for elem in elements:
-            dia_list = []
+                exc_name_2 = self.ui.e2_object.currentText()
+                if exc_name_2 != '' and self.ui.e2_cb.get_value():
+                    elem_dict = {}
+                    elem_dict['name'] = deepcopy(exc_name_2)
+                    elem_dict['tools'] = deepcopy(self.app.collection.get_by_name(exc_name_2).tools)
+                    exc_list.append(elem_dict)
 
-            name = elem['name']
-            for tool in elem['tools']:
-                tool_dia = float('%.*f' % (4, float(elem['tools'][tool]['C'])))
-                if tool_dia < float(size):
-                    dia_list.append(tool_dia)
-            obj_violations['name'] = name
-            obj_violations['dia'] = dia_list
-            violations.append(deepcopy(obj_violations))
+                hole_clearance = float(self.ui.clearance_d2d_entry.get_value())
+                self.results.append(self.pool.apply_async(self.check_holes_clearance, args=(exc_list, hole_clearance)))
 
-        return rule, violations
+            # RULE: Check Holes Size
+            if self.ui.drill_size_cb.get_value():
+                exc_list = []
+                exc_name_1 = self.ui.e1_object.currentText()
+                if exc_name_1 != '' and self.ui.e1_cb.get_value():
+                    elem_dict = {}
+                    elem_dict['name'] = deepcopy(exc_name_1)
+                    elem_dict['tools'] = deepcopy(self.app.collection.get_by_name(exc_name_1).tools)
+                    exc_list.append(elem_dict)
 
-    @staticmethod
-    def check_holes_clearance(elements, size):
-        log.debug("RulesCheck.check_holes_clearance()")
-        rule = _("Hole to Hole Clearance")
+                exc_name_2 = self.ui.e2_object.currentText()
+                if exc_name_2 != '' and self.ui.e2_cb.get_value():
+                    elem_dict = {}
+                    elem_dict['name'] = deepcopy(exc_name_2)
+                    elem_dict['tools'] = deepcopy(self.app.collection.get_by_name(exc_name_2).tools)
+                    exc_list.append(elem_dict)
 
-        violations = []
-        obj_violations = {}
-        obj_violations.update({
-            'name': '',
-            'points': list()
-        })
+                drill_size = float(self.ui.drill_size_entry.get_value())
+                self.results.append(self.pool.apply_async(self.check_holes_size, args=(exc_list, drill_size)))
 
-        total_geo = []
-        for elem in elements:
-            for tool in elem['tools']:
-                if 'solid_geometry' in elem['tools'][tool]:
-                    geometry = elem['tools'][tool]['solid_geometry']
-                    for geo in geometry:
-                        total_geo.append(geo)
+            output = []
+            for p in self.results:
+                output.append(p.get())
 
-        min_dict = {}
-        idx = 1
-        for geo in total_geo:
-            for s_geo in total_geo[idx:]:
+            self.tool_finished.emit(output)
 
-                # minimize the number of distances by not taking into considerations those that are too small
-                dist = geo.distance(s_geo)
-                loc_1, loc_2 = nearest_points(geo, s_geo)
+            log.debug("RuleCheck() finished")
 
-                dx = loc_1.x - loc_2.x
-                dy = loc_1.y - loc_2.y
-                loc = min(loc_1.x, loc_2.x) + (abs(dx) / 2), min(loc_1.y, loc_2.y) + (abs(dy) / 2)
+        self.app.worker_task.emit({'fcn': worker_job, 'params': [self.app]})
 
-                if dist in min_dict:
-                    min_dict[dist].append(loc)
+    def on_tool_finished(self, res):
+        def init(new_obj, app_obj):
+            txt = ''
+            for el in res:
+                txt += '<b>RULE NAME:</b>&nbsp;&nbsp;&nbsp;&nbsp;%s<BR>' % str(el[0]).upper()
+                if isinstance(el[1][0]['name'], list):
+                    for name in el[1][0]['name']:
+                        txt += 'File name: %s<BR>' % str(name)
                 else:
-                    min_dict[dist] = [loc]
-            idx += 1
-
-        points_list = set()
-        for dist in min_dict.keys():
-            if float(dist) < size:
-                for location in min_dict[dist]:
-                    points_list.add(location)
+                    txt += 'File name: %s<BR>' % str(el[1][0]['name'])
 
-        name_list = []
-        for elem in elements:
-            name_list.append(elem['name'])
+                point_txt = ''
+                try:
+                    if el[1][0]['points']:
+                        txt += '{title}: <span style="color:{color};background-color:{h_color}"' \
+                               '>&nbsp;{status} </span>.<BR>'.format(title=_("STATUS"),
+                                                                     h_color='red',
+                                                                     color='white',
+                                                                     status=_("FAILED"))
+                        if 'Failed' in el[1][0]['points'][0]:
+                            point_txt = el[1][0]['points'][0]
+                        else:
+                            for pt in el[1][0]['points']:
+                                point_txt += '(%.*f, %.*f)' % (self.decimals, float(pt[0]), self.decimals, float(pt[1]))
+                                point_txt += ', '
+                        txt += 'Violations: %s<BR>' % str(point_txt)
+                    else:
+                        txt += '{title}: <span style="color:{color};background-color:{h_color}"' \
+                               '>&nbsp;{status} </span>.<BR>'.format(title=_("STATUS"),
+                                                                     h_color='green',
+                                                                     color='white',
+                                                                     status=_("PASSED"))
+                        txt += '%s<BR>' % _("Violations: There are no violations for the current rule.")
+                except KeyError:
+                    pass
 
-        obj_violations['name'] = name_list
-        obj_violations['points'] = list(points_list)
-        violations.append(deepcopy(obj_violations))
+                try:
+                    if el[1][0]['dia']:
+                        txt += '{title}: <span style="color:{color};background-color:{h_color}"' \
+                               '>&nbsp;{status} </span>.<BR>'.format(title=_("STATUS"),
+                                                                     h_color='red',
+                                                                     color='white',
+                                                                     status=_("FAILED"))
+                        if 'Failed' in el[1][0]['dia']:
+                            point_txt = el[1][0]['dia']
+                        else:
+                            for pt in el[1][0]['dia']:
+                                point_txt += '%.*f' % (self.decimals, float(pt))
+                                point_txt += ', '
+                        txt += 'Violations: %s<BR>' % str(point_txt)
+                    else:
+                        txt += '{title}: <span style="color:{color};background-color:{h_color}"' \
+                               '>&nbsp;{status} </span>.<BR>'.format(title=_("STATUS"),
+                                                                     h_color='green',
+                                                                     color='white',
+                                                                     status=_("PASSED"))
+                        txt += '%s<BR>' % _("Violations: There are no violations for the current rule.")
+                except KeyError:
+                    pass
 
-        return rule, violations
+                txt += '<BR><BR>'
+            new_obj.source_file = txt
+            new_obj.read_only = True
 
-    @staticmethod
-    def check_traces_size(elements, size):
-        log.debug("RulesCheck.check_traces_size()")
+        self.app.app_obj.new_object('document', name='Rules_check_results', initialize=init, plot=False)
 
-        rule = _("Trace Size")
+    def reset_fields(self):
+        # self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        # self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        pass
 
-        violations = []
-        obj_violations = {}
-        obj_violations.update({
-            'name': '',
-            'size': list(),
-            'points': list()
-        })
 
-        for elem in elements:
-            dia_list = []
-            points_list = []
-            name = elem['name']
+class RulesUI:
+    
+    toolName = _("Check Rules")
 
-            for apid in elem['apertures']:
-                try:
-                    tool_dia = float(elem['apertures'][apid]['size'])
-                    if tool_dia < float(size) and tool_dia != 0.0:
-                        dia_list.append(tool_dia)
-                        for geo_el in elem['apertures'][apid]['geometry']:
-                            if 'solid' in geo_el.keys():
-                                geo = geo_el['solid']
-                                pt = geo.representative_point()
-                                points_list.append((pt.x, pt.y))
-                except Exception:
-                    # An exception  will be raised for the 'size' key in case of apertures of type AM (macro) which does
-                    # not have the size key
-                    pass
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
 
-            obj_violations['name'] = name
-            obj_violations['size'] = dia_list
-            obj_violations['points'] = points_list
-            violations.append(deepcopy(obj_violations))
-        return rule, violations
+        # ## Title
+        title_label = FCLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
 
-    @staticmethod
-    def check_gerber_annular_ring(obj_list, size, rule):
-        rule_title = rule
+        # Form Layout
+        self.grid_layout = QtWidgets.QGridLayout()
+        self.layout.addLayout(self.grid_layout)
 
-        violations = []
-        obj_violations = {}
-        obj_violations.update({
-            'name': '',
-            'points': list()
-        })
+        self.grid_layout.setColumnStretch(0, 0)
+        self.grid_layout.setColumnStretch(1, 3)
+        self.grid_layout.setColumnStretch(2, 0)
 
-        # added it so I won't have errors of using before declaring
-        gerber_obj = {}
-        gerber_extra_obj = {}
-        exc_obj = {}
-        exc_extra_obj = {}
+        self.gerber_title_lbl = FCLabel('<b>%s</b>:' % _("GERBER"))
+        self.gerber_title_lbl.setToolTip(
+            _("Gerber objects for which to check rules.")
+        )
 
-        if len(obj_list) == 2:
-            gerber_obj = obj_list[0]
-            exc_obj = obj_list[1]
-            if 'apertures' in gerber_obj and 'tools' in exc_obj:
-                pass
-            else:
-                return 'Fail. At least one Gerber and one Excellon object is required to check Minimum Annular Ring'
-        elif len(obj_list) == 3:
-            o1 = obj_list[0]
-            o2 = obj_list[1]
-            o3 = obj_list[2]
-            if 'apertures' in o1 and 'apertures' in o2:
-                gerber_obj = o1
-                gerber_extra_obj = o2
-                exc_obj = o3
-            elif 'tools' in o2 and 'tools' in o3:
-                gerber_obj = o1
-                exc_obj = o2
-                exc_extra_obj = o3
-        elif len(obj_list) == 4:
-            gerber_obj = obj_list[0]
-            gerber_extra_obj = obj_list[1]
-            exc_obj = obj_list[2]
-            exc_extra_obj = obj_list[3]
-        else:
-            return 'Fail. Not enough objects to check Minimum Annular Ring'
+        self.all_obj_cb = FCCheckBox()
 
-        total_geo_grb = []
-        for apid in gerber_obj['apertures']:
-            if 'geometry' in gerber_obj['apertures'][apid]:
-                geometry = gerber_obj['apertures'][apid]['geometry']
-                for geo_el in geometry:
-                    if 'solid' in geo_el and geo_el['solid'] is not None:
-                        total_geo_grb.append(geo_el['solid'])
+        self.grid_layout.addWidget(self.gerber_title_lbl, 0, 0, 1, 2)
+        self.grid_layout.addWidget(self.all_obj_cb, 0, 2)
 
-        if len(obj_list) == 3 and gerber_extra_obj:
-            # add the second Gerber geometry to the first one if it exists
-            for apid in gerber_extra_obj['apertures']:
-                if 'geometry' in gerber_extra_obj['apertures'][apid]:
-                    geometry = gerber_extra_obj['apertures'][apid]['geometry']
-                    for geo_el in geometry:
-                        if 'solid' in geo_el and geo_el['solid'] is not None:
-                            total_geo_grb.append(geo_el['solid'])
+        # Copper Top object
+        self.copper_t_object = FCComboBox()
+        self.copper_t_object.setModel(self.app.collection)
+        self.copper_t_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.copper_t_object.is_last = True
+        self.copper_t_object.obj_type = "Gerber"
 
-        total_geo_grb = MultiPolygon(total_geo_grb)
-        total_geo_grb = total_geo_grb.buffer(0)
+        self.copper_t_object_lbl = FCLabel('%s:' % _("Top"))
+        self.copper_t_object_lbl.setToolTip(
+            _("The Top Gerber Copper object for which rules are checked.")
+        )
 
-        total_geo_exc = []
-        for tool in exc_obj['tools']:
-            if 'solid_geometry' in exc_obj['tools'][tool]:
-                geometry = exc_obj['tools'][tool]['solid_geometry']
-                for geo in geometry:
-                    total_geo_exc.append(geo)
+        self.copper_t_cb = FCCheckBox()
 
-        if len(obj_list) == 3 and exc_extra_obj:
-            # add the second Excellon geometry to the first one if it exists
-            for tool in exc_extra_obj['tools']:
-                if 'solid_geometry' in exc_extra_obj['tools'][tool]:
-                    geometry = exc_extra_obj['tools'][tool]['solid_geometry']
-                    for geo in geometry:
-                        total_geo_exc.append(geo)
+        self.grid_layout.addWidget(self.copper_t_object_lbl, 1, 0)
+        self.grid_layout.addWidget(self.copper_t_object, 1, 1)
+        self.grid_layout.addWidget(self.copper_t_cb, 1, 2)
 
-        if isinstance(total_geo_grb, Polygon):
-            len_1 = 1
-            total_geo_grb = [total_geo_grb]
-        else:
-            len_1 = len(total_geo_grb)
+        # Copper Bottom object
+        self.copper_b_object = FCComboBox()
+        self.copper_b_object.setModel(self.app.collection)
+        self.copper_b_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.copper_b_object.is_last = True
+        self.copper_b_object.obj_type = "Gerber"
 
-        if isinstance(total_geo_exc, Polygon):
-            len_2 = 1
-            total_geo_exc = [total_geo_exc]
-        else:
-            len_2 = len(total_geo_exc)
+        self.copper_b_object_lbl = FCLabel('%s:' % _("Bottom"))
+        self.copper_b_object_lbl.setToolTip(
+            _("The Bottom Gerber Copper object for which rules are checked.")
+        )
 
-        iterations = len_1 * len_2
-        log.debug("RulesCheck.check_gerber_annular_ring(). Iterations: %s" % str(iterations))
+        self.copper_b_cb = FCCheckBox()
 
-        min_dict = {}
-        dist = None
-        for geo in total_geo_grb:
-            for s_geo in total_geo_exc:
-                try:
-                    # minimize the number of distances by not taking into considerations those that are too small
-                    dist = abs(geo.exterior.distance(s_geo))
-                except Exception as e:
-                    log.debug("RulesCheck.check_gerber_annular_ring() --> %s" % str(e))
+        self.grid_layout.addWidget(self.copper_b_object_lbl, 2, 0)
+        self.grid_layout.addWidget(self.copper_b_object, 2, 1)
+        self.grid_layout.addWidget(self.copper_b_cb, 2, 2)
 
-                if dist > 0:
-                    if float(dist) < float(size):
-                        loc_1, loc_2 = nearest_points(geo.exterior, s_geo)
+        # SolderMask Top object
+        self.sm_t_object = FCComboBox()
+        self.sm_t_object.setModel(self.app.collection)
+        self.sm_t_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.sm_t_object.is_last = True
+        self.sm_t_object.obj_type = "Gerber"
 
-                        dx = loc_1.x - loc_2.x
-                        dy = loc_1.y - loc_2.y
-                        loc = min(loc_1.x, loc_2.x) + (abs(dx) / 2), min(loc_1.y, loc_2.y) + (abs(dy) / 2)
+        self.sm_t_object_lbl = FCLabel('%s:' % _("SM Top"))
+        self.sm_t_object_lbl.setToolTip(
+            _("The Top Gerber Solder Mask object for which rules are checked.")
+        )
 
-                        if dist in min_dict:
-                            min_dict[dist].append(loc)
-                        else:
-                            min_dict[dist] = [loc]
-                else:
-                    if dist in min_dict:
-                        min_dict[dist].append(s_geo.representative_point())
-                    else:
-                        min_dict[dist] = [s_geo.representative_point()]
+        self.sm_t_cb = FCCheckBox()
 
-        points_list = []
-        for dist in min_dict.keys():
-            for location in min_dict[dist]:
-                points_list.append(location)
+        self.grid_layout.addWidget(self.sm_t_object_lbl, 3, 0)
+        self.grid_layout.addWidget(self.sm_t_object, 3, 1)
+        self.grid_layout.addWidget(self.sm_t_cb, 3, 2)
 
-        name_list = []
-        try:
-            if gerber_obj:
-                name_list.append(gerber_obj['name'])
-        except KeyError:
-            pass
-        try:
-            if gerber_extra_obj:
-                name_list.append(gerber_extra_obj['name'])
-        except KeyError:
-            pass
+        # SolderMask Bottom object
+        self.sm_b_object = FCComboBox()
+        self.sm_b_object.setModel(self.app.collection)
+        self.sm_b_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.sm_b_object.is_last = True
+        self.sm_b_object.obj_type = "Gerber"
 
-        try:
-            if exc_obj:
-                name_list.append(exc_obj['name'])
-        except KeyError:
-            pass
+        self.sm_b_object_lbl = FCLabel('%s:' % _("SM Bottom"))
+        self.sm_b_object_lbl.setToolTip(
+            _("The Bottom Gerber Solder Mask object for which rules are checked.")
+        )
 
-        try:
-            if exc_extra_obj:
-                name_list.append(exc_extra_obj['name'])
-        except KeyError:
-            pass
+        self.sm_b_cb = FCCheckBox()
 
-        obj_violations['name'] = name_list
-        obj_violations['points'] = points_list
-        violations.append(deepcopy(obj_violations))
-        return rule_title, violations
+        self.grid_layout.addWidget(self.sm_b_object_lbl, 4, 0)
+        self.grid_layout.addWidget(self.sm_b_object, 4, 1)
+        self.grid_layout.addWidget(self.sm_b_cb, 4, 2)
 
-    def execute(self):
-        self.results = []
+        # SilkScreen Top object
+        self.ss_t_object = FCComboBox()
+        self.ss_t_object.setModel(self.app.collection)
+        self.ss_t_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ss_t_object.is_last = True
+        self.ss_t_object.obj_type = "Gerber"
 
-        log.debug("RuleCheck() executing")
+        self.ss_t_object_lbl = FCLabel('%s:' % _("Silk Top"))
+        self.ss_t_object_lbl.setToolTip(
+            _("The Top Gerber Silkscreen object for which rules are checked.")
+        )
 
-        def worker_job(app_obj):
-            self.app.proc_container.new(_("Working..."))
+        self.ss_t_cb = FCCheckBox()
 
-            # RULE: Check Trace Size
-            if self.trace_size_cb.get_value():
-                copper_list = []
-                copper_name_1 = self.copper_t_object.currentText()
-                if copper_name_1 != '' and self.copper_t_cb.get_value():
-                    elem_dict = {}
-                    elem_dict['name'] = deepcopy(copper_name_1)
-                    elem_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_name_1).apertures)
-                    copper_list.append(elem_dict)
+        self.grid_layout.addWidget(self.ss_t_object_lbl, 5, 0)
+        self.grid_layout.addWidget(self.ss_t_object, 5, 1)
+        self.grid_layout.addWidget(self.ss_t_cb, 5, 2)
 
-                copper_name_2 = self.copper_b_object.currentText()
-                if copper_name_2 != '' and self.copper_b_cb.get_value():
-                    elem_dict = {}
-                    elem_dict['name'] = deepcopy(copper_name_2)
-                    elem_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_name_2).apertures)
-                    copper_list.append(elem_dict)
+        # SilkScreen Bottom object
+        self.ss_b_object = FCComboBox()
+        self.ss_b_object.setModel(self.app.collection)
+        self.ss_b_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ss_b_object.is_last = True
+        self.ss_b_object.obj_type = "Gerber"
 
-                trace_size = float(self.trace_size_entry.get_value())
-                self.results.append(self.pool.apply_async(self.check_traces_size, args=(copper_list, trace_size)))
+        self.ss_b_object_lbl = FCLabel('%s:' % _("Silk Bottom"))
+        self.ss_b_object_lbl.setToolTip(
+            _("The Bottom Gerber Silkscreen object for which rules are checked.")
+        )
 
-            # RULE: Check Copper to Copper Clearance
-            if self.clearance_copper2copper_cb.get_value():
+        self.ss_b_cb = FCCheckBox()
 
-                try:
-                    copper_copper_clearance = float(self.clearance_copper2copper_entry.get_value())
-                except Exception as e:
-                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Copper to Copper clearance"),
-                        _("Value is not valid.")))
-                    return
+        self.grid_layout.addWidget(self.ss_b_object_lbl, 6, 0)
+        self.grid_layout.addWidget(self.ss_b_object, 6, 1)
+        self.grid_layout.addWidget(self.ss_b_cb, 6, 2)
 
-                if self.copper_t_cb.get_value():
-                    copper_t_obj = self.copper_t_object.currentText()
-                    copper_t_dict = {}
+        # Outline object
+        self.outline_object = FCComboBox()
+        self.outline_object.setModel(self.app.collection)
+        self.outline_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.outline_object.is_last = True
+        self.outline_object.obj_type = "Gerber"
 
-                    if copper_t_obj != '':
-                        copper_t_dict['name'] = deepcopy(copper_t_obj)
-                        copper_t_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_t_obj).apertures)
+        self.outline_object_lbl = FCLabel('%s:' % _("Outline"))
+        self.outline_object_lbl.setToolTip(
+            _("The Gerber Outline (Cutout) object for which rules are checked.")
+        )
 
-                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
-                                                                  args=(copper_t_dict,
-                                                                        copper_copper_clearance,
-                                                                        _("TOP -> Copper to Copper clearance"))))
-                if self.copper_b_cb.get_value():
-                    copper_b_obj = self.copper_b_object.currentText()
-                    copper_b_dict = {}
-                    if copper_b_obj != '':
-                        copper_b_dict['name'] = deepcopy(copper_b_obj)
-                        copper_b_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_b_obj).apertures)
+        self.out_cb = FCCheckBox()
 
-                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
-                                                                  args=(copper_b_dict,
-                                                                        copper_copper_clearance,
-                                                                        _("BOTTOM -> Copper to Copper clearance"))))
+        self.grid_layout.addWidget(self.outline_object_lbl, 7, 0)
+        self.grid_layout.addWidget(self.outline_object, 7, 1)
+        self.grid_layout.addWidget(self.out_cb, 7, 2)
 
-                if self.copper_t_cb.get_value() is False and self.copper_b_cb.get_value() is False:
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Copper to Copper clearance"),
-                        _("At least one Gerber object has to be selected for this rule but none is selected.")))
-                    return
+        self.grid_layout.addWidget(FCLabel(""), 8, 0, 1, 3)
 
-            # RULE: Check Copper to Outline Clearance
-            if self.clearance_copper2ol_cb.get_value() and self.out_cb.get_value():
-                top_dict = {}
-                bottom_dict = {}
-                outline_dict = {}
+        self.excellon_title_lbl = FCLabel('<b>%s</b>:' % _("EXCELLON"))
+        self.excellon_title_lbl.setToolTip(
+            _("Excellon objects for which to check rules.")
+        )
 
-                copper_top = self.copper_t_object.currentText()
-                if copper_top != '' and self.copper_t_cb.get_value():
-                    top_dict['name'] = deepcopy(copper_top)
-                    top_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_top).apertures)
+        self.grid_layout.addWidget(self.excellon_title_lbl, 9, 0, 1, 3)
 
-                copper_bottom = self.copper_b_object.currentText()
-                if copper_bottom != '' and self.copper_b_cb.get_value():
-                    bottom_dict['name'] = deepcopy(copper_bottom)
-                    bottom_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_bottom).apertures)
+        # Excellon 1 object
+        self.e1_object = FCComboBox()
+        self.e1_object.setModel(self.app.collection)
+        self.e1_object.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.e1_object.is_last = True
+        self.e1_object.obj_type = "Excellon"
 
-                copper_outline = self.outline_object.currentText()
-                if copper_outline != '' and self.out_cb.get_value():
-                    outline_dict['name'] = deepcopy(copper_outline)
-                    outline_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_outline).apertures)
+        self.e1_object_lbl = FCLabel('%s:' % _("Excellon 1"))
+        self.e1_object_lbl.setToolTip(
+            _("Excellon object for which to check rules.\n"
+              "Holds the plated holes or a general Excellon file content.")
+        )
 
-                try:
-                    copper_outline_clearance = float(self.clearance_copper2ol_entry.get_value())
-                except Exception as e:
-                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Copper to Outline clearance"),
-                        _("Value is not valid.")))
-                    return
+        self.e1_cb = FCCheckBox()
 
-                if not top_dict and not bottom_dict or not outline_dict:
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Copper to Outline clearance"),
-                        _("One of the copper Gerber objects or the Outline Gerber object is not valid.")))
-                    return
-                objs = []
-                if top_dict:
-                    objs.append(top_dict)
-                if bottom_dict:
-                    objs.append(bottom_dict)
+        self.grid_layout.addWidget(self.e1_object_lbl, 10, 0)
+        self.grid_layout.addWidget(self.e1_object, 10, 1)
+        self.grid_layout.addWidget(self.e1_cb, 10, 2)
 
-                if outline_dict:
-                    objs.append(outline_dict)
-                else:
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Copper to Outline clearance"),
-                        _("Outline Gerber object presence is mandatory for this rule but it is not selected.")))
-                    return
+        # Excellon 2 object
+        self.e2_object = FCComboBox()
+        self.e2_object.setModel(self.app.collection)
+        self.e2_object.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.e2_object.is_last = True
+        self.e2_object.obj_type = "Excellon"
 
-                self.results.append(self.pool.apply_async(self.check_gerber_clearance,
-                                                          args=(objs,
-                                                                copper_outline_clearance,
-                                                                _("Copper to Outline clearance"))))
+        self.e2_object_lbl = FCLabel('%s:' % _("Excellon 2"))
+        self.e2_object_lbl.setToolTip(
+            _("Excellon object for which to check rules.\n"
+              "Holds the non-plated holes.")
+        )
+
+        self.e2_cb = FCCheckBox()
 
-            # RULE: Check Silk to Silk Clearance
-            if self.clearance_silk2silk_cb.get_value():
-                silk_dict = {}
+        self.grid_layout.addWidget(self.e2_object_lbl, 11, 0)
+        self.grid_layout.addWidget(self.e2_object, 11, 1)
+        self.grid_layout.addWidget(self.e2_cb, 11, 2)
 
-                try:
-                    silk_silk_clearance = float(self.clearance_silk2silk_entry.get_value())
-                except Exception as e:
-                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Silk to Silk clearance"),
-                        _("Value is not valid.")))
-                    return
+        self.grid_layout.addWidget(FCLabel(""), 12, 0, 1, 3)
 
-                if self.ss_t_cb.get_value():
-                    silk_obj = self.ss_t_object.currentText()
-                    if silk_obj != '':
-                        silk_dict['name'] = deepcopy(silk_obj)
-                        silk_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_obj).apertures)
+        # Control All
+        self.all_cb = FCCheckBox('%s' % _("All Rules"))
+        self.all_cb.setToolTip(
+            _("This check/uncheck all the rules below.")
+        )
+        self.all_cb.setStyleSheet(
+            """
+            QCheckBox {font-weight: bold; color: green}
+            """
+        )
+        self.layout.addWidget(self.all_cb)
 
-                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
-                                                                  args=(silk_dict,
-                                                                        silk_silk_clearance,
-                                                                        _("TOP -> Silk to Silk clearance"))))
-                if self.ss_b_cb.get_value():
-                    silk_obj = self.ss_b_object.currentText()
-                    if silk_obj != '':
-                        silk_dict['name'] = deepcopy(silk_obj)
-                        silk_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_obj).apertures)
+        # Form Layout
+        self.form_layout_1 = QtWidgets.QFormLayout()
+        self.layout.addLayout(self.form_layout_1)
 
-                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
-                                                                  args=(silk_dict,
-                                                                        silk_silk_clearance,
-                                                                        _("BOTTOM -> Silk to Silk clearance"))))
+        self.form_layout_1.addRow(FCLabel(""))
 
-                if self.ss_t_cb.get_value() is False and self.ss_b_cb.get_value() is False:
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Silk to Silk clearance"),
-                        _("At least one Gerber object has to be selected for this rule but none is selected.")))
-                    return
+        # Trace size
+        self.trace_size_cb = FCCheckBox('%s:' % _("Trace Size"))
+        self.trace_size_cb.setToolTip(
+            _("This checks if the minimum size for traces is met.")
+        )
+        self.form_layout_1.addRow(self.trace_size_cb)
 
-            # RULE: Check Silk to Solder Mask Clearance
-            if self.clearance_silk2sm_cb.get_value():
-                silk_t_dict = {}
-                sm_t_dict = {}
-                silk_b_dict = {}
-                sm_b_dict = {}
+        # Trace size value
+        self.trace_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.trace_size_entry.set_range(0.00001, 999.99999)
+        self.trace_size_entry.set_precision(self.decimals)
+        self.trace_size_entry.setSingleStep(0.1)
 
-                top_ss = False
-                bottom_ss = False
-                top_sm = False
-                bottom_sm = False
+        self.trace_size_lbl = FCLabel('%s:' % _("Min value"))
+        self.trace_size_lbl.setToolTip(
+            _("Minimum acceptable trace size.")
+        )
+        self.form_layout_1.addRow(self.trace_size_lbl, self.trace_size_entry)
 
-                silk_top = self.ss_t_object.currentText()
-                if silk_top != '' and self.ss_t_cb.get_value():
-                    silk_t_dict['name'] = deepcopy(silk_top)
-                    silk_t_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_top).apertures)
-                    top_ss = True
+        self.ts = OptionalInputSection(self.trace_size_cb, [self.trace_size_lbl, self.trace_size_entry])
 
-                silk_bottom = self.ss_b_object.currentText()
-                if silk_bottom != '' and self.ss_b_cb.get_value():
-                    silk_b_dict['name'] = deepcopy(silk_bottom)
-                    silk_b_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_bottom).apertures)
-                    bottom_ss = True
+        # Copper2copper clearance
+        self.clearance_copper2copper_cb = FCCheckBox('%s:' % _("Copper to Copper clearance"))
+        self.clearance_copper2copper_cb.setToolTip(
+            _("This checks if the minimum clearance between copper\n"
+              "features is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_copper2copper_cb)
 
-                sm_top = self.sm_t_object.currentText()
-                if sm_top != '' and self.sm_t_cb.get_value():
-                    sm_t_dict['name'] = deepcopy(sm_top)
-                    sm_t_dict['apertures'] = deepcopy(self.app.collection.get_by_name(sm_top).apertures)
-                    top_sm = True
+        # Copper2copper clearance value
+        self.clearance_copper2copper_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.clearance_copper2copper_entry.set_range(0.00001, 999.99999)
+        self.clearance_copper2copper_entry.set_precision(self.decimals)
+        self.clearance_copper2copper_entry.setSingleStep(0.1)
 
-                sm_bottom = self.sm_b_object.currentText()
-                if sm_bottom != '' and self.sm_b_cb.get_value():
-                    sm_b_dict['name'] = deepcopy(sm_bottom)
-                    sm_b_dict['apertures'] = deepcopy(self.app.collection.get_by_name(sm_bottom).apertures)
-                    bottom_sm = True
+        self.clearance_copper2copper_lbl = FCLabel('%s:' % _("Min value"))
+        self.clearance_copper2copper_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_copper2copper_lbl, self.clearance_copper2copper_entry)
 
-                try:
-                    silk_sm_clearance = float(self.clearance_silk2sm_entry.get_value())
-                except Exception as e:
-                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Silk to Solder Mask Clearance"),
-                        _("Value is not valid.")))
-                    return
+        self.c2c = OptionalInputSection(
+            self.clearance_copper2copper_cb, [self.clearance_copper2copper_lbl, self.clearance_copper2copper_entry])
 
-                if (not silk_t_dict and not silk_b_dict) or (not sm_t_dict and not sm_b_dict):
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Silk to Solder Mask Clearance"),
-                        _("One or more of the Gerber objects is not valid.")))
-                    return
+        # Copper2outline clearance
+        self.clearance_copper2ol_cb = FCCheckBox('%s:' % _("Copper to Outline clearance"))
+        self.clearance_copper2ol_cb.setToolTip(
+            _("This checks if the minimum clearance between copper\n"
+              "features and the outline is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_copper2ol_cb)
 
-                if top_ss is True and top_sm is True:
-                    objs = [silk_t_dict, sm_t_dict]
-                    self.results.append(self.pool.apply_async(self.check_gerber_clearance,
-                                                              args=(objs,
-                                                                    silk_sm_clearance,
-                                                                    _("TOP -> Silk to Solder Mask Clearance"))))
-                elif bottom_ss is True and bottom_sm is True:
-                    objs = [silk_b_dict, sm_b_dict]
-                    self.results.append(self.pool.apply_async(self.check_gerber_clearance,
-                                                              args=(objs,
-                                                                    silk_sm_clearance,
-                                                                    _("BOTTOM -> Silk to Solder Mask Clearance"))))
-                else:
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Silk to Solder Mask Clearance"),
-                        _("Both Silk and Solder Mask Gerber objects has to be either both Top or both Bottom.")))
-                    return
+        # Copper2outline clearance value
+        self.clearance_copper2ol_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.clearance_copper2ol_entry.set_range(0.00001, 999.99999)
+        self.clearance_copper2ol_entry.set_precision(self.decimals)
+        self.clearance_copper2ol_entry.setSingleStep(0.1)
 
-            # RULE: Check Silk to Outline Clearance
-            if self.clearance_silk2ol_cb.get_value():
-                top_dict = {}
-                bottom_dict = {}
-                outline_dict = {}
+        self.clearance_copper2ol_lbl = FCLabel('%s:' % _("Min value"))
+        self.clearance_copper2ol_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_copper2ol_lbl, self.clearance_copper2ol_entry)
 
-                silk_top = self.ss_t_object.currentText()
-                if silk_top != '' and self.ss_t_cb.get_value():
-                    top_dict['name'] = deepcopy(silk_top)
-                    top_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_top).apertures)
+        self.c2ol = OptionalInputSection(
+            self.clearance_copper2ol_cb, [self.clearance_copper2ol_lbl, self.clearance_copper2ol_entry])
 
-                silk_bottom = self.ss_b_object.currentText()
-                if silk_bottom != '' and self.ss_b_cb.get_value():
-                    bottom_dict['name'] = deepcopy(silk_bottom)
-                    bottom_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_bottom).apertures)
+        # Silkscreen2silkscreen clearance
+        self.clearance_silk2silk_cb = FCCheckBox('%s:' % _("Silk to Silk Clearance"))
+        self.clearance_silk2silk_cb.setToolTip(
+            _("This checks if the minimum clearance between silkscreen\n"
+              "features and silkscreen features is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2silk_cb)
 
-                copper_outline = self.outline_object.currentText()
-                if copper_outline != '' and self.out_cb.get_value():
-                    outline_dict['name'] = deepcopy(copper_outline)
-                    outline_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_outline).apertures)
+        # Copper2silkscreen clearance value
+        self.clearance_silk2silk_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.clearance_silk2silk_entry.set_range(0.00001, 999.99999)
+        self.clearance_silk2silk_entry.set_precision(self.decimals)
+        self.clearance_silk2silk_entry.setSingleStep(0.1)
 
-                try:
-                    copper_outline_clearance = float(self.clearance_copper2ol_entry.get_value())
-                except Exception as e:
-                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Silk to Outline Clearance"),
-                        _("Value is not valid.")))
-                    return
+        self.clearance_silk2silk_lbl = FCLabel('%s:' % _("Min value"))
+        self.clearance_silk2silk_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2silk_lbl, self.clearance_silk2silk_entry)
 
-                if not top_dict and not bottom_dict or not outline_dict:
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Silk to Outline Clearance"),
-                        _("One of the Silk Gerber objects or the Outline Gerber object is not valid.")))
-                    return
+        self.s2s = OptionalInputSection(
+            self.clearance_silk2silk_cb, [self.clearance_silk2silk_lbl, self.clearance_silk2silk_entry])
 
-                objs = []
-                if top_dict:
-                    objs.append(top_dict)
-                if bottom_dict:
-                    objs.append(bottom_dict)
+        # Silkscreen2soldermask clearance
+        self.clearance_silk2sm_cb = FCCheckBox('%s:' % _("Silk to Solder Mask Clearance"))
+        self.clearance_silk2sm_cb.setToolTip(
+            _("This checks if the minimum clearance between silkscreen\n"
+              "features and soldermask features is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2sm_cb)
 
-                if outline_dict:
-                    objs.append(outline_dict)
-                else:
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Silk to Outline Clearance"),
-                        _("Outline Gerber object presence is mandatory for this rule but it is not selected.")))
-                    return
+        # Silkscreen2soldermask clearance value
+        self.clearance_silk2sm_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.clearance_silk2sm_entry.set_range(0.00001, 999.99999)
+        self.clearance_silk2sm_entry.set_precision(self.decimals)
+        self.clearance_silk2sm_entry.setSingleStep(0.1)
 
-                self.results.append(self.pool.apply_async(self.check_gerber_clearance,
-                                                          args=(objs,
-                                                                copper_outline_clearance,
-                                                                _("Silk to Outline Clearance"))))
+        self.clearance_silk2sm_lbl = FCLabel('%s:' % _("Min value"))
+        self.clearance_silk2sm_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2sm_lbl, self.clearance_silk2sm_entry)
 
-            # RULE: Check Minimum Solder Mask Sliver
-            if self.clearance_silk2silk_cb.get_value():
-                sm_dict = {}
+        self.s2sm = OptionalInputSection(
+            self.clearance_silk2sm_cb, [self.clearance_silk2sm_lbl, self.clearance_silk2sm_entry])
 
-                try:
-                    sm_sm_clearance = float(self.clearance_sm2sm_entry.get_value())
-                except Exception as e:
-                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Minimum Solder Mask Sliver"),
-                        _("Value is not valid.")))
-                    return
+        # Silk2outline clearance
+        self.clearance_silk2ol_cb = FCCheckBox('%s:' % _("Silk to Outline Clearance"))
+        self.clearance_silk2ol_cb.setToolTip(
+            _("This checks if the minimum clearance between silk\n"
+              "features and the outline is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2ol_cb)
 
-                if self.sm_t_cb.get_value():
-                    solder_obj = self.sm_t_object.currentText()
-                    if solder_obj != '':
-                        sm_dict['name'] = deepcopy(solder_obj)
-                        sm_dict['apertures'] = deepcopy(self.app.collection.get_by_name(solder_obj).apertures)
+        # Silk2outline clearance value
+        self.clearance_silk2ol_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.clearance_silk2ol_entry.set_range(0.00001, 999.99999)
+        self.clearance_silk2ol_entry.set_precision(self.decimals)
+        self.clearance_silk2ol_entry.setSingleStep(0.1)
 
-                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
-                                                                  args=(sm_dict,
-                                                                        sm_sm_clearance,
-                                                                        _("TOP -> Minimum Solder Mask Sliver"))))
-                if self.sm_b_cb.get_value():
-                    solder_obj = self.sm_b_object.currentText()
-                    if solder_obj != '':
-                        sm_dict['name'] = deepcopy(solder_obj)
-                        sm_dict['apertures'] = deepcopy(self.app.collection.get_by_name(solder_obj).apertures)
+        self.clearance_silk2ol_lbl = FCLabel('%s:' % _("Min value"))
+        self.clearance_silk2ol_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2ol_lbl, self.clearance_silk2ol_entry)
 
-                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
-                                                                  args=(sm_dict,
-                                                                        sm_sm_clearance,
-                                                                        _("BOTTOM -> Minimum Solder Mask Sliver"))))
+        self.s2ol = OptionalInputSection(
+            self.clearance_silk2ol_cb, [self.clearance_silk2ol_lbl, self.clearance_silk2ol_entry])
 
-                if self.sm_t_cb.get_value() is False and self.sm_b_cb.get_value() is False:
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Minimum Solder Mask Sliver"),
-                        _("At least one Gerber object has to be selected for this rule but none is selected.")))
-                    return
+        # Soldermask2soldermask clearance
+        self.clearance_sm2sm_cb = FCCheckBox('%s:' % _("Minimum Solder Mask Sliver"))
+        self.clearance_sm2sm_cb.setToolTip(
+            _("This checks if the minimum clearance between soldermask\n"
+              "features and soldermask features is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_sm2sm_cb)
 
-            # RULE: Check Minimum Annular Ring
-            if self.ring_integrity_cb.get_value():
-                top_dict = {}
-                bottom_dict = {}
-                exc_1_dict = {}
-                exc_2_dict = {}
+        # Soldermask2soldermask clearance value
+        self.clearance_sm2sm_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.clearance_sm2sm_entry.set_range(0.00001, 999.99999)
+        self.clearance_sm2sm_entry.set_precision(self.decimals)
+        self.clearance_sm2sm_entry.setSingleStep(0.1)
 
-                copper_top = self.copper_t_object.currentText()
-                if copper_top != '' and self.copper_t_cb.get_value():
-                    top_dict['name'] = deepcopy(copper_top)
-                    top_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_top).apertures)
+        self.clearance_sm2sm_lbl = FCLabel('%s:' % _("Min value"))
+        self.clearance_sm2sm_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_sm2sm_lbl, self.clearance_sm2sm_entry)
 
-                copper_bottom = self.copper_b_object.currentText()
-                if copper_bottom != '' and self.copper_b_cb.get_value():
-                    bottom_dict['name'] = deepcopy(copper_bottom)
-                    bottom_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_bottom).apertures)
+        self.sm2sm = OptionalInputSection(
+            self.clearance_sm2sm_cb, [self.clearance_sm2sm_lbl, self.clearance_sm2sm_entry])
 
-                excellon_1 = self.e1_object.currentText()
-                if excellon_1 != '' and self.e1_cb.get_value():
-                    exc_1_dict['name'] = deepcopy(excellon_1)
-                    exc_1_dict['tools'] = deepcopy(
-                        self.app.collection.get_by_name(excellon_1).tools)
+        # Ring integrity check
+        self.ring_integrity_cb = FCCheckBox('%s:' % _("Minimum Annular Ring"))
+        self.ring_integrity_cb.setToolTip(
+            _("This checks if the minimum copper ring left by drilling\n"
+              "a hole into a pad is met.")
+        )
+        self.form_layout_1.addRow(self.ring_integrity_cb)
 
-                excellon_2 = self.e2_object.currentText()
-                if excellon_2 != '' and self.e2_cb.get_value():
-                    exc_2_dict['name'] = deepcopy(excellon_2)
-                    exc_2_dict['tools'] = deepcopy(
-                        self.app.collection.get_by_name(excellon_2).tools)
+        # Ring integrity value
+        self.ring_integrity_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.ring_integrity_entry.set_range(0.00001, 999.99999)
+        self.ring_integrity_entry.set_precision(self.decimals)
+        self.ring_integrity_entry.setSingleStep(0.1)
 
-                try:
-                    ring_val = float(self.ring_integrity_entry.get_value())
-                except Exception as e:
-                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Minimum Annular Ring"),
-                        _("Value is not valid.")))
-                    return
+        self.ring_integrity_lbl = FCLabel('%s:' % _("Min value"))
+        self.ring_integrity_lbl.setToolTip(
+            _("Minimum acceptable ring value.")
+        )
+        self.form_layout_1.addRow(self.ring_integrity_lbl, self.ring_integrity_entry)
 
-                if (not top_dict and not bottom_dict) or (not exc_1_dict and not exc_2_dict):
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Minimum Annular Ring"),
-                        _("One of the Copper Gerber objects or the Excellon objects is not valid.")))
-                    return
+        self.anr = OptionalInputSection(
+            self.ring_integrity_cb, [self.ring_integrity_lbl, self.ring_integrity_entry])
 
-                objs = []
-                if top_dict:
-                    objs.append(top_dict)
-                elif bottom_dict:
-                    objs.append(bottom_dict)
+        self.form_layout_1.addRow(FCLabel(""))
 
-                if exc_1_dict:
-                    objs.append(exc_1_dict)
-                elif exc_2_dict:
-                    objs.append(exc_2_dict)
-                else:
-                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
-                        _("Minimum Annular Ring"),
-                        _("Excellon object presence is mandatory for this rule but none is selected.")))
-                    return
+        # Hole2Hole clearance
+        self.clearance_d2d_cb = FCCheckBox('%s:' % _("Hole to Hole Clearance"))
+        self.clearance_d2d_cb.setToolTip(
+            _("This checks if the minimum clearance between a drill hole\n"
+              "and another drill hole is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_d2d_cb)
 
-                self.results.append(self.pool.apply_async(self.check_gerber_annular_ring,
-                                                          args=(objs,
-                                                                ring_val,
-                                                                _("Minimum Annular Ring"))))
+        # Hole2Hole clearance value
+        self.clearance_d2d_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.clearance_d2d_entry.set_range(0.00001, 999.99999)
+        self.clearance_d2d_entry.set_precision(self.decimals)
+        self.clearance_d2d_entry.setSingleStep(0.1)
 
-            # RULE: Check Hole to Hole Clearance
-            if self.clearance_d2d_cb.get_value():
-                exc_list = []
-                exc_name_1 = self.e1_object.currentText()
-                if exc_name_1 != '' and self.e1_cb.get_value():
-                    elem_dict = {}
-                    elem_dict['name'] = deepcopy(exc_name_1)
-                    elem_dict['tools'] = deepcopy(self.app.collection.get_by_name(exc_name_1).tools)
-                    exc_list.append(elem_dict)
+        self.clearance_d2d_lbl = FCLabel('%s:' % _("Min value"))
+        self.clearance_d2d_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_d2d_lbl, self.clearance_d2d_entry)
 
-                exc_name_2 = self.e2_object.currentText()
-                if exc_name_2 != '' and self.e2_cb.get_value():
-                    elem_dict = {}
-                    elem_dict['name'] = deepcopy(exc_name_2)
-                    elem_dict['tools'] = deepcopy(self.app.collection.get_by_name(exc_name_2).tools)
-                    exc_list.append(elem_dict)
+        self.d2d = OptionalInputSection(
+            self.clearance_d2d_cb, [self.clearance_d2d_lbl, self.clearance_d2d_entry])
 
-                hole_clearance = float(self.clearance_d2d_entry.get_value())
-                self.results.append(self.pool.apply_async(self.check_holes_clearance, args=(exc_list, hole_clearance)))
+        # Drill holes size check
+        self.drill_size_cb = FCCheckBox('%s:' % _("Hole Size"))
+        self.drill_size_cb.setToolTip(
+            _("This checks if the drill holes\n"
+              "sizes are above the threshold.")
+        )
+        self.form_layout_1.addRow(self.drill_size_cb)
 
-            # RULE: Check Holes Size
-            if self.drill_size_cb.get_value():
-                exc_list = []
-                exc_name_1 = self.e1_object.currentText()
-                if exc_name_1 != '' and self.e1_cb.get_value():
-                    elem_dict = {}
-                    elem_dict['name'] = deepcopy(exc_name_1)
-                    elem_dict['tools'] = deepcopy(self.app.collection.get_by_name(exc_name_1).tools)
-                    exc_list.append(elem_dict)
+        # Drile holes value
+        self.drill_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.drill_size_entry.set_range(0.00001, 999.99999)
+        self.drill_size_entry.set_precision(self.decimals)
+        self.drill_size_entry.setSingleStep(0.1)
 
-                exc_name_2 = self.e2_object.currentText()
-                if exc_name_2 != '' and self.e2_cb.get_value():
-                    elem_dict = {}
-                    elem_dict['name'] = deepcopy(exc_name_2)
-                    elem_dict['tools'] = deepcopy(self.app.collection.get_by_name(exc_name_2).tools)
-                    exc_list.append(elem_dict)
+        self.drill_size_lbl = FCLabel('%s:' % _("Min value"))
+        self.drill_size_lbl.setToolTip(
+            _("Minimum acceptable drill size.")
+        )
+        self.form_layout_1.addRow(self.drill_size_lbl, self.drill_size_entry)
 
-                drill_size = float(self.drill_size_entry.get_value())
-                self.results.append(self.pool.apply_async(self.check_holes_size, args=(exc_list, drill_size)))
+        self.ds = OptionalInputSection(
+            self.drill_size_cb, [self.drill_size_lbl, self.drill_size_entry])
 
-            output = []
-            for p in self.results:
-                output.append(p.get())
+        # Buttons
+        hlay_2 = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay_2)
 
-            self.tool_finished.emit(output)
+        # hlay_2.addStretch()
+        self.run_button = FCButton(_("Run Rules Check"))
+        self.run_button.setToolTip(
+            _("Panelize the specified object around the specified box.\n"
+              "In other words it creates multiple copies of the source object,\n"
+              "arranged in a 2D array of rows and columns.")
+        )
+        self.run_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        hlay_2.addWidget(self.run_button)
 
-            log.debug("RuleCheck() finished")
+        self.layout.addStretch()
 
-        self.app.worker_task.emit({'fcn': worker_job, 'params': [self.app]})
+        # ## Reset Tool
+        self.reset_button = FCButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.reset_button)
 
-    def on_tool_finished(self, res):
-        def init(new_obj, app_obj):
-            txt = ''
-            for el in res:
-                txt += '<b>RULE NAME:</b>&nbsp;&nbsp;&nbsp;&nbsp;%s<BR>' % str(el[0]).upper()
-                if isinstance(el[1][0]['name'], list):
-                    for name in el[1][0]['name']:
-                        txt += 'File name: %s<BR>' % str(name)
-                else:
-                    txt += 'File name: %s<BR>' % str(el[1][0]['name'])
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+    def on_all_cb_changed(self, state):
+        cb_items = [self.form_layout_1.itemAt(i).widget() for i in range(self.form_layout_1.count())
+                    if isinstance(self.form_layout_1.itemAt(i).widget(), FCCheckBox)]
 
-                point_txt = ''
-                try:
-                    if el[1][0]['points']:
-                        txt += '{title}: <span style="color:{color};background-color:{h_color}"' \
-                               '>&nbsp;{status} </span>.<BR>'.format(title=_("STATUS"),
-                                                                     h_color='red',
-                                                                     color='white',
-                                                                     status=_("FAILED"))
-                        if 'Failed' in el[1][0]['points'][0]:
-                            point_txt = el[1][0]['points'][0]
-                        else:
-                            for pt in el[1][0]['points']:
-                                point_txt += '(%.*f, %.*f)' % (self.decimals, float(pt[0]), self.decimals, float(pt[1]))
-                                point_txt += ', '
-                        txt += 'Violations: %s<BR>' % str(point_txt)
-                    else:
-                        txt += '{title}: <span style="color:{color};background-color:{h_color}"' \
-                               '>&nbsp;{status} </span>.<BR>'.format(title=_("STATUS"),
-                                                                     h_color='green',
-                                                                     color='white',
-                                                                     status=_("PASSED"))
-                        txt += '%s<BR>' % _("Violations: There are no violations for the current rule.")
-                except KeyError:
-                    pass
+        for cb in cb_items:
+            if state:
+                cb.setChecked(True)
+            else:
+                cb.setChecked(False)
 
-                try:
-                    if el[1][0]['dia']:
-                        txt += '{title}: <span style="color:{color};background-color:{h_color}"' \
-                               '>&nbsp;{status} </span>.<BR>'.format(title=_("STATUS"),
-                                                                     h_color='red',
-                                                                     color='white',
-                                                                     status=_("FAILED"))
-                        if 'Failed' in el[1][0]['dia']:
-                            point_txt = el[1][0]['dia']
-                        else:
-                            for pt in el[1][0]['dia']:
-                                point_txt += '%.*f' % (self.decimals, float(pt))
-                                point_txt += ', '
-                        txt += 'Violations: %s<BR>' % str(point_txt)
-                    else:
-                        txt += '{title}: <span style="color:{color};background-color:{h_color}"' \
-                               '>&nbsp;{status} </span>.<BR>'.format(title=_("STATUS"),
-                                                                     h_color='green',
-                                                                     color='white',
-                                                                     status=_("PASSED"))
-                        txt += '%s<BR>' % _("Violations: There are no violations for the current rule.")
-                except KeyError:
-                    pass
+    def on_all_objects_cb_changed(self, state):
+        cb_items = [self.grid_layout.itemAt(i).widget() for i in range(self.grid_layout.count())
+                    if isinstance(self.grid_layout.itemAt(i).widget(), FCCheckBox)]
 
-                txt += '<BR><BR>'
-            new_obj.source_file = txt
-            new_obj.read_only = True
+        for cb in cb_items:
+            if state:
+                cb.setChecked(True)
+            else:
+                cb.setChecked(False)
 
-        self.app.app_obj.new_object('document', name='Rules_check_results', initialize=init, plot=False)
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
 
-    def reset_fields(self):
-        # self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        # self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        pass
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 3 - 0
appTools/ToolShell.py

@@ -295,6 +295,9 @@ class FCShell(TermWidget):
         self._append_to_browser('in', "FlatCAM %s - " % version)
         self.append_output('%s\n\n' % _("Type >help< to get started"))
 
+        self.app.ui.shell_dock.setWidget(self)
+        self.app.log.debug("TCL Shell has been initialized.")
+
     def init_tcl(self):
         if hasattr(self, 'tcl') and self.tcl is not None:
             # self.tcl = None

Разница между файлами не показана из-за своего большого размера
+ 530 - 972
appTools/ToolSolderPaste.py


+ 212 - 186
appTools/ToolSub.py

@@ -5,7 +5,7 @@
 # MIT Licence                                              #
 # ##########################################################
 
-from PyQt5 import QtWidgets, QtCore
+from PyQt5 import QtWidgets, QtCore, QtGui
 
 from appTool import AppTool
 from appGUI.GUIElements import FCCheckBox, FCButton, FCComboBox
@@ -37,158 +37,17 @@ class ToolSub(AppTool):
     # meaning geometry that was deformed
     aperture_processing_finished = QtCore.pyqtSignal(str, list)
 
-    toolName = _("Subtract Tool")
-
     def __init__(self, app):
         self.app = app
         self.decimals = self.app.decimals
 
         AppTool.__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>%s</b>" % _("GERBER"))
-        form_layout.addRow(self.gerber_title)
-
-        # Target Gerber Object
-        self.target_gerber_combo = FCComboBox()
-        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_combo.is_last = True
-        self.target_gerber_combo.obj_type = "Gerber"
-
-        self.target_gerber_label = QtWidgets.QLabel('%s:' % _("Target"))
-        self.target_gerber_label.setToolTip(
-            _("Gerber object from which to subtract\n"
-              "the subtractor Gerber object.")
-        )
-
-        form_layout.addRow(self.target_gerber_label, self.target_gerber_combo)
-
-        # Substractor Gerber Object
-        self.sub_gerber_combo = FCComboBox()
-        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.is_last = True
-        self.sub_gerber_combo.obj_type = "Gerber"
-
-        self.sub_gerber_label = QtWidgets.QLabel('%s:' % _("Subtractor"))
-        self.sub_gerber_label.setToolTip(
-            _("Gerber object that will be subtracted\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(_('Subtract Gerber'))
-        self.intersect_btn.setToolTip(
-            _("Will remove the area occupied by the subtractor\n"
-              "Gerber from the Target Gerber.\n"
-              "Can be used to remove the overlapping silkscreen\n"
-              "over the soldermask.")
-        )
-        self.intersect_btn.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        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>%s</b>" % _("GEOMETRY"))
-        form_geo_layout.addRow(self.geo_title)
-
-        # Target Geometry Object
-        self.target_geo_combo = FCComboBox()
-        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_combo.is_last = True
-        self.target_geo_combo.obj_type = "Geometry"
-
-        self.target_geo_label = QtWidgets.QLabel('%s:' % _("Target"))
-        self.target_geo_label.setToolTip(
-            _("Geometry object from which to subtract\n"
-              "the subtractor Geometry object.")
-        )
-
-        form_geo_layout.addRow(self.target_geo_label, self.target_geo_combo)
-
-        # Substractor Geometry Object
-        self.sub_geo_combo = FCComboBox()
-        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.is_last = True
-        self.sub_geo_combo.obj_type = "Geometry"
-
-        self.sub_geo_label = QtWidgets.QLabel('%s:' % _("Subtractor"))
-        self.sub_geo_label.setToolTip(
-            _("Geometry object that will be subtracted\n"
-              "from the target Geometry object.")
-        )
-        e_lab_1 = QtWidgets.QLabel('')
-
-        form_geo_layout.addRow(self.sub_geo_label, self.sub_geo_combo)
-
-        self.close_paths_cb = FCCheckBox(_("Close paths"))
-        self.close_paths_cb.setToolTip(_("Checking this will close the paths cut by the Geometry subtractor object."))
-        self.tools_box.addWidget(self.close_paths_cb)
-
-        self.intersect_geo_btn = FCButton(_('Subtract Geometry'))
-        self.intersect_geo_btn.setToolTip(
-            _("Will remove the area occupied by the subtractor\n"
-              "Geometry from the Target Geometry.")
-        )
-        self.intersect_geo_btn.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.tools_box.addWidget(self.intersect_geo_btn)
-        self.tools_box.addWidget(e_lab_1)
-
-        self.tools_box.addStretch()
-
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.tools_box.addWidget(self.reset_button)
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = SubUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
 
         # QTimer for periodic check
         self.check_thread = QtCore.QTimer()
@@ -227,11 +86,14 @@ class ToolSub(AppTool):
         self.pool = self.app.pool
         self.results = []
 
-        self.intersect_btn.clicked.connect(self.on_grb_intersection_click)
-        self.intersect_geo_btn.clicked.connect(self.on_geo_intersection_click)
+        # Signals
+        self.ui.intersect_btn.clicked.connect(self.on_grb_intersection_click)
+        self.ui.intersect_geo_btn.clicked.connect(self.on_geo_intersection_click)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
+
+        # Custom Signals
         self.job_finished.connect(self.on_job_finished)
         self.aperture_processing_finished.connect(self.new_gerber_object)
-        self.reset_button.clicked.connect(self.set_tool_ui)
 
     def install(self, icon=None, separator=None, **kwargs):
         AppTool.install(self, icon, separator, shortcut='Alt+W', **kwargs)
@@ -269,8 +131,8 @@ class ToolSub(AppTool):
         self.new_solid_geometry = []
         self.target_options.clear()
 
-        self.tools_frame.show()
-        self.close_paths_cb.setChecked(self.app.defaults["tools_sub_close_paths"])
+        self.ui.tools_frame.show()
+        self.ui.close_paths_cb.setChecked(self.app.defaults["tools_sub_close_paths"])
 
     def on_grb_intersection_click(self):
         # reset previous values
@@ -280,7 +142,7 @@ class ToolSub(AppTool):
 
         self.sub_type = "gerber"
 
-        self.target_grb_obj_name = self.target_gerber_combo.currentText()
+        self.target_grb_obj_name = self.ui.target_gerber_combo.currentText()
         if self.target_grb_obj_name == '':
             self.app.inform.emit('[ERROR_NOTCL] %s' % _("No Target object loaded."))
             return
@@ -295,7 +157,7 @@ class ToolSub(AppTool):
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.obj_name))
             return "Could not retrieve object: %s" % self.target_grb_obj_name
 
-        self.sub_grb_obj_name = self.sub_gerber_combo.currentText()
+        self.sub_grb_obj_name = self.ui.sub_gerber_combo.currentText()
         if self.sub_grb_obj_name == '':
             self.app.inform.emit('[ERROR_NOTCL] %s' % _("No Subtractor object loaded."))
             return
@@ -484,10 +346,9 @@ class ToolSub(AppTool):
 
         self.sub_type = "geo"
 
-        self.target_geo_obj_name = self.target_geo_combo.currentText()
+        self.target_geo_obj_name = self.ui.target_geo_combo.currentText()
         if self.target_geo_obj_name == '':
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("No Target object loaded."))
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No Target object loaded."))
             return
 
         # Get target object.
@@ -495,14 +356,12 @@ class ToolSub(AppTool):
             self.target_geo_obj = self.app.collection.get_by_name(self.target_geo_obj_name)
         except Exception as e:
             log.debug("ToolSub.on_geo_intersection_click() --> %s" % str(e))
-            self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
-                                 (_("Could not retrieve object"), self.target_geo_obj_name))
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), 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()
+        self.sub_geo_obj_name = self.ui.sub_geo_combo.currentText()
         if self.sub_geo_obj_name == '':
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("No Subtractor object loaded."))
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No Subtractor object loaded."))
             return
 
         # Get substractor object.
@@ -510,8 +369,7 @@ class ToolSub(AppTool):
             self.sub_geo_obj = self.app.collection.get_by_name(self.sub_geo_obj_name)
         except Exception as e:
             log.debug("ToolSub.on_geo_intersection_click() --> %s" % str(e))
-            self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
-                                 (_("Could not retrieve object"), self.sub_geo_obj_name))
+            self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.sub_geo_obj_name))
             return "Could not retrieve object: %s" % self.sub_geo_obj_name
 
         if self.sub_geo_obj.multigeo:
@@ -528,11 +386,8 @@ class ToolSub(AppTool):
         # 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])
+            for key, v in self.target_geo_obj.tools[tool]:
+                self.new_tools[tool][key] = [] if key == 'solid_geometry' else deepcopy(v)
 
         # add the promises
         if self.target_geo_obj.multigeo:
@@ -549,12 +404,10 @@ class ToolSub(AppTool):
         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]})
+                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]})
+            self.app.worker_task.emit({'fcn': self.toolgeo_intersection, 'params': ["single", geo]})
 
     def toolgeo_intersection(self, tool, geo):
         new_geometry = []
@@ -567,7 +420,7 @@ class ToolSub(AppTool):
 
         with self.app.proc_container.new(text):
             # resulting paths are closed resulting into Polygons
-            if self.close_paths_cb.isChecked():
+            if self.ui.close_paths_cb.isChecked():
                 new_geo = (cascaded_union(geo)).difference(self.sub_union)
                 if new_geo:
                     if not new_geo.is_empty:
@@ -662,14 +515,12 @@ class ToolSub(AppTool):
         with self.app.proc_container.new(_("Generating new object ...")):
             ret = self.app.app_obj.new_object('geometry', outname, obj_init, autoselected=False)
             if ret == 'fail':
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _('Generating new object failed.'))
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _('Generating new object failed.'))
                 return
             # Register recent file
             self.app.file_opened.emit('geometry', outname)
             # GUI feedback
-            self.app.inform.emit('[success] %s: %s' %
-                                 (_("Created"), outname))
+            self.app.inform.emit('[success] %s: %s' % (_("Created"), outname))
 
             # cleanup
             self.new_tools.clear()
@@ -731,13 +582,12 @@ class ToolSub(AppTool):
         """
         if succcess is True:
             if self.sub_type == "gerber":
-                outname = self.target_gerber_combo.currentText() + '_sub'
+                outname = self.ui.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]})
+                self.app.worker_task.emit({'fcn': self.new_gerber_object, 'params': [outname]})
             else:
-                outname = self.target_geo_combo.currentText() + '_sub'
+                outname = self.ui.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]})
@@ -745,13 +595,189 @@ class ToolSub(AppTool):
             self.app.inform.emit('[ERROR_NOTCL] %s' % _('Generating new object failed.'))
 
     def reset_fields(self):
+        self.ui.target_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ui.sub_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+
+        self.ui.target_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
+        self.ui.sub_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
+
+    @staticmethod
+    def poly2rings(poly):
+        return [poly.exterior] + [interior for interior in poly.interiors]
+
+
+class SubUI:
+
+    toolName = _("Subtract Tool")
+
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(QtWidgets.QLabel(""))
+
+        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)
+
+        # Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.tools_box.addLayout(form_layout)
+
+        self.gerber_title = QtWidgets.QLabel("<b>%s</b>" % _("GERBER"))
+        form_layout.addRow(self.gerber_title)
+
+        # Target Gerber Object
+        self.target_gerber_combo = FCComboBox()
+        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_combo.is_last = True
+        self.target_gerber_combo.obj_type = "Gerber"
+
+        self.target_gerber_label = QtWidgets.QLabel('%s:' % _("Target"))
+        self.target_gerber_label.setToolTip(
+            _("Gerber object from which to subtract\n"
+              "the subtractor Gerber object.")
+        )
+
+        form_layout.addRow(self.target_gerber_label, self.target_gerber_combo)
+
+        # Substractor Gerber Object
+        self.sub_gerber_combo = FCComboBox()
+        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.is_last = True
+        self.sub_gerber_combo.obj_type = "Gerber"
+
+        self.sub_gerber_label = QtWidgets.QLabel('%s:' % _("Subtractor"))
+        self.sub_gerber_label.setToolTip(
+            _("Gerber object that will be subtracted\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(_('Subtract Gerber'))
+        self.intersect_btn.setToolTip(
+            _("Will remove the area occupied by the subtractor\n"
+              "Gerber from the Target Gerber.\n"
+              "Can be used to remove the overlapping silkscreen\n"
+              "over the soldermask.")
+        )
+        self.intersect_btn.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        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>%s</b>" % _("GEOMETRY"))
+        form_geo_layout.addRow(self.geo_title)
+
+        # Target Geometry Object
+        self.target_geo_combo = FCComboBox()
+        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_combo.is_last = True
+        self.target_geo_combo.obj_type = "Geometry"
+
+        self.target_geo_label = QtWidgets.QLabel('%s:' % _("Target"))
+        self.target_geo_label.setToolTip(
+            _("Geometry object from which to subtract\n"
+              "the subtractor Geometry object.")
+        )
+
+        form_geo_layout.addRow(self.target_geo_label, self.target_geo_combo)
+
+        # Substractor Geometry Object
+        self.sub_geo_combo = FCComboBox()
+        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.is_last = True
+        self.sub_geo_combo.obj_type = "Geometry"
 
-    @staticmethod
-    def poly2rings(poly):
-        return [poly.exterior] + [interior for interior in poly.interiors]
-# end of file
+        self.sub_geo_label = QtWidgets.QLabel('%s:' % _("Subtractor"))
+        self.sub_geo_label.setToolTip(
+            _("Geometry object that will be subtracted\n"
+              "from the target Geometry object.")
+        )
+        e_lab_1 = QtWidgets.QLabel('')
+
+        form_geo_layout.addRow(self.sub_geo_label, self.sub_geo_combo)
+
+        self.close_paths_cb = FCCheckBox(_("Close paths"))
+        self.close_paths_cb.setToolTip(_("Checking this will close the paths cut by the Geometry subtractor object."))
+        self.tools_box.addWidget(self.close_paths_cb)
+
+        self.intersect_geo_btn = FCButton(_('Subtract Geometry'))
+        self.intersect_geo_btn.setToolTip(
+            _("Will remove the area occupied by the subtractor\n"
+              "Geometry from the Target Geometry.")
+        )
+        self.intersect_geo_btn.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.tools_box.addWidget(self.intersect_geo_btn)
+        self.tools_box.addWidget(e_lab_1)
+
+        self.tools_box.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.tools_box.addWidget(self.reset_button)
+
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 793 - 762
appTools/ToolTransform.py

@@ -7,8 +7,8 @@
 
 from PyQt5 import QtWidgets, QtGui, QtCore
 from appTool import AppTool
-from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCButton, OptionalInputSection, FCEntry, FCComboBox, \
-    NumericalEvalTupleEntry
+from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCButton, OptionalInputSection, FCComboBox, \
+    NumericalEvalTupleEntry, FCLabel
 
 import numpy as np
 
@@ -23,924 +23,955 @@ if '_' not in builtins.__dict__:
 
 class ToolTransform(AppTool):
 
-    toolName = _("Object Transform")
-    rotateName = _("Rotate")
-    skewName = _("Skew/Shear")
-    scaleName = _("Scale")
-    flipName = _("Mirror (Flip)")
-    offsetName = _("Offset")
-    bufferName = _("Buffer")
-
     def __init__(self, app):
         AppTool.__init__(self, app)
         self.decimals = self.app.decimals
 
-        # ## Title
-        title_label = QtWidgets.QLabel("%s" % self.toolName)
-        title_label.setStyleSheet("""
-                        QLabel
-                        {
-                            font-size: 16px;
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(title_label)
-        self.layout.addWidget(QtWidgets.QLabel(''))
+        # #############################################################################
+        # ######################### Tool GUI ##########################################
+        # #############################################################################
+        self.ui = TransformUI(layout=self.layout, app=self.app)
+        self.toolName = self.ui.toolName
+        
+        # ## Signals
+        self.ui.ref_combo.currentIndexChanged.connect(self.ui.on_reference_changed)
+        self.ui.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
+        self.ui.point_button.clicked.connect(self.on_add_coords)
 
-        # ## Layout
-        grid0 = QtWidgets.QGridLayout()
-        self.layout.addLayout(grid0)
-        grid0.setColumnStretch(0, 0)
-        grid0.setColumnStretch(1, 1)
-        grid0.setColumnStretch(2, 0)
+        self.ui.rotate_button.clicked.connect(self.on_rotate)
 
-        grid0.addWidget(QtWidgets.QLabel(''))
+        self.ui.skewx_button.clicked.connect(self.on_skewx)
+        self.ui.skewy_button.clicked.connect(self.on_skewy)
 
-        # Reference
-        ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
-        ref_label.setToolTip(
-            _("The reference point for Rotate, Skew, Scale, Mirror.\n"
-              "Can be:\n"
-              "- Origin -> it is the 0, 0 point\n"
-              "- Selection -> the center of the bounding box of the selected objects\n"
-              "- Point -> a custom point defined by X,Y coordinates\n"
-              "- Object -> the center of the bounding box of a specific object")
-        )
-        self.ref_combo = FCComboBox()
-        self.ref_items = [_("Origin"), _("Selection"), _("Point"), _("Object")]
-        self.ref_combo.addItems(self.ref_items)
+        self.ui.scalex_button.clicked.connect(self.on_scalex)
+        self.ui.scaley_button.clicked.connect(self.on_scaley)
 
-        grid0.addWidget(ref_label, 0, 0)
-        grid0.addWidget(self.ref_combo, 0, 1, 1, 2)
+        self.ui.offx_button.clicked.connect(self.on_offx)
+        self.ui.offy_button.clicked.connect(self.on_offy)
 
-        self.point_label = QtWidgets.QLabel('%s:' % _("Value"))
-        self.point_label.setToolTip(
-            _("A point of reference in format X,Y.")
-        )
-        self.point_entry = NumericalEvalTupleEntry()
+        self.ui.flipx_button.clicked.connect(self.on_flipx)
+        self.ui.flipy_button.clicked.connect(self.on_flipy)
 
-        grid0.addWidget(self.point_label, 1, 0)
-        grid0.addWidget(self.point_entry, 1, 1, 1, 2)
+        self.ui.buffer_button.clicked.connect(self.on_buffer_by_distance)
+        self.ui.buffer_factor_button.clicked.connect(self.on_buffer_by_factor)
 
-        self.point_button = FCButton(_("Add"))
-        self.point_button.setToolTip(
-            _("Add point coordinates from clipboard.")
-        )
-        grid0.addWidget(self.point_button, 2, 0, 1, 3)
+        self.ui.reset_button.clicked.connect(self.set_tool_ui)
 
-        # Type of object to be used as reference
-        self.type_object_label = QtWidgets.QLabel('%s:' % _("Type"))
-        self.type_object_label.setToolTip(
-            _("The type of object used as reference.")
-        )
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("ToolTransform()")
 
-        self.type_obj_combo = FCComboBox()
-        self.type_obj_combo.addItem(_("Gerber"))
-        self.type_obj_combo.addItem(_("Excellon"))
-        self.type_obj_combo.addItem(_("Geometry"))
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
 
-        self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
-        self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
-        self.type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
+        AppTool.run(self)
+        self.set_tool_ui()
 
-        grid0.addWidget(self.type_object_label, 3, 0)
-        grid0.addWidget(self.type_obj_combo, 3, 1, 1, 2)
+        self.app.ui.notebook.setTabText(2, _("Transform Tool"))
 
-        # Object to be used as reference
-        self.object_combo = FCComboBox()
-        self.object_combo.setModel(self.app.collection)
-        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.object_combo.is_last = True
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='Alt+T', **kwargs)
 
-        self.object_combo.setToolTip(
-            _("The object used as reference.\n"
-              "The used point is the center of it's bounding box.")
-        )
-        grid0.addWidget(self.object_combo, 4, 0, 1, 3)
+    def set_tool_ui(self):
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 5, 0, 1, 3)
+        # ## Initialize form
+        self.ui.ref_combo.set_value(self.app.defaults["tools_transform_reference"])
+        self.ui.type_obj_combo.set_value(self.app.defaults["tools_transform_ref_object"])
+        self.ui.point_entry.set_value(self.app.defaults["tools_transform_ref_point"])
+        self.ui.rotate_entry.set_value(self.app.defaults["tools_transform_rotate"])
 
-        # ## Rotate Title
-        rotate_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.rotateName)
-        grid0.addWidget(rotate_title_label, 6, 0, 1, 3)
+        self.ui.skewx_entry.set_value(self.app.defaults["tools_transform_skew_x"])
+        self.ui.skewy_entry.set_value(self.app.defaults["tools_transform_skew_y"])
+        self.ui.skew_link_cb.set_value(self.app.defaults["tools_transform_skew_link"])
 
-        self.rotate_label = QtWidgets.QLabel('%s:' % _("Angle"))
-        self.rotate_label.setToolTip(
-            _("Angle for Rotation action, in degrees.\n"
-              "Float number between -360 and 359.\n"
-              "Positive numbers for CW motion.\n"
-              "Negative numbers for CCW motion.")
-        )
+        self.ui.scalex_entry.set_value(self.app.defaults["tools_transform_scale_x"])
+        self.ui.scaley_entry.set_value(self.app.defaults["tools_transform_scale_y"])
+        self.ui.scale_link_cb.set_value(self.app.defaults["tools_transform_scale_link"])
 
-        self.rotate_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.rotate_entry.set_precision(self.decimals)
-        self.rotate_entry.setSingleStep(45)
-        self.rotate_entry.setWrapping(True)
-        self.rotate_entry.set_range(-360, 360)
+        self.ui.offx_entry.set_value(self.app.defaults["tools_transform_offset_x"])
+        self.ui.offy_entry.set_value(self.app.defaults["tools_transform_offset_y"])
 
-        # self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.ui.buffer_entry.set_value(self.app.defaults["tools_transform_buffer_dis"])
+        self.ui.buffer_factor_entry.set_value(self.app.defaults["tools_transform_buffer_factor"])
+        self.ui.buffer_rounded_cb.set_value(self.app.defaults["tools_transform_buffer_corner"])
 
-        self.rotate_button = FCButton(_("Rotate"))
-        self.rotate_button.setToolTip(
-            _("Rotate the selected object(s).\n"
-              "The point of reference is the middle of\n"
-              "the bounding box for all selected objects.")
-        )
-        self.rotate_button.setMinimumWidth(90)
+        # initial state is hidden
+        self.ui.point_label.hide()
+        self.ui.point_entry.hide()
+        self.ui.point_button.hide()
 
-        grid0.addWidget(self.rotate_label, 7, 0)
-        grid0.addWidget(self.rotate_entry, 7, 1)
-        grid0.addWidget(self.rotate_button, 7, 2)
+        self.ui.type_object_label.hide()
+        self.ui.type_obj_combo.hide()
+        self.ui.object_combo.hide()
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 8, 0, 1, 3)
+    def on_type_obj_index_changed(self, index):
+        self.ui.object_combo.setRootModelIndex(self.app.collection.index(index, 0, QtCore.QModelIndex()))
+        self.ui.object_combo.setCurrentIndex(0)
+        self.ui.object_combo.obj_type = {
+            _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
+        }[self.ui.type_obj_combo.get_value()]
 
-        # ## Skew Title
-        skew_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.skewName)
-        grid0.addWidget(skew_title_label, 9, 0, 1, 2)
+    def on_calculate_reference(self):
+        ref_val = self.ui.ref_combo.currentIndex()
 
-        self.skew_link_cb = FCCheckBox()
-        self.skew_link_cb.setText(_("Link"))
-        self.skew_link_cb.setToolTip(
-            _("Link the Y entry to X entry and copy its content.")
-        )
+        if ref_val == 0:    # "Origin" reference
+            return 0, 0
+        elif ref_val == 1:  # "Selection" reference
+            sel_list = self.app.collection.get_selected()
+            if sel_list:
+                xmin, ymin, xmax, ymax = self.alt_bounds(obj_list=sel_list)
+                px = (xmax + xmin) * 0.5
+                py = (ymax + ymin) * 0.5
+                return px, py
+            else:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("No object selected."))
+                return "fail"
+        elif ref_val == 2:  # "Point" reference
+            point_val = self.uipoint_entry.get_value()
+            try:
+                px, py = eval('{}'.format(point_val))
+                return px, py
+            except Exception:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Incorrect format for Point value. Needs format X,Y"))
+                return "fail"
+        else:               # "Object" reference
+            obj_name = self.ui.object_combo.get_value()
+            ref_obj = self.app.collection.get_by_name(obj_name)
+            xmin, ymin, xmax, ymax = ref_obj.bounds()
+            px = (xmax + xmin) * 0.5
+            py = (ymax + ymin) * 0.5
+            return px, py
 
-        grid0.addWidget(self.skew_link_cb, 9, 2)
+    def on_add_coords(self):
+        val = self.app.clipboard.text()
+        self.ui.point_entry.set_value(val)
 
-        self.skewx_label = QtWidgets.QLabel('%s:' % _("X angle"))
-        self.skewx_label.setToolTip(
-            _("Angle for Skew action, in degrees.\n"
-              "Float number between -360 and 360.")
-        )
-        self.skewx_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        # self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.skewx_entry.set_precision(self.decimals)
-        self.skewx_entry.set_range(-360, 360)
+    def on_rotate(self):
+        value = float(self.ui.rotate_entry.get_value())
+        if value == 0:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Rotate transformation can not be done for a value of 0."))
+            return
+        point = self.on_calculate_reference()
+        if point == 'fail':
+            return
+        self.app.worker_task.emit({'fcn': self.on_rotate_action, 'params': [value, point]})
 
-        self.skewx_button = FCButton(_("Skew X"))
-        self.skewx_button.setToolTip(
-            _("Skew/shear the selected object(s).\n"
-              "The point of reference is the middle of\n"
-              "the bounding box for all selected objects."))
-        self.skewx_button.setMinimumWidth(90)
+    def on_flipx(self):
+        axis = 'Y'
+        point = self.on_calculate_reference()
+        if point == 'fail':
+            return
+        self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis, point]})
 
-        grid0.addWidget(self.skewx_label, 10, 0)
-        grid0.addWidget(self.skewx_entry, 10, 1)
-        grid0.addWidget(self.skewx_button, 10, 2)
+    def on_flipy(self):
+        axis = 'X'
+        point = self.on_calculate_reference()
+        if point == 'fail':
+            return
+        self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis, point]})
 
-        self.skewy_label = QtWidgets.QLabel('%s:' % _("Y angle"))
-        self.skewy_label.setToolTip(
-            _("Angle for Skew action, in degrees.\n"
-              "Float number between -360 and 360.")
-        )
-        self.skewy_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        # self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.skewy_entry.set_precision(self.decimals)
-        self.skewy_entry.set_range(-360, 360)
+    def on_skewx(self):
+        xvalue = float(self.ui.skewx_entry.get_value())
 
-        self.skewy_button = FCButton(_("Skew Y"))
-        self.skewy_button.setToolTip(
-            _("Skew/shear the selected object(s).\n"
-              "The point of reference is the middle of\n"
-              "the bounding box for all selected objects."))
-        self.skewy_button.setMinimumWidth(90)
+        if xvalue == 0:
+            return
 
-        grid0.addWidget(self.skewy_label, 12, 0)
-        grid0.addWidget(self.skewy_entry, 12, 1)
-        grid0.addWidget(self.skewy_button, 12, 2)
+        if self.ui.skew_link_cb.get_value():
+            yvalue = xvalue
+        else:
+            yvalue = 0
 
-        self.ois_sk = OptionalInputSection(self.skew_link_cb, [self.skewy_label, self.skewy_entry, self.skewy_button],
-                                           logic=False)
+        axis = 'X'
+        point = self.on_calculate_reference()
+        if point == 'fail':
+            return
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 14, 0, 1, 3)
+        self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, xvalue, yvalue, point]})
 
-        # ## Scale Title
-        scale_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.scaleName)
-        grid0.addWidget(scale_title_label, 15, 0, 1, 2)
+    def on_skewy(self):
+        xvalue = 0
+        yvalue = float(self.ui.skewy_entry.get_value())
 
-        self.scale_link_cb = FCCheckBox()
-        self.scale_link_cb.setText(_("Link"))
-        self.scale_link_cb.setToolTip(
-            _("Link the Y entry to X entry and copy its content.")
-        )
+        if yvalue == 0:
+            return
 
-        grid0.addWidget(self.scale_link_cb, 15, 2)
+        axis = 'Y'
+        point = self.on_calculate_reference()
+        if point == 'fail':
+            return
 
-        self.scalex_label = QtWidgets.QLabel('%s:' % _("X factor"))
-        self.scalex_label.setToolTip(
-            _("Factor for scaling on X axis.")
-        )
-        self.scalex_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        # self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.scalex_entry.set_precision(self.decimals)
-        self.scalex_entry.setMinimum(-1e6)
+        self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, xvalue, yvalue, point]})
 
-        self.scalex_button = FCButton(_("Scale X"))
-        self.scalex_button.setToolTip(
-            _("Scale the selected object(s).\n"
-              "The point of reference depends on \n"
-              "the Scale reference checkbox state."))
-        self.scalex_button.setMinimumWidth(90)
+    def on_scalex(self):
+        xvalue = float(self.ui.scalex_entry.get_value())
 
-        grid0.addWidget(self.scalex_label, 17, 0)
-        grid0.addWidget(self.scalex_entry, 17, 1)
-        grid0.addWidget(self.scalex_button, 17, 2)
+        if xvalue == 0 or xvalue == 1:
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("Scale transformation can not be done for a factor of 0 or 1."))
+            return
 
-        self.scaley_label = QtWidgets.QLabel('%s:' % _("Y factor"))
-        self.scaley_label.setToolTip(
-            _("Factor for scaling on Y axis.")
-        )
-        self.scaley_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        # self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.scaley_entry.set_precision(self.decimals)
-        self.scaley_entry.setMinimum(-1e6)
+        if self.ui.scale_link_cb.get_value():
+            yvalue = xvalue
+        else:
+            yvalue = 1
 
-        self.scaley_button = FCButton(_("Scale Y"))
-        self.scaley_button.setToolTip(
-            _("Scale the selected object(s).\n"
-              "The point of reference depends on \n"
-              "the Scale reference checkbox state."))
-        self.scaley_button.setMinimumWidth(90)
+        axis = 'X'
+        point = self.on_calculate_reference()
+        if point == 'fail':
+            return
 
-        grid0.addWidget(self.scaley_label, 19, 0)
-        grid0.addWidget(self.scaley_entry, 19, 1)
-        grid0.addWidget(self.scaley_button, 19, 2)
+        self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
 
-        self.ois_s = OptionalInputSection(self.scale_link_cb,
-                                          [
-                                              self.scaley_label,
-                                              self.scaley_entry,
-                                              self.scaley_button
-                                          ], logic=False)
+    def on_scaley(self):
+        xvalue = 1
+        yvalue = float(self.ui.scaley_entry.get_value())
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 21, 0, 1, 3)
+        if yvalue == 0 or yvalue == 1:
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("Scale transformation can not be done for a factor of 0 or 1."))
+            return
 
-        # ## Flip Title
-        flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
-        grid0.addWidget(flip_title_label, 23, 0, 1, 3)
+        axis = 'Y'
+        point = self.on_calculate_reference()
+        if point == 'fail':
+            return
 
-        self.flipx_button = FCButton(_("Flip on X"))
-        self.flipx_button.setToolTip(
-            _("Flip the selected object(s) over the X axis.")
-        )
+        self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
 
-        self.flipy_button = FCButton(_("Flip on Y"))
-        self.flipy_button.setToolTip(
-            _("Flip the selected object(s) over the X axis.")
-        )
+    def on_offx(self):
+        value = float(self.ui.offx_entry.get_value())
+        if value == 0:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0."))
+            return
+        axis = 'X'
 
-        hlay0 = QtWidgets.QHBoxLayout()
-        grid0.addLayout(hlay0, 25, 0, 1, 3)
+        self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
 
-        hlay0.addWidget(self.flipx_button)
-        hlay0.addWidget(self.flipy_button)
+    def on_offy(self):
+        value = float(self.ui.offy_entry.get_value())
+        if value == 0:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0."))
+            return
+        axis = 'Y'
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 27, 0, 1, 3)
+        self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
 
-        # ## Offset Title
-        offset_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.offsetName)
-        grid0.addWidget(offset_title_label, 29, 0, 1, 3)
+    def on_buffer_by_distance(self):
+        value = self.ui.buffer_entry.get_value()
+        join = 1 if self.ui.buffer_rounded_cb.get_value() else 2
 
-        self.offx_label = QtWidgets.QLabel('%s:' % _("X val"))
-        self.offx_label.setToolTip(
-            _("Distance to offset on X axis. In current units.")
-        )
-        self.offx_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        # self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.offx_entry.set_precision(self.decimals)
-        self.offx_entry.setMinimum(-1e6)
+        self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join]})
 
-        self.offx_button = FCButton(_("Offset X"))
-        self.offx_button.setToolTip(
-            _("Offset the selected object(s).\n"
-              "The point of reference is the middle of\n"
-              "the bounding box for all selected objects.\n"))
-        self.offx_button.setMinimumWidth(90)
+    def on_buffer_by_factor(self):
+        value = 1 + self.ui.buffer_factor_entry.get_value() / 100.0
+        join = 1 if self.ui.buffer_rounded_cb.get_value() else 2
 
-        grid0.addWidget(self.offx_label, 31, 0)
-        grid0.addWidget(self.offx_entry, 31, 1)
-        grid0.addWidget(self.offx_button, 31, 2)
+        # tell the buffer method to use the factor
+        factor = True
 
-        self.offy_label = QtWidgets.QLabel('%s:' % _("Y val"))
-        self.offy_label.setToolTip(
-            _("Distance to offset on Y axis. In current units.")
-        )
-        self.offy_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        # self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.offy_entry.set_precision(self.decimals)
-        self.offy_entry.setMinimum(-1e6)
+        self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join, factor]})
 
-        self.offy_button = FCButton(_("Offset Y"))
-        self.offy_button.setToolTip(
-            _("Offset the selected object(s).\n"
-              "The point of reference is the middle of\n"
-              "the bounding box for all selected objects.\n"))
-        self.offy_button.setMinimumWidth(90)
+    def on_rotate_action(self, num, point):
+        obj_list = self.app.collection.get_selected()
 
-        grid0.addWidget(self.offy_label, 32, 0)
-        grid0.addWidget(self.offy_entry, 32, 1)
-        grid0.addWidget(self.offy_button, 32, 2)
+        if not obj_list:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to rotate!"))
+            return
+        else:
+            with self.app.proc_container.new(_("Appying Rotate")):
+                try:
+                    px, py = point
+                    for sel_obj in obj_list:
+                        if sel_obj.kind == 'cncjob':
+                            self.app.inform.emit(_("CNCJob objects can't be rotated."))
+                        else:
+                            sel_obj.rotate(-num, point=(px, py))
+                            self.app.app_obj.object_changed.emit(sel_obj)
 
-        separator_line = QtWidgets.QFrame()
-        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
-        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 34, 0, 1, 3)
+                        # add information to the object that it was changed and how much
+                        sel_obj.options['rotate'] = num
+                        sel_obj.plot()
+                    self.app.inform.emit('[success] %s...' % _('Rotate done'))
+                except Exception as e:
+                    self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
+                                         (_("Due of"), str(e), _("action was not executed.")))
+                    return
 
-        # ## Buffer Title
-        buffer_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.bufferName)
-        grid0.addWidget(buffer_title_label, 35, 0, 1, 2)
+    def on_flip(self, axis, point):
+        obj_list = self.app.collection.get_selected()
 
-        self.buffer_rounded_cb = FCCheckBox('%s' % _("Rounded"))
-        self.buffer_rounded_cb.setToolTip(
-            _("If checked then the buffer will surround the buffered shape,\n"
-              "every corner will be rounded.\n"
-              "If not checked then the buffer will follow the exact geometry\n"
-              "of the buffered shape.")
-        )
+        if not obj_list:
+            self.app.inform.emit('[WARNING_NOTCL] %s!' % _("No object selected. Please Select an object to flip"))
+            return
+        else:
+            with self.app.proc_container.new(_("Applying Flip")):
+                try:
+                    px, py = point
 
-        grid0.addWidget(self.buffer_rounded_cb, 35, 2)
+                    # execute mirroring
+                    for sel_obj in obj_list:
+                        if sel_obj.kind == 'cncjob':
+                            self.app.inform.emit(_("CNCJob objects can't be mirrored/flipped."))
+                        else:
+                            if axis == 'X':
+                                sel_obj.mirror('X', (px, py))
+                                # add information to the object that it was changed and how much
+                                # the axis is reversed because of the reference
+                                if 'mirror_y' in sel_obj.options:
+                                    sel_obj.options['mirror_y'] = not sel_obj.options['mirror_y']
+                                else:
+                                    sel_obj.options['mirror_y'] = True
+                                self.app.inform.emit('[success] %s...' % _('Flip on the Y axis done'))
+                            elif axis == 'Y':
+                                sel_obj.mirror('Y', (px, py))
+                                # add information to the object that it was changed and how much
+                                # the axis is reversed because of the reference
+                                if 'mirror_x' in sel_obj.options:
+                                    sel_obj.options['mirror_x'] = not sel_obj.options['mirror_x']
+                                else:
+                                    sel_obj.options['mirror_x'] = True
+                                self.app.inform.emit('[success] %s...' % _('Flip on the X axis done'))
+                            self.app.app_obj.object_changed.emit(sel_obj)
+                        sel_obj.plot()
+                except Exception as e:
+                    self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
+                                         (_("Due of"), str(e), _("action was not executed.")))
+                    return
 
-        self.buffer_label = QtWidgets.QLabel('%s:' % _("Distance"))
-        self.buffer_label.setToolTip(
-            _("A positive value will create the effect of dilation,\n"
-              "while a negative value will create the effect of erosion.\n"
-              "Each geometry element of the object will be increased\n"
-              "or decreased with the 'distance'.")
-        )
+    def on_skew(self, axis, xvalue, yvalue, point):
+        obj_list = self.app.collection.get_selected()
 
-        self.buffer_entry = FCDoubleSpinner(callback=self.confirmation_message)
-        self.buffer_entry.set_precision(self.decimals)
-        self.buffer_entry.setSingleStep(0.1)
-        self.buffer_entry.setWrapping(True)
-        self.buffer_entry.set_range(-9999.9999, 9999.9999)
+        if xvalue in [90, 180] or yvalue in [90, 180] or xvalue == yvalue == 0:
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("Skew transformation can not be done for 0, 90 and 180 degrees."))
+            return
 
-        self.buffer_button = FCButton(_("Buffer D"))
-        self.buffer_button.setToolTip(
-            _("Create the buffer effect on each geometry,\n"
-              "element from the selected object, using the distance.")
-        )
-        self.buffer_button.setMinimumWidth(90)
+        if not obj_list:
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("No object selected. Please Select an object to shear/skew!"))
+            return
+        else:
+            with self.app.proc_container.new(_("Applying Skew")):
+                try:
+                    px, py = point
 
-        grid0.addWidget(self.buffer_label, 37, 0)
-        grid0.addWidget(self.buffer_entry, 37, 1)
-        grid0.addWidget(self.buffer_button, 37, 2)
+                    for sel_obj in obj_list:
+                        if sel_obj.kind == 'cncjob':
+                            self.app.inform.emit(_("CNCJob objects can't be skewed."))
+                        else:
+                            sel_obj.skew(xvalue, yvalue, point=(px, py))
+                            # add information to the object that it was changed and how much
+                            sel_obj.options['skew_x'] = xvalue
+                            sel_obj.options['skew_y'] = yvalue
+                            self.app.app_obj.object_changed.emit(sel_obj)
+                        sel_obj.plot()
+                    self.app.inform.emit('[success] %s %s %s...' % (_('Skew on the'),  str(axis), _("axis done")))
+                except Exception as e:
+                    self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
+                                         (_("Due of"), str(e), _("action was not executed.")))
+                    return
 
-        self.buffer_factor_label = QtWidgets.QLabel('%s:' % _("Value"))
-        self.buffer_factor_label.setToolTip(
-            _("A positive value will create the effect of dilation,\n"
-              "while a negative value will create the effect of erosion.\n"
-              "Each geometry element of the object will be increased\n"
-              "or decreased to fit the 'Value'. Value is a percentage\n"
-              "of the initial dimension.")
-        )
+    def on_scale(self, axis, xfactor, yfactor, point=None):
+        obj_list = self.app.collection.get_selected()
 
-        self.buffer_factor_entry = FCDoubleSpinner(callback=self.confirmation_message, suffix='%')
-        self.buffer_factor_entry.set_range(-100.0000, 1000.0000)
-        self.buffer_factor_entry.set_precision(self.decimals)
-        self.buffer_factor_entry.setWrapping(True)
-        self.buffer_factor_entry.setSingleStep(1)
+        if not obj_list:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to scale!"))
+            return
+        else:
+            with self.app.proc_container.new(_("Applying Scale")):
+                try:
+                    px, py = point
 
-        self.buffer_factor_button = FCButton(_("Buffer F"))
-        self.buffer_factor_button.setToolTip(
-            _("Create the buffer effect on each geometry,\n"
-              "element from the selected object, using the factor.")
-        )
-        self.buffer_factor_button.setMinimumWidth(90)
+                    for sel_obj in obj_list:
+                        if sel_obj.kind == 'cncjob':
+                            self.app.inform.emit(_("CNCJob objects can't be scaled."))
+                        else:
+                            sel_obj.scale(xfactor, yfactor, point=(px, py))
+                            # add information to the object that it was changed and how much
+                            sel_obj.options['scale_x'] = xfactor
+                            sel_obj.options['scale_y'] = yfactor
+                            self.app.app_obj.object_changed.emit(sel_obj)
+                        sel_obj.plot()
 
-        grid0.addWidget(self.buffer_factor_label, 38, 0)
-        grid0.addWidget(self.buffer_factor_entry, 38, 1)
-        grid0.addWidget(self.buffer_factor_button, 38, 2)
+                    self.app.inform.emit('[success] %s %s %s...' % (_('Scale on the'), str(axis), _('axis done')))
+                except Exception as e:
+                    self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
+                                         (_("Due of"), str(e), _("action was not executed.")))
+                    return
 
-        grid0.addWidget(QtWidgets.QLabel(''), 42, 0, 1, 3)
+    def on_offset(self, axis, num):
+        obj_list = self.app.collection.get_selected()
 
-        self.layout.addStretch()
+        if not obj_list:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to offset!"))
+            return
+        else:
+            with self.app.proc_container.new(_("Applying Offset")):
+                try:
+                    for sel_obj in obj_list:
+                        if sel_obj.kind == 'cncjob':
+                            self.app.inform.emit(_("CNCJob objects can't be offset."))
+                        else:
+                            if axis == 'X':
+                                sel_obj.offset((num, 0))
+                                # add information to the object that it was changed and how much
+                                sel_obj.options['offset_x'] = num
+                            elif axis == 'Y':
+                                sel_obj.offset((0, num))
+                                # add information to the object that it was changed and how much
+                                sel_obj.options['offset_y'] = num
+                            self.app.app_obj.object_changed.emit(sel_obj)
+                        sel_obj.plot()
 
-        # ## Reset Tool
-        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
-        self.reset_button.setToolTip(
-            _("Will reset the tool parameters.")
-        )
-        self.reset_button.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-        self.layout.addWidget(self.reset_button)
+                    self.app.inform.emit('[success] %s %s %s...' % (_('Offset on the'), str(axis), _('axis done')))
+                except Exception as e:
+                    self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed, due of"), str(e)))
+                    return
 
-        # ## Signals
-        self.ref_combo.currentIndexChanged.connect(self.on_reference_changed)
-        self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
-        self.point_button.clicked.connect(self.on_add_coords)
+    def on_buffer_action(self, value, join, factor=None):
+        obj_list = self.app.collection.get_selected()
 
-        self.rotate_button.clicked.connect(self.on_rotate)
+        if not obj_list:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to buffer!"))
+            return
+        else:
+            with self.app.proc_container.new(_("Applying Buffer")):
+                try:
+                    for sel_obj in obj_list:
+                        if sel_obj.kind == 'cncjob':
+                            self.app.inform.emit(_("CNCJob objects can't be buffered."))
+                        elif sel_obj.kind.lower() == 'gerber':
+                            sel_obj.buffer(value, join, factor)
+                            sel_obj.source_file = self.app.export_gerber(obj_name=sel_obj.options['name'],
+                                                                         filename=None, local_use=sel_obj,
+                                                                         use_thread=False)
+                        elif sel_obj.kind.lower() == 'excellon':
+                            sel_obj.buffer(value, join, factor)
+                            sel_obj.source_file = self.app.export_excellon(obj_name=sel_obj.options['name'],
+                                                                           filename=None, local_use=sel_obj,
+                                                                           use_thread=False)
+                        elif sel_obj.kind.lower() == 'geometry':
+                            sel_obj.buffer(value, join, factor)
 
-        self.skewx_button.clicked.connect(self.on_skewx)
-        self.skewy_button.clicked.connect(self.on_skewy)
+                        self.app.app_obj.object_changed.emit(sel_obj)
+                        sel_obj.plot()
 
-        self.scalex_button.clicked.connect(self.on_scalex)
-        self.scaley_button.clicked.connect(self.on_scaley)
+                    self.app.inform.emit('[success] %s...' % _('Buffer done'))
 
-        self.offx_button.clicked.connect(self.on_offx)
-        self.offy_button.clicked.connect(self.on_offy)
+                except Exception as e:
+                    self.app.log.debug("ToolTransform.on_buffer_action() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed, due of"), str(e)))
+                    return
 
-        self.flipx_button.clicked.connect(self.on_flipx)
-        self.flipy_button.clicked.connect(self.on_flipy)
+    @staticmethod
+    def alt_bounds(obj_list):
+        """
+        Returns coordinates of rectangular bounds
+        of an object with geometry: (xmin, ymin, xmax, ymax).
+        """
 
-        self.buffer_button.clicked.connect(self.on_buffer_by_distance)
-        self.buffer_factor_button.clicked.connect(self.on_buffer_by_factor)
+        def bounds_rec(lst):
+            minx = np.Inf
+            miny = np.Inf
+            maxx = -np.Inf
+            maxy = -np.Inf
 
-        self.reset_button.clicked.connect(self.set_tool_ui)
+            try:
+                for obj in lst:
+                    if obj.kind != 'cncjob':
+                        minx_, miny_, maxx_, maxy_ = bounds_rec(obj)
+                        minx = min(minx, minx_)
+                        miny = min(miny, miny_)
+                        maxx = max(maxx, maxx_)
+                        maxy = max(maxy, maxy_)
+                return minx, miny, maxx, maxy
+            except TypeError:
+                # it's an object, return it's bounds
+                return lst.bounds()
 
-    def run(self, toggle=True):
-        self.app.defaults.report_usage("ToolTransform()")
+        return bounds_rec(obj_list)
 
-        if toggle:
-            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
-            else:
-                try:
-                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        # if tab is populated with the tool but it does not have the focus, focus on it
-                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
-                            # focus on Tool Tab
-                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-                        else:
-                            self.app.ui.splitter.setSizes([0, 1])
-                except AttributeError:
-                    pass
-        else:
-            if self.app.ui.splitter.sizes()[0] == 0:
-                self.app.ui.splitter.setSizes([1, 1])
 
-        AppTool.run(self)
-        self.set_tool_ui()
+class TransformUI:
+    
+    toolName = _("Object Transform")
+    rotateName = _("Rotate")
+    skewName = _("Skew/Shear")
+    scaleName = _("Scale")
+    flipName = _("Mirror (Flip)")
+    offsetName = _("Offset")
+    bufferName = _("Buffer")
 
-        self.app.ui.notebook.setTabText(2, _("Transform Tool"))
+    def __init__(self, layout, app):
+        self.app = app
+        self.decimals = self.app.decimals
+        self.layout = layout
 
-    def install(self, icon=None, separator=None, **kwargs):
-        AppTool.install(self, icon, separator, shortcut='Alt+T', **kwargs)
+        # ## Title
+        title_label = FCLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                QLabel
+                                {
+                                    font-size: 16px;
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(FCLabel(""))
 
-    def set_tool_ui(self):
+        # ## Layout
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        grid0.setColumnStretch(2, 0)
 
-        # ## Initialize form
-        self.ref_combo.set_value(self.app.defaults["tools_transform_reference"])
-        self.type_obj_combo.set_value(self.app.defaults["tools_transform_ref_object"])
-        self.point_entry.set_value(self.app.defaults["tools_transform_ref_point"])
-        self.rotate_entry.set_value(self.app.defaults["tools_transform_rotate"])
+        grid0.addWidget(FCLabel(''))
 
-        self.skewx_entry.set_value(self.app.defaults["tools_transform_skew_x"])
-        self.skewy_entry.set_value(self.app.defaults["tools_transform_skew_y"])
-        self.skew_link_cb.set_value(self.app.defaults["tools_transform_skew_link"])
+        # Reference
+        ref_label = FCLabel('%s:' % _("Reference"))
+        ref_label.setToolTip(
+            _("The reference point for Rotate, Skew, Scale, Mirror.\n"
+              "Can be:\n"
+              "- Origin -> it is the 0, 0 point\n"
+              "- Selection -> the center of the bounding box of the selected objects\n"
+              "- Point -> a custom point defined by X,Y coordinates\n"
+              "- Object -> the center of the bounding box of a specific object")
+        )
+        self.ref_combo = FCComboBox()
+        self.ref_items = [_("Origin"), _("Selection"), _("Point"), _("Object")]
+        self.ref_combo.addItems(self.ref_items)
 
-        self.scalex_entry.set_value(self.app.defaults["tools_transform_scale_x"])
-        self.scaley_entry.set_value(self.app.defaults["tools_transform_scale_y"])
-        self.scale_link_cb.set_value(self.app.defaults["tools_transform_scale_link"])
+        grid0.addWidget(ref_label, 0, 0)
+        grid0.addWidget(self.ref_combo, 0, 1, 1, 2)
 
-        self.offx_entry.set_value(self.app.defaults["tools_transform_offset_x"])
-        self.offy_entry.set_value(self.app.defaults["tools_transform_offset_y"])
+        self.point_label = FCLabel('%s:' % _("Value"))
+        self.point_label.setToolTip(
+            _("A point of reference in format X,Y.")
+        )
+        self.point_entry = NumericalEvalTupleEntry()
 
-        self.buffer_entry.set_value(self.app.defaults["tools_transform_buffer_dis"])
-        self.buffer_factor_entry.set_value(self.app.defaults["tools_transform_buffer_factor"])
-        self.buffer_rounded_cb.set_value(self.app.defaults["tools_transform_buffer_corner"])
+        grid0.addWidget(self.point_label, 1, 0)
+        grid0.addWidget(self.point_entry, 1, 1, 1, 2)
 
-        # initial state is hidden
-        self.point_label.hide()
-        self.point_entry.hide()
-        self.point_button.hide()
+        self.point_button = FCButton(_("Add"))
+        self.point_button.setToolTip(
+            _("Add point coordinates from clipboard.")
+        )
+        grid0.addWidget(self.point_button, 2, 0, 1, 3)
 
-        self.type_object_label.hide()
-        self.type_obj_combo.hide()
-        self.object_combo.hide()
+        # Type of object to be used as reference
+        self.type_object_label = FCLabel('%s:' % _("Type"))
+        self.type_object_label.setToolTip(
+            _("The type of object used as reference.")
+        )
 
-    def on_type_obj_index_changed(self, index):
-        self.object_combo.setRootModelIndex(self.app.collection.index(index, 0, QtCore.QModelIndex()))
-        self.object_combo.setCurrentIndex(0)
-        self.object_combo.obj_type = {
-            _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
-        }[self.type_obj_combo.get_value()]
+        self.type_obj_combo = FCComboBox()
+        self.type_obj_combo.addItem(_("Gerber"))
+        self.type_obj_combo.addItem(_("Excellon"))
+        self.type_obj_combo.addItem(_("Geometry"))
 
-    def on_reference_changed(self, index):
-        if index == 0 or index == 1:  # "Origin" or "Selection" reference
-            self.point_label.hide()
-            self.point_entry.hide()
-            self.point_button.hide()
+        self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
+        self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
+        self.type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
 
-            self.type_object_label.hide()
-            self.type_obj_combo.hide()
-            self.object_combo.hide()
-        elif index == 2:    # "Point" reference
-            self.point_label.show()
-            self.point_entry.show()
-            self.point_button.show()
+        grid0.addWidget(self.type_object_label, 3, 0)
+        grid0.addWidget(self.type_obj_combo, 3, 1, 1, 2)
 
-            self.type_object_label.hide()
-            self.type_obj_combo.hide()
-            self.object_combo.hide()
-        else:   # "Object" reference
-            self.point_label.hide()
-            self.point_entry.hide()
-            self.point_button.hide()
+        # Object to be used as reference
+        self.object_combo = FCComboBox()
+        self.object_combo.setModel(self.app.collection)
+        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.object_combo.is_last = True
 
-            self.type_object_label.show()
-            self.type_obj_combo.show()
-            self.object_combo.show()
+        self.object_combo.setToolTip(
+            _("The object used as reference.\n"
+              "The used point is the center of it's bounding box.")
+        )
+        grid0.addWidget(self.object_combo, 4, 0, 1, 3)
 
-    def on_calculate_reference(self):
-        ref_val = self.ref_combo.currentIndex()
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 5, 0, 1, 3)
 
-        if ref_val == 0:    # "Origin" reference
-            return 0, 0
-        elif ref_val == 1:  # "Selection" reference
-            sel_list = self.app.collection.get_selected()
-            if sel_list:
-                xmin, ymin, xmax, ymax = self.alt_bounds(obj_list=sel_list)
-                px = (xmax + xmin) * 0.5
-                py = (ymax + ymin) * 0.5
-                return px, py
-            else:
-                self.app.inform.emit('[ERROR_NOTCL] %s' % _("No object selected."))
-                return "fail"
-        elif ref_val == 2:  # "Point" reference
-            point_val = self.point_entry.get_value()
-            try:
-                px, py = eval('{}'.format(point_val))
-                return px, py
-            except Exception:
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Incorrect format for Point value. Needs format X,Y"))
-                return "fail"
-        else:               # "Object" reference
-            obj_name = self.object_combo.get_value()
-            ref_obj = self.app.collection.get_by_name(obj_name)
-            xmin, ymin, xmax, ymax = ref_obj.bounds()
-            px = (xmax + xmin) * 0.5
-            py = (ymax + ymin) * 0.5
-            return px, py
+        # ## Rotate Title
+        rotate_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.rotateName)
+        grid0.addWidget(rotate_title_label, 6, 0, 1, 3)
 
-    def on_add_coords(self):
-        val = self.app.clipboard.text()
-        self.point_entry.set_value(val)
+        self.rotate_label = FCLabel('%s:' % _("Angle"))
+        self.rotate_label.setToolTip(
+            _("Angle for Rotation action, in degrees.\n"
+              "Float number between -360 and 359.\n"
+              "Positive numbers for CW motion.\n"
+              "Negative numbers for CCW motion.")
+        )
 
-    def on_rotate(self):
-        value = float(self.rotate_entry.get_value())
-        if value == 0:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Rotate transformation can not be done for a value of 0."))
-            return
-        point = self.on_calculate_reference()
-        if point == 'fail':
-            return
-        self.app.worker_task.emit({'fcn': self.on_rotate_action, 'params': [value, point]})
+        self.rotate_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.rotate_entry.set_precision(self.decimals)
+        self.rotate_entry.setSingleStep(45)
+        self.rotate_entry.setWrapping(True)
+        self.rotate_entry.set_range(-360, 360)
 
-    def on_flipx(self):
-        axis = 'Y'
-        point = self.on_calculate_reference()
-        if point == 'fail':
-            return
-        self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis, point]})
+        # self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
 
-    def on_flipy(self):
-        axis = 'X'
-        point = self.on_calculate_reference()
-        if point == 'fail':
-            return
-        self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis, point]})
+        self.rotate_button = FCButton(_("Rotate"))
+        self.rotate_button.setToolTip(
+            _("Rotate the selected object(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected objects.")
+        )
+        self.rotate_button.setMinimumWidth(90)
 
-    def on_skewx(self):
-        xvalue = float(self.skewx_entry.get_value())
+        grid0.addWidget(self.rotate_label, 7, 0)
+        grid0.addWidget(self.rotate_entry, 7, 1)
+        grid0.addWidget(self.rotate_button, 7, 2)
 
-        if xvalue == 0:
-            return
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 8, 0, 1, 3)
 
-        if self.skew_link_cb.get_value():
-            yvalue = xvalue
-        else:
-            yvalue = 0
+        # ## Skew Title
+        skew_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.skewName)
+        grid0.addWidget(skew_title_label, 9, 0, 1, 2)
 
-        axis = 'X'
-        point = self.on_calculate_reference()
-        if point == 'fail':
-            return
+        self.skew_link_cb = FCCheckBox()
+        self.skew_link_cb.setText(_("Link"))
+        self.skew_link_cb.setToolTip(
+            _("Link the Y entry to X entry and copy its content.")
+        )
 
-        self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, xvalue, yvalue, point]})
+        grid0.addWidget(self.skew_link_cb, 9, 2)
 
-    def on_skewy(self):
-        xvalue = 0
-        yvalue = float(self.skewy_entry.get_value())
+        self.skewx_label = FCLabel('%s:' % _("X angle"))
+        self.skewx_label.setToolTip(
+            _("Angle for Skew action, in degrees.\n"
+              "Float number between -360 and 360.")
+        )
+        self.skewx_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.skewx_entry.set_precision(self.decimals)
+        self.skewx_entry.set_range(-360, 360)
 
-        if yvalue == 0:
-            return
+        self.skewx_button = FCButton(_("Skew X"))
+        self.skewx_button.setToolTip(
+            _("Skew/shear the selected object(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected objects."))
+        self.skewx_button.setMinimumWidth(90)
 
-        axis = 'Y'
-        point = self.on_calculate_reference()
-        if point == 'fail':
-            return
+        grid0.addWidget(self.skewx_label, 10, 0)
+        grid0.addWidget(self.skewx_entry, 10, 1)
+        grid0.addWidget(self.skewx_button, 10, 2)
 
-        self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, xvalue, yvalue, point]})
+        self.skewy_label = FCLabel('%s:' % _("Y angle"))
+        self.skewy_label.setToolTip(
+            _("Angle for Skew action, in degrees.\n"
+              "Float number between -360 and 360.")
+        )
+        self.skewy_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.skewy_entry.set_precision(self.decimals)
+        self.skewy_entry.set_range(-360, 360)
 
-    def on_scalex(self):
-        xvalue = float(self.scalex_entry.get_value())
+        self.skewy_button = FCButton(_("Skew Y"))
+        self.skewy_button.setToolTip(
+            _("Skew/shear the selected object(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected objects."))
+        self.skewy_button.setMinimumWidth(90)
 
-        if xvalue == 0 or xvalue == 1:
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("Scale transformation can not be done for a factor of 0 or 1."))
-            return
+        grid0.addWidget(self.skewy_label, 12, 0)
+        grid0.addWidget(self.skewy_entry, 12, 1)
+        grid0.addWidget(self.skewy_button, 12, 2)
 
-        if self.scale_link_cb.get_value():
-            yvalue = xvalue
-        else:
-            yvalue = 1
+        self.ois_sk = OptionalInputSection(self.skew_link_cb, [self.skewy_label, self.skewy_entry, self.skewy_button],
+                                           logic=False)
 
-        axis = 'X'
-        point = self.on_calculate_reference()
-        if point == 'fail':
-            return
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 14, 0, 1, 3)
 
-        self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
+        # ## Scale Title
+        scale_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.scaleName)
+        grid0.addWidget(scale_title_label, 15, 0, 1, 2)
 
-    def on_scaley(self):
-        xvalue = 1
-        yvalue = float(self.scaley_entry.get_value())
+        self.scale_link_cb = FCCheckBox()
+        self.scale_link_cb.setText(_("Link"))
+        self.scale_link_cb.setToolTip(
+            _("Link the Y entry to X entry and copy its content.")
+        )
 
-        if yvalue == 0 or yvalue == 1:
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("Scale transformation can not be done for a factor of 0 or 1."))
-            return
+        grid0.addWidget(self.scale_link_cb, 15, 2)
 
-        axis = 'Y'
-        point = self.on_calculate_reference()
-        if point == 'fail':
-            return
+        self.scalex_label = FCLabel('%s:' % _("X factor"))
+        self.scalex_label.setToolTip(
+            _("Factor for scaling on X axis.")
+        )
+        self.scalex_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.scalex_entry.set_precision(self.decimals)
+        self.scalex_entry.setMinimum(-1e6)
 
-        self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
+        self.scalex_button = FCButton(_("Scale X"))
+        self.scalex_button.setToolTip(
+            _("Scale the selected object(s).\n"
+              "The point of reference depends on \n"
+              "the Scale reference checkbox state."))
+        self.scalex_button.setMinimumWidth(90)
 
-    def on_offx(self):
-        value = float(self.offx_entry.get_value())
-        if value == 0:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0."))
-            return
-        axis = 'X'
+        grid0.addWidget(self.scalex_label, 17, 0)
+        grid0.addWidget(self.scalex_entry, 17, 1)
+        grid0.addWidget(self.scalex_button, 17, 2)
 
-        self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
+        self.scaley_label = FCLabel('%s:' % _("Y factor"))
+        self.scaley_label.setToolTip(
+            _("Factor for scaling on Y axis.")
+        )
+        self.scaley_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.scaley_entry.set_precision(self.decimals)
+        self.scaley_entry.setMinimum(-1e6)
 
-    def on_offy(self):
-        value = float(self.offy_entry.get_value())
-        if value == 0:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0."))
-            return
-        axis = 'Y'
+        self.scaley_button = FCButton(_("Scale Y"))
+        self.scaley_button.setToolTip(
+            _("Scale the selected object(s).\n"
+              "The point of reference depends on \n"
+              "the Scale reference checkbox state."))
+        self.scaley_button.setMinimumWidth(90)
 
-        self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
+        grid0.addWidget(self.scaley_label, 19, 0)
+        grid0.addWidget(self.scaley_entry, 19, 1)
+        grid0.addWidget(self.scaley_button, 19, 2)
 
-    def on_buffer_by_distance(self):
-        value = self.buffer_entry.get_value()
-        join = 1 if self.buffer_rounded_cb.get_value() else 2
+        self.ois_s = OptionalInputSection(self.scale_link_cb,
+                                          [
+                                              self.scaley_label,
+                                              self.scaley_entry,
+                                              self.scaley_button
+                                          ], logic=False)
 
-        self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join]})
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 21, 0, 1, 3)
 
-    def on_buffer_by_factor(self):
-        value = 1 + self.buffer_factor_entry.get_value() / 100.0
-        join = 1 if self.buffer_rounded_cb.get_value() else 2
+        # ## Flip Title
+        flip_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.flipName)
+        grid0.addWidget(flip_title_label, 23, 0, 1, 3)
 
-        # tell the buffer method to use the factor
-        factor = True
+        self.flipx_button = FCButton(_("Flip on X"))
+        self.flipx_button.setToolTip(
+            _("Flip the selected object(s) over the X axis.")
+        )
 
-        self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join, factor]})
+        self.flipy_button = FCButton(_("Flip on Y"))
+        self.flipy_button.setToolTip(
+            _("Flip the selected object(s) over the X axis.")
+        )
 
-    def on_rotate_action(self, num, point):
-        obj_list = self.app.collection.get_selected()
+        hlay0 = QtWidgets.QHBoxLayout()
+        grid0.addLayout(hlay0, 25, 0, 1, 3)
 
-        if not obj_list:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to rotate!"))
-            return
-        else:
-            with self.app.proc_container.new(_("Appying Rotate")):
-                try:
-                    px, py = point
-                    for sel_obj in obj_list:
-                        if sel_obj.kind == 'cncjob':
-                            self.app.inform.emit(_("CNCJob objects can't be rotated."))
-                        else:
-                            sel_obj.rotate(-num, point=(px, py))
-                            self.app.app_obj.object_changed.emit(sel_obj)
+        hlay0.addWidget(self.flipx_button)
+        hlay0.addWidget(self.flipy_button)
 
-                        # add information to the object that it was changed and how much
-                        sel_obj.options['rotate'] = num
-                        sel_obj.plot()
-                    self.app.inform.emit('[success] %s...' % _('Rotate done'))
-                except Exception as e:
-                    self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
-                                         (_("Due of"), str(e), _("action was not executed.")))
-                    return
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 27, 0, 1, 3)
 
-    def on_flip(self, axis, point):
-        obj_list = self.app.collection.get_selected()
+        # ## Offset Title
+        offset_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.offsetName)
+        grid0.addWidget(offset_title_label, 29, 0, 1, 3)
 
-        if not obj_list:
-            self.app.inform.emit('[WARNING_NOTCL] %s!' % _("No object selected. Please Select an object to flip"))
-            return
-        else:
-            with self.app.proc_container.new(_("Applying Flip")):
-                try:
-                    px, py = point
+        self.offx_label = FCLabel('%s:' % _("X val"))
+        self.offx_label.setToolTip(
+            _("Distance to offset on X axis. In current units.")
+        )
+        self.offx_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.offx_entry.set_precision(self.decimals)
+        self.offx_entry.setMinimum(-1e6)
 
-                    # execute mirroring
-                    for sel_obj in obj_list:
-                        if sel_obj.kind == 'cncjob':
-                            self.app.inform.emit(_("CNCJob objects can't be mirrored/flipped."))
-                        else:
-                            if axis == 'X':
-                                sel_obj.mirror('X', (px, py))
-                                # add information to the object that it was changed and how much
-                                # the axis is reversed because of the reference
-                                if 'mirror_y' in sel_obj.options:
-                                    sel_obj.options['mirror_y'] = not sel_obj.options['mirror_y']
-                                else:
-                                    sel_obj.options['mirror_y'] = True
-                                self.app.inform.emit('[success] %s...' % _('Flip on the Y axis done'))
-                            elif axis == 'Y':
-                                sel_obj.mirror('Y', (px, py))
-                                # add information to the object that it was changed and how much
-                                # the axis is reversed because of the reference
-                                if 'mirror_x' in sel_obj.options:
-                                    sel_obj.options['mirror_x'] = not sel_obj.options['mirror_x']
-                                else:
-                                    sel_obj.options['mirror_x'] = True
-                                self.app.inform.emit('[success] %s...' % _('Flip on the X axis done'))
-                            self.app.app_obj.object_changed.emit(sel_obj)
-                        sel_obj.plot()
-                except Exception as e:
-                    self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
-                                         (_("Due of"), str(e), _("action was not executed.")))
-                    return
+        self.offx_button = FCButton(_("Offset X"))
+        self.offx_button.setToolTip(
+            _("Offset the selected object(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected objects.\n"))
+        self.offx_button.setMinimumWidth(90)
 
-    def on_skew(self, axis, xvalue, yvalue, point):
-        obj_list = self.app.collection.get_selected()
+        grid0.addWidget(self.offx_label, 31, 0)
+        grid0.addWidget(self.offx_entry, 31, 1)
+        grid0.addWidget(self.offx_button, 31, 2)
 
-        if xvalue in [90, 180] or yvalue in [90, 180] or xvalue == yvalue == 0:
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("Skew transformation can not be done for 0, 90 and 180 degrees."))
-            return
+        self.offy_label = FCLabel('%s:' % _("Y val"))
+        self.offy_label.setToolTip(
+            _("Distance to offset on Y axis. In current units.")
+        )
+        self.offy_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.offy_entry.set_precision(self.decimals)
+        self.offy_entry.setMinimum(-1e6)
 
-        if not obj_list:
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("No object selected. Please Select an object to shear/skew!"))
-            return
-        else:
-            with self.app.proc_container.new(_("Applying Skew")):
-                try:
-                    px, py = point
+        self.offy_button = FCButton(_("Offset Y"))
+        self.offy_button.setToolTip(
+            _("Offset the selected object(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected objects.\n"))
+        self.offy_button.setMinimumWidth(90)
 
-                    for sel_obj in obj_list:
-                        if sel_obj.kind == 'cncjob':
-                            self.app.inform.emit(_("CNCJob objects can't be skewed."))
-                        else:
-                            sel_obj.skew(xvalue, yvalue, point=(px, py))
-                            # add information to the object that it was changed and how much
-                            sel_obj.options['skew_x'] = xvalue
-                            sel_obj.options['skew_y'] = yvalue
-                            self.app.app_obj.object_changed.emit(sel_obj)
-                        sel_obj.plot()
-                    self.app.inform.emit('[success] %s %s %s...' % (_('Skew on the'),  str(axis), _("axis done")))
-                except Exception as e:
-                    self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
-                                         (_("Due of"), str(e), _("action was not executed.")))
-                    return
+        grid0.addWidget(self.offy_label, 32, 0)
+        grid0.addWidget(self.offy_entry, 32, 1)
+        grid0.addWidget(self.offy_button, 32, 2)
 
-    def on_scale(self, axis, xfactor, yfactor, point=None):
-        obj_list = self.app.collection.get_selected()
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 34, 0, 1, 3)
 
-        if not obj_list:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to scale!"))
-            return
-        else:
-            with self.app.proc_container.new(_("Applying Scale")):
-                try:
-                    px, py = point
+        # ## Buffer Title
+        buffer_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.bufferName)
+        grid0.addWidget(buffer_title_label, 35, 0, 1, 2)
 
-                    for sel_obj in obj_list:
-                        if sel_obj.kind == 'cncjob':
-                            self.app.inform.emit(_("CNCJob objects can't be scaled."))
-                        else:
-                            sel_obj.scale(xfactor, yfactor, point=(px, py))
-                            # add information to the object that it was changed and how much
-                            sel_obj.options['scale_x'] = xfactor
-                            sel_obj.options['scale_y'] = yfactor
-                            self.app.app_obj.object_changed.emit(sel_obj)
-                        sel_obj.plot()
+        self.buffer_rounded_cb = FCCheckBox('%s' % _("Rounded"))
+        self.buffer_rounded_cb.setToolTip(
+            _("If checked then the buffer will surround the buffered shape,\n"
+              "every corner will be rounded.\n"
+              "If not checked then the buffer will follow the exact geometry\n"
+              "of the buffered shape.")
+        )
 
-                    self.app.inform.emit('[success] %s %s %s...' % (_('Scale on the'), str(axis), _('axis done')))
-                except Exception as e:
-                    self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
-                                         (_("Due of"), str(e), _("action was not executed.")))
-                    return
+        grid0.addWidget(self.buffer_rounded_cb, 35, 2)
 
-    def on_offset(self, axis, num):
-        obj_list = self.app.collection.get_selected()
+        self.buffer_label = FCLabel('%s:' % _("Distance"))
+        self.buffer_label.setToolTip(
+            _("A positive value will create the effect of dilation,\n"
+              "while a negative value will create the effect of erosion.\n"
+              "Each geometry element of the object will be increased\n"
+              "or decreased with the 'distance'.")
+        )
 
-        if not obj_list:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to offset!"))
-            return
-        else:
-            with self.app.proc_container.new(_("Applying Offset")):
-                try:
-                    for sel_obj in obj_list:
-                        if sel_obj.kind == 'cncjob':
-                            self.app.inform.emit(_("CNCJob objects can't be offset."))
-                        else:
-                            if axis == 'X':
-                                sel_obj.offset((num, 0))
-                                # add information to the object that it was changed and how much
-                                sel_obj.options['offset_x'] = num
-                            elif axis == 'Y':
-                                sel_obj.offset((0, num))
-                                # add information to the object that it was changed and how much
-                                sel_obj.options['offset_y'] = num
-                            self.app.app_obj.object_changed.emit(sel_obj)
-                        sel_obj.plot()
+        self.buffer_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.buffer_entry.set_precision(self.decimals)
+        self.buffer_entry.setSingleStep(0.1)
+        self.buffer_entry.setWrapping(True)
+        self.buffer_entry.set_range(-9999.9999, 9999.9999)
 
-                    self.app.inform.emit('[success] %s %s %s...' % (_('Offset on the'), str(axis), _('axis done')))
-                except Exception as e:
-                    self.app.inform.emit('[ERROR_NOTCL] %s: %s.' %
-                                     (_("Action was not executed, due of"), str(e)))
-                    return
+        self.buffer_button = FCButton(_("Buffer D"))
+        self.buffer_button.setToolTip(
+            _("Create the buffer effect on each geometry,\n"
+              "element from the selected object, using the distance.")
+        )
+        self.buffer_button.setMinimumWidth(90)
 
-    def on_buffer_action(self, value, join, factor=None):
-        obj_list = self.app.collection.get_selected()
+        grid0.addWidget(self.buffer_label, 37, 0)
+        grid0.addWidget(self.buffer_entry, 37, 1)
+        grid0.addWidget(self.buffer_button, 37, 2)
 
-        if not obj_list:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to buffer!"))
-            return
-        else:
-            with self.app.proc_container.new(_("Applying Buffer")):
-                try:
-                    for sel_obj in obj_list:
-                        if sel_obj.kind == 'cncjob':
-                            self.app.inform.emit(_("CNCJob objects can't be buffered."))
-                        elif sel_obj.kind.lower() == 'gerber':
-                            sel_obj.buffer(value, join, factor)
-                            sel_obj.source_file = self.app.export_gerber(obj_name=sel_obj.options['name'],
-                                                                         filename=None, local_use=sel_obj,
-                                                                         use_thread=False)
-                        elif sel_obj.kind.lower() == 'excellon':
-                            sel_obj.buffer(value, join, factor)
-                            sel_obj.source_file = self.app.export_excellon(obj_name=sel_obj.options['name'],
-                                                                           filename=None, local_use=sel_obj,
-                                                                           use_thread=False)
-                        elif sel_obj.kind.lower() == 'geometry':
-                            sel_obj.buffer(value, join, factor)
+        self.buffer_factor_label = FCLabel('%s:' % _("Value"))
+        self.buffer_factor_label.setToolTip(
+            _("A positive value will create the effect of dilation,\n"
+              "while a negative value will create the effect of erosion.\n"
+              "Each geometry element of the object will be increased\n"
+              "or decreased to fit the 'Value'. Value is a percentage\n"
+              "of the initial dimension.")
+        )
 
-                        self.app.app_obj.object_changed.emit(sel_obj)
-                        sel_obj.plot()
+        self.buffer_factor_entry = FCDoubleSpinner(callback=self.confirmation_message, suffix='%')
+        self.buffer_factor_entry.set_range(-100.0000, 1000.0000)
+        self.buffer_factor_entry.set_precision(self.decimals)
+        self.buffer_factor_entry.setWrapping(True)
+        self.buffer_factor_entry.setSingleStep(1)
 
-                    self.app.inform.emit('[success] %s...' % _('Buffer done'))
+        self.buffer_factor_button = FCButton(_("Buffer F"))
+        self.buffer_factor_button.setToolTip(
+            _("Create the buffer effect on each geometry,\n"
+              "element from the selected object, using the factor.")
+        )
+        self.buffer_factor_button.setMinimumWidth(90)
 
-                except Exception as e:
-                    self.app.log.debug("ToolTransform.on_buffer_action() --> %s" % str(e))
-                    self.app.inform.emit('[ERROR_NOTCL] %s: %s.' %
-                                     (_("Action was not executed, due of"), str(e)))
-                    return
+        grid0.addWidget(self.buffer_factor_label, 38, 0)
+        grid0.addWidget(self.buffer_factor_entry, 38, 1)
+        grid0.addWidget(self.buffer_factor_button, 38, 2)
 
-    @staticmethod
-    def alt_bounds(obj_list):
-        """
-        Returns coordinates of rectangular bounds
-        of an object with geometry: (xmin, ymin, xmax, ymax).
-        """
+        grid0.addWidget(FCLabel(''), 42, 0, 1, 3)
 
-        def bounds_rec(lst):
-            minx = np.Inf
-            miny = np.Inf
-            maxx = -np.Inf
-            maxy = -np.Inf
+        self.layout.addStretch()
 
-            try:
-                for obj in lst:
-                    if obj.kind != 'cncjob':
-                        minx_, miny_, maxx_, maxy_ = bounds_rec(obj)
-                        minx = min(minx, minx_)
-                        miny = min(miny, miny_)
-                        maxx = max(maxx, maxx_)
-                        maxy = max(maxy, maxy_)
-                return minx, miny, maxx, maxy
-            except TypeError:
-                # it's an object, return it's bounds
-                return lst.bounds()
+        # ## Reset Tool
+        self.reset_button = FCButton(_("Reset Tool"))
+        self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.reset_button)
 
-        return bounds_rec(obj_list)
+        # #################################### FINSIHED GUI ###########################
+        # #############################################################################
+    
+    def on_reference_changed(self, index):
+        if index == 0 or index == 1:  # "Origin" or "Selection" reference
+            self.point_label.hide()
+            self.point_entry.hide()
+            self.point_button.hide()
+
+            self.type_object_label.hide()
+            self.type_obj_combo.hide()
+            self.object_combo.hide()
+        elif index == 2:    # "Point" reference
+            self.point_label.show()
+            self.point_entry.show()
+            self.point_button.show()
 
-# end of file
+            self.type_object_label.hide()
+            self.type_obj_combo.hide()
+            self.object_combo.hide()
+        else:   # "Object" reference
+            self.point_label.hide()
+            self.point_entry.hide()
+            self.point_button.hide()
+
+            self.type_object_label.show()
+            self.type_obj_combo.show()
+            self.object_combo.show()
+            
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 2 - 0
appTools/__init__.py

@@ -19,6 +19,8 @@ from appTools.ToolCutOut import CutOut
 from appTools.ToolNCC import NonCopperClear
 from appTools.ToolPaint import ToolPaint
 from appTools.ToolIsolation import ToolIsolation
+from appTools.ToolDrilling import ToolDrilling
+from appTools.ToolMilling import ToolMilling
 
 from appTools.ToolOptimal import ToolOptimal
 

+ 2 - 1
appTranslation.py

@@ -31,9 +31,10 @@ languages_dict = {
     'fr': 'French',
     'hu': 'Hungarian',
     'it': 'Italian',
+    'pt_BR': 'Brazilian Portuguese',
     'ro': 'Romanian',
     'ru': 'Russian',
-    'pt_BR': 'Brazilian Portuguese',
+    'tr': 'Turkish',
 }
 
 translations = {}

Разница между файлами не показана из-за своего большого размера
+ 447 - 283
app_Main.py


BIN
assets/Shapely-1.8.dev0-py2.py3-none-any.whl


BIN
assets/resources/apply32.png


BIN
assets/resources/apply_red32.png


BIN
assets/resources/dark_resources/apply32.png


BIN
assets/resources/dark_resources/apply_red32.png


BIN
assets/resources/dark_resources/down-arrow32.png


BIN
assets/resources/dark_resources/drilling_tool32.png


BIN
assets/resources/dark_resources/find32.png


BIN
assets/resources/dark_resources/gaps32.png


BIN
assets/resources/dark_resources/geometry16.png


BIN
assets/resources/dark_resources/geometry32.png


BIN
assets/resources/dark_resources/irregular32.png


BIN
assets/resources/dark_resources/left_arrow32.png


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