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

Merged in options_cleanup (pull request #3)

Options cleanup
Marius Stanciu 6 лет назад
Родитель
Сommit
fdad91f04e

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


+ 1224 - 2
FlatCAMCommon.py

@@ -1,10 +1,33 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
 # Author: Juan Pablo Caram (c)                             #
 # Date: 2/5/2014                                           #
 # Date: 2/5/2014                                           #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
+
+# ##########################################################
+# File Modified (major mod): Marius Adrian Stanciu         #
+# Date: 11/4/2019                                          #
+# ##########################################################
+
+from PyQt5 import QtGui, QtCore, QtWidgets
+from flatcamGUI.GUIElements import FCTable, FCEntry, FCButton, FCDoubleSpinner, FCComboBox, FCCheckBox
+from camlib import to_dict
+
+import sys
+import webbrowser
+import json
+
+from copy import deepcopy
+from datetime import datetime
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
 
 
 
 
 class LoudDict(dict):
 class LoudDict(dict):
@@ -69,3 +92,1202 @@ class FCSignal:
         except ValueError:
         except ValueError:
             print('Warning: function %s not removed '
             print('Warning: function %s not removed '
                   'from signal %s' % (func, self))
                   'from signal %s' % (func, self))
+
+
+class BookmarkManager(QtWidgets.QWidget):
+
+    mark_rows = QtCore.pyqtSignal()
+
+    def __init__(self, app, storage, parent=None):
+        super(BookmarkManager, self).__init__(parent)
+
+        self.app = app
+
+        assert isinstance(storage, dict), "Storage argument is not a dictionary"
+
+        self.bm_dict = deepcopy(storage)
+
+        # Icon and title
+        # self.setWindowIcon(parent.app_icon)
+        # self.setWindowTitle(_("Bookmark Manager"))
+        # self.resize(600, 400)
+
+        # title = QtWidgets.QLabel(
+        #     "<font size=8><B>FlatCAM</B></font><BR>"
+        # )
+        # title.setOpenExternalLinks(True)
+
+        # layouts
+        layout = QtWidgets.QVBoxLayout()
+        self.setLayout(layout)
+
+        table_hlay = QtWidgets.QHBoxLayout()
+        layout.addLayout(table_hlay)
+
+        self.table_widget = FCTable(drag_drop=True, protected_rows=[0, 1])
+        self.table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        table_hlay.addWidget(self.table_widget)
+
+        self.table_widget.setColumnCount(3)
+        self.table_widget.setColumnWidth(0, 20)
+        self.table_widget.setHorizontalHeaderLabels(
+            [
+                '#',
+                _('Title'),
+                _('Web Link')
+            ]
+        )
+        self.table_widget.horizontalHeaderItem(0).setToolTip(
+            _("Index.\n"
+              "The rows in gray color will populate the Bookmarks menu.\n"
+              "The number of gray colored rows is set in Preferences."))
+        self.table_widget.horizontalHeaderItem(1).setToolTip(
+            _("Description of the link that is set as an menu action.\n"
+              "Try to keep it short because it is installed as a menu item."))
+        self.table_widget.horizontalHeaderItem(2).setToolTip(
+            _("Web Link. E.g: https://your_website.org "))
+
+        # pal = QtGui.QPalette()
+        # pal.setColor(QtGui.QPalette.Background, Qt.white)
+
+        # New Bookmark
+        new_vlay = QtWidgets.QVBoxLayout()
+        layout.addLayout(new_vlay)
+
+        new_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("New Bookmark"))
+        new_vlay.addWidget(new_title_lbl)
+
+        form0 = QtWidgets.QFormLayout()
+        new_vlay.addLayout(form0)
+
+        title_lbl = QtWidgets.QLabel('%s:' % _("Title"))
+        self.title_entry = FCEntry()
+        form0.addRow(title_lbl, self.title_entry)
+
+        link_lbl = QtWidgets.QLabel('%s:' % _("Web Link"))
+        self.link_entry = FCEntry()
+        self.link_entry.set_value('http://')
+        form0.addRow(link_lbl, self.link_entry)
+
+        # Buttons Layout
+        button_hlay = QtWidgets.QHBoxLayout()
+        layout.addLayout(button_hlay)
+
+        add_entry_btn = FCButton(_("Add Entry"))
+        remove_entry_btn = FCButton(_("Remove Entry"))
+        export_list_btn = FCButton(_("Export List"))
+        import_list_btn = FCButton(_("Import List"))
+        # closebtn = QtWidgets.QPushButton(_("Close"))
+
+        # button_hlay.addStretch()
+        button_hlay.addWidget(add_entry_btn)
+        button_hlay.addWidget(remove_entry_btn)
+
+        button_hlay.addWidget(export_list_btn)
+        button_hlay.addWidget(import_list_btn)
+        # button_hlay.addWidget(closebtn)
+        # ##############################################################################
+        # ######################## SIGNALS #############################################
+        # ##############################################################################
+
+        add_entry_btn.clicked.connect(self.on_add_entry)
+        remove_entry_btn.clicked.connect(self.on_remove_entry)
+        export_list_btn.clicked.connect(self.on_export_bookmarks)
+        import_list_btn.clicked.connect(self.on_import_bookmarks)
+        self.title_entry.returnPressed.connect(self.on_add_entry)
+        self.link_entry.returnPressed.connect(self.on_add_entry)
+        # closebtn.clicked.connect(self.accept)
+
+        self.table_widget.drag_drop_sig.connect(self.mark_table_rows_for_actions)
+        self.build_bm_ui()
+
+    def build_bm_ui(self):
+
+        self.table_widget.setRowCount(len(self.bm_dict))
+
+        nr_crt = 0
+        sorted_bookmarks = sorted(list(self.bm_dict.items()), key=lambda x: int(x[0]))
+        for entry, bookmark in sorted_bookmarks:
+            row = nr_crt
+            nr_crt += 1
+
+            title = bookmark[0]
+            weblink = bookmark[1]
+
+            id_item = QtWidgets.QTableWidgetItem('%d' % int(nr_crt))
+            # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.table_widget.setItem(row, 0, id_item)  # Tool name/id
+
+            title_item = QtWidgets.QTableWidgetItem(title)
+            self.table_widget.setItem(row, 1, title_item)
+
+            weblink_txt = QtWidgets.QTextBrowser()
+            weblink_txt.setOpenExternalLinks(True)
+            weblink_txt.setFrameStyle(QtWidgets.QFrame.NoFrame)
+            weblink_txt.document().setDefaultStyleSheet("a{ text-decoration: none; }")
+
+            weblink_txt.setHtml('<a href=%s>%s</a>' % (weblink, weblink))
+
+            self.table_widget.setCellWidget(row, 2, weblink_txt)
+
+            vertical_header = self.table_widget.verticalHeader()
+            vertical_header.hide()
+
+            horizontal_header = self.table_widget.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.ResizeToContents)
+            horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
+
+        self.mark_table_rows_for_actions()
+
+        self.app.defaults["global_bookmarks"].clear()
+        for key, val in self.bm_dict.items():
+            self.app.defaults["global_bookmarks"][key] = deepcopy(val)
+
+    def on_add_entry(self, **kwargs):
+        """
+        Add a entry in the Bookmark Table and in the menu actions
+        :return: None
+        """
+        if 'title' in kwargs:
+            title = kwargs['title']
+        else:
+            title = self.title_entry.get_value()
+        if title == '':
+            self.app.inform.emit(f'[ERROR_NOTCL] {_("Title entry is empty.")}')
+            return 'fail'
+
+        if 'link' is kwargs:
+            link = kwargs['link']
+        else:
+            link = self.link_entry.get_value()
+
+        if link == 'http://':
+            self.app.inform.emit(f'[ERROR_NOTCL] {_("Web link entry is empty.")}')
+            return 'fail'
+
+        # if 'http' not in link or 'https' not in link:
+        #     link = 'http://' + link
+
+        for bookmark in self.bm_dict.values():
+            if title == bookmark[0] or link == bookmark[1]:
+                self.app.inform.emit(f'[ERROR_NOTCL] {_("Either the Title or the Weblink already in the table.")}')
+                return 'fail'
+
+        # for some reason if the last char in the weblink is a slash it does not make the link clickable
+        # so I remove it
+        if link[-1] == '/':
+            link = link[:-1]
+        # add the new entry to storage
+        new_entry = len(self.bm_dict) + 1
+        self.bm_dict[str(new_entry)] = [title, link]
+
+        # add the link to the menu but only if it is within the set limit
+        bm_limit = int(self.app.defaults["global_bookmarks_limit"])
+        if len(self.bm_dict) < bm_limit:
+            act = QtWidgets.QAction(parent=self.app.ui.menuhelp_bookmarks)
+            act.setText(title)
+            act.setIcon(QtGui.QIcon('share/link16.png'))
+            act.triggered.connect(lambda: webbrowser.open(link))
+            self.app.ui.menuhelp_bookmarks.insertAction(self.app.ui.menuhelp_bookmarks_manager, act)
+
+        self.app.inform.emit(f'[success] {_("Bookmark added.")}')
+
+        # add the new entry to the bookmark manager table
+        self.build_bm_ui()
+
+    def on_remove_entry(self):
+        """
+        Remove an Entry in the Bookmark table and from the menu actions
+        :return:
+        """
+        index_list = []
+        for model_index in self.table_widget.selectionModel().selectedRows():
+            index = QtCore.QPersistentModelIndex(model_index)
+            index_list.append(index)
+            title_to_remove = self.table_widget.item(model_index.row(), 1).text()
+
+            if title_to_remove == 'FlatCAM' or title_to_remove == 'Backup Site':
+                self.app.inform.emit('[WARNING_NOTCL] %s.' % _("This bookmark can not be removed"))
+                self.build_bm_ui()
+                return
+            else:
+                for k, bookmark in list(self.bm_dict.items()):
+                    if title_to_remove == bookmark[0]:
+                        # remove from the storage
+                        self.bm_dict.pop(k, None)
+
+                        for act in self.app.ui.menuhelp_bookmarks.actions():
+                            if act.text() == title_to_remove:
+                                # disconnect the signal
+                                try:
+                                    act.triggered.disconnect()
+                                except TypeError:
+                                    pass
+                                # remove the action from the menu
+                                self.app.ui.menuhelp_bookmarks.removeAction(act)
+
+        # house keeping: it pays to have keys increased by one
+        new_key = 0
+        new_dict = dict()
+        for k, v in self.bm_dict.items():
+            # we start with key 1 so we can use the len(self.bm_dict)
+            # when adding bookmarks (keys in bm_dict)
+            new_key += 1
+            new_dict[str(new_key)] = v
+
+        self.bm_dict = deepcopy(new_dict)
+        new_dict.clear()
+
+        self.app.inform.emit(f'[success] {_("Bookmark removed.")}')
+
+        # for index in index_list:
+        #     self.table_widget.model().removeRow(index.row())
+        self.build_bm_ui()
+
+    def on_export_bookmarks(self):
+        self.app.report_usage("on_export_bookmarks")
+        self.app.log.debug("on_export_bookmarks()")
+
+        date = str(datetime.today()).rpartition('.')[0]
+        date = ''.join(c for c in date if c not in ':-')
+        date = date.replace(' ', '_')
+
+        filter__ = "Text File (*.TXT);;All Files (*.*)"
+        filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export FlatCAM Bookmarks"),
+                                                             directory='{l_save}/FlatCAM_{n}_{date}'.format(
+                                                                 l_save=str(self.app.get_last_save_folder()),
+                                                                 n=_("Bookmarks"),
+                                                                 date=date),
+                                                             filter=filter__)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("FlatCAM bookmarks export cancelled."))
+            return
+        else:
+            try:
+                f = open(filename, 'w')
+                f.close()
+            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
+            except IOError:
+                self.app.log.debug('Creating a new bookmarks file ...')
+                f = open(filename, 'w')
+                f.close()
+            except:
+                e = sys.exc_info()[0]
+                self.app.log.error("Could not load defaults file.")
+                self.app.log.error(str(e))
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load bookmarks file."))
+                return
+
+            # Save Bookmarks to a file
+            try:
+                with open(filename, "w") as f:
+                    for title, link in self.bm_dict.items():
+                        line2write = str(title) + ':' + str(link) + '\n'
+                        f.write(line2write)
+            except:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to write bookmarks to file."))
+                return
+        self.app.inform.emit('[success] %s: %s' % (_("Exported bookmarks to"), filename))
+
+    def on_import_bookmarks(self):
+        self.app.log.debug("on_import_bookmarks()")
+
+        filter_ = "Text File (*.txt);;All Files (*.*)"
+        filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Bookmarks"), filter=filter_)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("FlatCAM bookmarks import cancelled."))
+        else:
+            try:
+                with open(filename) as f:
+                    bookmarks = f.readlines()
+            except IOError:
+                self.app.log.error("Could not load bookmarks file.")
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load bookmarks file."))
+                return
+
+            for line in bookmarks:
+                proc_line = line.replace(' ', '').partition(':')
+                self.on_add_entry(title=proc_line[0], link=proc_line[2])
+
+            self.app.inform.emit('[success] %s: %s' % (_("Imported Bookmarks from"), filename))
+
+    def mark_table_rows_for_actions(self):
+        for row in range(self.table_widget.rowCount()):
+            item_to_paint = self.table_widget.item(row, 0)
+            if row < self.app.defaults["global_bookmarks_limit"]:
+                item_to_paint.setBackground(QtGui.QColor('gray'))
+                # item_to_paint.setForeground(QtGui.QColor('black'))
+            else:
+                item_to_paint.setBackground(QtGui.QColor('white'))
+                # item_to_paint.setForeground(QtGui.QColor('black'))
+
+    def rebuild_actions(self):
+        # rebuild the storage to reflect the order of the lines
+        self.bm_dict.clear()
+        for row in range(self.table_widget.rowCount()):
+            title = self.table_widget.item(row, 1).text()
+            wlink = self.table_widget.cellWidget(row, 2).toPlainText()
+
+            entry = int(row) + 1
+            self.bm_dict.update(
+                {
+                    str(entry): [title, wlink]
+                }
+            )
+
+        self.app.install_bookmarks(book_dict=self.bm_dict)
+
+    # def accept(self):
+    #     self.rebuild_actions()
+    #     super().accept()
+
+    def closeEvent(self, QCloseEvent):
+        self.rebuild_actions()
+        super().closeEvent(QCloseEvent)
+
+
+class ToolsDB(QtWidgets.QWidget):
+
+    mark_tools_rows = QtCore.pyqtSignal()
+
+    def __init__(self, app, callback_on_edited, callback_on_tool_request, parent=None):
+        super(ToolsDB, self).__init__(parent)
+
+        self.app = app
+        self.decimals = 4
+        self.callback_app = callback_on_edited
+
+        self.on_tool_request = callback_on_tool_request
+
+        self.offset_item_options = ["Path", "In", "Out", "Custom"]
+        self.type_item_options = [_("Iso"), _("Rough"), _("Finish")]
+        self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
+
+        '''
+        dict to hold all the tools in the Tools DB
+        format:
+        {
+            tool_id: {
+                'name': 'new_tool'
+                'tooldia': self.app.defaults["geometry_cnctooldia"]
+                'offset': 'Path'
+                'offset_value': 0.0
+                'type':  _('Rough'),
+                'tool_type': 'C1'
+                'data': dict()
+            }
+        }
+        '''
+        self.db_tool_dict = dict()
+
+        # layouts
+        layout = QtWidgets.QVBoxLayout()
+        self.setLayout(layout)
+
+        table_hlay = QtWidgets.QHBoxLayout()
+        layout.addLayout(table_hlay)
+
+        self.table_widget = FCTable(drag_drop=True)
+        self.table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        table_hlay.addWidget(self.table_widget)
+
+        self.table_widget.setColumnCount(26)
+        # self.table_widget.setColumnWidth(0, 20)
+        self.table_widget.setHorizontalHeaderLabels(
+            [
+                '#',
+                _("Tool Name"),
+                _("Tool Dia"),
+                _("Tool Offset"),
+                _("Custom Offset"),
+                _("Tool Type"),
+                _("Tool Shape"),
+                _("Cut Z"),
+                _("MultiDepth"),
+                _("DPP"),
+                _("V-Dia"),
+                _("V-Angle"),
+                _("Travel Z"),
+                _("FR"),
+                _("FR Z"),
+                _("FR Rapids"),
+                _("Spindle Speed"),
+                _("Dwell"),
+                _("Dwelltime"),
+                _("Postprocessor"),
+                _("ExtraCut"),
+                _("Toolchange"),
+                _("Toolchange XY"),
+                _("Toolchange Z"),
+                _("Start Z"),
+                _("End Z"),
+            ]
+        )
+        self.table_widget.horizontalHeaderItem(0).setToolTip(
+            _("Tool Index."))
+        self.table_widget.horizontalHeaderItem(1).setToolTip(
+            _("Tool name.\n"
+              "This is not used in the app, it's function\n"
+              "is to serve as a note for the user."))
+        self.table_widget.horizontalHeaderItem(2).setToolTip(
+            _("Tool Diameter."))
+        self.table_widget.horizontalHeaderItem(3).setToolTip(
+            _("Tool Offset.\n"
+              "Can be of a few types:\n"
+              "Path = zero offset\n"
+              "In = offset inside by half of tool diameter\n"
+              "Out = offset outside by half of tool diameter\n"
+              "Custom = custom offset using the Custom Offset value"))
+        self.table_widget.horizontalHeaderItem(4).setToolTip(
+            _("Custom Offset.\n"
+              "A value to be used as offset from the current path."))
+        self.table_widget.horizontalHeaderItem(5).setToolTip(
+            _("Tool Type.\n"
+              "Can be:\n"
+              "Iso = isolation cut\n"
+              "Rough = rough cut, low feedrate, multiple passes\n"
+              "Finish = finishing cut, high feedrate"))
+        self.table_widget.horizontalHeaderItem(6).setToolTip(
+            _("Tool Shape. \n"
+              "Can be:\n"
+              "C1 ... C4 = circular tool with x flutes\n"
+              "B = ball tip milling tool\n"
+              "V = v-shape milling tool"))
+        self.table_widget.horizontalHeaderItem(7).setToolTip(
+            _("Cutting Depth.\n"
+              "The depth at which to cut into material."))
+        self.table_widget.horizontalHeaderItem(8).setToolTip(
+            _("Multi Depth.\n"
+              "Selecting this will allow cutting in multiple passes,\n"
+              "each pass adding a DPP parameter depth."))
+        self.table_widget.horizontalHeaderItem(9).setToolTip(
+            _("DPP. Depth per Pass.\n"
+              "The value used to cut into material on each pass."))
+        self.table_widget.horizontalHeaderItem(10).setToolTip(
+            _("V-Dia.\n"
+              "Diameter of the tip for V-Shape Tools."))
+        self.table_widget.horizontalHeaderItem(11).setToolTip(
+            _("V-Agle.\n"
+              "Angle at the tip for the V-Shape Tools."))
+        self.table_widget.horizontalHeaderItem(12).setToolTip(
+            _("Clearance Height.\n"
+              "Height at which the milling bit will travel between cuts,\n"
+              "above the surface of the material, avoiding all fixtures."))
+        self.table_widget.horizontalHeaderItem(13).setToolTip(
+            _("FR. Feedrate\n"
+              "The speed on XY plane used while cutting into material."))
+        self.table_widget.horizontalHeaderItem(14).setToolTip(
+            _("FR Z. Feedrate Z\n"
+              "The speed on Z plane."))
+        self.table_widget.horizontalHeaderItem(15).setToolTip(
+            _("FR Rapids. Feedrate Rapids\n"
+              "Speed used while moving as fast as possible.\n"
+              "This is used only by some devices that can't use\n"
+              "the G0 g-code command. Mostly 3D printers."))
+        self.table_widget.horizontalHeaderItem(16).setToolTip(
+            _("Spindle Speed.\n"
+              "If it's left empty it will not be used.\n"
+              "The speed of the spindle in RPM."))
+        self.table_widget.horizontalHeaderItem(17).setToolTip(
+            _("Dwell.\n"
+              "Check this if a delay is needed to allow\n"
+              "the spindle motor to reach it's set speed."))
+        self.table_widget.horizontalHeaderItem(18).setToolTip(
+            _("Dwell Time.\n"
+              "A delay used to allow the motor spindle reach it's set speed."))
+        self.table_widget.horizontalHeaderItem(19).setToolTip(
+            _("Postprocessor.\n"
+              "A selection of files that will alter the generated G-code\n"
+              "to fit for a number of use cases."))
+        self.table_widget.horizontalHeaderItem(20).setToolTip(
+            _("Extra Cut.\n"
+              "If checked, after a isolation is finished an extra cut\n"
+              "will be added where the start and end of isolation meet\n"
+              "such as that this point is covered by this extra cut to\n"
+              "ensure a complete isolation."))
+        self.table_widget.horizontalHeaderItem(21).setToolTip(
+            _("Toolchange.\n"
+              "It will create a toolchange event.\n"
+              "The kind of toolchange is determined by\n"
+              "the postprocessor file."))
+        self.table_widget.horizontalHeaderItem(22).setToolTip(
+            _("Toolchange XY.\n"
+              "A set of coordinates in the format (x, y).\n"
+              "Will determine the cartesian position of the point\n"
+              "where the tool change event take place."))
+        self.table_widget.horizontalHeaderItem(23).setToolTip(
+            _("Toolchange Z.\n"
+              "The position on Z plane where the tool change event take place."))
+        self.table_widget.horizontalHeaderItem(24).setToolTip(
+            _("Start Z.\n"
+              "If it's left empty it will not be used.\n"
+              "A position on Z plane to move immediately after job start."))
+        self.table_widget.horizontalHeaderItem(25).setToolTip(
+            _("End Z.\n"
+              "A position on Z plane to move immediately after job stop."))
+
+        # pal = QtGui.QPalette()
+        # pal.setColor(QtGui.QPalette.Background, Qt.white)
+
+        # New Bookmark
+        new_vlay = QtWidgets.QVBoxLayout()
+        layout.addLayout(new_vlay)
+
+        new_tool_lbl = QtWidgets.QLabel('<b>%s</b>' % _("New Tool"))
+        new_vlay.addWidget(new_tool_lbl, alignment=QtCore.Qt.AlignBottom)
+
+        self.buttons_frame = QtWidgets.QFrame()
+        self.buttons_frame.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self.buttons_frame)
+        self.buttons_box = QtWidgets.QHBoxLayout()
+        self.buttons_box.setContentsMargins(0, 0, 0, 0)
+        self.buttons_frame.setLayout(self.buttons_box)
+        self.buttons_frame.show()
+
+        add_entry_btn = FCButton(_("Add Tool to Tools DB"))
+        add_entry_btn.setToolTip(
+            _("Add a new tool in the Tools Database.\n"
+              "You can edit it after it is added.")
+        )
+        remove_entry_btn = FCButton(_("Remove Tool from Tools DB"))
+        remove_entry_btn.setToolTip(
+            _("Remove a selection of tools in the Tools Database.")
+        )
+        export_db_btn = FCButton(_("Export Tool DB"))
+        export_db_btn.setToolTip(
+            _("Save the Tools Database to a custom text file.")
+        )
+        import_db_btn = FCButton(_("Import Tool DB"))
+        import_db_btn.setToolTip(
+            _("Load the Tools Database information's from a custom text file.")
+        )
+        # button_hlay.addStretch()
+        self.buttons_box.addWidget(add_entry_btn)
+        self.buttons_box.addWidget(remove_entry_btn)
+
+        self.buttons_box.addWidget(export_db_btn)
+        self.buttons_box.addWidget(import_db_btn)
+        # self.buttons_box.addWidget(closebtn)
+
+        self.add_tool_from_db = FCButton(_("Add Tool from Tools DB"))
+        add_entry_btn.setToolTip(
+            _("Add a new tool in the Tools Table of the\n"
+              "active Geometry object after selecting a tool\n"
+              "in the Tools Database.")
+        )
+        self.add_tool_from_db.hide()
+
+        hlay = QtWidgets.QHBoxLayout()
+        layout.addLayout(hlay)
+        hlay.addWidget(self.add_tool_from_db)
+        hlay.addStretch()
+
+        # ##############################################################################
+        # ######################## SIGNALS #############################################
+        # ##############################################################################
+
+        add_entry_btn.clicked.connect(self.on_add_entry)
+        remove_entry_btn.clicked.connect(self.on_remove_entry)
+        export_db_btn.clicked.connect(self.on_export_tools_db_file)
+        import_db_btn.clicked.connect(self.on_import_tools_db_file)
+        # closebtn.clicked.connect(self.accept)
+
+        self.add_tool_from_db.clicked.connect(self.on_tool_requested_from_app)
+
+        self.setup_db_ui()
+
+    def setup_db_ui(self):
+        filename = self.app.data_path + '/tools_db.FlatConfig'
+
+        # 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.db_tool_dict = json.loads(tools)
+        except:
+            e = sys.exc_info()[0]
+            self.app.log.error(str(e))
+            self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
+            return
+
+        self.app.inform.emit('[success] %s: %s' % (_("Loaded FlatCAM Tools DB from"), filename))
+
+        self.build_db_ui()
+
+    def build_db_ui(self):
+        self.ui_disconnect()
+        self.table_widget.setRowCount(len(self.db_tool_dict))
+
+        nr_crt = 0
+        for toolid, dict_val in self.db_tool_dict.items():
+            row = nr_crt
+            nr_crt += 1
+
+            t_name = dict_val['name']
+            self.add_tool_table_line(row, name=t_name, widget=self.table_widget, tooldict=dict_val)
+
+            vertical_header = self.table_widget.verticalHeader()
+            vertical_header.hide()
+
+            horizontal_header = self.table_widget.horizontalHeader()
+            horizontal_header.setMinimumSectionSize(10)
+            horizontal_header.setDefaultSectionSize(70)
+
+            self.table_widget.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+            for x in range(27):
+                self.table_widget.resizeColumnToContents(x)
+
+            horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+            # horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+            # horizontal_header.setSectionResizeMode(13, QtWidgets.QHeaderView.Fixed)
+
+            horizontal_header.resizeSection(0, 20)
+            # horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
+            # horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
+
+        self.ui_connect()
+
+    def add_tool_table_line(self, row, name, widget, tooldict):
+        data = tooldict['data']
+
+        nr_crt = row + 1
+        id_item = QtWidgets.QTableWidgetItem('%d' % int(nr_crt))
+        id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        widget.setItem(row, 0, id_item)  # Tool name/id
+
+        tool_name_item = QtWidgets.QTableWidgetItem(name)
+        widget.setItem(row, 1, tool_name_item)
+
+        dia_item = FCDoubleSpinner()
+        dia_item.set_precision(self.decimals)
+        dia_item.setSingleStep(0.1)
+        dia_item.set_range(0.0, 9999.9999)
+        dia_item.set_value(float(tooldict['tooldia']))
+        widget.setCellWidget(row, 2, dia_item)
+
+        tool_offset_item = FCComboBox()
+        for item in self.offset_item_options:
+            tool_offset_item.addItem(item)
+        tool_offset_item.set_value(tooldict['offset'])
+        widget.setCellWidget(row, 3, tool_offset_item)
+
+        c_offset_item = FCDoubleSpinner()
+        c_offset_item.set_precision(self.decimals)
+        c_offset_item.setSingleStep(0.1)
+        c_offset_item.set_range(-9999.9999, 9999.9999)
+        c_offset_item.set_value(float(tooldict['offset_value']))
+        widget.setCellWidget(row, 4, c_offset_item)
+
+        tt_item = FCComboBox()
+        for item in self.type_item_options:
+            tt_item.addItem(item)
+        tt_item.set_value(tooldict['type'])
+        widget.setCellWidget(row, 5, tt_item)
+
+        tshape_item = FCComboBox()
+        for item in self.tool_type_item_options:
+            tshape_item.addItem(item)
+        tshape_item.set_value(tooldict['tool_type'])
+        widget.setCellWidget(row, 6, tshape_item)
+
+        cutz_item = FCDoubleSpinner()
+        cutz_item.set_precision(self.decimals)
+        cutz_item.setSingleStep(0.1)
+        if self.app.defaults['global_machinist_setting']:
+            cutz_item.set_range(-9999.9999, 9999.9999)
+        else:
+            cutz_item.set_range(-9999.9999, -0.0000)
+
+        cutz_item.set_value(float(data['cutz']))
+        widget.setCellWidget(row, 7, cutz_item)
+
+        multidepth_item = FCCheckBox()
+        multidepth_item.set_value(data['multidepth'])
+        widget.setCellWidget(row, 8, multidepth_item)
+
+        depth_per_pass_item = FCDoubleSpinner()
+        depth_per_pass_item.set_precision(self.decimals)
+        depth_per_pass_item.setSingleStep(0.1)
+        depth_per_pass_item.set_range(0.0, 9999.9999)
+        depth_per_pass_item.set_value(float(data['depthperpass']))
+        widget.setCellWidget(row, 9, depth_per_pass_item)
+
+        vtip_dia_item = FCDoubleSpinner()
+        vtip_dia_item.set_precision(self.decimals)
+        vtip_dia_item.setSingleStep(0.1)
+        vtip_dia_item.set_range(0.0, 9999.9999)
+        vtip_dia_item.set_value(float(data['vtipdia']))
+        widget.setCellWidget(row, 10, vtip_dia_item)
+
+        vtip_angle_item = FCDoubleSpinner()
+        vtip_angle_item.set_precision(self.decimals)
+        vtip_angle_item.setSingleStep(0.1)
+        vtip_angle_item.set_range(-360.0, 360.0)
+        vtip_angle_item.set_value(float(data['vtipangle']))
+        widget.setCellWidget(row, 11, vtip_angle_item)
+
+        travelz_item = FCDoubleSpinner()
+        travelz_item.set_precision(self.decimals)
+        travelz_item.setSingleStep(0.1)
+        if self.app.defaults['global_machinist_setting']:
+            travelz_item.set_range(-9999.9999, 9999.9999)
+        else:
+            travelz_item.set_range(0.0000, 9999.9999)
+
+        travelz_item.set_value(float(data['travelz']))
+        widget.setCellWidget(row, 12, travelz_item)
+
+        fr_item = FCDoubleSpinner()
+        fr_item.set_precision(self.decimals)
+        fr_item.set_range(0.0, 9999.9999)
+        fr_item.set_value(float(data['feedrate']))
+        widget.setCellWidget(row, 13, fr_item)
+
+        frz_item = FCDoubleSpinner()
+        frz_item.set_precision(self.decimals)
+        frz_item.set_range(0.0, 9999.9999)
+        frz_item.set_value(float(data['feedrate_z']))
+        widget.setCellWidget(row, 14, frz_item)
+
+        frrapids_item = FCDoubleSpinner()
+        frrapids_item.set_precision(self.decimals)
+        frrapids_item.set_range(0.0, 9999.9999)
+        frrapids_item.set_value(float(data['feedrate_rapid']))
+        widget.setCellWidget(row, 15, frrapids_item)
+
+        spindlespeed_item = QtWidgets.QTableWidgetItem(str(data['spindlespeed']) if data['spindlespeed'] else '')
+        widget.setItem(row, 16, spindlespeed_item)
+
+        dwell_item = FCCheckBox()
+        dwell_item.set_value(data['dwell'])
+        widget.setCellWidget(row, 17, dwell_item)
+
+        dwelltime_item = FCDoubleSpinner()
+        dwelltime_item.set_precision(self.decimals)
+        dwelltime_item.set_range(0.0, 9999.9999)
+        dwelltime_item.set_value(float(data['dwelltime']))
+        widget.setCellWidget(row, 18, dwelltime_item)
+
+        pp_item = FCComboBox()
+        for item in self.app.postprocessors:
+            pp_item.addItem(item)
+        pp_item.set_value(data['ppname_g'])
+        widget.setCellWidget(row, 19, pp_item)
+
+        ecut_item = FCCheckBox()
+        ecut_item.set_value(data['extracut'])
+        widget.setCellWidget(row, 20, ecut_item)
+
+        toolchange_item = FCCheckBox()
+        toolchange_item.set_value(data['toolchange'])
+        widget.setCellWidget(row, 21, toolchange_item)
+
+        toolchangexy_item = QtWidgets.QTableWidgetItem(str(data['toolchangexy']) if data['toolchangexy'] else '')
+        widget.setItem(row, 22, toolchangexy_item)
+
+        toolchangez_item = FCDoubleSpinner()
+        toolchangez_item.set_precision(self.decimals)
+        toolchangez_item.setSingleStep(0.1)
+        if self.app.defaults['global_machinist_setting']:
+            toolchangez_item.set_range(-9999.9999, 9999.9999)
+        else:
+            toolchangez_item.set_range(0.0000, 9999.9999)
+
+        toolchangez_item.set_value(float(data['toolchangez']))
+        widget.setCellWidget(row, 23, toolchangez_item)
+
+        startz_item = QtWidgets.QTableWidgetItem(str(data['startz']) if data['startz'] else '')
+        widget.setItem(row, 24, startz_item)
+
+        endz_item = FCDoubleSpinner()
+        endz_item.set_precision(self.decimals)
+        endz_item.setSingleStep(0.1)
+        if self.app.defaults['global_machinist_setting']:
+            endz_item.set_range(-9999.9999, 9999.9999)
+        else:
+            endz_item.set_range(0.0000, 9999.9999)
+
+        endz_item.set_value(float(data['endz']))
+        widget.setCellWidget(row, 25, endz_item)
+
+    def on_add_entry(self):
+        """
+        Add a tool in the DB Tool Table
+        :return: None
+        """
+        new_toolid = len(self.db_tool_dict) + 1
+
+        dict_elem = dict()
+        default_data = dict()
+
+        default_data.update({
+            "cutz": float(self.app.defaults["geometry_cutz"]),
+            "multidepth": self.app.defaults["geometry_multidepth"],
+            "depthperpass": float(self.app.defaults["geometry_depthperpass"]),
+            "vtipdia": float(self.app.defaults["geometry_vtipdia"]),
+            "vtipangle": float(self.app.defaults["geometry_vtipangle"]),
+            "travelz": float(self.app.defaults["geometry_travelz"]),
+            "feedrate": float(self.app.defaults["geometry_feedrate"]),
+            "feedrate_z": float(self.app.defaults["geometry_feedrate_z"]),
+            "feedrate_rapid": float(self.app.defaults["geometry_feedrate_rapid"]),
+            "spindlespeed": self.app.defaults["geometry_spindlespeed"],
+            "dwell": self.app.defaults["geometry_dwell"],
+            "dwelltime": float(self.app.defaults["geometry_dwelltime"]),
+            "ppname_g": self.app.defaults["geometry_ppname_g"],
+            "extracut": self.app.defaults["geometry_extracut"],
+            "toolchange": self.app.defaults["geometry_toolchange"],
+            "toolchangexy": self.app.defaults["geometry_toolchangexy"],
+            "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
+            "startz": self.app.defaults["geometry_startz"],
+            "endz": float(self.app.defaults["geometry_endz"])
+        })
+
+        dict_elem['name'] = 'new_tool'
+        dict_elem['tooldia'] = self.app.defaults["geometry_cnctooldia"]
+        dict_elem['offset'] = 'Path'
+        dict_elem['offset_value'] = 0.0
+        dict_elem['type'] = _('Rough')
+        dict_elem['tool_type'] = 'C1'
+
+        dict_elem['data'] = default_data
+
+        self.db_tool_dict.update(
+            {
+                new_toolid: deepcopy(dict_elem)
+            }
+        )
+
+        self.app.inform.emit(f'[success] {_("Tool added to DB.")}')
+
+        # add the new entry to the Tools DB table
+        self.build_db_ui()
+        self.callback_on_edited()
+
+    def on_remove_entry(self):
+        """
+        Remove a Tool in the Tools DB table
+        :return:
+        """
+        index_list = []
+        for model_index in self.table_widget.selectionModel().selectedRows():
+            index = QtCore.QPersistentModelIndex(model_index)
+            index_list.append(index)
+            toolname_to_remove = self.table_widget.item(model_index.row(), 0).text()
+
+            for toolid, dict_val in list(self.db_tool_dict.items()):
+                if int(toolname_to_remove) == int(toolid):
+                    # remove from the storage
+                    self.db_tool_dict.pop(toolid, None)
+
+        self.app.inform.emit(f'[success] {_("Tool removed from Tools DB.")}')
+
+        self.build_db_ui()
+        self.callback_on_edited()
+
+    def on_export_tools_db_file(self):
+        self.app.report_usage("on_export_tools_db_file")
+        self.app.log.debug("on_export_tools_db_file()")
+
+        date = str(datetime.today()).rpartition('.')[0]
+        date = ''.join(c for c in date if c not in ':-')
+        date = date.replace(' ', '_')
+
+        filter__ = "Text File (*.TXT);;All Files (*.*)"
+        filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export Tools Database"),
+                                                             directory='{l_save}/FlatCAM_{n}_{date}'.format(
+                                                                 l_save=str(self.app.get_last_save_folder()),
+                                                                 n=_("Tools_Database"),
+                                                                 date=date),
+                                                             filter=filter__)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("FlatCAM Tools DB export cancelled."))
+            return
+        else:
+            try:
+                f = open(filename, 'w')
+                f.close()
+            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
+            except IOError:
+                self.app.log.debug('Creating a new Tools DB file ...')
+                f = open(filename, 'w')
+                f.close()
+            except:
+                e = sys.exc_info()[0]
+                self.app.log.error("Could not load Tools DB file.")
+                self.app.log.error(str(e))
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load Tools DB file."))
+                return
+
+            # Save update options
+            try:
+                # Save Tools DB in a file
+                try:
+                    with open(filename, "w") as f:
+                        json.dump(self.db_tool_dict, f, default=to_dict, indent=2)
+                except Exception as e:
+                    self.app.log.debug("App.on_save_tools_db() --> %s" % str(e))
+                    self.inform.emit(f'[ERROR_NOTCL] {_("Failed to write Tools DB to file.")}')
+                    return
+            except:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to write Tools DB to file."))
+                return
+
+        self.app.inform.emit('[success] %s: %s' % (_("Exported Tools DB to"), filename))
+
+    def on_import_tools_db_file(self):
+        self.app.report_usage("on_import_tools_db_file")
+        self.app.log.debug("on_import_tools_db_file()")
+
+        filter__ = "Text File (*.TXT);;All Files (*.*)"
+        filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Tools DB"), filter=filter__)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("FlatCAM Tools DB import cancelled."))
+        else:
+            try:
+                with open(filename) as f:
+                    tools_in_db = f.read()
+            except IOError:
+                self.app.log.error("Could not load Tools DB file.")
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load Tools DB file."))
+                return
+
+            try:
+                self.db_tool_dict = json.loads(tools_in_db)
+            except:
+                e = sys.exc_info()[0]
+                self.app.log.error(str(e))
+                self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
+                return
+
+            self.app.inform.emit('[success] %s: %s' % (_("Loaded FlatCAM Tools DB from"), filename))
+            self.build_db_ui()
+            self.callback_on_edited()
+
+    def on_save_tools_db(self, silent=False):
+        self.app.log.debug("ToolsDB.on_save_button() --> Saving Tools Database to file.")
+
+        filename = self.app.data_path + "/tools_db.FlatConfig"
+
+        # Preferences save, update the color of the Tools DB Tab text
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
+                self.app.ui.plot_tab_area.tabBar.setTabTextColor(idx, QtGui.QColor('black'))
+
+                # Save Tools DB in a file
+                try:
+                    f = open(filename, "w")
+                    json.dump(self.db_tool_dict, f, default=to_dict, indent=2)
+                    f.close()
+                except Exception as e:
+                    self.app.log.debug("ToolsDB.on_save_tools_db() --> %s" % str(e))
+                    self.app.inform.emit(f'[ERROR_NOTCL] {_("Failed to write Tools DB to file.")}')
+                    return
+
+                if not silent:
+                    self.app.inform.emit('[success] %s: %s' % (_("Exported Tools DB to"), filename))
+
+    def ui_connect(self):
+        try:
+            try:
+                self.table_widget.itemChanged.disconnect(self.callback_on_edited)
+            except (TypeError, AttributeError):
+                pass
+            self.table_widget.itemChanged.connect(self.callback_on_edited)
+        except AttributeError:
+            pass
+
+        for row in range(self.table_widget.rowCount()):
+            for col in range(self.table_widget.columnCount()):
+                # ComboBox
+                try:
+                    try:
+                        self.table_widget.cellWidget(row, col).currentIndexChanged.disconnect(self.callback_on_edited)
+                    except (TypeError, AttributeError):
+                        pass
+                    self.table_widget.cellWidget(row, col).currentIndexChanged.connect(self.callback_on_edited)
+                except AttributeError:
+                    pass
+
+                # CheckBox
+                try:
+                    try:
+                        self.table_widget.cellWidget(row, col).toggled.disconnect(self.callback_on_edited)
+                    except (TypeError, AttributeError):
+                        pass
+                    self.table_widget.cellWidget(row, col).toggled.connect(self.callback_on_edited)
+                except AttributeError:
+                    pass
+
+                # SpinBox, DoubleSpinBox
+                try:
+                    try:
+                        self.table_widget.cellWidget(row, col).valueChanged.disconnect(self.callback_on_edited)
+                    except (TypeError, AttributeError):
+                        pass
+                    self.table_widget.cellWidget(row, col).valueChanged.connect(self.callback_on_edited)
+                except AttributeError:
+                    pass
+
+    def ui_disconnect(self):
+        try:
+            self.table_widget.itemChanged.disconnect(self.callback_on_edited)
+        except (TypeError, AttributeError):
+            pass
+
+        for row in range(self.table_widget.rowCount()):
+            for col in range(self.table_widget.columnCount()):
+                # ComboBox
+                try:
+                    self.table_widget.cellWidget(row, col).currentIndexChanged.disconnect(self.callback_on_edited)
+                except (TypeError, AttributeError):
+                    pass
+
+                # CheckBox
+                try:
+                    self.table_widget.cellWidget(row, col).toggled.disconnect(self.callback_on_edited)
+                except (TypeError, AttributeError):
+                    pass
+
+                # SpinBox, DoubleSpinBox
+                try:
+                    self.table_widget.cellWidget(row, col).valueChanged.disconnect(self.callback_on_edited)
+                except (TypeError, AttributeError):
+                    pass
+
+    def callback_on_edited(self):
+
+        # update the dictionary storage self.db_tool_dict
+        self.db_tool_dict.clear()
+        dict_elem = dict()
+        default_data = dict()
+
+        for row in range(self.table_widget.rowCount()):
+            new_toolid = row + 1
+            for col in range(self.table_widget.columnCount()):
+                column_header_text = self.table_widget.horizontalHeaderItem(col).text()
+                if column_header_text == 'Tool Name':
+                    dict_elem['name'] = self.table_widget.item(row, col).text()
+                elif column_header_text == 'Tool Dia':
+                    dict_elem['tooldia'] = self.table_widget.cellWidget(row, col).get_value()
+                elif column_header_text == 'Tool Offset':
+                    dict_elem['offset'] = self.table_widget.cellWidget(row, col).get_value()
+                elif column_header_text == 'Custom Offset':
+                    dict_elem['offset_value'] = self.table_widget.cellWidget(row, col).get_value()
+                elif column_header_text == 'Tool Type':
+                    dict_elem['type'] = self.table_widget.cellWidget(row, col).get_value()
+                elif column_header_text == 'Tool Shape':
+                    dict_elem['tool_type'] = self.table_widget.cellWidget(row, col).get_value()
+                else:
+
+                    if column_header_text == 'Cut Z':
+                        default_data['cutz'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'MultiDepth':
+                        default_data['multidepth'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'DPP':
+                        default_data['depthperpass'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'V-Dia':
+                        default_data['vtipdia'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'V-Angle':
+                        default_data['vtipangle'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Travel Z':
+                        default_data['travelz'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'FR':
+                        default_data['feedrate'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'FR Z':
+                        default_data['feedrate_z'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'FR Rapids':
+                        default_data['feedrate_rapid'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Spindle Speed':
+                        default_data['spindlespeed'] = float(self.table_widget.item(row, col).text()) \
+                            if self.table_widget.item(row, col).text() is not '' else None
+                    elif column_header_text == 'Dwell':
+                        default_data['dwell'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Dwelltime':
+                        default_data['dwelltime'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Postprocessor':
+                        default_data['ppname_g'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'ExtraCut':
+                        default_data['extracut'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Toolchange':
+                        default_data['toolchange'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Toolchange XY':
+                        default_data['toolchangexy'] = self.table_widget.item(row, col).text()
+                    elif column_header_text == 'Toolchange Z':
+                        default_data['toolchangez'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == 'Start Z':
+                        default_data['startz'] = float(self.table_widget.item(row, col).text()) \
+                            if self.table_widget.item(row, col).text() is not '' else None
+                    elif column_header_text == 'End Z':
+                        default_data['endz'] = self.table_widget.cellWidget(row, col).get_value()
+
+            dict_elem['data'] = default_data
+            self.db_tool_dict.update(
+                {
+                    new_toolid: deepcopy(dict_elem)
+                }
+            )
+
+        self.callback_app()
+
+    def on_tool_requested_from_app(self):
+        if not self.table_widget.selectionModel().selectedRows():
+            self.app.inform.emit('[WARNING_NOTCL] %s...' % _("No Tool/row selected in the Tools Database table"))
+            return
+        elif len(self.table_widget.selectionModel().selectedRows()) > 1:
+            self.app.inform.emit('[WARNING_NOTCL] %s...' %
+                                 _("Only one tool can be selected in the Tools Database table"))
+            return
+
+        # only one model in list since the conditions above assure this
+        model_index = self.table_widget.selectionModel().selectedRows()[0]
+        selected_row = model_index.row()
+        tool_uid = selected_row + 1
+        for key in self.db_tool_dict.keys():
+            if str(key) == str(tool_uid):
+                selected_tool = self.db_tool_dict[key]
+                self.on_tool_request(tool=selected_tool)
+
+    def resize_new_tool_table_widget(self, min_size, max_size):
+        """
+        Resize the table widget responsible for adding new tool in the Tool Database
+
+        :param min_size: passed by rangeChanged signal or the self.new_tool_table_widget.horizontalScrollBar()
+        :param max_size: passed by rangeChanged signal or the self.new_tool_table_widget.horizontalScrollBar()
+        :return:
+        """
+        t_height = self.t_height
+        if max_size > min_size:
+            t_height = self.t_height + self.new_tool_table_widget.verticalScrollBar().height()
+
+        self.new_tool_table_widget.setMaximumHeight(t_height)
+
+    def closeEvent(self, QCloseEvent):
+        super().closeEvent(QCloseEvent)

+ 229 - 160
FlatCAMObj.py

@@ -3306,104 +3306,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
     optionChanged = QtCore.pyqtSignal(str)
     optionChanged = QtCore.pyqtSignal(str)
     ui_type = GeometryObjectUI
     ui_type = GeometryObjectUI
 
 
-    def merge(self, geo_list, geo_final, multigeo=None):
-        """
-        Merges the geometry of objects in grb_list into
-        the geometry of geo_final.
-
-        :param geo_list: List of FlatCAMGerber Objects to join.
-        :param geo_final: Destination FlatCAMGerber object.
-        :return: None
-        """
-
-        if geo_final.solid_geometry is None:
-            geo_final.solid_geometry = []
-
-        if type(geo_final.solid_geometry) is not list:
-            geo_final.solid_geometry = [geo_final.solid_geometry]
-
-        for geo in geo_list:
-            for option in geo.options:
-                if option is not 'name':
-                    try:
-                        geo_final.options[option] = geo.options[option]
-                    except Exception as e:
-                        log.warning("Failed to copy option %s. Error: %s" % (str(option), str(e)))
-
-            # Expand lists
-            if type(geo) is list:
-                FlatCAMGeometry.merge(self, geo_list=geo, geo_final=geo_final)
-            # If not list, just append
-            else:
-                # merge solid_geometry, useful for singletool geometry, for multitool each is empty
-                if multigeo is None or multigeo is False:
-                    geo_final.multigeo = False
-                    try:
-                        geo_final.solid_geometry.append(geo.solid_geometry)
-                    except Exception as e:
-                        log.debug("FlatCAMGeometry.merge() --> %s" % str(e))
-                else:
-                    geo_final.multigeo = True
-                    # if multigeo the solid_geometry is empty in the object attributes because it now lives in the
-                    # tools object attribute, as a key value
-                    geo_final.solid_geometry = []
-
-                # find the tool_uid maximum value in the geo_final
-                geo_final_uid_list = []
-                for key in geo_final.tools:
-                    geo_final_uid_list.append(int(key))
-
-                try:
-                    max_uid = max(geo_final_uid_list, key=int)
-                except ValueError:
-                    max_uid = 0
-
-                # add and merge tools. If what we try to merge as Geometry is Excellon's and/or Gerber's then don't try
-                # to merge the obj.tools as it is likely there is none to merge.
-                if not isinstance(geo, FlatCAMGerber) and not isinstance(geo, FlatCAMExcellon):
-                    for tool_uid in geo.tools:
-                        max_uid += 1
-                        geo_final.tools[max_uid] = deepcopy(geo.tools[tool_uid])
-
-    @staticmethod
-    def get_pts(o):
-        """
-        Returns a list of all points in the object, where
-        the object can be a MultiPolygon, Polygon, Not a polygon, or a list
-        of such. Search is done recursively.
-
-        :param: geometric object
-        :return: List of points
-        :rtype: list
-        """
-        pts = []
-
-        # Iterable: descend into each item.
-        try:
-            for subo in o:
-                pts += FlatCAMGeometry.get_pts(subo)
-
-        # Non-iterable
-        except TypeError:
-            if o is not None:
-                if type(o) == MultiPolygon:
-                    for poly in o:
-                        pts += FlatCAMGeometry.get_pts(poly)
-                # ## Descend into .exerior and .interiors
-                elif type(o) == Polygon:
-                    pts += FlatCAMGeometry.get_pts(o.exterior)
-                    for i in o.interiors:
-                        pts += FlatCAMGeometry.get_pts(i)
-                elif type(o) == MultiLineString:
-                    for line in o:
-                        pts += FlatCAMGeometry.get_pts(line)
-                # ## Has .coords: list them.
-                else:
-                    pts += list(o.coords)
-            else:
-                return
-        return pts
-
     def __init__(self, name):
     def __init__(self, name):
         FlatCAMObj.__init__(self, name)
         FlatCAMObj.__init__(self, name)
         Geometry.__init__(self, geo_steps_per_circle=int(self.app.defaults["geometry_circle_steps"]))
         Geometry.__init__(self, geo_steps_per_circle=int(self.app.defaults["geometry_circle_steps"]))
@@ -3477,7 +3379,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
 
         # flag to store if the geometry is part of a special group of geometries that can't be processed by the default
         # flag to store if the geometry is part of a special group of geometries that can't be processed by the default
         # engine of FlatCAM. Most likely are generated by some of tools and are special cases of geometries.
         # engine of FlatCAM. Most likely are generated by some of tools and are special cases of geometries.
-        self. special_group = None
+        self.special_group = None
 
 
         self.old_pp_state = self.app.defaults["geometry_multidepth"]
         self.old_pp_state = self.app.defaults["geometry_multidepth"]
         self.old_toolchangeg_state = self.app.defaults["geometry_toolchange"]
         self.old_toolchangeg_state = self.app.defaults["geometry_toolchange"]
@@ -3506,9 +3408,9 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             tool_idx += 1
             tool_idx += 1
             row_no = tool_idx - 1
             row_no = tool_idx - 1
 
 
-            id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
-            id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.ui.geo_tools_table.setItem(row_no, 0, id)  # Tool name/id
+            tool_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
+            tool_id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.geo_tools_table.setItem(row_no, 0, tool_id)  # Tool name/id
 
 
             # Make sure that the tool diameter when in MM is with no more than 2 decimals.
             # 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.
             # There are no tool bits in MM with more than 3 decimals diameter.
@@ -3754,7 +3656,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             # again float type; dict's don't like having keys changed when iterated through therefore the need for the
             # again float type; dict's don't like having keys changed when iterated through therefore the need for the
             # following convoluted way of changing the keys from string to float type
             # following convoluted way of changing the keys from string to float type
             temp_tools = {}
             temp_tools = {}
-            new_key = 0.0
             for tooluid_key in self.tools:
             for tooluid_key in self.tools:
                 val = deepcopy(self.tools[tooluid_key])
                 val = deepcopy(self.tools[tooluid_key])
                 new_key = deepcopy(int(tooluid_key))
                 new_key = deepcopy(int(tooluid_key))
@@ -3775,6 +3676,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             return
             return
 
 
         self.ui.geo_tools_table.setupContextMenu()
         self.ui.geo_tools_table.setupContextMenu()
+        self.ui.geo_tools_table.addContextMenu(
+            _("Add from Tool DB"), self.on_tool_add_from_db_clicked, icon=QtGui.QIcon("share/plus16.png"))
         self.ui.geo_tools_table.addContextMenu(
         self.ui.geo_tools_table.addContextMenu(
             _("Copy"), self.on_tool_copy, icon=QtGui.QIcon("share/copy16.png"))
             _("Copy"), self.on_tool_copy, icon=QtGui.QIcon("share/copy16.png"))
         self.ui.geo_tools_table.addContextMenu(
         self.ui.geo_tools_table.addContextMenu(
@@ -3813,6 +3716,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         self.ui.tipdia_entry.valueChanged.connect(self.update_cutz)
         self.ui.tipdia_entry.valueChanged.connect(self.update_cutz)
         self.ui.tipangle_entry.valueChanged.connect(self.update_cutz)
         self.ui.tipangle_entry.valueChanged.connect(self.update_cutz)
 
 
+        self.ui.addtool_from_db_btn.clicked.connect(self.on_tool_add_from_db_clicked)
+
     def set_tool_offset_visibility(self, current_row):
     def set_tool_offset_visibility(self, current_row):
         if current_row is None:
         if current_row is None:
             return
             return
@@ -4064,8 +3969,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         self.ser_attrs.append('tools')
         self.ser_attrs.append('tools')
 
 
         if change_message is False:
         if change_message is False:
-            self.app.inform.emit('[success] %s' %
-                                 _("Tool added in Tool Table."))
+            self.app.inform.emit('[success] %s' % _("Tool added in Tool Table."))
         else:
         else:
             change_message = False
             change_message = False
             self.app.inform.emit('[WARNING_NOTCL] %s' %
             self.app.inform.emit('[WARNING_NOTCL] %s' %
@@ -4076,6 +3980,73 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         if self.ui.geo_tools_table.rowCount() != 0:
         if self.ui.geo_tools_table.rowCount() != 0:
             self.ui.geo_param_frame.setDisabled(False)
             self.ui.geo_param_frame.setDisabled(False)
 
 
+    def on_tool_add_from_db_clicked(self):
+        """
+        Called when the user wants to add a new tool from Tools Database. It will create the Tools Database object
+        and display the Tools Database tab in the form needed for the Tool adding
+        :return: None
+        """
+        self.app.on_tools_database()
+        self.app.tools_db_tab.buttons_frame.hide()
+        self.app.tools_db_tab.add_tool_from_db.show()
+
+    def on_tool_from_db_inserted(self, tool):
+        """
+        Called from the Tools DB object through a App method when adding a tool from Tools Database
+        :param tool: a dict with the tool data
+        :return: None
+        """
+
+        self.ui_disconnect()
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+
+        tooldia = float(tool['tooldia'])
+
+        # construct a list of all 'tooluid' in the self.tools
+        tool_uid_list = []
+        for tooluid_key in self.tools:
+            tool_uid_item = int(tooluid_key)
+            tool_uid_list.append(tool_uid_item)
+
+        # find maximum from the temp_uid, add 1 and this is the new 'tooluid'
+        if not tool_uid_list:
+            max_uid = 0
+        else:
+            max_uid = max(tool_uid_list)
+        self.tooluid = max_uid + 1
+
+        tooldia = float('%.*f' % (self.decimals, tooldia))
+
+        self.tools.update({
+            self.tooluid: {
+                'tooldia': tooldia,
+                'offset': tool['offset'],
+                'offset_value': float(tool['offset_value']),
+                'type': tool['type'],
+                'tool_type': tool['tool_type'],
+                'data': deepcopy(tool['data']),
+                'solid_geometry': self.solid_geometry
+            }
+        })
+
+        self.tools[self.tooluid]['data']['name'] = self.options['name']
+
+        self.ui.tool_offset_entry.hide()
+        self.ui.tool_offset_lbl.hide()
+
+        # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
+        try:
+            self.ser_attrs.remove('tools')
+        except TypeError:
+            pass
+        self.ser_attrs.append('tools')
+
+        self.build_ui()
+
+        # if there is no 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)
+
     def on_tool_copy(self, all=None):
     def on_tool_copy(self, all=None):
         self.ui_disconnect()
         self.ui_disconnect()
 
 
@@ -4670,16 +4641,16 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # test to see if we have tools available in the tool table
         # test to see if we have tools available in the tool table
         if self.ui.geo_tools_table.selectedItems():
         if self.ui.geo_tools_table.selectedItems():
             for x in 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
+                # 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())
                 tooluid = int(self.ui.geo_tools_table.item(x.row(), 5).text())
 
 
                 for tooluid_key, tooluid_value in self.tools.items():
                 for tooluid_key, tooluid_value in self.tools.items():
@@ -4956,16 +4927,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 elif dia_cnc_dict['offset'].lower() == 'out':
                 elif dia_cnc_dict['offset'].lower() == 'out':
                     tool_offset = tooldia_val / 2
                     tool_offset = tooldia_val / 2
                 elif dia_cnc_dict['offset'].lower() == 'custom':
                 elif dia_cnc_dict['offset'].lower() == 'custom':
-                    try:
-                        offset_value = float(self.ui.tool_offset_entry.get_value())
-                    except ValueError:
-                        # try to convert comma to decimal point. if it's still not working error message and return
-                        try:
-                            offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.'))
-                        except ValueError:
-                            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                                 _("Wrong value format entered, use a number."))
-                            return
+                    offset_value = float(self.ui.tool_offset_entry.get_value())
                     if offset_value:
                     if offset_value:
                         tool_offset = float(offset_value)
                         tool_offset = float(offset_value)
                     else:
                     else:
@@ -5169,27 +5131,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             job_obj.segx = segx
             job_obj.segx = segx
             job_obj.segy = segy
             job_obj.segy = segy
 
 
-            try:
-                job_obj.z_pdepth = float(self.options["z_pdepth"])
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                         _('Wrong value format for self.defaults["z_pdepth"] or '
-                                           'self.options["z_pdepth"]'))
-
-            try:
-                job_obj.feedrate_probe = float(self.options["feedrate_probe"])
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    job_obj.feedrate_probe = float(self.options["feedrate_probe"].replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                         _('Wrong value format for self.defaults["feedrate_probe"] '
-                                           'or self.options["feedrate_probe"]'))
+            job_obj.z_pdepth = float(self.options["z_pdepth"])
+            job_obj.feedrate_probe = float(self.options["feedrate_probe"])
 
 
             job_obj.options['xmin'] = self.options['xmin']
             job_obj.options['xmin'] = self.options['xmin']
             job_obj.options['ymin'] = self.options['ymin']
             job_obj.options['ymin'] = self.options['ymin']
@@ -5630,6 +5573,105 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             self.ui.plot_cb.setChecked(True)
             self.ui.plot_cb.setChecked(True)
         self.ui_connect()
         self.ui_connect()
 
 
+    def merge(self, geo_list, geo_final, multigeo=None):
+        """
+        Merges the geometry of objects in grb_list into
+        the geometry of geo_final.
+
+        :param geo_list: List of FlatCAMGerber Objects to join.
+        :param geo_final: Destination FlatCAMGerber object.
+        :param multigeo: if the merged geometry objects are of type MultiGeo
+        :return: None
+        """
+
+        if geo_final.solid_geometry is None:
+            geo_final.solid_geometry = []
+
+        if type(geo_final.solid_geometry) is not list:
+            geo_final.solid_geometry = [geo_final.solid_geometry]
+
+        for geo in geo_list:
+            for option in geo.options:
+                if option is not 'name':
+                    try:
+                        geo_final.options[option] = deepcopy(geo.options[option])
+                    except Exception as e:
+                        log.warning("Failed to copy option %s. Error: %s" % (str(option), str(e)))
+
+            # Expand lists
+            if type(geo) is list:
+                FlatCAMGeometry.merge(self, geo_list=geo, geo_final=geo_final)
+            # If not list, just append
+            else:
+                # merge solid_geometry, useful for singletool geometry, for multitool each is empty
+                if multigeo is None or multigeo is False:
+                    geo_final.multigeo = False
+                    try:
+                        geo_final.solid_geometry.append(deepcopy(geo.solid_geometry))
+                    except Exception as e:
+                        log.debug("FlatCAMGeometry.merge() --> %s" % str(e))
+                else:
+                    geo_final.multigeo = True
+                    # if multigeo the solid_geometry is empty in the object attributes because it now lives in the
+                    # tools object attribute, as a key value
+                    geo_final.solid_geometry = []
+
+                # find the tool_uid maximum value in the geo_final
+                geo_final_uid_list = []
+                for key in geo_final.tools:
+                    geo_final_uid_list.append(int(key))
+
+                try:
+                    max_uid = max(geo_final_uid_list, key=int)
+                except ValueError:
+                    max_uid = 0
+
+                # add and merge tools. If what we try to merge as Geometry is Excellon's and/or Gerber's then don't try
+                # to merge the obj.tools as it is likely there is none to merge.
+                if not isinstance(geo, FlatCAMGerber) and not isinstance(geo, FlatCAMExcellon):
+                    for tool_uid in geo.tools:
+                        max_uid += 1
+                        geo_final.tools[max_uid] = deepcopy(geo.tools[tool_uid])
+
+    @staticmethod
+    def get_pts(o):
+        """
+        Returns a list of all points in the object, where
+        the object can be a MultiPolygon, Polygon, Not a polygon, or a list
+        of such. Search is done recursively.
+
+        :param: geometric object
+        :return: List of points
+        :rtype: list
+        """
+        pts = []
+
+        # Iterable: descend into each item.
+        try:
+            for subo in o:
+                pts += FlatCAMGeometry.get_pts(subo)
+
+        # Non-iterable
+        except TypeError:
+            if o is not None:
+                if type(o) == MultiPolygon:
+                    for poly in o:
+                        pts += FlatCAMGeometry.get_pts(poly)
+                # ## Descend into .exerior and .interiors
+                elif type(o) == Polygon:
+                    pts += FlatCAMGeometry.get_pts(o.exterior)
+                    for i in o.interiors:
+                        pts += FlatCAMGeometry.get_pts(i)
+                elif type(o) == MultiLineString:
+                    for line in o:
+                        pts += FlatCAMGeometry.get_pts(line)
+                # ## Has .coords: list them.
+                else:
+                    pts += list(o.coords)
+            else:
+                return
+        return pts
+
 
 
 class FlatCAMCNCjob(FlatCAMObj, CNCjob):
 class FlatCAMCNCjob(FlatCAMObj, CNCjob):
     """
     """
@@ -6002,8 +6044,8 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
             _filter_ = "HPGL Files (*.plt);;" \
             _filter_ = "HPGL Files (*.plt);;" \
                        "All Files (*.*)"
                        "All Files (*.*)"
         else:
         else:
-            _filter_ = "G-Code Files (*.nc);;G-Code Files (*.txt);;G-Code Files (*.tap);;G-Code Files (*.cnc);;" \
-                       "G-Code Files (*.g-code);;All Files (*.*)"
+            _filter_ = "G-Code Files (*.nc);;G-Code Files (*.txt);;G-Code Files (*.tap);;G-Code Files (*.ngc);;" \
+                       "G-Code Files (*.cnc);;G-Code Files (*.g-code);;All Files (*.*)"
 
 
         try:
         try:
             dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
             dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
@@ -6094,13 +6136,24 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         self.app.inform.emit('[success] %s...' %
         self.app.inform.emit('[success] %s...' %
                              _('Loaded Machine Code into Code Editor'))
                              _('Loaded Machine Code into Code Editor'))
 
 
-    def gcode_header(self):
+    def gcode_header(self, comment_start_symbol=None, comment_stop_symbol=None):
+        """
+        Will create a header to be added to all GCode files generated by FlatCAM
+
+        :param comment_start_symbol: a symbol to be used as the first symbol in a comment
+        :param comment_stop_symbol:  a symbol to be used as the last symbol in a comment
+        :return: a string with a GCode header
+        """
+
         log.debug("FlatCAMCNCJob.gcode_header()")
         log.debug("FlatCAMCNCJob.gcode_header()")
         time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
         time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
         marlin = False
         marlin = False
         hpgl = False
         hpgl = False
         probe_pp = False
         probe_pp = False
 
 
+        start_comment = comment_start_symbol if comment_start_symbol is not None else '('
+        stop_comment = comment_stop_symbol if comment_stop_symbol is not None else ')'
+
         try:
         try:
             for key in self.cnc_tools:
             for key in self.cnc_tools:
                 ppg = self.cnc_tools[key]['data']['ppname_g']
                 ppg = self.cnc_tools[key]['data']['ppname_g']
@@ -6174,17 +6227,17 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
             gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
             gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
             gcode += '(Created on ' + time_str + ')\n' + '\n'
             gcode += '(Created on ' + time_str + ')\n' + '\n'
         else:
         else:
-            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'
+            gcode = '%sG-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s%s\n' % \
+                    (start_comment, str(self.app.version), str(self.app.version_date), stop_comment) + '\n'
 
 
-            gcode += '(Name: ' + str(self.options['name']) + ')\n'
-            gcode += '(Type: ' + "G-code from " + str(self.options['type']) + ')\n'
+            gcode += '%sName: ' % start_comment + str(self.options['name']) + '%s\n' % stop_comment
+            gcode += '%sType: ' % start_comment + "G-code from " + str(self.options['type']) + '%s\n' % stop_comment
 
 
             # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
             # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
             #     gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
             #     gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
 
 
-            gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
-            gcode += '(Created on ' + time_str + ')\n' + '\n'
+            gcode += '%sUnits: ' % start_comment + self.units.upper() + '%s\n' % stop_comment + "\n"
+            gcode += '%sCreated on ' % start_comment + time_str + '%s\n' % stop_comment + '\n'
 
 
         return gcode
         return gcode
 
 
@@ -6200,6 +6253,15 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
             return 'M02'
             return 'M02'
 
 
     def export_gcode(self, filename=None, preamble='', postamble='', to_file=False):
     def export_gcode(self, filename=None, preamble='', postamble='', to_file=False):
+        """
+        This will save the GCode from the Gcode object to a file on the OS filesystem
+
+        :param filename: filename for the GCode file
+        :param preamble: a custom Gcode block to be added at the beginning of the Gcode file
+        :param postamble: a custom Gcode block to be added at the end of the Gcode file
+        :param to_file: if False then no actual file is saved but the app will know that a file was created
+        :return: None
+        """
         gcode = ''
         gcode = ''
         roland = False
         roland = False
         hpgl = False
         hpgl = False
@@ -6264,7 +6326,9 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
                                      _("G-code does not have a units code: either G20 or G21"))
                                      _("G-code does not have a units code: either G20 or G21"))
                 return
                 return
 
 
-            g = gcode[:g_idx] + preamble + '\n' + gcode[g_idx:] + postamble + self.gcode_footer()
+            footer = self.app.defaults['cncjob_footer']
+            end_gcode = self.gcode_footer() if footer is True else ''
+            g = gcode[:g_idx] + preamble + '\n' + gcode[g_idx:] + postamble + end_gcode
 
 
         # if toolchange custom is used, replace M6 code with the code from the Toolchange Custom Text box
         # if toolchange custom is used, replace M6 code with the code from the Toolchange Custom Text box
         if self.ui.toolchange_cb.get_value() is True:
         if self.ui.toolchange_cb.get_value() is True:
@@ -6281,15 +6345,20 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
                 self.app.inform.emit('[success] %s' %
                 self.app.inform.emit('[success] %s' %
                                      _("Toolchange G-code was replaced by a custom code."))
                                      _("Toolchange G-code was replaced by a custom code."))
 
 
-        # lines = StringIO(self.gcode)
         lines = StringIO(g)
         lines = StringIO(g)
 
 
         # Write
         # Write
         if filename is not None:
         if filename is not None:
             try:
             try:
-                with open(filename, 'w') as f:
-                    for line in lines:
-                        f.write(line)
+                force_windows_line_endings = self.app.defaults['cncjob_line_ending']
+                if force_windows_line_endings and sys.platform != 'win32':
+                    with open(filename, 'w', newline='\r\n') as f:
+                        for line in lines:
+                            f.write(line)
+                else:
+                    with open(filename, 'w') as f:
+                        for line in lines:
+                            f.write(line)
             except FileNotFoundError:
             except FileNotFoundError:
                 self.app.inform.emit('[WARNING_NOTCL] %s' %
                 self.app.inform.emit('[WARNING_NOTCL] %s' %
                                      _("No such file or directory"))
                                      _("No such file or directory"))

+ 1 - 1
FlatCAMPostProc.py

@@ -18,7 +18,7 @@ postprocessors = {}
 
 
 
 
 class ABCPostProcRegister(ABCMeta):
 class ABCPostProcRegister(ABCMeta):
-    # handles postprocessors registration on instantation
+    # handles postprocessors registration on instantiation
     def __new__(cls, clsname, bases, attrs):
     def __new__(cls, clsname, bases, attrs):
         newclass = super(ABCPostProcRegister, cls).__new__(cls, clsname, bases, attrs)
         newclass = super(ABCPostProcRegister, cls).__new__(cls, clsname, bases, attrs)
         if object not in bases:
         if object not in bases:

+ 34 - 0
README.md

@@ -9,9 +9,43 @@ CAD program, and create G-Code for Isolation routing.
 
 
 =================================================
 =================================================
 
 
+9.11.2019
+
+- fixed a new bug that did not allow to open the FlatCAM Preferences files by doubleclick in Windows
+- added a new feature: Tools Database for Geometry objects; resolved issue #308
+- added tooltips for the Tools Database table headers and buttons
+
+
+8.11.2019
+
+- updated the make file for freezed executable
+
+7.11.2019
+
+- added the '.ngc' file extension to the GCode Save file dialog filter
+- made the 'M2' Gcode command footer optional, default is False (can be set using the TclCommand: set_sys cncjob_footer True)
+- added a setting in Preferences to force the GCode output to have the Windows line-endings even for non-Windows OS's
+
+6.11.2019
+
+- the "CRTL+S" key combo when the Preferences Tab is in focus will save the Preferences instead of saving the Project
+- fixed bug in the Paint Tool that did not allow choosing a Paint Method that was not Standard
+- made sure that in the FlatCAMGeometry.merge() all the source data is deepcopy-ed in the final object
+- the font color of the Preferences tab will change to red if settings are not saved and it will revert to default when saved
+- fixed issue #333. The Geometry Editor Paint tool was not working and using it resulted in an error
+
+5.11.2019
+
+- added a new setting named 'Allow Machinist Unsafe Settings' that will allow the Travel Z and Cut Z to take both positive and negative values
+- fixed some issues when editing a multigeo geometry
+
 4.11.2019
 4.11.2019
 
 
 - wip
 - wip
+- getting rid of all the Options GUI and related functions as it is no longer supported
+- updated the UI in Geometry UI
+- optimized the order of the defaults storage declaration and the update of the Preferences GUI from the defaults
+- started to add a Tool Database
 
 
 3.11.2019
 3.11.2019
 
 

+ 113 - 116
camlib.py

@@ -7,7 +7,7 @@
 # ########################################################## ##
 # ########################################################## ##
 
 
 
 
-from PyQt5 import QtWidgets
+from PyQt5 import QtWidgets, QtCore
 from io import StringIO
 from io import StringIO
 
 
 import numpy as np
 import numpy as np
@@ -497,10 +497,6 @@ class Geometry(object):
             from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
             from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
             self.temp_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='camlib.geometry')
             self.temp_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='camlib.geometry')
 
 
-        # if geo_steps_per_circle is None:
-        #     geo_steps_per_circle = int(Geometry.defaults["geo_steps_per_circle"])
-        # self.geo_steps_per_circle = geo_steps_per_circle
-
     def plot_temp_shapes(self, element, color='red'):
     def plot_temp_shapes(self, element, color='red'):
 
 
         try:
         try:
@@ -2147,6 +2143,12 @@ class CNCjob(Geometry):
         "excellon_optimization_type": "B",
         "excellon_optimization_type": "B",
     }
     }
 
 
+    settings = QtCore.QSettings("Open Source", "FlatCAM")
+    if settings.contains("machinist"):
+        machinist_setting = settings.value('machinist', type=int)
+    else:
+        machinist_setting = 0
+
     def __init__(self,
     def __init__(self,
                  units="in", kind="generic", tooldia=0.0,
                  units="in", kind="generic", tooldia=0.0,
                  z_cut=-0.002, z_move=0.1,
                  z_cut=-0.002, z_move=0.1,
@@ -2372,21 +2374,21 @@ class CNCjob(Geometry):
         self.exc_drills = deepcopy(exobj.drills)
         self.exc_drills = deepcopy(exobj.drills)
         self.exc_tools = deepcopy(exobj.tools)
         self.exc_tools = deepcopy(exobj.tools)
 
 
-        if drillz > 0:
-            self.app.inform.emit('[WARNING] %s' %
-                                 _("The Cut Z parameter has positive value. "
-                                   "It is the depth value to drill into material.\n"
-                                   "The Cut Z parameter needs to have a negative value, assuming it is a typo "
-                                   "therefore the app will convert the value to negative. "
-                                   "Check the resulting CNC code (Gcode etc)."))
-            self.z_cut = -drillz
-        elif drillz == 0:
-            self.app.inform.emit('[WARNING] %s: %s' %
-                                 (_("The Cut Z parameter is zero. There will be no cut, skipping file"),
-                                  exobj.options['name']))
-            return 'fail'
-        else:
-            self.z_cut = drillz
+        self.z_cut = drillz
+        if self.machinist_setting == 0:
+            if drillz > 0:
+                self.app.inform.emit('[WARNING] %s' %
+                                     _("The Cut Z parameter has positive value. "
+                                       "It is the depth value to drill into material.\n"
+                                       "The Cut Z parameter needs to have a negative value, assuming it is a typo "
+                                       "therefore the app will convert the value to negative. "
+                                       "Check the resulting CNC code (Gcode etc)."))
+                self.z_cut = -drillz
+            elif drillz == 0:
+                self.app.inform.emit('[WARNING] %s: %s' %
+                                     (_("The Cut Z parameter is zero. There will be no cut, skipping file"),
+                                      exobj.options['name']))
+                return 'fail'
 
 
         self.z_toolchange = toolchangez
         self.z_toolchange = toolchangez
 
 
@@ -2516,8 +2518,7 @@ class CNCjob(Geometry):
         measured_up_to_zero_distance = 0.0
         measured_up_to_zero_distance = 0.0
         measured_lift_distance = 0.0
         measured_lift_distance = 0.0
 
 
-        self.app.inform.emit('%s...' %
-                             _("Starting G-Code"))
+        self.app.inform.emit('%s...' % _("Starting G-Code"))
 
 
         current_platform = platform.architecture()[0]
         current_platform = platform.architecture()[0]
         if current_platform == '64bit':
         if current_platform == '64bit':
@@ -2671,8 +2672,7 @@ class CNCjob(Geometry):
                                         old_disp_number = disp_number
                                         old_disp_number = disp_number
 
 
                             else:
                             else:
-                                self.app.inform.emit('[ERROR_NOTCL] %s...' %
-                                                     _('G91 coordinates not implemented'))
+                                self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
                                 return 'fail'
                                 return 'fail'
                 else:
                 else:
                     log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
                     log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
@@ -2818,8 +2818,7 @@ class CNCjob(Geometry):
                                         old_disp_number = disp_number
                                         old_disp_number = disp_number
 
 
                             else:
                             else:
-                                self.app.inform.emit('[ERROR_NOTCL] %s...' %
-                                                     _('G91 coordinates not implemented'))
+                                self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
                                 return 'fail'
                                 return 'fail'
                 else:
                 else:
                     log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
                     log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
@@ -2924,8 +2923,7 @@ class CNCjob(Geometry):
                                     self.app.proc_container.update_view_text(' %d%%' % disp_number)
                                     self.app.proc_container.update_view_text(' %d%%' % disp_number)
                                     old_disp_number = disp_number
                                     old_disp_number = disp_number
                         else:
                         else:
-                            self.app.inform.emit('[ERROR_NOTCL] %s...' %
-                                                 _('G91 coordinates not implemented'))
+                            self.app.inform.emit('[ERROR_NOTCL] %s...' %  _('G91 coordinates not implemented'))
                             return 'fail'
                             return 'fail'
                     else:
                     else:
                         log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
                         log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
@@ -3002,10 +3000,10 @@ class CNCjob(Geometry):
 
 
         self.tooldia = float(tooldia) if tooldia else None
         self.tooldia = float(tooldia) if tooldia else None
         self.z_cut = float(z_cut) if z_cut else None
         self.z_cut = float(z_cut) if z_cut else None
-        self.z_move = float(z_move) if z_move else None
+        self.z_move = float(z_move) if z_move is not None else None
 
 
         self.feedrate = float(feedrate) if feedrate else None
         self.feedrate = float(feedrate) if feedrate else None
-        self.z_feedrate = float(feedrate_z) if feedrate_z else None
+        self.z_feedrate = float(feedrate_z) if feedrate_z is not None else None
         self.feedrate_rapid = float(feedrate_rapid) if feedrate_rapid else None
         self.feedrate_rapid = float(feedrate_rapid) if feedrate_rapid else None
 
 
         self.spindlespeed = int(spindlespeed) if spindlespeed else None
         self.spindlespeed = int(spindlespeed) if spindlespeed else None
@@ -3013,13 +3011,13 @@ class CNCjob(Geometry):
         self.dwell = dwell
         self.dwell = dwell
         self.dwelltime = float(dwelltime) if dwelltime else None
         self.dwelltime = float(dwelltime) if dwelltime else None
 
 
-        self.startz = float(startz) if startz else None
-        self.z_end = float(endz) if endz else None
+        self.startz = float(startz) if startz is not None else None
+        self.z_end = float(endz) if endz is not None else None
 
 
         self.z_depthpercut = float(depthpercut) if depthpercut else None
         self.z_depthpercut = float(depthpercut) if depthpercut else None
         self.multidepth = multidepth
         self.multidepth = multidepth
 
 
-        self.z_toolchange = float(toolchangez) if toolchangez else None
+        self.z_toolchange = float(toolchangez) if toolchangez is not None else None
 
 
         # it servers in the postprocessor file
         # it servers in the postprocessor file
         self.tool = tool_no
         self.tool = tool_no
@@ -3044,46 +3042,47 @@ class CNCjob(Geometry):
         if self.z_cut is None:
         if self.z_cut is None:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("Cut_Z parameter is None or zero. Most likely a bad combinations of "
                                  _("Cut_Z parameter is None or zero. Most likely a bad combinations of "
-                                 "other parameters."))
+                                   "other parameters."))
             return 'fail'
             return 'fail'
 
 
-        if self.z_cut > 0:
-            self.app.inform.emit('[WARNING] %s' %
-                                 _("The Cut Z parameter has positive value. "
-                                 "It is the depth value to cut into material.\n"
-                                 "The Cut Z parameter needs to have a negative value, assuming it is a typo "
-                                 "therefore the app will convert the value to negative."
-                                 "Check the resulting CNC code (Gcode etc)."))
-            self.z_cut = -self.z_cut
-        elif self.z_cut == 0:
-            self.app.inform.emit('[WARNING] %s: %s' %
-                                 (_("The Cut Z parameter is zero. There will be no cut, skipping file"),
-                                  self.options['name']))
-            return 'fail'
+        if self.machinist_setting == 0:
+            if self.z_cut > 0:
+                self.app.inform.emit('[WARNING] %s' %
+                                     _("The Cut Z parameter has positive value. "
+                                       "It is the depth value to cut into material.\n"
+                                       "The Cut Z parameter needs to have a negative value, assuming it is a typo "
+                                       "therefore the app will convert the value to negative."
+                                       "Check the resulting CNC code (Gcode etc)."))
+                self.z_cut = -self.z_cut
+            elif self.z_cut == 0:
+                self.app.inform.emit('[WARNING] %s: %s' %
+                                     (_("The Cut Z parameter is zero. There will be no cut, skipping file"),
+                                      self.options['name']))
+                return 'fail'
+
+            if self.z_move is None:
+                self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                     _("Travel Z parameter is None or zero."))
+                return 'fail'
+
+            if self.z_move < 0:
+                self.app.inform.emit('[WARNING] %s' %
+                                     _("The Travel Z parameter has negative value. "
+                                     "It is the height value to travel between cuts.\n"
+                                     "The Z Travel parameter needs to have a positive value, assuming it is a typo "
+                                     "therefore the app will convert the value to positive."
+                                     "Check the resulting CNC code (Gcode etc)."))
+                self.z_move = -self.z_move
+            elif self.z_move == 0:
+                self.app.inform.emit('[WARNING] %s: %s' %
+                                     (_("The Z Travel parameter is zero. This is dangerous, skipping file"),
+                                      self.options['name']))
+                return 'fail'
 
 
         # made sure that depth_per_cut is no more then the z_cut
         # made sure that depth_per_cut is no more then the z_cut
         if abs(self.z_cut) < self.z_depthpercut:
         if abs(self.z_cut) < self.z_depthpercut:
             self.z_depthpercut = abs(self.z_cut)
             self.z_depthpercut = abs(self.z_cut)
 
 
-        if self.z_move is None:
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("Travel Z parameter is None or zero."))
-            return 'fail'
-
-        if self.z_move < 0:
-            self.app.inform.emit('[WARNING] %s' %
-                                 _("The Travel Z parameter has negative value. "
-                                 "It is the height value to travel between cuts.\n"
-                                 "The Z Travel parameter needs to have a positive value, assuming it is a typo "
-                                 "therefore the app will convert the value to positive."
-                                 "Check the resulting CNC code (Gcode etc)."))
-            self.z_move = -self.z_move
-        elif self.z_move == 0:
-            self.app.inform.emit('[WARNING] %s: %s' %
-                                 (_("The Z Travel parameter is zero. This is dangerous, skipping file"),
-                                  self.options['name']))
-            return 'fail'
-
         # ## Index first and last points in paths
         # ## Index first and last points in paths
         # What points to index.
         # What points to index.
         def get_pts(o):
         def get_pts(o):
@@ -3356,11 +3355,11 @@ class CNCjob(Geometry):
         except ValueError:
         except ValueError:
             self.tooldia = [float(el) for el in tooldia.split(',') if el != ''] if tooldia else None
             self.tooldia = [float(el) for el in tooldia.split(',') if el != ''] if tooldia else None
 
 
-        self.z_cut = float(z_cut) if z_cut else None
-        self.z_move = float(z_move) if z_move else None
+        self.z_cut = float(z_cut) if z_cut is not None else None
+        self.z_move = float(z_move) if z_move is not None else None
 
 
         self.feedrate = float(feedrate) if feedrate else None
         self.feedrate = float(feedrate) if feedrate else None
-        self.z_feedrate = float(feedrate_z) if feedrate_z else None
+        self.z_feedrate = float(feedrate_z) if feedrate_z is not None else None
         self.feedrate_rapid = float(feedrate_rapid) if feedrate_rapid else None
         self.feedrate_rapid = float(feedrate_rapid) if feedrate_rapid else None
 
 
         self.spindlespeed = int(spindlespeed) if spindlespeed else None
         self.spindlespeed = int(spindlespeed) if spindlespeed else None
@@ -3368,11 +3367,11 @@ class CNCjob(Geometry):
         self.dwell = dwell
         self.dwell = dwell
         self.dwelltime = float(dwelltime) if dwelltime else None
         self.dwelltime = float(dwelltime) if dwelltime else None
 
 
-        self.startz = float(startz) if startz else None
-        self.z_end = float(endz) if endz else None
+        self.startz = float(startz) if startz is not None else None
+        self.z_end = float(endz) if endz is not None else None
         self.z_depthpercut = float(depthpercut) if depthpercut else None
         self.z_depthpercut = float(depthpercut) if depthpercut else None
         self.multidepth = multidepth
         self.multidepth = multidepth
-        self.z_toolchange = float(toolchangez) if toolchangez else None
+        self.z_toolchange = float(toolchangez) if toolchangez is not None else None
 
 
         try:
         try:
             if toolchangexy == '':
             if toolchangexy == '':
@@ -3391,44 +3390,45 @@ class CNCjob(Geometry):
         self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default'
         self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default'
         self.f_plunge = self.app.defaults["geometry_f_plunge"]
         self.f_plunge = self.app.defaults["geometry_f_plunge"]
 
 
-        if self.z_cut is None:
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("Cut_Z parameter is None or zero. Most likely a bad combinations of "
-                                   "other parameters."))
-            return 'fail'
-
-        if self.z_cut > 0:
-            self.app.inform.emit('[WARNING] %s' %
-                                 _("The Cut Z parameter has positive value. "
-                                   "It is the depth value to cut into material.\n"
-                                   "The Cut Z parameter needs to have a negative value, assuming it is a typo "
-                                   "therefore the app will convert the value to negative."
-                                   "Check the resulting CNC code (Gcode etc)."))
-            self.z_cut = -self.z_cut
-        elif self.z_cut == 0:
-            self.app.inform.emit('[WARNING] %s: %s' %
-                                 (_("The Cut Z parameter is zero. There will be no cut, skipping file"),
-                                  geometry.options['name']))
-            return 'fail'
-
-        if self.z_move is None:
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("Travel Z parameter is None or zero."))
-            return 'fail'
-
-        if self.z_move < 0:
-            self.app.inform.emit('[WARNING] %s' %
-                                 _("The Travel Z parameter has negative value. "
-                                   "It is the height value to travel between cuts.\n"
-                                   "The Z Travel parameter needs to have a positive value, assuming it is a typo "
-                                   "therefore the app will convert the value to positive."
-                                   "Check the resulting CNC code (Gcode etc)."))
-            self.z_move = -self.z_move
-        elif self.z_move == 0:
-            self.app.inform.emit('[WARNING] %s: %s' %
-                                 (_("The Z Travel parameter is zero. "
-                                   "This is dangerous, skipping file"), self.options['name']))
-            return 'fail'
+        if self.machinist_setting == 0:
+            if self.z_cut is None:
+                self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                     _("Cut_Z parameter is None or zero. Most likely a bad combinations of "
+                                       "other parameters."))
+                return 'fail'
+
+            if self.z_cut > 0:
+                self.app.inform.emit('[WARNING] %s' %
+                                     _("The Cut Z parameter has positive value. "
+                                       "It is the depth value to cut into material.\n"
+                                       "The Cut Z parameter needs to have a negative value, assuming it is a typo "
+                                       "therefore the app will convert the value to negative."
+                                       "Check the resulting CNC code (Gcode etc)."))
+                self.z_cut = -self.z_cut
+            elif self.z_cut == 0:
+                self.app.inform.emit('[WARNING] %s: %s' %
+                                     (_("The Cut Z parameter is zero. There will be no cut, skipping file"),
+                                      geometry.options['name']))
+                return 'fail'
+
+            if self.z_move is None:
+                self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                     _("Travel Z parameter is None or zero."))
+                return 'fail'
+
+            if self.z_move < 0:
+                self.app.inform.emit('[WARNING] %s' %
+                                     _("The Travel Z parameter has negative value. "
+                                       "It is the height value to travel between cuts.\n"
+                                       "The Z Travel parameter needs to have a positive value, assuming it is a typo "
+                                       "therefore the app will convert the value to positive."
+                                       "Check the resulting CNC code (Gcode etc)."))
+                self.z_move = -self.z_move
+            elif self.z_move == 0:
+                self.app.inform.emit('[WARNING] %s: %s' %
+                                     (_("The Z Travel parameter is zero. "
+                                       "This is dangerous, skipping file"), self.options['name']))
+                return 'fail'
 
 
         # made sure that depth_per_cut is no more then the z_cut
         # made sure that depth_per_cut is no more then the z_cut
         if abs(self.z_cut) < self.z_depthpercut:
         if abs(self.z_cut) < self.z_depthpercut:
@@ -3590,12 +3590,9 @@ class CNCjob(Geometry):
         self.gcode += self.doformat(p.spindle_stop_code)
         self.gcode += self.doformat(p.spindle_stop_code)
         self.gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1])
         self.gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1])
         self.gcode += self.doformat(p.end_code, x=0, y=0)
         self.gcode += self.doformat(p.end_code, x=0, y=0)
-        self.app.inform.emit('%s... %s %s' %
-                             (_("Finished G-Code generation"),
-                              str(path_count),
-                             _(" paths traced.")
-                              )
-                             )
+        self.app.inform.emit(
+            '%s... %s %s' % (_("Finished G-Code generation"), str(path_count), _(" paths traced."))
+        )
 
 
         return self.gcode
         return self.gcode
 
 

+ 2 - 2
flatcamEditors/FlatCAMExcEditor.py

@@ -1449,7 +1449,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.decimals = 4
         self.decimals = 4
 
 
         # ## Current application units in Upper Case
         # ## Current application units in Upper Case
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
 
 
         self.exc_edit_widget = QtWidgets.QWidget()
         self.exc_edit_widget = QtWidgets.QWidget()
         # ## Box for custom widgets
         # ## Box for custom widgets
@@ -2099,7 +2099,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             "corner_snap": False,
             "corner_snap": False,
             "grid_gap_link": True
             "grid_gap_link": True
         }
         }
-        self.app.options_read_form()
+        self.options.update(self.app.options)
 
 
         for option in self.options:
         for option in self.options:
             if option in self.app.options:
             if option in self.app.options:

+ 7 - 8
flatcamEditors/FlatCAMGeoEditor.py

@@ -3139,7 +3139,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
             "corner_snap": False,
             "corner_snap": False,
             "grid_gap_link": True
             "grid_gap_link": True
         }
         }
-        self.app.options_read_form()
+        self.options.update(self.app.options)
 
 
         for option in self.options:
         for option in self.options:
             if option in self.app.options:
             if option in self.app.options:
@@ -4664,17 +4664,16 @@ class FlatCAMGeoEditor(QtCore.QObject):
                         poly_buf = Polygon(geo_obj).buffer(-margin)
                         poly_buf = Polygon(geo_obj).buffer(-margin)
 
 
                     if method == "seed":
                     if method == "seed":
-                        cp = Geometry.clear_polygon2(poly_buf,
-                                                     tooldia, self.app.defaults["geometry_circle_steps"],
+                        cp = Geometry.clear_polygon2(self, polygon_to_clear=poly_buf, tooldia=tooldia,
+                                                     steps_per_circle=self.app.defaults["geometry_circle_steps"],
                                                      overlap=overlap, contour=contour, connect=connect)
                                                      overlap=overlap, contour=contour, connect=connect)
                     elif method == "lines":
                     elif method == "lines":
-                        cp = Geometry.clear_polygon3(poly_buf,
-                                                     tooldia, self.app.defaults["geometry_circle_steps"],
+                        cp = Geometry.clear_polygon3(self, polygon=poly_buf, tooldia=tooldia,
+                                                     steps_per_circle=self.app.defaults["geometry_circle_steps"],
                                                      overlap=overlap, contour=contour, connect=connect)
                                                      overlap=overlap, contour=contour, connect=connect)
-
                     else:
                     else:
-                        cp = Geometry.clear_polygon(poly_buf,
-                                                    tooldia, self.app.defaults["geometry_circle_steps"],
+                        cp = Geometry.clear_polygon(self, polygon=poly_buf, tooldia=tooldia,
+                                                    steps_per_circle=self.app.defaults["geometry_circle_steps"],
                                                     overlap=overlap, contour=contour, connect=connect)
                                                     overlap=overlap, contour=contour, connect=connect)
 
 
                     if cp is not None:
                     if cp is not None:

+ 3 - 3
flatcamEditors/FlatCAMGrbEditor.py

@@ -2356,7 +2356,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.decimals = 4
         self.decimals = 4
 
 
         # Current application units in Upper Case
         # Current application units in Upper Case
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
 
 
         self.grb_edit_widget = QtWidgets.QWidget()
         self.grb_edit_widget = QtWidgets.QWidget()
         layout = QtWidgets.QVBoxLayout()
         layout = QtWidgets.QVBoxLayout()
@@ -2947,7 +2947,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             "corner_snap": False,
             "corner_snap": False,
             "grid_gap_link": True
             "grid_gap_link": True
         }
         }
-        self.app.options_read_form()
+        self.options.update(self.app.options)
 
 
         for option in self.options:
         for option in self.options:
             if option in self.app.options:
             if option in self.app.options:
@@ -3023,7 +3023,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
 
     def set_ui(self):
     def set_ui(self):
         # updated units
         # updated units
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
 
 
         if self.units == "IN":
         if self.units == "IN":
             self.decimals = 4
             self.decimals = 4

+ 2 - 2
flatcamEditors/FlatCAMTextEditor.py

@@ -193,12 +193,12 @@ class TextEditor(QtWidgets.QWidget):
 
 
         try:
         try:
             filename = str(QtWidgets.QFileDialog.getSaveFileName(
             filename = str(QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export G-Code ..."),
+                caption=_("Export Code ..."),
                 directory=self.app.defaults["global_last_folder"] + '/' + str(obj_name),
                 directory=self.app.defaults["global_last_folder"] + '/' + str(obj_name),
                 filter=_filter_
                 filter=_filter_
             )[0])
             )[0])
         except TypeError:
         except TypeError:
-            filename = str(QtWidgets.QFileDialog.getSaveFileName(caption=_("Export G-Code ..."), filter=_filter_)[0])
+            filename = str(QtWidgets.QFileDialog.getSaveFileName(caption=_("Export Code ..."), filter=_filter_)[0])
 
 
         if filename == "":
         if filename == "":
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export Code cancelled."))
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export Code cancelled."))

+ 48 - 406
flatcamGUI/FlatCAMGUI.py

@@ -337,21 +337,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.menuedit.addSeparator()
         self.menuedit.addSeparator()
         self.menueditpreferences = self.menuedit.addAction(QtGui.QIcon('share/pref.png'), _('&Preferences\tSHIFT+P'))
         self.menueditpreferences = self.menuedit.addAction(QtGui.QIcon('share/pref.png'), _('&Preferences\tSHIFT+P'))
 
 
-        # ## Options # ##
-        self.menuoptions = self.menu.addMenu(_('Options'))
-        # self.menuoptions_transfer = self.menuoptions.addMenu(QtGui.QIcon('share/transfer.png'), 'Transfer options')
-        # self.menuoptions_transfer_a2p = self.menuoptions_transfer.addAction("Application to Project")
-        # self.menuoptions_transfer_p2a = self.menuoptions_transfer.addAction("Project to Application")
-        # self.menuoptions_transfer_p2o = self.menuoptions_transfer.addAction("Project to Object")
-        # self.menuoptions_transfer_o2p = self.menuoptions_transfer.addAction("Object to Project")
-        # self.menuoptions_transfer_a2o = self.menuoptions_transfer.addAction("Application to Object")
-        # self.menuoptions_transfer_o2a = self.menuoptions_transfer.addAction("Object to Application")
-
-        # Separator
-        # self.menuoptions.addSeparator()
+        # ########################################################################
+        # ########################## OPTIONS # ###################################
+        # ########################################################################
 
 
-        # self.menuoptions_transform = self.menuoptions.addMenu(QtGui.QIcon('share/transform.png'),
-        #                                                       '&Transform Object')
+        self.menuoptions = self.menu.addMenu(_('Options'))
         self.menuoptions_transform_rotate = self.menuoptions.addAction(QtGui.QIcon('share/rotate.png'),
         self.menuoptions_transform_rotate = self.menuoptions.addAction(QtGui.QIcon('share/rotate.png'),
                                                                        _("&Rotate Selection\tSHIFT+(R)"))
                                                                        _("&Rotate Selection\tSHIFT+(R)"))
         # Separator
         # Separator
@@ -373,6 +363,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
 
         self.menuoptions_view_source = self.menuoptions.addAction(QtGui.QIcon('share/source32.png'),
         self.menuoptions_view_source = self.menuoptions.addAction(QtGui.QIcon('share/source32.png'),
                                                                   _("View source\tALT+S"))
                                                                   _("View source\tALT+S"))
+        self.menuoptions_tools_db = self.menuoptions.addAction(QtGui.QIcon('share/database32.png'),
+                                                               _("Tools DataBase\tCTRL+D"))
         # Separator
         # Separator
         self.menuoptions.addSeparator()
         self.menuoptions.addSeparator()
 
 
@@ -958,6 +950,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ########################## PREFERENCES AREA Tab # ######################
         # ########################## PREFERENCES AREA Tab # ######################
         # ########################################################################
         # ########################################################################
         self.preferences_tab = QtWidgets.QWidget()
         self.preferences_tab = QtWidgets.QWidget()
+        self.preferences_tab.setObjectName("preferences_tab")
         self.pref_tab_layout = QtWidgets.QVBoxLayout(self.preferences_tab)
         self.pref_tab_layout = QtWidgets.QVBoxLayout(self.preferences_tab)
         self.pref_tab_layout.setContentsMargins(2, 2, 2, 2)
         self.pref_tab_layout.setContentsMargins(2, 2, 2, 2)
 
 
@@ -978,13 +971,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.hlay1 = QtWidgets.QHBoxLayout()
         self.hlay1 = QtWidgets.QHBoxLayout()
         self.general_tab_lay.addLayout(self.hlay1)
         self.general_tab_lay.addLayout(self.hlay1)
 
 
-        self.options_combo = QtWidgets.QComboBox()
-        self.options_combo.addItem(_("APP.  DEFAULTS"))
-        self.options_combo.addItem(_("PROJ. OPTIONS "))
-        self.hlay1.addWidget(self.options_combo)
-
-        # disable this button as it may no longer be useful
-        self.options_combo.setVisible(False)
         self.hlay1.addStretch()
         self.hlay1.addStretch()
 
 
         self.general_scroll_area = QtWidgets.QScrollArea()
         self.general_scroll_area = QtWidgets.QScrollArea()
@@ -1236,6 +1222,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>CTRL+C</strong></td>
                         <td height="20"><strong>CTRL+C</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>CTRL+D</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>CTRL+E</strong></td>
                         <td height="20"><strong>CTRL+E</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
@@ -1437,7 +1427,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 _("Flip on X_axis"), _("Flip on Y_axis"), _("Zoom Out"), _("Zoom In"),
                 _("Flip on X_axis"), _("Flip on Y_axis"), _("Zoom Out"), _("Zoom In"),
 
 
                 # CTRL section
                 # CTRL section
-                _("Select All"), _("Copy Obj"),
+                _("Select All"), _("Copy Obj"), _("Open Tools Database"),
                 _("Open Excellon File"), _("Open Gerber File"), _("New Project"), _("Distance Tool"),
                 _("Open Excellon File"), _("Open Gerber File"), _("New Project"), _("Distance Tool"),
                 _("Open Project"), _("PDF Import Tool"), _("Save Project As"), _("Toggle Plot Area"),
                 _("Open Project"), _("PDF Import Tool"), _("Save Project As"), _("Toggle Plot Area"),
 
 
@@ -2022,15 +2012,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.tools2_defaults_form = Tools2PreferencesUI()
         self.tools2_defaults_form = Tools2PreferencesUI()
         self.util_defaults_form = UtilPreferencesUI()
         self.util_defaults_form = UtilPreferencesUI()
 
 
-        self.general_options_form = GeneralPreferencesUI()
-        self.gerber_options_form = GerberPreferencesUI()
-        self.excellon_options_form = ExcellonPreferencesUI()
-        self.geometry_options_form = GeometryPreferencesUI()
-        self.cncjob_options_form = CNCJobPreferencesUI()
-        self.tools_options_form = ToolsPreferencesUI()
-        self.tools2_options_form = Tools2PreferencesUI()
-        self.util_options_form = UtilPreferencesUI()
-
         QtWidgets.qApp.installEventFilter(self)
         QtWidgets.qApp.installEventFilter(self)
 
 
         # restore the Toolbar State from file
         # restore the Toolbar State from file
@@ -2399,6 +2380,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == QtCore.Qt.Key_C:
                 if key == QtCore.Qt.Key_C:
                     self.app.on_copy_object()
                     self.app.on_copy_object()
 
 
+                # Copy an FlatCAM object
+                if key == QtCore.Qt.Key_D:
+                    self.app.on_tools_database()
+
                 # Open Excellon file
                 # Open Excellon file
                 if key == QtCore.Qt.Key_E:
                 if key == QtCore.Qt.Key_E:
                     self.app.on_fileopenexcellon()
                     self.app.on_fileopenexcellon()
@@ -2425,6 +2410,17 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
 
                 # Save Project
                 # Save Project
                 if key == QtCore.Qt.Key_S:
                 if key == QtCore.Qt.Key_S:
+                    widget_name = self.plot_tab_area.currentWidget().objectName()
+                    if widget_name == 'preferences_tab':
+                        self.app.on_save_button()
+                        return
+
+                    if widget_name == 'database_tab':
+                        # Tools DB saved, update flag
+                        self.app.tools_db_changed_flag = False
+                        self.app.tools_db_tab.on_save_tools_db()
+                        return
+
                     self.app.on_file_saveproject()
                     self.app.on_file_saveproject()
 
 
                 # Toggle Plot Area
                 # Toggle Plot Area
@@ -2764,7 +2760,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
                         messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
                         messagebox.exec_()
                         messagebox.exec_()
                     return
                     return
-
             elif modifiers == QtCore.Qt.ShiftModifier:
             elif modifiers == QtCore.Qt.ShiftModifier:
                 # Run Distance Minimum Tool
                 # Run Distance Minimum Tool
                 if key == QtCore.Qt.Key_M or key == 'M':
                 if key == QtCore.Qt.Key_M or key == 'M':
@@ -2854,10 +2849,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == QtCore.Qt.Key_Space or key == 'Space':
                 if key == QtCore.Qt.Key_Space or key == 'Space':
                     self.app.geo_editor.transform_tool.on_rotate_key()
                     self.app.geo_editor.transform_tool.on_rotate_key()
 
 
+                # Zoom Out
                 if key == QtCore.Qt.Key_Minus or key == '-':
                 if key == QtCore.Qt.Key_Minus or key == '-':
                     self.app.plotcanvas.zoom(1 / self.app.defaults['global_zoom_ratio'],
                     self.app.plotcanvas.zoom(1 / self.app.defaults['global_zoom_ratio'],
                                              [self.app.geo_editor.snap_x, self.app.geo_editor.snap_y])
                                              [self.app.geo_editor.snap_x, self.app.geo_editor.snap_y])
 
 
+                # Zoom In
                 if key == QtCore.Qt.Key_Equal or key == '=':
                 if key == QtCore.Qt.Key_Equal or key == '=':
                     self.app.plotcanvas.zoom(self.app.defaults['global_zoom_ratio'],
                     self.app.plotcanvas.zoom(self.app.defaults['global_zoom_ratio'],
                                              [self.app.geo_editor.snap_x, self.app.geo_editor.snap_y])
                                              [self.app.geo_editor.snap_x, self.app.geo_editor.snap_y])
@@ -3649,9 +3646,27 @@ class FlatCAMActivityView(QtWidgets.QWidget):
     This class create and control the activity icon displayed in the App status bar
     This class create and control the activity icon displayed in the App status bar
     """
     """
 
 
-    def __init__(self, movie="share/active.gif", icon='share/active_static.png', parent=None):
+    def __init__(self, app, parent=None):
         super().__init__(parent=parent)
         super().__init__(parent=parent)
 
 
+        self.app = app
+
+        if self.app.defaults["global_activity_icon"] == "Ball green":
+            icon = 'share/active_2_static.png'
+            movie = "share/active_2.gif"
+        elif self.app.defaults["global_activity_icon"] == "Ball black":
+            icon = 'share/active_static.png'
+            movie = "share/active.gif"
+        elif self.app.defaults["global_activity_icon"] == "Arrow green":
+            icon = 'share/active_3_static.png'
+            movie = "share/active_3.gif"
+        elif self.app.defaults["global_activity_icon"] == "Eclipse green":
+            icon = 'share/active_4_static.png'
+            movie = "share/active_4.gif"
+        else:
+            icon = 'share/active_static.png'
+            movie = "share/active.gif"
+
         self.setMinimumWidth(200)
         self.setMinimumWidth(200)
         self.movie_path = movie
         self.movie_path = movie
         self.icon_path = icon
         self.icon_path = icon
@@ -3797,377 +3812,4 @@ class FlatCAMSystemTray(QtWidgets.QSystemTrayIcon):
 
 
         exitAction.triggered.connect(self.app.final_save)
         exitAction.triggered.connect(self.app.final_save)
 
 
-
-class BookmarkManager(QtWidgets.QWidget):
-
-    mark_rows = QtCore.pyqtSignal()
-
-    def __init__(self, app, storage, parent=None):
-        super(BookmarkManager, self).__init__(parent)
-
-        self.app = app
-
-        assert isinstance(storage, dict), "Storage argument is not a dictionary"
-
-        self.bm_dict = deepcopy(storage)
-
-        # Icon and title
-        # self.setWindowIcon(parent.app_icon)
-        # self.setWindowTitle(_("Bookmark Manager"))
-        # self.resize(600, 400)
-
-        # title = QtWidgets.QLabel(
-        #     "<font size=8><B>FlatCAM</B></font><BR>"
-        # )
-        # title.setOpenExternalLinks(True)
-
-        # layouts
-        layout = QtWidgets.QVBoxLayout()
-        self.setLayout(layout)
-
-        table_hlay = QtWidgets.QHBoxLayout()
-        layout.addLayout(table_hlay)
-
-        self.table_widget = FCTable(drag_drop=True, protected_rows=[0, 1])
-        self.table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
-        table_hlay.addWidget(self.table_widget)
-
-        self.table_widget.setColumnCount(3)
-        self.table_widget.setColumnWidth(0, 20)
-        self.table_widget.setHorizontalHeaderLabels(
-            [
-                '#',
-                _('Title'),
-                _('Web Link')
-            ]
-        )
-        self.table_widget.horizontalHeaderItem(0).setToolTip(
-            _("Index.\n"
-              "The rows in gray color will populate the Bookmarks menu.\n"
-              "The number of gray colored rows is set in Preferences."))
-        self.table_widget.horizontalHeaderItem(1).setToolTip(
-            _("Description of the link that is set as an menu action.\n"
-              "Try to keep it short because it is installed as a menu item."))
-        self.table_widget.horizontalHeaderItem(2).setToolTip(
-            _("Web Link. E.g: https://your_website.org "))
-
-        # pal = QtGui.QPalette()
-        # pal.setColor(QtGui.QPalette.Background, Qt.white)
-
-        # New Bookmark
-        new_vlay = QtWidgets.QVBoxLayout()
-        layout.addLayout(new_vlay)
-
-        new_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("New Bookmark"))
-        new_vlay.addWidget(new_title_lbl)
-
-        form0 = QtWidgets.QFormLayout()
-        new_vlay.addLayout(form0)
-
-        title_lbl = QtWidgets.QLabel('%s:' % _("Title"))
-        self.title_entry = FCEntry()
-        form0.addRow(title_lbl, self.title_entry)
-
-        link_lbl = QtWidgets.QLabel('%s:' % _("Web Link"))
-        self.link_entry = FCEntry()
-        self.link_entry.set_value('http://')
-        form0.addRow(link_lbl, self.link_entry)
-
-        # Buttons Layout
-        button_hlay = QtWidgets.QHBoxLayout()
-        layout.addLayout(button_hlay)
-
-        add_entry_btn = FCButton(_("Add Entry"))
-        remove_entry_btn = FCButton(_("Remove Entry"))
-        export_list_btn = FCButton(_("Export List"))
-        import_list_btn = FCButton(_("Import List"))
-        closebtn = QtWidgets.QPushButton(_("Close"))
-
-        # button_hlay.addStretch()
-        button_hlay.addWidget(add_entry_btn)
-        button_hlay.addWidget(remove_entry_btn)
-
-        button_hlay.addWidget(export_list_btn)
-        button_hlay.addWidget(import_list_btn)
-        # button_hlay.addWidget(closebtn)
-        # ##############################################################################
-        # ######################## SIGNALS #############################################
-        # ##############################################################################
-
-        add_entry_btn.clicked.connect(self.on_add_entry)
-        remove_entry_btn.clicked.connect(self.on_remove_entry)
-        export_list_btn.clicked.connect(self.on_export_bookmarks)
-        import_list_btn.clicked.connect(self.on_import_bookmarks)
-        self.title_entry.returnPressed.connect(self.on_add_entry)
-        self.link_entry.returnPressed.connect(self.on_add_entry)
-        # closebtn.clicked.connect(self.accept)
-
-        self.table_widget.drag_drop_sig.connect(self.mark_table_rows_for_actions)
-        self.build_bm_ui()
-
-    def build_bm_ui(self):
-
-        self.table_widget.setRowCount(len(self.bm_dict))
-
-        nr_crt = 0
-        sorted_bookmarks = sorted(list(self.bm_dict.items()), key=lambda x: int(x[0]))
-        for entry, bookmark in sorted_bookmarks:
-            row = nr_crt
-            nr_crt += 1
-
-            title = bookmark[0]
-            weblink = bookmark[1]
-
-            id_item = QtWidgets.QTableWidgetItem('%d' % int(nr_crt))
-            # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.table_widget.setItem(row, 0, id_item)  # Tool name/id
-
-            title_item = QtWidgets.QTableWidgetItem(title)
-            self.table_widget.setItem(row, 1, title_item)
-
-            weblink_txt = QtWidgets.QTextBrowser()
-            weblink_txt.setOpenExternalLinks(True)
-            weblink_txt.setFrameStyle(QtWidgets.QFrame.NoFrame)
-            weblink_txt.document().setDefaultStyleSheet("a{ text-decoration: none; }")
-
-            weblink_txt.setHtml('<a href=%s>%s</a>' % (weblink, weblink))
-
-            self.table_widget.setCellWidget(row, 2, weblink_txt)
-
-            vertical_header = self.table_widget.verticalHeader()
-            vertical_header.hide()
-
-            horizontal_header = self.table_widget.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.ResizeToContents)
-            horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
-
-        self.mark_table_rows_for_actions()
-
-        self.app.defaults["global_bookmarks"].clear()
-        for key, val in self.bm_dict.items():
-            self.app.defaults["global_bookmarks"][key] = deepcopy(val)
-
-    def on_add_entry(self, **kwargs):
-        """
-        Add a entry in the Bookmark Table and in the menu actions
-        :return: None
-        """
-        if 'title' in kwargs:
-            title = kwargs['title']
-        else:
-            title = self.title_entry.get_value()
-        if title == '':
-            self.app.inform.emit(f'[ERROR_NOTCL] {_("Title entry is empty.")}')
-            return 'fail'
-
-        if 'link' is kwargs:
-            link = kwargs['link']
-        else:
-            link = self.link_entry.get_value()
-
-        if link == 'http://':
-            self.app.inform.emit(f'[ERROR_NOTCL] {_("Web link entry is empty.")}')
-            return 'fail'
-
-        # if 'http' not in link or 'https' not in link:
-        #     link = 'http://' + link
-
-        for bookmark in self.bm_dict.values():
-            if title == bookmark[0] or link == bookmark[1]:
-                self.app.inform.emit(f'[ERROR_NOTCL] {_("Either the Title or the Weblink already in the table.")}')
-                return 'fail'
-
-        # for some reason if the last char in the weblink is a slash it does not make the link clickable
-        # so I remove it
-        if link[-1] == '/':
-            link = link[:-1]
-        # add the new entry to storage
-        new_entry = len(self.bm_dict) + 1
-        self.bm_dict[str(new_entry)] = [title, link]
-
-        # add the link to the menu but only if it is within the set limit
-        bm_limit = int(self.app.defaults["global_bookmarks_limit"])
-        if len(self.bm_dict) < bm_limit:
-            act = QtWidgets.QAction(parent=self.app.ui.menuhelp_bookmarks)
-            act.setText(title)
-            act.setIcon(QtGui.QIcon('share/link16.png'))
-            act.triggered.connect(lambda: webbrowser.open(link))
-            self.app.ui.menuhelp_bookmarks.insertAction(self.app.ui.menuhelp_bookmarks_manager, act)
-
-        self.app.inform.emit(f'[success] {_("Bookmark added.")}')
-
-        # add the new entry to the bookmark manager table
-        self.build_bm_ui()
-
-    def on_remove_entry(self):
-        """
-        Remove an Entry in the Bookmark table and from the menu actions
-        :return:
-        """
-        index_list = []
-        for model_index in self.table_widget.selectionModel().selectedRows():
-            index = QtCore.QPersistentModelIndex(model_index)
-            index_list.append(index)
-            title_to_remove = self.table_widget.item(model_index.row(), 1).text()
-
-            if title_to_remove == 'FlatCAM' or title_to_remove == 'Backup Site':
-                self.app.inform.emit('[WARNING_NOTCL] %s.' % _("This bookmark can not be removed"))
-                self.build_bm_ui()
-                return
-            else:
-                for k, bookmark in list(self.bm_dict.items()):
-                    if title_to_remove == bookmark[0]:
-                        # remove from the storage
-                        self.bm_dict.pop(k, None)
-
-                        for act in self.app.ui.menuhelp_bookmarks.actions():
-                            if act.text() == title_to_remove:
-                                # disconnect the signal
-                                try:
-                                    act.triggered.disconnect()
-                                except TypeError:
-                                    pass
-                                # remove the action from the menu
-                                self.app.ui.menuhelp_bookmarks.removeAction(act)
-
-        # house keeping: it pays to have keys increased by one
-        new_key = 0
-        new_dict = dict()
-        for k, v in self.bm_dict.items():
-            # we start with key 1 so we can use the len(self.bm_dict)
-            # when adding bookmarks (keys in bm_dict)
-            new_key += 1
-            new_dict[str(new_key)] = v
-
-        self.bm_dict = deepcopy(new_dict)
-        new_dict.clear()
-
-        self.app.inform.emit(f'[success] {_("Bookmark removed.")}')
-
-        # for index in index_list:
-        #     self.table_widget.model().removeRow(index.row())
-        self.build_bm_ui()
-
-    def on_export_bookmarks(self):
-        self.app.report_usage("on_export_bookmarks")
-        self.app.log.debug("on_export_bookmarks()")
-
-        date = str(datetime.today()).rpartition('.')[0]
-        date = ''.join(c for c in date if c not in ':-')
-        date = date.replace(' ', '_')
-
-        filter__ = "Text File (*.TXT);;All Files (*.*)"
-        filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export FlatCAM Preferences"),
-                                                             directory='{l_save}/FlatCAM_{n}_{date}'.format(
-                                                                 l_save=str(self.app.get_last_save_folder()),
-                                                                 n=_("Bookmarks"),
-                                                                 date=date),
-                                                             filter=filter__)
-
-        filename = str(filename)
-
-        if filename == "":
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("FlatCAM bookmarks export cancelled."))
-            return
-        else:
-            try:
-                f = open(filename, 'w')
-                f.close()
-            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
-            except IOError:
-                self.app.log.debug('Creating a new bookmarks file ...')
-                f = open(filename, 'w')
-                f.close()
-            except:
-                e = sys.exc_info()[0]
-                self.app.log.error("Could not load defaults file.")
-                self.app.log.error(str(e))
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Could not load bookmarks file."))
-                return
-
-            # Save update options
-            try:
-                with open(filename, "w") as f:
-                    for title, link in self.bm_dict.items():
-                        line2write = str(title) + ':' + str(link) + '\n'
-                        f.write(line2write)
-            except:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Failed to write bookmarks to file."))
-                return
-        self.app.inform.emit('[success] %s: %s' %
-                             (_("Exported bookmarks to"), filename))
-
-    def on_import_bookmarks(self):
-        self.app.log.debug("on_import_bookmarks()")
-
-        filter_ = "Text File (*.txt);;All Files (*.*)"
-        filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Bookmarks"),
-                                                             filter=filter_)
-
-        filename = str(filename)
-
-        if filename == "":
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("FlatCAM bookmarks import cancelled."))
-        else:
-            try:
-                with open(filename) as f:
-                    bookmarks = f.readlines()
-            except IOError:
-                self.app.log.error("Could not load bookmarks file.")
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Could not load bookmarks file."))
-                return
-
-            for line in bookmarks:
-                proc_line = line.replace(' ', '').partition(':')
-                self.on_add_entry(title=proc_line[0], link=proc_line[2])
-
-            self.app.inform.emit('[success] %s: %s' %
-                                 (_("Imported Bookmarks from"), filename))
-
-    def mark_table_rows_for_actions(self):
-        for row in range(self.table_widget.rowCount()):
-            item_to_paint = self.table_widget.item(row, 0)
-            if row < self.app.defaults["global_bookmarks_limit"]:
-                item_to_paint.setBackground(QtGui.QColor('gray'))
-                # item_to_paint.setForeground(QtGui.QColor('black'))
-            else:
-                item_to_paint.setBackground(QtGui.QColor('white'))
-                # item_to_paint.setForeground(QtGui.QColor('black'))
-
-    def rebuild_actions(self):
-        # rebuild the storage to reflect the order of the lines
-        self.bm_dict.clear()
-        for row in range(self.table_widget.rowCount()):
-            title = self.table_widget.item(row, 1).text()
-            wlink = self.table_widget.cellWidget(row, 2).toPlainText()
-
-            entry = int(row) + 1
-            self.bm_dict.update(
-                {
-                    str(entry): [title, wlink]
-                }
-            )
-
-        self.app.install_bookmarks(book_dict=self.bm_dict)
-
-    # def accept(self):
-    #     self.rebuild_actions()
-    #     super().accept()
-
-    def closeEvent(self, QCloseEvent):
-        self.rebuild_actions()
-        super().closeEvent(QCloseEvent)
-
 # end of file
 # end of file

+ 3 - 3
flatcamGUI/GUIElements.py

@@ -12,7 +12,7 @@
 # ##########################################################
 # ##########################################################
 
 
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5 import QtGui, QtCore, QtWidgets
-from PyQt5.QtCore import Qt, pyqtSlot
+from PyQt5.QtCore import Qt, pyqtSlot, QSettings
 from PyQt5.QtWidgets import QTextEdit, QCompleter, QAction
 from PyQt5.QtWidgets import QTextEdit, QCompleter, QAction
 from PyQt5.QtGui import QKeySequence, QTextCursor
 from PyQt5.QtGui import QKeySequence, QTextCursor
 
 
@@ -376,9 +376,9 @@ class FCEntry(QtWidgets.QLineEdit):
     def get_value(self):
     def get_value(self):
         return str(self.text())
         return str(self.text())
 
 
-    def set_value(self, val):
+    def set_value(self, val, decimals=4):
         if type(val) is float:
         if type(val) is float:
-            self.setText('%.4f' % val)
+            self.setText('%.*f' % (decimals, val))
         else:
         else:
             self.setText(str(val))
             self.setText(str(val))
 
 

+ 96 - 67
flatcamGUI/ObjectUI.py

@@ -22,6 +22,12 @@ fcTranslate.apply_language('strings')
 if '_' not in builtins.__dict__:
 if '_' not in builtins.__dict__:
     _ = gettext.gettext
     _ = gettext.gettext
 
 
+settings = QtCore.QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
 
 
 class ObjectUI(QtWidgets.QWidget):
 class ObjectUI(QtWidgets.QWidget):
     """
     """
@@ -385,7 +391,7 @@ class GerberObjectUI(ObjectUI):
               "- conventional / useful when there is no backlash compensation")
               "- conventional / useful when there is no backlash compensation")
         )
         )
         self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
         self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
-                                            {'label': _('Conv.'), 'value': 'cv'}])
+                                            {'label': _('Conventional'), 'value': 'cv'}])
         grid1.addWidget(self.milling_type_label, 7, 0)
         grid1.addWidget(self.milling_type_label, 7, 0)
         grid1.addWidget(self.milling_type_radio, 7, 1, 1, 2)
         grid1.addWidget(self.milling_type_radio, 7, 1, 1, 2)
 
 
@@ -754,7 +760,12 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(cutzlabel, 0, 0)
         grid1.addWidget(cutzlabel, 0, 0)
         self.cutz_entry = FCDoubleSpinner()
         self.cutz_entry = FCDoubleSpinner()
         self.cutz_entry.set_precision(self.decimals)
         self.cutz_entry.set_precision(self.decimals)
-        self.cutz_entry.setRange(-9999.9999, -0.000001)
+
+        if machinist_setting == 0:
+            self.cutz_entry.setRange(-9999.9999, -0.000001)
+        else:
+            self.cutz_entry.setRange(-9999.9999, 9999.9999)
+
         self.cutz_entry.setSingleStep(0.1)
         self.cutz_entry.setSingleStep(0.1)
 
 
         grid1.addWidget(self.cutz_entry, 0, 1)
         grid1.addWidget(self.cutz_entry, 0, 1)
@@ -768,7 +779,12 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(travelzlabel, 1, 0)
         grid1.addWidget(travelzlabel, 1, 0)
         self.travelz_entry = FCDoubleSpinner()
         self.travelz_entry = FCDoubleSpinner()
         self.travelz_entry.set_precision(self.decimals)
         self.travelz_entry.set_precision(self.decimals)
-        self.travelz_entry.setRange(0.0, 9999.9999)
+
+        if machinist_setting == 0:
+            self.travelz_entry.setRange(0.00001, 9999.9999)
+        else:
+            self.travelz_entry.setRange(-9999.9999, 9999.9999)
+
         self.travelz_entry.setSingleStep(0.1)
         self.travelz_entry.setSingleStep(0.1)
 
 
         grid1.addWidget(self.travelz_entry, 1, 1)
         grid1.addWidget(self.travelz_entry, 1, 1)
@@ -790,7 +806,12 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(toolchzlabel, 3, 0)
         grid1.addWidget(toolchzlabel, 3, 0)
         self.toolchangez_entry = FCDoubleSpinner()
         self.toolchangez_entry = FCDoubleSpinner()
         self.toolchangez_entry.set_precision(self.decimals)
         self.toolchangez_entry.set_precision(self.decimals)
-        self.toolchangez_entry.setRange(0.0, 9999.9999)
+
+        if machinist_setting == 0:
+            self.toolchangez_entry.setRange(0.0, 9999.9999)
+        else:
+            self.toolchangez_entry.setRange(-9999.9999, 9999.9999)
+
         self.toolchangez_entry.setSingleStep(0.1)
         self.toolchangez_entry.setSingleStep(0.1)
 
 
         grid1.addWidget(self.toolchangez_entry, 3, 1)
         grid1.addWidget(self.toolchangez_entry, 3, 1)
@@ -815,7 +836,12 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.eendz_label, 5, 0)
         grid1.addWidget(self.eendz_label, 5, 0)
         self.eendz_entry = FCDoubleSpinner()
         self.eendz_entry = FCDoubleSpinner()
         self.eendz_entry.set_precision(self.decimals)
         self.eendz_entry.set_precision(self.decimals)
-        self.eendz_entry.setRange(0.0, 9999.9999)
+
+        if machinist_setting == 0:
+            self.eendz_entry.setRange(0.0, 9999.9999)
+        else:
+            self.eendz_entry.setRange(-9999.9999, 9999.9999)
+
         self.eendz_entry.setSingleStep(0.1)
         self.eendz_entry.setSingleStep(0.1)
 
 
         grid1.addWidget(self.eendz_entry, 5, 1)
         grid1.addWidget(self.eendz_entry, 5, 1)
@@ -934,12 +960,11 @@ class ExcellonObjectUI(ObjectUI):
         grid2.setColumnStretch(0, 0)
         grid2.setColumnStretch(0, 0)
         grid2.setColumnStretch(1, 1)
         grid2.setColumnStretch(1, 1)
 
 
-        choose_tools_label = QtWidgets.QLabel(
-            _("Select from the Tools Table above\n"
-              "the hole dias that are to be drilled.\n"
-              "Use the # column to make the selection.")
-        )
-        grid2.addWidget(choose_tools_label, 0, 0, 1, 3)
+        # 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
         # ### Choose what to use for Gcode creation: Drills, Slots or Both
         gcode_type_label = QtWidgets.QLabel('<b>%s</b>' % _('Gcode'))
         gcode_type_label = QtWidgets.QLabel('<b>%s</b>' % _('Gcode'))
@@ -967,17 +992,12 @@ class ExcellonObjectUI(ObjectUI):
         # ### Milling Holes Drills ####
         # ### Milling Holes Drills ####
         self.mill_hole_label = QtWidgets.QLabel('<b>%s</b>' % _('Mill Holes'))
         self.mill_hole_label = QtWidgets.QLabel('<b>%s</b>' % _('Mill Holes'))
         self.mill_hole_label.setToolTip(
         self.mill_hole_label.setToolTip(
-            _("Create Geometry for milling holes.")
+            _("Create Geometry for milling holes.\n"
+              "Select from the Tools Table above the hole dias to be\n"
+              "milled. Use the # column to make the selection.")
         )
         )
         grid2.addWidget(self.mill_hole_label, 3, 0, 1, 3)
         grid2.addWidget(self.mill_hole_label, 3, 0, 1, 3)
 
 
-        self.choose_tools_label2 = QtWidgets.QLabel(
-            _("Select from the Tools Table above\n"
-              "the hole dias that are to be milled.\n"
-              "Use the # column to make the selection.")
-        )
-        grid2.addWidget(self.choose_tools_label2, 4, 0, 1, 3)
-
         self.tdlabel = QtWidgets.QLabel('%s:' % _('Drill Tool dia'))
         self.tdlabel = QtWidgets.QLabel('%s:' % _('Drill Tool dia'))
         self.tdlabel.setToolTip(
         self.tdlabel.setToolTip(
             _("Diameter of the cutting tool.")
             _("Diameter of the cutting tool.")
@@ -993,9 +1013,9 @@ class ExcellonObjectUI(ObjectUI):
               "for milling DRILLS toolpaths.")
               "for milling DRILLS toolpaths.")
         )
         )
 
 
-        grid2.addWidget(self.tdlabel, 5, 0)
-        grid2.addWidget(self.tooldia_entry, 5, 1)
-        grid2.addWidget(self.generate_milling_button, 5, 2)
+        grid2.addWidget(self.tdlabel, 4, 0)
+        grid2.addWidget(self.tooldia_entry, 4, 1)
+        grid2.addWidget(self.generate_milling_button, 4, 2)
 
 
         self.stdlabel = QtWidgets.QLabel('%s:' % _('Slot Tool dia'))
         self.stdlabel = QtWidgets.QLabel('%s:' % _('Slot Tool dia'))
         self.stdlabel.setToolTip(
         self.stdlabel.setToolTip(
@@ -1014,9 +1034,9 @@ class ExcellonObjectUI(ObjectUI):
               "for milling SLOTS toolpaths.")
               "for milling SLOTS toolpaths.")
         )
         )
 
 
-        grid2.addWidget(self.stdlabel, 6, 0)
-        grid2.addWidget(self.slot_tooldia_entry, 6, 1)
-        grid2.addWidget(self.generate_milling_slots_button, 6, 2)
+        grid2.addWidget(self.stdlabel, 5, 0)
+        grid2.addWidget(self.slot_tooldia_entry, 5, 1)
+        grid2.addWidget(self.generate_milling_slots_button, 5, 2)
 
 
     def hide_drills(self, state=True):
     def hide_drills(self, state=True):
         if state is True:
         if state is True:
@@ -1152,6 +1172,8 @@ class GeometryObjectUI(ObjectUI):
         # Tool Offset
         # Tool Offset
         self.grid1 = QtWidgets.QGridLayout()
         self.grid1 = QtWidgets.QGridLayout()
         self.geo_tools_box.addLayout(self.grid1)
         self.geo_tools_box.addLayout(self.grid1)
+        self.grid1.setColumnStretch(0, 0)
+        self.grid1.setColumnStretch(1, 1)
 
 
         self.tool_offset_lbl = QtWidgets.QLabel('%s:' % _('Tool Offset'))
         self.tool_offset_lbl = QtWidgets.QLabel('%s:' % _('Tool Offset'))
         self.tool_offset_lbl.setToolTip(
         self.tool_offset_lbl.setToolTip(
@@ -1162,70 +1184,57 @@ class GeometryObjectUI(ObjectUI):
                 "cut and negative for 'inside' cut."
                 "cut and negative for 'inside' cut."
             )
             )
         )
         )
-        self.grid1.addWidget(self.tool_offset_lbl, 0, 0)
         self.tool_offset_entry = FCDoubleSpinner()
         self.tool_offset_entry = FCDoubleSpinner()
         self.tool_offset_entry.set_precision(self.decimals)
         self.tool_offset_entry.set_precision(self.decimals)
         self.tool_offset_entry.setRange(-9999.9999, 9999.9999)
         self.tool_offset_entry.setRange(-9999.9999, 9999.9999)
         self.tool_offset_entry.setSingleStep(0.1)
         self.tool_offset_entry.setSingleStep(0.1)
 
 
-        spacer_lbl = QtWidgets.QLabel(" ")
-        spacer_lbl.setMinimumWidth(80)
-
-        self.grid1.addWidget(self.tool_offset_entry, 0, 1)
-        self.grid1.addWidget(spacer_lbl, 0, 2)
-
-        # ### Add a new Tool ####
-        hlay = QtWidgets.QHBoxLayout()
-        self.geo_tools_box.addLayout(hlay)
+        self.grid1.addWidget(self.tool_offset_lbl, 0, 0)
+        self.grid1.addWidget(self.tool_offset_entry, 0, 1, 1, 2)
 
 
-        # self.addtool_label = QtWidgets.QLabel('<b>Tool</b>')
-        # self.addtool_label.setToolTip(
-        #     "Add/Copy/Delete a tool to the tool list."
-        # )
         self.addtool_entry_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('Tool Dia'))
         self.addtool_entry_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('Tool Dia'))
         self.addtool_entry_lbl.setToolTip(
         self.addtool_entry_lbl.setToolTip(
-            _(
-                "Diameter for the new tool"
-            )
+            _("Diameter for the new tool")
         )
         )
         self.addtool_entry = FCDoubleSpinner()
         self.addtool_entry = FCDoubleSpinner()
         self.addtool_entry.set_precision(self.decimals)
         self.addtool_entry.set_precision(self.decimals)
         self.addtool_entry.setRange(0.00001, 9999.9999)
         self.addtool_entry.setRange(0.00001, 9999.9999)
         self.addtool_entry.setSingleStep(0.1)
         self.addtool_entry.setSingleStep(0.1)
 
 
-        hlay.addWidget(self.addtool_entry_lbl)
-        hlay.addWidget(self.addtool_entry)
-
-        grid2 = QtWidgets.QGridLayout()
-        self.geo_tools_box.addLayout(grid2)
-
         self.addtool_btn = QtWidgets.QPushButton(_('Add'))
         self.addtool_btn = QtWidgets.QPushButton(_('Add'))
         self.addtool_btn.setToolTip(
         self.addtool_btn.setToolTip(
-            _(
-                "Add a new tool to the Tool Table\n"
-                "with the diameter specified above."
-            )
+            _("Add a new tool to the Tool Table\n"
+              "with the specified diameter.")
+        )
+
+        self.grid1.addWidget(self.addtool_entry_lbl, 1, 0)
+        self.grid1.addWidget(self.addtool_entry, 1, 1)
+        self.grid1.addWidget(self.addtool_btn, 1, 2)
+
+        self.addtool_from_db_btn = QtWidgets.QPushButton(_('Add Tool from DataBase'))
+        self.addtool_from_db_btn.setToolTip(
+            _("Add a new tool to the Tool Table\n"
+              "from the Tool DataBase.")
         )
         )
+        self.grid1.addWidget(self.addtool_from_db_btn, 2, 0, 1, 3)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.geo_tools_box.addLayout(grid2)
 
 
         self.copytool_btn = QtWidgets.QPushButton(_('Copy'))
         self.copytool_btn = QtWidgets.QPushButton(_('Copy'))
         self.copytool_btn.setToolTip(
         self.copytool_btn.setToolTip(
-            _(
-                "Copy a selection of tools in the Tool Table\n"
-                "by first selecting a row in the Tool Table."
-            )
+            _("Copy a selection of tools in the Tool Table\n"
+              "by first selecting a row in the Tool Table.")
         )
         )
 
 
         self.deltool_btn = QtWidgets.QPushButton(_('Delete'))
         self.deltool_btn = QtWidgets.QPushButton(_('Delete'))
         self.deltool_btn.setToolTip(
         self.deltool_btn.setToolTip(
-            _(
-                "Delete a selection of tools in the Tool Table\n"
-                "by first selecting a row in the Tool Table."
-            )
+            _("Delete a selection of tools in the Tool Table\n"
+              "by first selecting a row in the Tool Table.")
         )
         )
 
 
-        grid2.addWidget(self.addtool_btn, 0, 0)
-        grid2.addWidget(self.copytool_btn, 0, 1)
-        grid2.addWidget(self.deltool_btn, 0, 2)
+        grid2.addWidget(self.copytool_btn, 0, 0)
+        grid2.addWidget(self.deltool_btn, 0, 1)
 
 
         self.empty_label = QtWidgets.QLabel('')
         self.empty_label = QtWidgets.QLabel('')
         self.geo_tools_box.addWidget(self.empty_label)
         self.geo_tools_box.addWidget(self.empty_label)
@@ -1295,7 +1304,12 @@ class GeometryObjectUI(ObjectUI):
         )
         )
         self.cutz_entry = FCDoubleSpinner()
         self.cutz_entry = FCDoubleSpinner()
         self.cutz_entry.set_precision(self.decimals)
         self.cutz_entry.set_precision(self.decimals)
-        self.cutz_entry.setRange(-9999.9999, -0.00001)
+
+        if machinist_setting == 0:
+            self.cutz_entry.setRange(-9999.9999, -0.00001)
+        else:
+            self.cutz_entry.setRange(-9999.9999, 9999.9999)
+
         self.cutz_entry.setSingleStep(0.1)
         self.cutz_entry.setSingleStep(0.1)
 
 
         self.grid3.addWidget(cutzlabel, 3, 0)
         self.grid3.addWidget(cutzlabel, 3, 0)
@@ -1335,7 +1349,12 @@ class GeometryObjectUI(ObjectUI):
         )
         )
         self.travelz_entry = FCDoubleSpinner()
         self.travelz_entry = FCDoubleSpinner()
         self.travelz_entry.set_precision(self.decimals)
         self.travelz_entry.set_precision(self.decimals)
-        self.travelz_entry.setRange(0, 9999.9999)
+
+        if machinist_setting == 0:
+            self.travelz_entry.setRange(0.00001, 9999.9999)
+        else:
+            self.travelz_entry.setRange(-9999.9999, 9999.9999)
+
         self.travelz_entry.setSingleStep(0.1)
         self.travelz_entry.setSingleStep(0.1)
 
 
         self.grid3.addWidget(travelzlabel, 5, 0)
         self.grid3.addWidget(travelzlabel, 5, 0)
@@ -1358,7 +1377,12 @@ class GeometryObjectUI(ObjectUI):
         )
         )
         self.toolchangez_entry = FCDoubleSpinner()
         self.toolchangez_entry = FCDoubleSpinner()
         self.toolchangez_entry.set_precision(self.decimals)
         self.toolchangez_entry.set_precision(self.decimals)
-        self.toolchangez_entry.setRange(0, 9999.9999)
+
+        if machinist_setting == 0:
+            self.toolchangez_entry.setRange(0, 9999.9999)
+        else:
+            self.toolchangez_entry.setRange(-9999.9999, 9999.9999)
+
         self.toolchangez_entry.setSingleStep(0.1)
         self.toolchangez_entry.setSingleStep(0.1)
 
 
         self.grid3.addWidget(self.toolchangeg_cb, 6, 0, 1, 2)
         self.grid3.addWidget(self.toolchangeg_cb, 6, 0, 1, 2)
@@ -1385,7 +1409,12 @@ class GeometryObjectUI(ObjectUI):
         )
         )
         self.gendz_entry = FCDoubleSpinner()
         self.gendz_entry = FCDoubleSpinner()
         self.gendz_entry.set_precision(self.decimals)
         self.gendz_entry.set_precision(self.decimals)
-        self.gendz_entry.setRange(0, 9999.9999)
+
+        if machinist_setting == 0:
+            self.gendz_entry.setRange(0, 9999.9999)
+        else:
+            self.gendz_entry.setRange(-9999.9999, 9999.9999)
+
         self.gendz_entry.setSingleStep(0.1)
         self.gendz_entry.setSingleStep(0.1)
 
 
         self.grid3.addWidget(self.endzlabel, 9, 0)
         self.grid3.addWidget(self.endzlabel, 9, 0)

+ 1 - 1
flatcamGUI/PlotCanvas.py

@@ -120,7 +120,7 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
         a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
         a3l_mm = np.array([(0, 0), (420, 0), (420, 297), (0, 297)])
         a3l_mm = np.array([(0, 0), (420, 0), (420, 297), (0, 297)])
 
 
-        if self.fcapp.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
+        if self.fcapp.defaults['units'].upper() == 'MM':
             if self.fcapp.defaults['global_workspaceT'] == 'A4P':
             if self.fcapp.defaults['global_workspaceT'] == 'A4P':
                 a = a4p_mm
                 a = a4p_mm
             elif self.fcapp.defaults['global_workspaceT'] == 'A4L':
             elif self.fcapp.defaults['global_workspaceT'] == 'A4L':

+ 76 - 14
flatcamGUI/PreferencesUI.py

@@ -18,6 +18,12 @@ fcTranslate.apply_language('strings')
 if '_' not in builtins.__dict__:
 if '_' not in builtins.__dict__:
     _ = gettext.gettext
     _ = gettext.gettext
 
 
+settings = QtCore.QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
 
 
 class OptionsGroupUI(QtWidgets.QGroupBox):
 class OptionsGroupUI(QtWidgets.QGroupBox):
     def __init__(self, title, parent=None):
     def __init__(self, title, parent=None):
@@ -1166,6 +1172,7 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
 
 
         self.proj_ois = OptionalInputSection(self.save_type_cb, [self.compress_label, self.compress_spinner], True)
         self.proj_ois = OptionalInputSection(self.save_type_cb, [self.compress_label, self.compress_spinner], True)
 
 
+        # Bookmarks Limit in the Help Menu
         self.bm_limit_spinner = FCSpinner()
         self.bm_limit_spinner = FCSpinner()
         self.bm_limit_label = QtWidgets.QLabel('%s:' % _('Bookmarks limit'))
         self.bm_limit_label = QtWidgets.QLabel('%s:' % _('Bookmarks limit'))
         self.bm_limit_label.setToolTip(
         self.bm_limit_label.setToolTip(
@@ -1177,6 +1184,18 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.bm_limit_label, 18, 0)
         grid0.addWidget(self.bm_limit_label, 18, 0)
         grid0.addWidget(self.bm_limit_spinner, 18, 1)
         grid0.addWidget(self.bm_limit_spinner, 18, 1)
 
 
+        # Machinist settings that allow unsafe settings
+        self.machinist_cb = FCCheckBox(_("Allow Machinist Unsafe Settings"))
+        self.machinist_cb.setToolTip(
+            _("If checked, some of the application settings will be allowed\n"
+              "to have values that are usually unsafe to use.\n"
+              "Like Z travel negative values or Z Cut positive values.\n"
+              "It will applied at the next application start.\n"
+              "<<WARNING>>: Don't change this unless you know what you are doing !!!")
+        )
+
+        grid0.addWidget(self.machinist_cb, 19, 0, 1, 2)
+
         self.layout.addStretch()
         self.layout.addStretch()
 
 
         if sys.platform != 'win32':
         if sys.platform != 'win32':
@@ -2154,7 +2173,12 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
         )
         )
         grid2.addWidget(cutzlabel, 0, 0)
         grid2.addWidget(cutzlabel, 0, 0)
         self.cutz_entry = FCDoubleSpinner()
         self.cutz_entry = FCDoubleSpinner()
-        self.cutz_entry.set_range(-9999, -0.000001)
+
+        if machinist_setting == 0:
+            self.cutz_entry.set_range(-9999.9999, -0.000001)
+        else:
+            self.cutz_entry.set_range(-9999.9999, 9999.9999)
+
         self.cutz_entry.setSingleStep(0.1)
         self.cutz_entry.setSingleStep(0.1)
         self.cutz_entry.set_precision(4)
         self.cutz_entry.set_precision(4)
         grid2.addWidget(self.cutz_entry, 0, 1)
         grid2.addWidget(self.cutz_entry, 0, 1)
@@ -2168,7 +2192,11 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
         grid2.addWidget(travelzlabel, 1, 0)
         grid2.addWidget(travelzlabel, 1, 0)
         self.travelz_entry = FCDoubleSpinner()
         self.travelz_entry = FCDoubleSpinner()
         self.travelz_entry.set_precision(4)
         self.travelz_entry.set_precision(4)
-        self.travelz_entry.set_range(0, 999)
+
+        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(self.travelz_entry, 1, 1)
         grid2.addWidget(self.travelz_entry, 1, 1)
 
 
@@ -2190,7 +2218,11 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
         grid2.addWidget(toolchangezlabel, 3, 0)
         grid2.addWidget(toolchangezlabel, 3, 0)
         self.toolchangez_entry = FCDoubleSpinner()
         self.toolchangez_entry = FCDoubleSpinner()
         self.toolchangez_entry.set_precision(4)
         self.toolchangez_entry.set_precision(4)
-        self.toolchangez_entry.set_range(0, 999)
+
+        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(self.toolchangez_entry, 3, 1)
         grid2.addWidget(self.toolchangez_entry, 3, 1)
 
 
@@ -2202,7 +2234,11 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
         )
         )
         self.eendz_entry = FCDoubleSpinner()
         self.eendz_entry = FCDoubleSpinner()
         self.eendz_entry.set_precision(4)
         self.eendz_entry.set_precision(4)
-        self.eendz_entry.set_range(0, 999)
+
+        if machinist_setting == 0:
+            self.eendz_entry.set_range(0.0000, 9999.9999)
+        else:
+            self.eendz_entry.set_range(-9999.9999, 9999.9999)
 
 
         grid2.addWidget(endzlabel, 4, 0)
         grid2.addWidget(endzlabel, 4, 0)
         grid2.addWidget(self.eendz_entry, 4, 1)
         grid2.addWidget(self.eendz_entry, 4, 1)
@@ -2975,7 +3011,12 @@ class GeometryOptPrefGroupUI(OptionsGroupUI):
               "below the copper surface.")
               "below the copper surface.")
         )
         )
         self.cutz_entry = FCDoubleSpinner()
         self.cutz_entry = FCDoubleSpinner()
-        self.cutz_entry.set_range(-999.999, -0.000001)
+
+        if machinist_setting == 0:
+            self.cutz_entry.set_range(-9999.9999, -0.000001)
+        else:
+            self.cutz_entry.set_range(-9999.9999, 9999.9999)
+
         self.cutz_entry.set_precision(4)
         self.cutz_entry.set_precision(4)
         self.cutz_entry.setSingleStep(0.1)
         self.cutz_entry.setSingleStep(0.1)
         self.cutz_entry.setWrapping(True)
         self.cutz_entry.setWrapping(True)
@@ -3023,7 +3064,12 @@ class GeometryOptPrefGroupUI(OptionsGroupUI):
               "moving without cutting.")
               "moving without cutting.")
         )
         )
         self.travelz_entry = FCDoubleSpinner()
         self.travelz_entry = FCDoubleSpinner()
-        self.travelz_entry.set_range(0, 99999)
+
+        if machinist_setting == 0:
+            self.travelz_entry.set_range(0.0001, 9999.9999)
+        else:
+            self.travelz_entry.set_range(-9999.9999, 9999.9999)
+
         self.travelz_entry.set_precision(4)
         self.travelz_entry.set_precision(4)
         self.travelz_entry.setSingleStep(0.1)
         self.travelz_entry.setSingleStep(0.1)
         self.travelz_entry.setWrapping(True)
         self.travelz_entry.setWrapping(True)
@@ -3052,7 +3098,12 @@ class GeometryOptPrefGroupUI(OptionsGroupUI):
             )
             )
         )
         )
         self.toolchangez_entry = FCDoubleSpinner()
         self.toolchangez_entry = FCDoubleSpinner()
-        self.toolchangez_entry.set_range(0, 99999)
+
+        if machinist_setting == 0:
+            self.toolchangez_entry.set_range(0.000, 9999.9999)
+        else:
+            self.toolchangez_entry.set_range(-9999.9999, 9999.9999)
+
         self.toolchangez_entry.set_precision(4)
         self.toolchangez_entry.set_precision(4)
         self.toolchangez_entry.setSingleStep(0.1)
         self.toolchangez_entry.setSingleStep(0.1)
         self.toolchangez_entry.setWrapping(True)
         self.toolchangez_entry.setWrapping(True)
@@ -3067,7 +3118,12 @@ class GeometryOptPrefGroupUI(OptionsGroupUI):
               "the last move at the end of the job.")
               "the last move at the end of the job.")
         )
         )
         self.gendz_entry = FCDoubleSpinner()
         self.gendz_entry = FCDoubleSpinner()
-        self.gendz_entry.set_range(0, 99999)
+
+        if machinist_setting == 0:
+            self.gendz_entry.set_range(0.000, 9999.9999)
+        else:
+            self.gendz_entry.set_range(-9999.9999, 9999.9999)
+
         self.gendz_entry.set_precision(4)
         self.gendz_entry.set_precision(4)
         self.gendz_entry.setSingleStep(0.1)
         self.gendz_entry.setSingleStep(0.1)
         self.gendz_entry.setWrapping(True)
         self.gendz_entry.setWrapping(True)
@@ -3402,18 +3458,15 @@ class CNCJobGenPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(QtWidgets.QLabel(''), 1, 2)
         grid0.addWidget(QtWidgets.QLabel(''), 1, 2)
 
 
         # Display Annotation
         # Display Annotation
-        self.annotation_label = QtWidgets.QLabel('%s:' % _("Display Annotation"))
-        self.annotation_label.setToolTip(
+        self.annotation_cb = FCCheckBox(_("Display Annotation"))
+        self.annotation_cb.setToolTip(
             _("This selects if to display text annotation on the plot.\n"
             _("This selects if to display text annotation on the plot.\n"
               "When checked it will display numbers in order for each end\n"
               "When checked it will display numbers in order for each end\n"
               "of a travel line."
               "of a travel line."
               )
               )
         )
         )
-        self.annotation_cb = FCCheckBox()
 
 
-        grid0.addWidget(self.annotation_label, 2, 0)
-        grid0.addWidget(self.annotation_cb, 2, 1)
-        grid0.addWidget(QtWidgets.QLabel(''), 2, 2)
+        grid0.addWidget(self.annotation_cb, 2, 0, 1, 3)
 
 
         # ###################################################################
         # ###################################################################
         # Number of circle steps for circular aperture linear approximation #
         # Number of circle steps for circular aperture linear approximation #
@@ -3491,6 +3544,15 @@ class CNCJobGenPrefGroupUI(OptionsGroupUI):
         coords_type_label.hide()
         coords_type_label.hide()
         self.coords_type_radio.hide()
         self.coords_type_radio.hide()
 
 
+        # Line Endings
+        self.line_ending_cb = FCCheckBox(_("Force Windows style line-ending"))
+        self.line_ending_cb.setToolTip(
+            _("When checked will force a Windows style line-ending\n"
+              "(\\r\\n) on non-Windows OS's.")
+        )
+
+        grid0.addWidget(self.line_ending_cb, 9, 0, 1, 3)
+
         self.layout.addStretch()
         self.layout.addStretch()
 
 
 
 

+ 1 - 1
flatcamTools/ToolDistance.py

@@ -34,7 +34,7 @@ class Distance(FlatCAMTool):
 
 
         self.app = app
         self.app = app
         self.canvas = self.app.plotcanvas
         self.canvas = self.app.plotcanvas
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+        self.units = self.app.defaults['units'].lower()
 
 
         # ## Title
         # ## Title
         title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
         title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)

+ 1 - 1
flatcamTools/ToolDistanceMin.py

@@ -35,7 +35,7 @@ class DistanceMin(FlatCAMTool):
 
 
         self.app = app
         self.app = app
         self.canvas = self.app.plotcanvas
         self.canvas = self.app.plotcanvas
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+        self.units = self.app.defaults['units'].lower()
 
 
         # ## Title
         # ## Title
         title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
         title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)

+ 1 - 1
flatcamTools/ToolNonCopperClear.py

@@ -421,7 +421,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.ncc_offset_spinner.set_precision(4)
         self.ncc_offset_spinner.set_precision(4)
         self.ncc_offset_spinner.setWrapping(True)
         self.ncc_offset_spinner.setWrapping(True)
 
 
-        units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        units = self.app.defaults['units'].upper()
         if units == 'MM':
         if units == 'MM':
             self.ncc_offset_spinner.setSingleStep(0.1)
             self.ncc_offset_spinner.setSingleStep(0.1)
         else:
         else:

+ 1 - 1
flatcamTools/ToolOptimal.py

@@ -39,7 +39,7 @@ class ToolOptimal(FlatCAMTool):
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.units = self.app.defaults['units'].upper()
         self.decimals = 4
         self.decimals = 4
 
 
         # ############################################################################
         # ############################################################################

+ 7 - 23
flatcamTools/ToolPaint.py

@@ -1281,21 +1281,14 @@ class ToolPaint(FlatCAMTool, Gerber):
                     obj.solid_geometry = obj.solid_geometry.buffer(0)
                     obj.solid_geometry = obj.solid_geometry.buffer(0)
 
 
         poly = self.find_polygon(point=inside_pt, geoset=obj.solid_geometry)
         poly = self.find_polygon(point=inside_pt, geoset=obj.solid_geometry)
-        paint_method = method if method is None else self.paintmethod_combo.get_value()
+
+        paint_method = method if method is not None else self.paintmethod_combo.get_value()
 
 
         if margin is not None:
         if margin is not None:
             paint_margin = margin
             paint_margin = margin
         else:
         else:
-            try:
-                paint_margin = float(self.paintmargin_entry.get_value())
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                         _("Wrong value format entered, use a number."))
-                    return
+            paint_margin = float(self.paintmargin_entry.get_value())
+
         # determine if to use the progressive plotting
         # determine if to use the progressive plotting
         if self.app.defaults["tools_paint_plotting"] == 'progressive':
         if self.app.defaults["tools_paint_plotting"] == 'progressive':
             prog_plot = True
             prog_plot = True
@@ -1558,21 +1551,12 @@ class ToolPaint(FlatCAMTool, Gerber):
         Usage of the different one is related to when this function is called from a TcL command.
         Usage of the different one is related to when this function is called from a TcL command.
         :return:
         :return:
         """
         """
-        paint_method = method if method is None else self.paintmethod_combo.get_value()
+        paint_method = method if method is not None else self.paintmethod_combo.get_value()
 
 
         if margin is not None:
         if margin is not None:
             paint_margin = margin
             paint_margin = margin
         else:
         else:
-            try:
-                paint_margin = float(self.paintmargin_entry.get_value())
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                         _("Wrong value format entered, use a number."))
-                    return
+            paint_margin = float(self.paintmargin_entry.get_value())
 
 
         # determine if to use the progressive plotting
         # determine if to use the progressive plotting
         if self.app.defaults["tools_paint_plotting"] == 'progressive':
         if self.app.defaults["tools_paint_plotting"] == 'progressive':
@@ -2035,7 +2019,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         Usage of the different one is related to when this function is called from a TcL command.
         Usage of the different one is related to when this function is called from a TcL command.
         :return:
         :return:
         """
         """
-        paint_method = method if method is None else self.paintmethod_combo.get_value()
+        paint_method = method if method is not None else self.paintmethod_combo.get_value()
 
 
         if margin is not None:
         if margin is not None:
             paint_margin = margin
             paint_margin = margin

+ 13 - 1
make_win.py → make_freezed.py

@@ -89,7 +89,19 @@ else:
 
 
 print("INCLUDE_FILES", include_files)
 print("INCLUDE_FILES", include_files)
 
 
+
+def getTargetName():
+    my_OS = platform.system()
+    if my_OS == 'Linux':
+        return "FlatCAM"
+    elif my_OS == 'Windows':
+        return "FlatCAM.exe"
+    else:
+        return "FlatCAM.dmg"
+
+
 # execfile('clean.py')
 # execfile('clean.py')
+exe = Executable("FlatCAM.py", icon='share/flatcam_icon48.ico', base=base, targetName=getTargetName())
 
 
 setup(
 setup(
     name="FlatCAM",
     name="FlatCAM",
@@ -97,5 +109,5 @@ setup(
     version="8.9",
     version="8.9",
     description="FlatCAM: 2D Computer Aided PCB Manufacturing",
     description="FlatCAM: 2D Computer Aided PCB Manufacturing",
     options=dict(build_exe=buildOptions),
     options=dict(build_exe=buildOptions),
-    executables=[Executable("FlatCAM.py", icon='share/flatcam_icon48.ico', base=base)]
+    executables=[exe]
 )
 )

BIN
share/database32.png


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