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

Convertion to Qt. Major refactoring.

Juan Pablo Caram 11 лет назад
Родитель
Сommit
16734f5d1a

+ 13 - 11
FlatCAM.py

@@ -1,13 +1,15 @@
-############################################################
-# FlatCAM: 2D Post-processing for Manufacturing            #
-# http://caram.cl/software/flatcam                         #
-# Author: Juan Pablo Caram (c)                             #
-# Date: 2/5/2014                                           #
-# MIT Licence                                              #
-############################################################
+import sys
+from PyQt4 import QtGui
+from FlatCAMApp import App
 
-from gi.repository import Gtk
-from FlatCAMApp import *
+def debug_trace():
+  '''Set a tracepoint in the Python debugger that works with Qt'''
+  from PyQt4.QtCore import pyqtRemoveInputHook
+  #from pdb import set_trace
+  pyqtRemoveInputHook()
+  #set_trace()
 
-app = App()
-Gtk.main()
+debug_trace()
+app = QtGui.QApplication(sys.argv)
+fc = App()
+sys.exit(app.exec_())

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


+ 39 - 0
FlatCAMCommon.py

@@ -0,0 +1,39 @@
+class LoudDict(dict):
+    """
+    A Dictionary with a callback for
+    item changes.
+    """
+
+    def __init__(self, *args, **kwargs):
+        dict.__init__(self, *args, **kwargs)
+        self.callback = lambda x: None
+
+    def __setitem__(self, key, value):
+        """
+        Overridden __setitem__ method. Will emit 'changed(QString)'
+        if the item was changed, with key as parameter.
+        """
+        if key in self and self.__getitem__(key) == value:
+            return
+
+        dict.__setitem__(self, key, value)
+        self.callback(key)
+
+    def update(self, *args, **kwargs):
+        if len(args) > 1:
+            raise TypeError("update expected at most 1 arguments, got %d" % len(args))
+        other = dict(*args, **kwargs)
+        for key in other:
+            self[key] = other[key]
+
+    def set_change_callback(self, callback):
+        """
+        Assigns a function as callback on item change. The callback
+        will receive the key of the object that was changed.
+
+        :param callback: Function to call on item change.
+        :type callback: func
+        :return: None
+        """
+
+        self.callback = callback

+ 688 - 0
FlatCAMGUI.py

@@ -0,0 +1,688 @@
+from PyQt4 import QtGui, QtCore, Qt
+from GUIElements import *
+
+
+class FlatCAMGUI(QtGui.QMainWindow):
+
+    def __init__(self):
+        super(FlatCAMGUI, self).__init__()
+
+        # Divine icon pack by Ipapun @ finicons.com
+
+        ############
+        ### Menu ###
+        ############
+        self.menu = self.menuBar()
+
+        ### File ###
+        self.menufile = self.menu.addMenu('&File')
+
+        # New
+        self.menufilenew = QtGui.QAction(QtGui.QIcon('share/file16.png'), '&New', self)
+        self.menufile.addAction(self.menufilenew)
+        # Open recent
+
+        # Recent
+        self.recent = self.menufile.addMenu(QtGui.QIcon('share/folder16.png'), "Open recent ...")
+
+        # Open gerber
+        self.menufileopengerber = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open &Gerber ...', self)
+        self.menufile.addAction(self.menufileopengerber)
+
+        # Open Excellon ...
+        self.menufileopenexcellon = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open &Excellon ...', self)
+        self.menufile.addAction(self.menufileopenexcellon)
+
+        # Open G-Code ...
+        self.menufileopengcode = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open G-&Code ...', self)
+        self.menufile.addAction(self.menufileopengcode)
+
+        # Open Project ...
+        self.menufileopenproject = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open &Project ...', self)
+        self.menufile.addAction(self.menufileopenproject)
+
+        # Save Project
+        self.menufilesaveproject = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), '&Save Project', self)
+        self.menufile.addAction(self.menufilesaveproject)
+
+        # Save Project As ...
+        self.menufilesaveprojectas = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), 'Save Project &As ...', self)
+        self.menufile.addAction(self.menufilesaveprojectas)
+
+        # Save Project Copy ...
+        self.menufilesaveprojectcopy = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), 'Save Project C&opy ...', self)
+        self.menufile.addAction(self.menufilesaveprojectcopy)
+
+        # Save Defaults
+        self.menufilesavedefaults = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), 'Save &Defaults', self)
+        self.menufile.addAction(self.menufilesavedefaults)
+
+        # Quit
+        exit_action = QtGui.QAction(QtGui.QIcon('share/power16.png'), '&Exit', self)
+        # exitAction.setShortcut('Ctrl+Q')
+        # exitAction.setStatusTip('Exit application')
+        exit_action.triggered.connect(QtGui.qApp.quit)
+
+        self.menufile.addAction(exit_action)
+
+        ### Edit ###
+        self.menuedit = self.menu.addMenu('&Edit')
+        self.menueditdelete = self.menuedit.addAction(QtGui.QIcon('share/trash16.png'), 'Delete')
+
+        ### Options ###
+        self.menuoptions = self.menu.addMenu('&Options')
+        self.menuoptions_transfer = self.menuoptions.addMenu('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")
+
+        ### View ###
+        self.menuview = self.menu.addMenu('&View')
+        self.menuviewdisableall = self.menuview.addAction(QtGui.QIcon('share/clear_plot16.png'), 'Disable all plots')
+        self.menuviewdisableother = self.menuview.addAction(QtGui.QIcon('share/clear_plot16.png'),
+                                                            'Disable all plots but this one')
+        self.menuviewenable = self.menuview.addAction(QtGui.QIcon('share/replot16.png'), 'Enable all plots')
+
+        ### Tool ###
+        self.menutool = self.menu.addMenu('&Tool')
+
+        ### Help ###
+        self.menuhelp = self.menu.addMenu('&Help')
+        self.menuhelp_about = self.menuhelp.addAction(QtGui.QIcon('share/tv16.png'), 'About FlatCAM')
+        self.menuhelp_manual = self.menuhelp.addAction(QtGui.QIcon('share/globe16.png'), 'Manual')
+
+        ###############
+        ### Toolbar ###
+        ###############
+        self.toolbar = QtGui.QToolBar()
+        self.addToolBar(self.toolbar)
+
+        self.zoom_fit_btn = self.toolbar.addAction(QtGui.QIcon('share/zoom_fit32.png'), "&Zoom Fit")
+        self.zoom_out_btn = self.toolbar.addAction(QtGui.QIcon('share/zoom_out32.png'), "&Zoom Out")
+        self.zoom_in_btn = self.toolbar.addAction(QtGui.QIcon('share/zoom_in32.png'), "&Zoom In")
+        self.clear_plot_btn = self.toolbar.addAction(QtGui.QIcon('share/clear_plot32.png'), "&Clear Plot")
+        self.replot_btn = self.toolbar.addAction(QtGui.QIcon('share/replot32.png'), "&Replot")
+        self.delete_btn = self.toolbar.addAction(QtGui.QIcon('share/delete32.png'), "&Delete")
+
+        ################
+        ### Splitter ###
+        ################
+        self.splitter = QtGui.QSplitter()
+        self.setCentralWidget(self.splitter)
+
+        ################
+        ### Notebook ###
+        ################
+        self.notebook = QtGui.QTabWidget()
+        # self.notebook.setMinimumWidth(250)
+
+        ### Projet ###
+        project_tab = QtGui.QWidget()
+        project_tab.setMinimumWidth(250)  # Hack
+        self.project_tab_layout = QtGui.QVBoxLayout(project_tab)
+        self.project_tab_layout.setContentsMargins(2, 2, 2, 2)
+        self.notebook.addTab(project_tab, "Project")
+
+        ### Selected ###
+        self.selected_tab = QtGui.QWidget()
+        self.selected_tab_layout = QtGui.QVBoxLayout(self.selected_tab)
+        self.selected_tab_layout.setContentsMargins(2, 2, 2, 2)
+        self.selected_scroll_area = VerticalScrollArea()
+        self.selected_tab_layout.addWidget(self.selected_scroll_area)
+        self.notebook.addTab(self.selected_tab, "Selected")
+
+        ### Options ###
+        self.options_tab = QtGui.QWidget()
+        self.options_tab.setContentsMargins(0, 0, 0, 0)
+        self.options_tab_layout = QtGui.QVBoxLayout(self.options_tab)
+        self.options_tab_layout.setContentsMargins(2, 2, 2, 2)
+
+        hlay1 = QtGui.QHBoxLayout()
+        self.options_tab_layout.addLayout(hlay1)
+
+        self.icon = QtGui.QLabel()
+        self.icon.setPixmap(QtGui.QPixmap('share/gear48.png'))
+        hlay1.addWidget(self.icon)
+
+        self.options_combo = QtGui.QComboBox()
+        self.options_combo.addItem("APPLICATION DEFAULTS")
+        self.options_combo.addItem("PROJECT OPTIONS")
+        hlay1.addWidget(self.options_combo)
+        hlay1.addStretch()
+
+        self.options_scroll_area = VerticalScrollArea()
+        self.options_tab_layout.addWidget(self.options_scroll_area)
+
+        self.notebook.addTab(self.options_tab, "Options")
+
+        ### Tool ###
+        self.tool_tab = QtGui.QWidget()
+        self.tool_tab_layout = QtGui.QVBoxLayout(self.tool_tab)
+        self.tool_tab_layout.setContentsMargins(2, 2, 2, 2)
+        self.notebook.addTab(self.tool_tab, "Tool")
+        self.tool_scroll_area = VerticalScrollArea()
+        self.tool_tab_layout.addWidget(self.tool_scroll_area)
+
+        self.splitter.addWidget(self.notebook)
+
+        ######################
+        ### Plot and other ###
+        ######################
+        right_widget = QtGui.QWidget()
+        # right_widget.setContentsMargins(0, 0, 0, 0)
+        self.splitter.addWidget(right_widget)
+        self.right_layout = QtGui.QVBoxLayout()
+        self.right_layout.setMargin(0)
+        # self.right_layout.setContentsMargins(0, 0, 0, 0)
+        right_widget.setLayout(self.right_layout)
+
+
+        ################
+        ### Info bar ###
+        ################
+        infobar = self.statusBar()
+
+        self.info_label = QtGui.QLabel("Welcome to FlatCAM.")
+        self.info_label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
+        infobar.addWidget(self.info_label, stretch=1)
+
+        self.position_label = QtGui.QLabel("")
+        self.position_label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
+        self.position_label.setMinimumWidth(110)
+        infobar.addWidget(self.position_label)
+
+        self.units_label = QtGui.QLabel("[in]")
+        # self.units_label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
+        self.units_label.setMargin(2)
+        infobar.addWidget(self.units_label)
+
+        self.progress_bar = QtGui.QProgressBar()
+        self.progress_bar.setMinimum(0)
+        self.progress_bar.setMaximum(100)
+        infobar.addWidget(self.progress_bar)
+
+        #############
+        ### Icons ###
+        #############
+        app_icon = QtGui.QIcon()
+        app_icon.addFile('share/flatcam_icon16.png', QtCore.QSize(16, 16))
+        app_icon.addFile('share/flatcam_icon24.png', QtCore.QSize(24, 24))
+        app_icon.addFile('share/flatcam_icon32.png', QtCore.QSize(32, 32))
+        app_icon.addFile('share/flatcam_icon48.png', QtCore.QSize(48, 48))
+        app_icon.addFile('share/flatcam_icon128.png', QtCore.QSize(128, 128))
+        app_icon.addFile('share/flatcam_icon256.png', QtCore.QSize(256, 256))
+        self.setWindowIcon(app_icon)
+
+        self.setGeometry(100, 100, 750, 500)
+        self.setWindowTitle('FlatCAM - 0.5')
+        self.show()
+
+
+class OptionsGroupUI(QtGui.QGroupBox):
+    def __init__(self, title, parent=None):
+        QtGui.QGroupBox.__init__(self, title, parent=parent)
+        self.setStyleSheet("""
+        QGroupBox
+        {
+            font-size: 16px;
+            font-weight: bold;
+        }
+        """)
+
+        self.layout = QtGui.QVBoxLayout()
+        self.setLayout(self.layout)
+
+
+class GerberOptionsGroupUI(OptionsGroupUI):
+    def __init__(self, parent=None):
+        OptionsGroupUI.__init__(self, "Gerber Options", parent=parent)
+
+        ## Plot options
+        self.plot_options_label = QtGui.QLabel("<b>Plot Options:</b>")
+        self.layout.addWidget(self.plot_options_label)
+
+        grid0 = QtGui.QGridLayout()
+        self.layout.addLayout(grid0)
+        # Plot CB
+        self.plot_cb = FCCheckBox(label='Plot')
+        self.plot_options_label.setToolTip(
+            "Plot (show) this object."
+        )
+        grid0.addWidget(self.plot_cb, 0, 0)
+
+        # Solid CB
+        self.solid_cb = FCCheckBox(label='Solid')
+        self.solid_cb.setToolTip(
+            "Solid color polygons."
+        )
+        grid0.addWidget(self.solid_cb, 0, 1)
+
+        # Multicolored CB
+        self.multicolored_cb = FCCheckBox(label='Multicolored')
+        self.multicolored_cb.setToolTip(
+            "Draw polygons in different colors."
+        )
+        grid0.addWidget(self.multicolored_cb, 0, 2)
+
+        ## Isolation Routing
+        self.isolation_routing_label = QtGui.QLabel("<b>Isolation Routing:</b>")
+        self.isolation_routing_label.setToolTip(
+            "Create a Geometry object with\n"
+            "toolpaths to cut outside polygons."
+        )
+        self.layout.addWidget(self.isolation_routing_label)
+
+        grid1 = QtGui.QGridLayout()
+        self.layout.addLayout(grid1)
+        tdlabel = QtGui.QLabel('Tool dia:')
+        tdlabel.setToolTip(
+            "Diameter of the cutting tool."
+        )
+        grid1.addWidget(tdlabel, 0, 0)
+        self.iso_tool_dia_entry = LengthEntry()
+        grid1.addWidget(self.iso_tool_dia_entry, 0, 1)
+
+        passlabel = QtGui.QLabel('Width (# passes):')
+        passlabel.setToolTip(
+            "Width of the isolation gap in\n"
+            "number (integer) of tool widths."
+        )
+        grid1.addWidget(passlabel, 1, 0)
+        self.iso_width_entry = IntEntry()
+        grid1.addWidget(self.iso_width_entry, 1, 1)
+
+        overlabel = QtGui.QLabel('Pass overlap:')
+        overlabel.setToolTip(
+            "How much (fraction of tool width)\n"
+            "to overlap each pass."
+        )
+        grid1.addWidget(overlabel, 2, 0)
+        self.iso_overlap_entry = FloatEntry()
+        grid1.addWidget(self.iso_overlap_entry, 2, 1)
+
+        ## Board cuttout
+        self.board_cutout_label = QtGui.QLabel("<b>Board cutout:</b>")
+        self.board_cutout_label.setToolTip(
+            "Create toolpaths to cut around\n"
+            "the PCB and separate it from\n"
+            "the original board."
+        )
+        self.layout.addWidget(self.board_cutout_label)
+
+        grid2 = QtGui.QGridLayout()
+        self.layout.addLayout(grid2)
+        tdclabel = QtGui.QLabel('Tool dia:')
+        tdclabel.setToolTip(
+            "Diameter of the cutting tool."
+        )
+        grid2.addWidget(tdclabel, 0, 0)
+        self.cutout_tooldia_entry = LengthEntry()
+        grid2.addWidget(self.cutout_tooldia_entry, 0, 1)
+
+        marginlabel = QtGui.QLabel('Margin:')
+        marginlabel.setToolTip(
+            "Distance from objects at which\n"
+            "to draw the cutout."
+        )
+        grid2.addWidget(marginlabel, 1, 0)
+        self.cutout_margin_entry = LengthEntry()
+        grid2.addWidget(self.cutout_margin_entry, 1, 1)
+
+        gaplabel = QtGui.QLabel('Gap size:')
+        gaplabel.setToolTip(
+            "Size of the gaps in the toolpath\n"
+            "that will remain to hold the\n"
+            "board in place."
+        )
+        grid2.addWidget(gaplabel, 2, 0)
+        self.cutout_gap_entry = LengthEntry()
+        grid2.addWidget(self.cutout_gap_entry, 2, 1)
+
+        gapslabel = QtGui.QLabel('Gaps:')
+        gapslabel.setToolTip(
+            "Where to place the gaps, Top/Bottom\n"
+            "Left/Rigt, or on all 4 sides."
+        )
+        grid2.addWidget(gapslabel, 3, 0)
+        self.gaps_radio = RadioSet([{'label': '2 (T/B)', 'value': 'tb'},
+                                    {'label': '2 (L/R)', 'value': 'lr'},
+                                    {'label': '4', 'value': '4'}])
+        grid2.addWidget(self.gaps_radio, 3, 1)
+
+        ## Non-copper regions
+        self.noncopper_label = QtGui.QLabel("<b>Non-copper regions:</b>")
+        self.noncopper_label.setToolTip(
+            "Create polygons covering the\n"
+            "areas without copper on the PCB.\n"
+            "Equivalent to the inverse of this\n"
+            "object. Can be used to remove all\n"
+            "copper from a specified region."
+        )
+        self.layout.addWidget(self.noncopper_label)
+
+        grid3 = QtGui.QGridLayout()
+        self.layout.addLayout(grid3)
+
+        # Margin
+        bmlabel = QtGui.QLabel('Boundary Margin:')
+        bmlabel.setToolTip(
+            "Specify the edge of the PCB\n"
+            "by drawing a box around all\n"
+            "objects with this minimum\n"
+            "distance."
+        )
+        grid3.addWidget(bmlabel, 0, 0)
+        self.noncopper_margin_entry = LengthEntry()
+        grid3.addWidget(self.noncopper_margin_entry, 0, 1)
+
+        # Rounded corners
+        self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners")
+        self.noncopper_rounded_cb.setToolTip(
+            "Creates a Geometry objects with polygons\n"
+            "covering the copper-free areas of the PCB."
+        )
+        grid3.addWidget(self.noncopper_rounded_cb, 1, 0, 1, 2)
+
+        ## Bounding box
+        self.boundingbox_label = QtGui.QLabel('<b>Bounding Box:</b>')
+        self.layout.addWidget(self.boundingbox_label)
+
+        grid4 = QtGui.QGridLayout()
+        self.layout.addLayout(grid4)
+
+        bbmargin = QtGui.QLabel('Boundary Margin:')
+        bbmargin.setToolTip(
+            "Distance of the edges of the box\n"
+            "to the nearest polygon."
+        )
+        grid4.addWidget(bbmargin, 0, 0)
+        self.bbmargin_entry = LengthEntry()
+        grid4.addWidget(self.bbmargin_entry, 0, 1)
+
+        self.bbrounded_cb = FCCheckBox(label="Rounded corners")
+        self.bbrounded_cb.setToolTip(
+            "If the bounding box is \n"
+            "to have rounded corners\n"
+            "their radius is equal to\n"
+            "the margin."
+        )
+        grid4.addWidget(self.bbrounded_cb, 1, 0, 1, 2)
+
+
+class ExcellonOptionsGroupUI(OptionsGroupUI):
+    def __init__(self, parent=None):
+        OptionsGroupUI.__init__(self, "Excellon Options", parent=parent)
+
+        ## Plot options
+        self.plot_options_label = QtGui.QLabel("<b>Plot Options:</b>")
+        self.layout.addWidget(self.plot_options_label)
+
+        grid0 = QtGui.QGridLayout()
+        self.layout.addLayout(grid0)
+        self.plot_cb = FCCheckBox(label='Plot')
+        self.plot_cb.setToolTip(
+            "Plot (show) this object."
+        )
+        grid0.addWidget(self.plot_cb, 0, 0)
+        self.solid_cb = FCCheckBox(label='Solid')
+        self.solid_cb.setToolTip(
+            "Solid circles."
+        )
+        grid0.addWidget(self.solid_cb, 0, 1)
+
+        ## Create CNC Job
+        self.cncjob_label = QtGui.QLabel('<b>Create CNC Job</b>')
+        self.cncjob_label.setToolTip(
+            "Create a CNC Job object\n"
+            "for this drill object."
+        )
+        self.layout.addWidget(self.cncjob_label)
+
+        grid1 = QtGui.QGridLayout()
+        self.layout.addLayout(grid1)
+
+        cutzlabel = QtGui.QLabel('Cut Z:')
+        cutzlabel.setToolTip(
+            "Drill depth (negative)\n"
+            "below the copper surface."
+        )
+        grid1.addWidget(cutzlabel, 0, 0)
+        self.cutz_entry = LengthEntry()
+        grid1.addWidget(self.cutz_entry, 0, 1)
+
+        travelzlabel = QtGui.QLabel('Travel Z:')
+        travelzlabel.setToolTip(
+            "Tool height when travelling\n"
+            "across the XY plane."
+        )
+        grid1.addWidget(travelzlabel, 1, 0)
+        self.travelz_entry = LengthEntry()
+        grid1.addWidget(self.travelz_entry, 1, 1)
+
+        frlabel = QtGui.QLabel('Feed rate:')
+        frlabel.setToolTip(
+            "Tool speed while drilling\n"
+            "(in units per minute)."
+        )
+        grid1.addWidget(frlabel, 2, 0)
+        self.feedrate_entry = LengthEntry()
+        grid1.addWidget(self.feedrate_entry, 2, 1)
+
+
+class GeometryOptionsGroupUI(OptionsGroupUI):
+    def __init__(self, parent=None):
+        OptionsGroupUI.__init__(self, "Geometry Options", parent=parent)
+
+        ## Plot options
+        self.plot_options_label = QtGui.QLabel("<b>Plot Options:</b>")
+        self.layout.addWidget(self.plot_options_label)
+
+        # Plot CB
+        self.plot_cb = FCCheckBox(label='Plot')
+        self.plot_cb.setToolTip(
+            "Plot (show) this object."
+        )
+        self.layout.addWidget(self.plot_cb)
+
+        ## Create CNC Job
+        self.cncjob_label = QtGui.QLabel('<b>Create CNC Job:</b>')
+        self.cncjob_label.setToolTip(
+            "Create a CNC Job object\n"
+            "tracing the contours of this\n"
+            "Geometry object."
+        )
+        self.layout.addWidget(self.cncjob_label)
+
+        grid1 = QtGui.QGridLayout()
+        self.layout.addLayout(grid1)
+
+        cutzlabel = QtGui.QLabel('Cut Z:')
+        cutzlabel.setToolTip(
+            "Cutting depth (negative)\n"
+            "below the copper surface."
+        )
+        grid1.addWidget(cutzlabel, 0, 0)
+        self.cutz_entry = LengthEntry()
+        grid1.addWidget(self.cutz_entry, 0, 1)
+
+        # Travel Z
+        travelzlabel = QtGui.QLabel('Travel Z:')
+        travelzlabel.setToolTip(
+            "Height of the tool when\n"
+            "moving without cutting."
+        )
+        grid1.addWidget(travelzlabel, 1, 0)
+        self.travelz_entry = LengthEntry()
+        grid1.addWidget(self.travelz_entry, 1, 1)
+
+        # Feedrate
+        frlabel = QtGui.QLabel('Feed Rate:')
+        frlabel.setToolTip(
+            "Cutting speed in the XY\n"
+            "plane in units per minute"
+        )
+        grid1.addWidget(frlabel, 2, 0)
+        self.cncfeedrate_entry = LengthEntry()
+        grid1.addWidget(self.cncfeedrate_entry, 2, 1)
+
+        # Tooldia
+        tdlabel = QtGui.QLabel('Tool dia:')
+        tdlabel.setToolTip(
+            "The diameter of the cutting\n"
+            "tool (just for display)."
+        )
+        grid1.addWidget(tdlabel, 3, 0)
+        self.cnctooldia_entry = LengthEntry()
+        grid1.addWidget(self.cnctooldia_entry, 3, 1)
+
+        ## Paint area
+        self.paint_label = QtGui.QLabel('<b>Paint Area:</b>')
+        self.paint_label.setToolTip(
+            "Creates tool paths to cover the\n"
+            "whole area of a polygon (remove\n"
+            "all copper). You will be asked\n"
+            "to click on the desired polygon."
+        )
+        self.layout.addWidget(self.paint_label)
+
+        grid2 = QtGui.QGridLayout()
+        self.layout.addLayout(grid2)
+
+        # Tool dia
+        ptdlabel = QtGui.QLabel('Tool dia:')
+        ptdlabel.setToolTip(
+            "Diameter of the tool to\n"
+            "be used in the operation."
+        )
+        grid2.addWidget(ptdlabel, 0, 0)
+
+        self.painttooldia_entry = LengthEntry()
+        grid2.addWidget(self.painttooldia_entry, 0, 1)
+
+        # Overlap
+        ovlabel = QtGui.QLabel('Overlap:')
+        ovlabel.setToolTip(
+            "How much (fraction) of the tool\n"
+            "width to overlap each tool pass."
+        )
+        grid2.addWidget(ovlabel, 1, 0)
+        self.paintoverlap_entry = LengthEntry()
+        grid2.addWidget(self.paintoverlap_entry, 1, 1)
+
+        # Margin
+        marginlabel = QtGui.QLabel('Margin:')
+        marginlabel.setToolTip(
+            "Distance by which to avoid\n"
+            "the edges of the polygon to\n"
+            "be painted."
+        )
+        grid2.addWidget(marginlabel, 2, 0)
+        self.paintmargin_entry = LengthEntry()
+        grid2.addWidget(self.paintmargin_entry)
+
+
+class CNCJobOptionsGroupUI(OptionsGroupUI):
+    def __init__(self, parent=None):
+        OptionsGroupUI.__init__(self, "CNC Job Options", parent=None)
+
+        ## Plot options
+        self.plot_options_label = QtGui.QLabel("<b>Plot Options:</b>")
+        self.layout.addWidget(self.plot_options_label)
+
+        grid0 = QtGui.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        # Plot CB
+        # self.plot_cb = QtGui.QCheckBox('Plot')
+        self.plot_cb = FCCheckBox('Plot')
+        self.plot_cb.setToolTip(
+            "Plot (show) this object."
+        )
+        grid0.addWidget(self.plot_cb, 0, 0)
+
+        # Tool dia for plot
+        tdlabel = QtGui.QLabel('Tool dia:')
+        tdlabel.setToolTip(
+            "Diameter of the tool to be\n"
+            "rendered in the plot."
+        )
+        grid0.addWidget(tdlabel, 1, 0)
+        self.tooldia_entry = LengthEntry()
+        grid0.addWidget(self.tooldia_entry, 1, 1)
+
+        ## Export G-Code
+        self.export_gcode_label = QtGui.QLabel("<b>Export G-Code:</b>")
+        self.export_gcode_label.setToolTip(
+            "Export and save G-Code to\n"
+            "make this object to a file."
+        )
+        self.layout.addWidget(self.export_gcode_label)
+
+        # Append text to Gerber
+        appendlabel = QtGui.QLabel('Append to G-Code:')
+        appendlabel.setToolTip(
+            "Type here any G-Code commands you would\n"
+            "like to append to the generated file.\n"
+            "I.e.: M2 (End of program)"
+        )
+        self.layout.addWidget(appendlabel)
+
+        self.append_text = FCTextArea()
+        self.layout.addWidget(self.append_text)
+
+
+class GlobalOptionsUI(QtGui.QWidget):
+    def __init__(self, parent=None):
+        QtGui.QWidget.__init__(self, parent=parent)
+
+        layout = QtGui.QVBoxLayout()
+        self.setLayout(layout)
+
+        hlay1 = QtGui.QHBoxLayout()
+        layout.addLayout(hlay1)
+        unitslabel = QtGui.QLabel('Units:')
+        hlay1.addWidget(unitslabel)
+        self.units_radio = RadioSet([{'label': 'inch', 'value': 'IN'},
+                                     {'label': 'mm', 'value': 'MM'}])
+        hlay1.addWidget(self.units_radio)
+
+        ####### Gerber #######
+        # gerberlabel = QtGui.QLabel('<b>Gerber Options</b>')
+        # layout.addWidget(gerberlabel)
+        self.gerber_group = GerberOptionsGroupUI()
+        # self.gerber_group.setFrameStyle(QtGui.QFrame.StyledPanel)
+        layout.addWidget(self.gerber_group)
+
+        ####### Excellon #######
+        # excellonlabel = QtGui.QLabel('<b>Excellon Options</b>')
+        # layout.addWidget(excellonlabel)
+        self.excellon_group = ExcellonOptionsGroupUI()
+        # self.excellon_group.setFrameStyle(QtGui.QFrame.StyledPanel)
+        layout.addWidget(self.excellon_group)
+
+        ####### Geometry #######
+        # geometrylabel = QtGui.QLabel('<b>Geometry Options</b>')
+        # layout.addWidget(geometrylabel)
+        self.geometry_group = GeometryOptionsGroupUI()
+        # self.geometry_group.setStyle(QtGui.QFrame.StyledPanel)
+        layout.addWidget(self.geometry_group)
+
+        ####### CNC #######
+        # cnclabel = QtGui.QLabel('<b>CNC Job Options</b>')
+        # layout.addWidget(cnclabel)
+        self.cncjob_group = CNCJobOptionsGroupUI()
+        # self.cncjob_group.setStyle(QtGui.QFrame.StyledPanel)
+        layout.addWidget(self.cncjob_group)
+
+# def main():
+#
+#     app = QtGui.QApplication(sys.argv)
+#     fc = FlatCAMGUI()
+#     sys.exit(app.exec_())
+#
+#
+# if __name__ == '__main__':
+#     main()

+ 274 - 246
FlatCAMObj.py

@@ -1,66 +1,15 @@
-############################################################
-# FlatCAM: 2D Post-processing for Manufacturing            #
-# http://caram.cl/software/flatcam                         #
-# Author: Juan Pablo Caram (c)                             #
-# Date: 2/5/2014                                           #
-# MIT Licence                                              #
-############################################################
-
-from gi.repository import Gtk
-from gi.repository import Gdk
-from gi.repository import GLib
-from gi.repository import GObject
-
-import inspect  # TODO: Remove
-
+from PyQt4 import QtCore
+from ObjectUI import *
 import FlatCAMApp
+import inspect  # TODO: For debugging only.
 from camlib import *
-from ObjectUI import *
-
-
-class LoudDict(dict):
-    """
-    A Dictionary with a callback for
-    item changes.
-    """
-
-    def __init__(self, *args, **kwargs):
-        super(LoudDict, self).__init__(*args, **kwargs)
-        self.callback = lambda x: None
-        self.silence = False
-
-    def set_change_callback(self, callback):
-        """
-        Assigns a function as callback on item change. The callback
-        will receive the key of the object that was changed.
-
-        :param callback: Function to call on item change.
-        :type callback: func
-        :return: None
-        """
-
-        self.callback = callback
-
-    def __setitem__(self, key, value):
-        """
-        Overridden __setitem__ method. Will call self.callback
-        if the item was changed and self.silence is False.
-        """
-        super(LoudDict, self).__setitem__(key, value)
-        try:
-            if self.__getitem__(key) == value:
-                return
-        except KeyError:
-            pass
-        if self.silence:
-            return
-        self.callback(key)
+from FlatCAMCommon import LoudDict
 
 
 ########################################
 ##            FlatCAMObj              ##
 ########################################
-class FlatCAMObj(GObject.GObject, object):
+class FlatCAMObj(QtCore.QObject):
     """
     Base type of objects handled in FlatCAM. These become interactive
     in the GUI, can be plotted, and their options can be modified
@@ -71,7 +20,7 @@ class FlatCAMObj(GObject.GObject, object):
     # The app should set this value.
     app = None
 
-    def __init__(self, name, ui):
+    def __init__(self, name):
         """
 
         :param name: Name of the object given by the user.
@@ -79,53 +28,60 @@ class FlatCAMObj(GObject.GObject, object):
         :type ui: ObjectUI
         :return: FlatCAMObj
         """
-        GObject.GObject.__init__(self)
+        QtCore.QObject.__init__(self)
 
         # View
-        self.ui = ui
+        self.ui = None
 
         self.options = LoudDict(name=name)
         self.options.set_change_callback(self.on_options_change)
 
-        self.form_fields = {"name": self.ui.name_entry}
-        self.radios = {}  # Name value pairs for radio sets
-        self.radios_inv = {}  # Inverse of self.radios
+        self.form_fields = {}
+
         self.axes = None  # Matplotlib axes
         self.kind = None  # Override with proper name
 
         self.muted_ui = False
 
-        self.ui.name_entry.connect('activate', self.on_name_activate)
-        self.ui.offset_button.connect('clicked', self.on_offset_button_click)
-        self.ui.offset_button.connect('activate', self.on_offset_button_click)
-        self.ui.scale_button.connect('clicked', self.on_scale_button_click)
-        self.ui.scale_button.connect('activate', self.on_scale_button_click)
+        # assert isinstance(self.ui, ObjectUI)
+        # self.ui.name_entry.returnPressed.connect(self.on_name_activate)
+        # self.ui.offset_button.clicked.connect(self.on_offset_button_click)
+        # self.ui.scale_button.clicked.connect(self.on_scale_button_click)
+
+    def on_options_change(self, key):
+        self.emit(QtCore.SIGNAL("optionChanged"), key)
+
+    def set_ui(self, ui):
+        self.ui = ui
+
+        self.form_fields = {"name": self.ui.name_entry}
+
+        assert isinstance(self.ui, ObjectUI)
+        self.ui.name_entry.returnPressed.connect(self.on_name_activate)
+        self.ui.offset_button.clicked.connect(self.on_offset_button_click)
+        self.ui.scale_button.clicked.connect(self.on_scale_button_click)
 
     def __str__(self):
         return "<FlatCAMObj({:12s}): {:20s}>".format(self.kind, self.options["name"])
 
-    def on_name_activate(self, *args):
+    def on_name_activate(self):
         old_name = copy(self.options["name"])
-        new_name = self.ui.name_entry.get_text()
-        self.options["name"] = self.ui.name_entry.get_text()
+        new_name = self.ui.name_entry.get_value()
+        self.options["name"] = self.ui.name_entry.get_value()
         self.app.info("Name changed from %s to %s" % (old_name, new_name))
 
-    def on_offset_button_click(self, *args):
+    def on_offset_button_click(self):
         self.read_form()
         vect = self.ui.offsetvector_entry.get_value()
         self.offset(vect)
         self.plot()
 
-    def on_scale_button_click(self, *args):
+    def on_scale_button_click(self):
         self.read_form()
         factor = self.ui.scale_entry.get_value()
         self.scale(factor)
         self.plot()
 
-    def on_options_change(self, key):
-        self.form_fields[key].set_value(self.options[key])
-        return
-
     def setup_axes(self, figure):
         """
         1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
@@ -189,21 +145,24 @@ class FlatCAMObj(GObject.GObject, object):
         self.muted_ui = True
         FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.build_ui()")
 
-        # Where the UI for this object is drawn
-        # box_selected = self.app.builder.get_object("box_selected")
-        box_selected = self.app.builder.get_object("vp_selected")
-
         # Remove anything else in the box
-        box_children = box_selected.get_children()
-        for child in box_children:
-            box_selected.remove(child)
+        # box_children = self.app.ui.notebook.selected_contents.get_children()
+        # for child in box_children:
+        #     self.app.ui.notebook.selected_contents.remove(child)
+        # while self.app.ui.selected_layout.count():
+        #     self.app.ui.selected_layout.takeAt(0)
 
         # Put in the UI
         # box_selected.pack_start(sw, True, True, 0)
-        box_selected.add(self.ui)
+        # self.app.ui.notebook.selected_contents.add(self.ui)
+        # self.app.ui.selected_layout.addWidget(self.ui)
+        try:
+            self.app.ui.selected_scroll_area.takeWidget()
+        except:
+            self.app.log.debug("Nothing to remove")
+        self.app.ui.selected_scroll_area.setWidget(self.ui)
         self.to_form()
-        GLib.idle_add(box_selected.show_all)
-        GLib.idle_add(self.ui.show_all)
+
         self.muted_ui = False
 
     def set_form_item(self, option):
@@ -243,6 +202,7 @@ class FlatCAMObj(GObject.GObject, object):
         :return: Whether to continue plotting or not depending on the "plot" option.
         :rtype: bool
         """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.plot()")
 
         # Axes must exist and be attached to canvas.
         if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes:
@@ -254,7 +214,7 @@ class FlatCAMObj(GObject.GObject, object):
             return False
 
         # Clear axes or we will plot on top of them.
-        self.axes.cla()
+        self.axes.cla()  # TODO: Thread safe?
         # GLib.idle_add(self.axes.cla)
         return True
 
@@ -284,29 +244,14 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
     Represents Gerber code.
     """
 
+    ui_type = GerberObjectUI
+
     def __init__(self, name):
         Gerber.__init__(self)
-        FlatCAMObj.__init__(self, name, GerberObjectUI())
+        FlatCAMObj.__init__(self, name)
 
         self.kind = "gerber"
 
-        self.form_fields.update({
-            "plot": self.ui.plot_cb,
-            "multicolored": self.ui.multicolored_cb,
-            "solid": self.ui.solid_cb,
-            "isotooldia": self.ui.iso_tool_dia_entry,
-            "isopasses": self.ui.iso_width_entry,
-            "isooverlap": self.ui.iso_overlap_entry,
-            "cutouttooldia": self.ui.cutout_tooldia_entry,
-            "cutoutmargin": self.ui.cutout_margin_entry,
-            "cutoutgapsize": self.ui.cutout_gap_entry,
-            "gaps": self.ui.gaps_radio,
-            "noncoppermargin": self.ui.noncopper_margin_entry,
-            "noncopperrounded": self.ui.noncopper_rounded_cb,
-            "bboxmargin": self.ui.bbmargin_entry,
-            "bboxrounded": self.ui.bbrounded_cb
-        })
-
         # The 'name' is already in self.options from FlatCAMObj
         # Automatically updates the UI
         self.options.update({
@@ -331,21 +276,45 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         # from predecessors.
         self.ser_attrs += ['options', 'kind']
 
+        # assert isinstance(self.ui, GerberObjectUI)
+        # self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
+        # self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
+        # self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
+        # self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
+        # self.ui.generate_cutout_button.clicked.connect(self.on_generatecutout_button_click)
+        # self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
+        # self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
+
+    def set_ui(self, ui):
+        FlatCAMObj.set_ui(self, ui)
+
+        FlatCAMApp.App.log.debug("FlatCAMGerber.set_ui()")
+
+        self.form_fields.update({
+            "plot": self.ui.plot_cb,
+            "multicolored": self.ui.multicolored_cb,
+            "solid": self.ui.solid_cb,
+            "isotooldia": self.ui.iso_tool_dia_entry,
+            "isopasses": self.ui.iso_width_entry,
+            "isooverlap": self.ui.iso_overlap_entry,
+            "cutouttooldia": self.ui.cutout_tooldia_entry,
+            "cutoutmargin": self.ui.cutout_margin_entry,
+            "cutoutgapsize": self.ui.cutout_gap_entry,
+            "gaps": self.ui.gaps_radio,
+            "noncoppermargin": self.ui.noncopper_margin_entry,
+            "noncopperrounded": self.ui.noncopper_rounded_cb,
+            "bboxmargin": self.ui.bbmargin_entry,
+            "bboxrounded": self.ui.bbrounded_cb
+        })
+
         assert isinstance(self.ui, GerberObjectUI)
-        self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
-        self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
-        self.ui.solid_cb.connect('clicked', self.on_solid_cb_click)
-        self.ui.solid_cb.connect('activate', self.on_solid_cb_click)
-        self.ui.multicolored_cb.connect('clicked', self.on_multicolored_cb_click)
-        self.ui.multicolored_cb.connect('activate', self.on_multicolored_cb_click)
-        self.ui.generate_iso_button.connect('clicked', self.on_iso_button_click)
-        self.ui.generate_iso_button.connect('activate', self.on_iso_button_click)
-        self.ui.generate_cutout_button.connect('clicked', self.on_generatecutout_button_click)
-        self.ui.generate_cutout_button.connect('activate', self.on_generatecutout_button_click)
-        self.ui.generate_bb_button.connect('clicked', self.on_generatebb_button_click)
-        self.ui.generate_bb_button.connect('activate', self.on_generatebb_button_click)
-        self.ui.generate_noncopper_button.connect('clicked', self.on_generatenoncopper_button_click)
-        self.ui.generate_noncopper_button.connect('activate', self.on_generatenoncopper_button_click)
+        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
+        self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
+        self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
+        self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
+        self.ui.generate_cutout_button.clicked.connect(self.on_generatecutout_button_click)
+        self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
+        self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
 
     def on_generatenoncopper_button_click(self, *args):
         self.read_form()
@@ -478,17 +447,13 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
     def plot(self):
 
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMGerber.plot()")
+
         # Does all the required setup and returns False
         # if the 'ptint' option is set to False.
         if not FlatCAMObj.plot(self):
             return
 
-        # if self.options["mergepolys"]:
-        #     geometry = self.solid_geometry
-        # else:
-        #     geometry = self.buffered_paths + \
-        #                 [poly['polygon'] for poly in self.regions] + \
-        #                 self.flash_geometry
         geometry = self.solid_geometry
 
         # Make sure geometry is iterable.
@@ -523,8 +488,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     x, y = ints.coords.xy
                     self.axes.plot(x, y, linespec)
 
-        # self.app.plotcanvas.auto_adjust_axes()
-        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
+        self.app.plotcanvas.auto_adjust_axes()
+        #GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
+        #self.emit(QtCore.SIGNAL("plotChanged"), self)
 
     def serialize(self):
         return {
@@ -538,29 +504,21 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
     Represents Excellon/Drill code.
     """
 
+    ui_type = ExcellonObjectUI
+
     def __init__(self, name):
         Excellon.__init__(self)
-        FlatCAMObj.__init__(self, name, ExcellonObjectUI())
+        FlatCAMObj.__init__(self, name)
 
         self.kind = "excellon"
 
-        self.form_fields.update({
-            "name": self.ui.name_entry,
-            "plot": self.ui.plot_cb,
-            "solid": self.ui.solid_cb,
-            "drillz": self.ui.cutz_entry,
-            "travelz": self.ui.travelz_entry,
-            "feedrate": self.ui.feedrate_entry,
-            "toolselection": self.ui.tools_entry
-        })
-
         self.options.update({
             "plot": True,
             "solid": False,
             "drillz": -0.1,
             "travelz": 0.1,
             "feedrate": 5.0,
-            "toolselection": ""
+            # "toolselection": ""
         })
 
         # TODO: Document this.
@@ -571,49 +529,100 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         # from predecessors.
         self.ser_attrs += ['options', 'kind']
 
+    def build_ui(self):
+        FlatCAMObj.build_ui(self)
+
+        # Populate tool list
+        n = len(self.tools)
+        self.ui.tools_table.setColumnCount(2)
+        self.ui.tools_table.setHorizontalHeaderLabels(['#', 'Diameter'])
+        self.ui.tools_table.setRowCount(n)
+        self.ui.tools_table.setSortingEnabled(False)
+        i = 0
+        for tool in self.tools:
+            id = QtGui.QTableWidgetItem(tool)
+            id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.tools_table.setItem(i, 0, id)  # Tool name/id
+            dia = QtGui.QTableWidgetItem(str(self.tools[tool]['C']))
+            dia.setFlags(QtCore.Qt.ItemIsEnabled)
+            self.ui.tools_table.setItem(i, 1, dia)  # Diameter
+            i += 1
+        self.ui.tools_table.resizeColumnsToContents()
+        self.ui.tools_table.resizeRowsToContents()
+        self.ui.tools_table.horizontalHeader().setStretchLastSection(True)
+        self.ui.tools_table.verticalHeader().hide()
+        self.ui.tools_table.setSortingEnabled(True)
+
+    def set_ui(self, ui):
+        FlatCAMObj.set_ui(self, ui)
+
+        FlatCAMApp.App.log.debug("FlatCAMExcellon.set_ui()")
+
+        self.form_fields.update({
+            "plot": self.ui.plot_cb,
+            "solid": self.ui.solid_cb,
+            "drillz": self.ui.cutz_entry,
+            "travelz": self.ui.travelz_entry,
+            "feedrate": self.ui.feedrate_entry,
+            # "toolselection": self.ui.tools_entry
+        })
+
         assert isinstance(self.ui, ExcellonObjectUI)
-        self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
-        self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
-        self.ui.solid_cb.connect('clicked', self.on_solid_cb_click)
-        self.ui.solid_cb.connect('activate', self.on_solid_cb_click)
-        self.ui.choose_tools_button.connect('clicked', lambda args: self.show_tool_chooser())
-        self.ui.choose_tools_button.connect('activate', lambda args: self.show_tool_chooser())
-        self.ui.generate_cnc_button.connect('clicked', self.on_create_cncjob_button_click)
-        self.ui.generate_cnc_button.connect('activate', self.on_create_cncjob_button_click)
+        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
+        self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
+        # self.ui.choose_tools_button.clicked.connect(self.show_tool_chooser)
+        self.ui.generate_cnc_button.clicked.connect(self.on_create_cncjob_button_click)
 
     def on_create_cncjob_button_click(self, *args):
         self.read_form()
+
+        # Get the tools from the list
+        tools = [str(x.text()) for x in self.ui.tools_table.selectedItems()]
+
+        if len(tools) == 0:
+            self.app.inform.emit("Please select one or more tools from the list and try again.")
+            return
+
         job_name = self.options["name"] + "_cnc"
 
         # Object initialization function for app.new_object()
         def job_init(job_obj, app_obj):
             assert isinstance(job_obj, FlatCAMCNCjob)
 
-            GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
+            # GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
+            app_obj.progress.emit(20)
             job_obj.z_cut = self.options["drillz"]
             job_obj.z_move = self.options["travelz"]
             job_obj.feedrate = self.options["feedrate"]
             # There could be more than one drill size...
             # job_obj.tooldia =   # TODO: duplicate variable!
             # job_obj.options["tooldia"] =
-            job_obj.generate_from_excellon_by_tool(self, self.options["toolselection"])
 
-            GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
+            tools_csv = ','.join(tools)
+            # job_obj.generate_from_excellon_by_tool(self, self.options["toolselection"])
+            job_obj.generate_from_excellon_by_tool(self, tools_csv)
+
+            # GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
+            app_obj.progress.emit(50)
             job_obj.gcode_parse()
 
-            GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
+            # GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
+            app_obj.progress.emit(60)
             job_obj.create_geometry()
 
-            GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
+            # GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
+            app_obj.progress.emit(80)
 
         # To be run in separate thread
         def job_thread(app_obj):
             app_obj.new_object("cncjob", job_name, job_init)
-            GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
-            GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
+            # GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
+            app_obj.progress.emit(100)
+            # GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
 
         # Send to worker
-        self.app.worker.add_task(job_thread, [self.app])
+        # self.app.worker.add_task(job_thread, [self.app])
+        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
 
     def on_plot_cb_click(self, *args):
         if self.muted_ui:
@@ -663,32 +672,34 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                     x, y = ints.coords.xy
                     self.axes.plot(x, y, 'g-')
 
-        #self.app.plotcanvas.auto_adjust_axes()
-        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
+        self.app.plotcanvas.auto_adjust_axes()
+        # GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
+        # self.emit(QtCore.SIGNAL("plotChanged"), self)
 
     def show_tool_chooser(self):
-        win = Gtk.Window()
-        box = Gtk.Box(spacing=2)
-        box.set_orientation(Gtk.Orientation(1))
-        win.add(box)
-        for tool in self.tools:
-            self.tool_cbs[tool] = Gtk.CheckButton(label=tool + ": " + str(self.tools[tool]))
-            box.pack_start(self.tool_cbs[tool], False, False, 1)
-        button = Gtk.Button(label="Accept")
-        box.pack_start(button, False, False, 1)
-        win.show_all()
-
-        def on_accept(widget):
-            win.destroy()
-            tool_list = []
-            for toolx in self.tool_cbs:
-                if self.tool_cbs[toolx].get_active():
-                    tool_list.append(toolx)
-            self.options["toolselection"] = ", ".join(tool_list)
-            self.to_form()
-
-        button.connect("activate", on_accept)
-        button.connect("clicked", on_accept)
+        # win = Gtk.Window()
+        # box = Gtk.Box(spacing=2)
+        # box.set_orientation(Gtk.Orientation(1))
+        # win.add(box)
+        # for tool in self.tools:
+        #     self.tool_cbs[tool] = Gtk.CheckButton(label=tool + ": " + str(self.tools[tool]))
+        #     box.pack_start(self.tool_cbs[tool], False, False, 1)
+        # button = Gtk.Button(label="Accept")
+        # box.pack_start(button, False, False, 1)
+        # win.show_all()
+        #
+        # def on_accept(widget):
+        #     win.destroy()
+        #     tool_list = []
+        #     for toolx in self.tool_cbs:
+        #         if self.tool_cbs[toolx].get_active():
+        #             tool_list.append(toolx)
+        #     self.options["toolselection"] = ", ".join(tool_list)
+        #     self.to_form()
+        #
+        # button.connect("activate", on_accept)
+        # button.connect("clicked", on_accept)
+        return
 
 
 class FlatCAMCNCjob(FlatCAMObj, CNCjob):
@@ -696,23 +707,21 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
     Represents G-Code.
     """
 
+    ui_type = CNCObjectUI
+
     def __init__(self, name, units="in", kind="generic", z_move=0.1,
                  feedrate=3.0, z_cut=-0.002, tooldia=0.0):
+        FlatCAMApp.App.log.debug("Creating CNCJob object...")
         CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
                         feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
-        FlatCAMObj.__init__(self, name, CNCObjectUI())
+        FlatCAMObj.__init__(self, name)
 
         self.kind = "cncjob"
 
         self.options.update({
             "plot": True,
-            "tooldia": 0.4 / 25.4  # 0.4mm in inches
-        })
-
-        self.form_fields.update({
-            "name": self.ui.name_entry,
-            "plot": self.ui.plot_cb,
-            "tooldia": self.ui.tooldia_entry
+            "tooldia": 0.4 / 25.4,  # 0.4mm in inches
+            "append": ""
         })
 
         # Attributes to be included in serialization
@@ -720,25 +729,47 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         # from predecessors.
         self.ser_attrs += ['options', 'kind']
 
-        self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
-        self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
-        self.ui.updateplot_button.connect('clicked', self.on_updateplot_button_click)
-        self.ui.updateplot_button.connect('activate', self.on_updateplot_button_click)
-        self.ui.export_gcode_button.connect('clicked', self.on_exportgcode_button_click)
-        self.ui.export_gcode_button.connect('activate', self.on_exportgcode_button_click)
+    def set_ui(self, ui):
+        FlatCAMObj.set_ui(self, ui)
+
+        FlatCAMApp.App.log.debug("FlatCAMCNCJob.set_ui()")
+
+        assert isinstance(self.ui, CNCObjectUI)
+
+        self.form_fields.update({
+            "plot": self.ui.plot_cb,
+            "tooldia": self.ui.tooldia_entry,
+            "append": self.ui.append_text
+        })
+
+        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
+        self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
+        self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
 
     def on_updateplot_button_click(self, *args):
+        """
+        Callback for the "Updata Plot" button. Reads the form for updates
+        and plots the object.
+        """
         self.read_form()
         self.plot()
 
     def on_exportgcode_button_click(self, *args):
-        def on_success(app_obj, filename):
-            f = open(filename, 'w')
-            f.write(self.gcode)
-            f.close()
-            app_obj.info("Saved to: " + filename)
 
-        self.app.file_chooser_save_action(on_success)
+        try:
+            filename = QtGui.QFileDialog.getSaveFileName(caption="Export G-Code ...",
+                                                         directory=self.app.last_folder)
+        except TypeError:
+            filename = QtGui.QFileDialog.getSaveFileName(caption="Export G-Code ...")
+
+        postamble = str(self.ui.append_text.get_value())
+
+        f = open(filename, 'w')
+        f.write(self.gcode + "\n" + postamble)
+        f.close()
+
+        self.app.file_opened.emit("cncjob", filename)
+        self.app.inform.emit("Saved to: " + filename)
 
     def on_plot_cb_click(self, *args):
         if self.muted_ui:
@@ -755,8 +786,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
 
         self.plot2(self.axes, tooldia=self.options["tooldia"])
 
-        #self.app.plotcanvas.auto_adjust_axes()
-        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
+        self.app.plotcanvas.auto_adjust_axes()
 
     def convert_units(self, units):
         factor = CNCjob.convert_units(self, units)
@@ -770,26 +800,14 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
     format.
     """
 
+    ui_type = GeometryObjectUI
+
     def __init__(self, name):
-        FlatCAMObj.__init__(self, name, GeometryObjectUI())
+        FlatCAMObj.__init__(self, name)
         Geometry.__init__(self)
 
         self.kind = "geometry"
 
-        self.form_fields.update({
-            "name": self.ui.name_entry,
-            "plot": self.ui.plot_cb,
-            # "solid": self.ui.sol,
-            # "multicolored": self.ui.,
-            "cutz": self.ui.cutz_entry,
-            "travelz": self.ui.travelz_entry,
-            "feedrate": self.ui.cncfeedrate_entry,
-            "cnctooldia": self.ui.cnctooldia_entry,
-            "painttooldia": self.ui.painttooldia_entry,
-            "paintoverlap": self.ui.paintoverlap_entry,
-            "paintmargin": self.ui.paintmargin_entry
-        })
-
         self.options.update({
             "plot": True,
             # "solid": False,
@@ -803,31 +821,34 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             "paintmargin": 0.01
         })
 
-        # self.form_kinds.update({
-        #     "plot": "cb",
-        #     "solid": "cb",
-        #     "multicolored": "cb",
-        #     "cutz": "entry_eval",
-        #     "travelz": "entry_eval",
-        #     "feedrate": "entry_eval",
-        #     "cnctooldia": "entry_eval",
-        #     "painttooldia": "entry_eval",
-        #     "paintoverlap": "entry_eval",
-        #     "paintmargin": "entry_eval"
-        # })
-
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # from predecessors.
         self.ser_attrs += ['options', 'kind']
 
+    def set_ui(self, ui):
+        FlatCAMObj.set_ui(self, ui)
+
+        FlatCAMApp.App.log.debug("FlatCAMGeometry.set_ui()")
+
         assert isinstance(self.ui, GeometryObjectUI)
-        self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
-        self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
-        self.ui.generate_cnc_button.connect('clicked', self.on_generatecnc_button_click)
-        self.ui.generate_cnc_button.connect('activate', self.on_generatecnc_button_click)
-        self.ui.generate_paint_button.connect('clicked', self.on_paint_button_click)
-        self.ui.generate_paint_button.connect('activate', self.on_paint_button_click)
+
+        self.form_fields.update({
+            "plot": self.ui.plot_cb,
+            # "solid": self.ui.sol,
+            # "multicolored": self.ui.,
+            "cutz": self.ui.cutz_entry,
+            "travelz": self.ui.travelz_entry,
+            "feedrate": self.ui.cncfeedrate_entry,
+            "cnctooldia": self.ui.cnctooldia_entry,
+            "painttooldia": self.ui.painttooldia_entry,
+            "paintoverlap": self.ui.paintoverlap_entry,
+            "paintmargin": self.ui.paintmargin_entry
+        })
+
+        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
+        self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
+        self.ui.generate_paint_button.clicked.connect(self.on_paint_button_click)
 
     def on_paint_button_click(self, *args):
         self.app.info("Click inside the desired polygon.")
@@ -868,35 +889,41 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             # Propagate options
             job_obj.options["tooldia"] = self.options["cnctooldia"]
 
-            GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
+            # GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
+            app_obj.progress.emit(20)
             job_obj.z_cut = self.options["cutz"]
             job_obj.z_move = self.options["travelz"]
             job_obj.feedrate = self.options["feedrate"]
 
-            GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
+            # GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
+            app_obj.progress.emit(40)
             # TODO: The tolerance should not be hard coded. Just for testing.
             job_obj.generate_from_geometry(self, tolerance=0.0005)
 
-            GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
+            # GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
+            app_obj.progress.emit(50)
             job_obj.gcode_parse()
 
             # TODO: job_obj.create_geometry creates stuff that is not used.
             #GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
             #job_obj.create_geometry()
 
-            GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
+            # GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
+            app_obj.progress.emit(80)
 
         # To be run in separate thread
         def job_thread(app_obj):
             app_obj.new_object("cncjob", job_name, job_init)
-            GLib.idle_add(lambda: app_obj.info("CNCjob created: %s" % job_name))
-            GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
-            GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, "Idle"))
+            # GLib.idle_add(lambda: app_obj.info("CNCjob created: %s" % job_name))
+            # GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
+            # GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, "Idle"))
+            app_obj.inform.emit("CNCjob created: %s" % job_name)
+            app_obj.progress.emit(100)
 
         # Send to worker
-        self.app.worker.add_task(job_thread, [self.app])
+        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
 
-    def on_plot_cb_click(self, *args):
+    def on_plot_cb_click(self, *args):  # TODO: args not needed
         if self.muted_ui:
             return
         self.read_form_item('plot')
@@ -994,5 +1021,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
             FlatCAMApp.App.log.warning("Did not plot:", str(type(geo)))
 
-        #self.app.plotcanvas.auto_adjust_axes()
-        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
+        self.app.plotcanvas.auto_adjust_axes()
+        # GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
+        # self.emit(QtCore.SIGNAL("plotChanged"), self)

+ 258 - 0
FlatCAMTool.py

@@ -0,0 +1,258 @@
+from PyQt4 import QtGui, QtCore
+from shapely.geometry import Point
+from shapely import affinity
+from math import sqrt
+
+import FlatCAMApp
+from GUIElements import *
+from FlatCAMObj import FlatCAMGerber, FlatCAMExcellon
+
+
+class FlatCAMTool(QtGui.QWidget):
+
+    toolName = "FlatCAM Generic Tool"
+
+    def __init__(self, app, parent=None):
+        """
+
+        :param app: The application this tool will run in.
+        :type app: App
+        :param parent: Qt Parent
+        :return: FlatCAMTool
+        """
+        QtGui.QWidget.__init__(self, parent)
+
+        # self.setSizePolicy(QtGui.QSizePolicy.Maximum, QtGui.QSizePolicy.Maximum)
+
+        self.layout = QtGui.QVBoxLayout()
+        self.setLayout(self.layout)
+
+        self.app = app
+
+        self.menuAction = None
+
+    def install(self):
+        self.menuAction = self.app.ui.menutool.addAction(self.toolName)
+        self.menuAction.triggered.connect(self.run)
+
+    def run(self):
+        # Remove anything else in the GUI
+        self.app.ui.tool_scroll_area.takeWidget()
+
+        # Put ourself in the GUI
+        self.app.ui.tool_scroll_area.setWidget(self)
+
+        # Switch notebook to tool page
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+
+        self.show()
+
+
+class DblSidedTool(FlatCAMTool):
+
+    toolName = "Double-Sided PCB Tool"
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        ## Title
+        title_label = QtGui.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
+        self.layout.addWidget(title_label)
+
+        ## Form Layout
+        form_layout = QtGui.QFormLayout()
+        self.layout.addLayout(form_layout)
+
+        ## Layer to mirror
+        self.object_combo = QtGui.QComboBox()
+        self.object_combo.setModel(self.app.collection)
+        form_layout.addRow("Bottom Layer:", self.object_combo)
+
+        ## Axis
+        self.mirror_axis = RadioSet([{'label': 'X', 'value': 'X'},
+                                     {'label': 'Y', 'value': 'Y'}])
+        form_layout.addRow("Mirror Axis:", self.mirror_axis)
+
+        ## Axis Location
+        self.axis_location = RadioSet([{'label': 'Point', 'value': 'point'},
+                                       {'label': 'Box', 'value': 'box'}])
+        form_layout.addRow("Axis Location:", self.axis_location)
+
+        ## Point/Box
+        self.point_box_container = QtGui.QVBoxLayout()
+        form_layout.addRow("Point/Box:", self.point_box_container)
+        self.point = EvalEntry()
+        self.point_box_container.addWidget(self.point)
+        self.box_combo = QtGui.QComboBox()
+        self.box_combo.setModel(self.app.collection)
+        self.point_box_container.addWidget(self.box_combo)
+        self.box_combo.hide()
+
+        ## Alignment holes
+        self.alignment_holes = EvalEntry()
+        form_layout.addRow("Alignment Holes:", self.alignment_holes)
+
+        ## Drill diameter for alignment holes
+        self.drill_dia = LengthEntry()
+        form_layout.addRow("Drill diam.:", self.drill_dia)
+
+        ## Buttons
+        hlay = QtGui.QHBoxLayout()
+        self.layout.addLayout(hlay)
+        hlay.addStretch()
+        self.create_alignment_hole_button = QtGui.QPushButton("Create Alignment Drill")
+        self.mirror_object_button = QtGui.QPushButton("Mirror Object")
+        hlay.addWidget(self.create_alignment_hole_button)
+        hlay.addWidget(self.mirror_object_button)
+
+        self.layout.addStretch()
+
+        ## Signals
+        self.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes)
+        self.mirror_object_button.clicked.connect(self.on_mirror)
+
+        self.axis_location.group_toggle_fn = self.on_toggle_pointbox
+
+        ## Initialize form
+        self.mirror_axis.set_value('X')
+        self.axis_location.set_value('point')
+
+    def on_create_alignment_holes(self):
+        axis = self.mirror_axis.get_value()
+        mode = self.axis_location.get_value()
+
+        if mode == "point":
+            px, py = self.point.get_value()
+        else:
+            selection_index = self.box_combo.currentIndex()
+            bb_obj = self.app.collection.object_list[selection_index]  # TODO: Direct access??
+            xmin, ymin, xmax, ymax = bb_obj.bounds()
+            px = 0.5*(xmin+xmax)
+            py = 0.5*(ymin+ymax)
+
+        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+        dia = self.drill_dia.get_value()
+        tools = {"1": {"C": dia}}
+
+        holes = self.alignment_holes.get_value()
+        drills = []
+
+        for hole in holes:
+            point = Point(hole)
+            point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
+            drills.append({"point": point, "tool": "1"})
+            drills.append({"point": point_mirror, "tool": "1"})
+
+        def obj_init(obj_inst, app_inst):
+            obj_inst.tools = tools
+            obj_inst.drills = drills
+            obj_inst.create_geometry()
+
+        self.app.new_object("excellon", "Alignment Drills", obj_init)
+
+    def on_mirror(self):
+        selection_index = self.object_combo.currentIndex()
+        fcobj = self.app.collection.object_list[selection_index]
+
+        # For now, lets limit to Gerbers and Excellons.
+        # assert isinstance(gerb, FlatCAMGerber)
+        if not isinstance(fcobj, FlatCAMGerber) and not isinstance(fcobj, FlatCAMExcellon):
+            self.info("ERROR: Only Gerber and Excellon objects can be mirrored.")
+            return
+
+        axis = self.mirror_axis.get_value()
+        mode = self.axis_location.get_value()
+
+        if mode == "point":
+            px, py = self.point.get_value()
+        else:
+            selection_index = self.box_combo.currentIndex()
+            bb_obj = self.app.collection.object_list[selection_index]  # TODO: Direct access??
+            xmin, ymin, xmax, ymax = bb_obj.bounds()
+            px = 0.5*(xmin+xmax)
+            py = 0.5*(ymin+ymax)
+
+        fcobj.mirror(axis, [px, py])
+        fcobj.plot()
+
+    def on_toggle_pointbox(self):
+        if self.axis_location.get_value() == "point":
+            self.point.show()
+            self.box_combo.hide()
+        else:
+            self.point.hide()
+            self.box_combo.show()
+
+
+class Measurement(FlatCAMTool):
+
+    toolName = "Measurement Tool"
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        # self.setContentsMargins(0, 0, 0, 0)
+        self.layout.setMargin(0)
+        self.layout.setContentsMargins(0, 0, 3, 0)
+
+        self.setSizePolicy(QtGui.QSizePolicy.Ignored, QtGui.QSizePolicy.Maximum)
+
+        self.point1 = None
+        self.point2 = None
+        self.label = QtGui.QLabel("Click on a reference point ...")
+        self.label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
+        self.label.setMargin(3)
+        self.layout.addWidget(self.label)
+        # self.layout.setMargin(0)
+        self.setVisible(False)
+
+        self.click_subscription = None
+        self.move_subscription = None
+
+    def install(self):
+        FlatCAMTool.install(self)
+        self.app.ui.right_layout.addWidget(self)
+        self.app.plotcanvas.mpl_connect('key_press_event', self.on_key_press)
+
+    def run(self):
+        self.toggle()
+
+    def on_click(self, event):
+        if self.point1 is None:
+            self.point1 = (event.xdata, event.ydata)
+        else:
+            self.point2 = copy(self.point1)
+            self.point1 = (event.xdata, event.ydata)
+        self.on_move(event)
+
+    def on_key_press(self, event):
+        if event.key == 'm':
+            self.toggle()
+
+    def toggle(self):
+        if self.isVisible():
+            self.setVisible(False)
+            self.app.plotcanvas.mpl_disconnect(self.move_subscription)
+            self.app.plotcanvas.mpl_disconnect(self.click_subscription)
+        else:
+            self.setVisible(True)
+            self.move_subscription = self.app.plotcanvas.mpl_connect('motion_notify_event', self.on_move)
+            self.click_subscription = self.app.plotcanvas.mpl_connect('button_press_event', self.on_click)
+
+    def on_move(self, event):
+        if self.point1 is None:
+            self.label.setText("Click on a reference point...")
+        else:
+            try:
+                dx = event.xdata - self.point1[0]
+                dy = event.ydata - self.point1[1]
+                d = sqrt(dx**2 + dy**2)
+                self.label.setText("D = %.4f  D(x) = %.4f  D(y) = %.4f" % (d, dx, dy))
+            except TypeError:
+                pass
+        if self.update is not None:
+            self.update()
+
+
+

+ 19 - 33
FlatCAMWorker.py

@@ -1,43 +1,29 @@
-############################################################
-# FlatCAM: 2D Post-processing for Manufacturing            #
-# http://caram.cl/software/flatcam                         #
-# Author: Juan Pablo Caram (c)                             #
-# Date: 2/5/2014                                           #
-# MIT Licence                                              #
-############################################################
+from PyQt4 import QtCore
+#import Queue
+import FlatCAMApp
 
-import threading
-import Queue
 
-
-class Worker(threading.Thread):
+class Worker(QtCore.QObject):
     """
     Implements a queue of tasks to be carried out in order
     in a single independent thread.
     """
 
-    def __init__(self):
+    def __init__(self, app, name=None):
         super(Worker, self).__init__()
-        self.queue = Queue.Queue()
-        self.stoprequest = threading.Event()
+        self.app = app
+        self.name = name
 
     def run(self):
-        while not self.stoprequest.isSet():
-            try:
-                task = self.queue.get(True, 0.05)
-                self.do_task(task)
-            except Queue.Empty:
-                continue
-
-    @staticmethod
-    def do_task(task):
-        task['fcn'](*task['params'])
-        return
-
-    def add_task(self, target, params=list()):
-        self.queue.put({'fcn': target, 'params': params})
-        return
-
-    def join(self, timeout=None):
-        self.stoprequest.set()
-        super(Worker, self).join()
+        FlatCAMApp.App.log.debug("Worker Started!")
+        self.app.worker_task.connect(self.do_worker_task)
+
+    def do_worker_task(self, task):
+        FlatCAMApp.App.log.debug("Running task: %s" % str(task))
+        if 'worker_name' in task and task['worker_name'] == self.name:
+            task['fcn'](*task['params'])
+            return
+
+        if 'worker_name' not in task and self.name is None:
+            task['fcn'](*task['params'])
+            return

+ 46 - 0
FlatCAM_GTK/FCNoteBook.py

@@ -0,0 +1,46 @@
+from gi.repository import Gtk
+
+
+class FCNoteBook(Gtk.Notebook):
+
+    def __init__(self):
+        Gtk.Notebook.__init__(self, vexpand=True, vexpand_set=True, valign=1, expand=True)
+
+        ###############
+        ### Project ###
+        ###############
+        self.project_contents = Gtk.VBox(vexpand=True, valign=0, vexpand_set=True, expand=True)
+        sw1 = Gtk.ScrolledWindow(vexpand=True, valign=0, vexpand_set=True, expand=True)
+        sw1.add_with_viewport(self.project_contents)
+        self.project_page_num = self.append_page(sw1, Gtk.Label("Project"))
+
+        ################
+        ### Selected ###
+        ################
+        self.selected_contents = Gtk.VBox()
+        sw2 = Gtk.ScrolledWindow()
+        sw2.add_with_viewport(self.selected_contents)
+        self.selected_page_num = self.append_page(sw2, Gtk.Label("Selected"))
+
+        ###############
+        ### Options ###
+        ###############
+        self.options_contents_super = Gtk.VBox()
+        sw3 = Gtk.ScrolledWindow()
+        sw3.add_with_viewport(self.options_contents_super)
+        self.options_page_num = self.append_page(sw3, Gtk.Label("Options"))
+
+        hb = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+        ico = Gtk.Image.new_from_file("share/gear32.png")
+        hb.pack_start(ico, expand=False, fill=False, padding=0)
+        self.combo_options = Gtk.ComboBoxText()
+        hb.pack_start(self.combo_options, expand=True, fill=True, padding=0)
+        self.options_contents_super.pack_start(hb, expand=False, fill=False, padding=0)
+        self.options_contents = Gtk.VBox()
+        self.options_contents_super.pack_start(self.options_contents, expand=False, fill=False, padding=0)
+
+        ############
+        ### Tool ###
+        ############
+        self.tool_contents = Gtk.VBox()
+        self.tool_page_num = self.append_page(self.tool_contents, Gtk.Label("Tool"))

+ 15 - 0
FlatCAM_GTK/FlatCAM.py

@@ -0,0 +1,15 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://caram.cl/software/flatcam                         #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+from gi.repository import Gtk
+
+from FlatCAM_GTK.FlatCAMApp import *
+
+
+app = App()
+Gtk.main()

+ 0 - 0
FlatCAM.ui → FlatCAM_GTK/FlatCAM.ui


+ 2478 - 0
FlatCAM_GTK/FlatCAMApp.py

@@ -0,0 +1,2478 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://caram.cl/software/flatcam                         #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+import threading
+import sys
+import urllib
+import random
+
+from gi.repository import Gtk, GdkPixbuf, GObject, Gdk, GLib
+
+
+
+
+
+
+# from shapely import speedups
+# Importing shapely speedups was causing the following errors:
+# 'C:\WinPython-32\python-2.7.6\Lib\site-packages\gnome\lib/gio/modules\
+# libgiognutls.dll': The specified module could not be found.
+# Failed to load module: C:\WinPython-32\python-2.7.6\Lib\site-packages\gnome\lib/gio/modules\libgiognutls.dll
+# 'C:\WinPython-32\python-2.7.6\Lib\site-packages\gnome\lib/gio/modules\
+# libgiolibproxy.dll': The specified module could not be found.
+# Failed to load module: C:\WinPython-32\python-2.7.6\Lib\site-packages\gnome\lib/gio/modules\libgiolibproxy.dll
+
+
+########################################
+##      Imports part of FlatCAM       ##
+########################################
+from FlatCAM_GTK.FlatCAMWorker import Worker
+from FlatCAM_GTK.ObjectCollection import *
+from FlatCAM_GTK.FlatCAMObj import *
+from FlatCAM_GTK.PlotCanvas import *
+from FlatCAM_GTK.FlatCAMGUI import *
+
+
+class GerberOptionsGroupUI(Gtk.VBox):
+    def __init__(self):
+        Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
+
+        ## Plot options
+        self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.plot_options_label.set_markup("<b>Plot Options:</b>")
+        self.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
+
+        grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid0, expand=True, fill=False, padding=2)
+
+        # Plot CB
+        self.plot_cb = FCCheckBox(label='Plot')
+        grid0.attach(self.plot_cb, 0, 0, 1, 1)
+
+        # Solid CB
+        self.solid_cb = FCCheckBox(label='Solid')
+        grid0.attach(self.solid_cb, 1, 0, 1, 1)
+
+        # Multicolored CB
+        self.multicolored_cb = FCCheckBox(label='Multicolored')
+        grid0.attach(self.multicolored_cb, 2, 0, 1, 1)
+
+        ## Isolation Routing
+        self.isolation_routing_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.isolation_routing_label.set_markup("<b>Isolation Routing:</b>")
+        self.pack_start(self.isolation_routing_label, expand=True, fill=False, padding=2)
+
+        grid = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid, expand=True, fill=False, padding=2)
+
+        l1 = Gtk.Label('Tool diam:', xalign=1)
+        grid.attach(l1, 0, 0, 1, 1)
+        self.iso_tool_dia_entry = LengthEntry()
+        grid.attach(self.iso_tool_dia_entry, 1, 0, 1, 1)
+
+        l2 = Gtk.Label('Width (# passes):', xalign=1)
+        grid.attach(l2, 0, 1, 1, 1)
+        self.iso_width_entry = IntEntry()
+        grid.attach(self.iso_width_entry, 1, 1, 1, 1)
+
+        l3 = Gtk.Label('Pass overlap:', xalign=1)
+        grid.attach(l3, 0, 2, 1, 1)
+        self.iso_overlap_entry = FloatEntry()
+        grid.attach(self.iso_overlap_entry, 1, 2, 1, 1)
+
+        ## Board cuttout
+        self.isolation_routing_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.isolation_routing_label.set_markup("<b>Board cutout:</b>")
+        self.pack_start(self.isolation_routing_label, expand=True, fill=False, padding=2)
+
+        grid2 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid2, expand=True, fill=False, padding=2)
+
+        l4 = Gtk.Label('Tool dia:', xalign=1)
+        grid2.attach(l4, 0, 0, 1, 1)
+        self.cutout_tooldia_entry = LengthEntry()
+        grid2.attach(self.cutout_tooldia_entry, 1, 0, 1, 1)
+
+        l5 = Gtk.Label('Margin:', xalign=1)
+        grid2.attach(l5, 0, 1, 1, 1)
+        self.cutout_margin_entry = LengthEntry()
+        grid2.attach(self.cutout_margin_entry, 1, 1, 1, 1)
+
+        l6 = Gtk.Label('Gap size:', xalign=1)
+        grid2.attach(l6, 0, 2, 1, 1)
+        self.cutout_gap_entry = LengthEntry()
+        grid2.attach(self.cutout_gap_entry, 1, 2, 1, 1)
+
+        l7 = Gtk.Label('Gaps:', xalign=1)
+        grid2.attach(l7, 0, 3, 1, 1)
+        self.gaps_radio = RadioSet([{'label': '2 (T/B)', 'value': 'tb'},
+                                    {'label': '2 (L/R)', 'value': 'lr'},
+                                    {'label': '4', 'value': '4'}])
+        grid2.attach(self.gaps_radio, 1, 3, 1, 1)
+
+        ## Non-copper regions
+        self.noncopper_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.noncopper_label.set_markup("<b>Non-copper regions:</b>")
+        self.pack_start(self.noncopper_label, expand=True, fill=False, padding=2)
+
+        grid3 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid3, expand=True, fill=False, padding=2)
+
+        l8 = Gtk.Label('Boundary margin:', xalign=1)
+        grid3.attach(l8, 0, 0, 1, 1)
+        self.noncopper_margin_entry = LengthEntry()
+        grid3.attach(self.noncopper_margin_entry, 1, 0, 1, 1)
+
+        self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners")
+        grid3.attach(self.noncopper_rounded_cb, 0, 1, 2, 1)
+
+        ## Bounding box
+        self.boundingbox_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.boundingbox_label.set_markup('<b>Bounding Box:</b>')
+        self.pack_start(self.boundingbox_label, expand=True, fill=False, padding=2)
+
+        grid4 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid4, expand=True, fill=False, padding=2)
+
+        l9 = Gtk.Label('Boundary Margin:', xalign=1)
+        grid4.attach(l9, 0, 0, 1, 1)
+        self.bbmargin_entry = LengthEntry()
+        grid4.attach(self.bbmargin_entry, 1, 0, 1, 1)
+
+        self.bbrounded_cb = FCCheckBox(label="Rounded corners")
+        grid4.attach(self.bbrounded_cb, 0, 1, 2, 1)
+
+
+class ExcellonOptionsGroupUI(Gtk.VBox):
+    def __init__(self):
+        Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
+
+        ## Plot options
+        self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.plot_options_label.set_markup("<b>Plot Options:</b>")
+        self.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
+
+        grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid0, expand=True, fill=False, padding=2)
+
+        self.plot_cb = FCCheckBox(label='Plot')
+        grid0.attach(self.plot_cb, 0, 0, 1, 1)
+
+        self.solid_cb = FCCheckBox(label='Solid')
+        grid0.attach(self.solid_cb, 1, 0, 1, 1)
+
+        ## Create CNC Job
+        self.cncjob_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.cncjob_label.set_markup('<b>Create CNC Job</b>')
+        self.pack_start(self.cncjob_label, expand=True, fill=False, padding=2)
+
+        grid1 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid1, expand=True, fill=False, padding=2)
+
+        l1 = Gtk.Label('Cut Z:', xalign=1)
+        grid1.attach(l1, 0, 0, 1, 1)
+        self.cutz_entry = LengthEntry()
+        grid1.attach(self.cutz_entry, 1, 0, 1, 1)
+
+        l2 = Gtk.Label('Travel Z:', xalign=1)
+        grid1.attach(l2, 0, 1, 1, 1)
+        self.travelz_entry = LengthEntry()
+        grid1.attach(self.travelz_entry, 1, 1, 1, 1)
+
+        l3 = Gtk.Label('Feed rate:', xalign=1)
+        grid1.attach(l3, 0, 2, 1, 1)
+        self.feedrate_entry = LengthEntry()
+        grid1.attach(self.feedrate_entry, 1, 2, 1, 1)
+
+
+class GeometryOptionsGroupUI(Gtk.VBox):
+    def __init__(self):
+        Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
+
+        ## Plot options
+        self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.plot_options_label.set_markup("<b>Plot Options:</b>")
+        self.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
+
+        grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid0, expand=True, fill=False, padding=2)
+
+        # Plot CB
+        self.plot_cb = FCCheckBox(label='Plot')
+        grid0.attach(self.plot_cb, 0, 0, 1, 1)
+
+        ## Create CNC Job
+        self.cncjob_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.cncjob_label.set_markup('<b>Create CNC Job:</b>')
+        self.pack_start(self.cncjob_label, expand=True, fill=False, padding=2)
+
+        grid1 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid1, expand=True, fill=False, padding=2)
+
+        # Cut Z
+        l1 = Gtk.Label('Cut Z:', xalign=1)
+        grid1.attach(l1, 0, 0, 1, 1)
+        self.cutz_entry = LengthEntry()
+        grid1.attach(self.cutz_entry, 1, 0, 1, 1)
+
+        # Travel Z
+        l2 = Gtk.Label('Travel Z:', xalign=1)
+        grid1.attach(l2, 0, 1, 1, 1)
+        self.travelz_entry = LengthEntry()
+        grid1.attach(self.travelz_entry, 1, 1, 1, 1)
+
+        l3 = Gtk.Label('Feed rate:', xalign=1)
+        grid1.attach(l3, 0, 2, 1, 1)
+        self.cncfeedrate_entry = LengthEntry()
+        grid1.attach(self.cncfeedrate_entry, 1, 2, 1, 1)
+
+        l4 = Gtk.Label('Tool dia:', xalign=1)
+        grid1.attach(l4, 0, 3, 1, 1)
+        self.cnctooldia_entry = LengthEntry()
+        grid1.attach(self.cnctooldia_entry, 1, 3, 1, 1)
+
+        ## Paint Area
+        self.paint_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.paint_label.set_markup('<b>Paint Area:</b>')
+        self.pack_start(self.paint_label, expand=True, fill=False, padding=2)
+
+        grid2 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid2, expand=True, fill=False, padding=2)
+
+        # Tool dia
+        l5 = Gtk.Label('Tool dia:', xalign=1)
+        grid2.attach(l5, 0, 0, 1, 1)
+        self.painttooldia_entry = LengthEntry()
+        grid2.attach(self.painttooldia_entry, 1, 0, 1, 1)
+
+        # Overlap
+        l6 = Gtk.Label('Overlap:', xalign=1)
+        grid2.attach(l6, 0, 1, 1, 1)
+        self.paintoverlap_entry = LengthEntry()
+        grid2.attach(self.paintoverlap_entry, 1, 1, 1, 1)
+
+        # Margin
+        l7 = Gtk.Label('Margin:', xalign=1)
+        grid2.attach(l7, 0, 2, 1, 1)
+        self.paintmargin_entry = LengthEntry()
+        grid2.attach(self.paintmargin_entry, 1, 2, 1, 1)
+
+
+class CNCJobOptionsGroupUI(Gtk.VBox):
+    def __init__(self):
+        Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
+
+        ## Plot options
+        self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.plot_options_label.set_markup("<b>Plot Options:</b>")
+        self.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
+
+        grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid0, expand=True, fill=False, padding=2)
+
+        # Plot CB
+        self.plot_cb = FCCheckBox(label='Plot')
+        grid0.attach(self.plot_cb, 0, 0, 2, 1)
+
+        # Tool dia for plot
+        l1 = Gtk.Label('Tool dia:', xalign=1)
+        grid0.attach(l1, 0, 1, 1, 1)
+        self.tooldia_entry = LengthEntry()
+        grid0.attach(self.tooldia_entry, 1, 1, 1, 1)
+
+
+class GlobalOptionsUI(Gtk.VBox):
+    def __init__(self):
+        Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
+
+        box1 = Gtk.Box()
+        self.pack_start(box1, expand=False, fill=False, padding=2)
+        l1 = Gtk.Label('Units:')
+        box1.pack_start(l1, expand=False, fill=False, padding=2)
+        self.units_radio = RadioSet([{'label': 'inch', 'value': 'IN'},
+                                     {'label': 'mm', 'value': 'MM'}])
+        box1.pack_start(self.units_radio, expand=False, fill=False, padding=2)
+
+        ####### Gerber #######
+        l2 = Gtk.Label(margin=5)
+        l2.set_markup('<b>Gerber Options</b>')
+        frame1 = Gtk.Frame(label_widget=l2)
+        self.pack_start(frame1, expand=False, fill=False, padding=2)
+        self.gerber_group = GerberOptionsGroupUI()
+        frame1.add(self.gerber_group)
+
+        ######## Excellon #########
+        l3 = Gtk.Label(margin=5)
+        l3.set_markup('<b>Excellon Options</b>')
+        frame2 = Gtk.Frame(label_widget=l3)
+        self.pack_start(frame2, expand=False, fill=False, padding=2)
+        self.excellon_group = ExcellonOptionsGroupUI()
+        frame2.add(self.excellon_group)
+
+        ########## Geometry ##########
+        l4 = Gtk.Label(margin=5)
+        l4.set_markup('<b>Geometry Options</b>')
+        frame3 = Gtk.Frame(label_widget=l4)
+        self.pack_start(frame3, expand=False, fill=False, padding=2)
+        self.geometry_group = GeometryOptionsGroupUI()
+        frame3.add(self.geometry_group)
+
+        ########## CNC ############
+        l5 = Gtk.Label(margin=5)
+        l5.set_markup('<b>CNC Job Options</b>')
+        frame4 = Gtk.Frame(label_widget=l5)
+        self.pack_start(frame4, expand=False, fill=False, padding=2)
+        self.cncjob_group = CNCJobOptionsGroupUI()
+        frame4.add(self.cncjob_group)
+
+
+########################################
+##                App                 ##
+########################################
+class App:
+    """
+    The main application class. The constructor starts the GUI.
+    """
+
+    log = logging.getLogger('base')
+    log.setLevel(logging.DEBUG)
+    #log.setLevel(logging.WARNING)
+    formatter = logging.Formatter('[%(levelname)s] %(message)s')
+    handler = logging.StreamHandler()
+    handler.setFormatter(formatter)
+    log.addHandler(handler)
+
+    version_url = "http://caram.cl/flatcam/VERSION"
+
+    def __init__(self):
+        """
+        Starts the application. Takes no parameters.
+
+        :return: app
+        :rtype: App
+        """
+
+        App.log.info("FlatCAM Starting...")
+
+        # if speedups.available:
+        #     App.log.info("Enabling geometry speedups...")
+        #     speedups.enable()
+
+        # Needed to interact with the GUI from other threads.
+        App.log.debug("GObject.threads_init()...")
+        GObject.threads_init()
+
+        #### GUI ####
+        # Glade init
+        # App.log.debug("Building GUI from Glade file...")
+        # self.gladefile = "FlatCAM.ui"
+        # self.builder = Gtk.Builder()
+        # self.builder.add_from_file(self.gladefile)
+        #
+        # # References to UI widgets
+        # self.window = self.builder.get_object("window1")
+        # self.position_label = self.builder.get_object("label3")
+        # self.grid = self.builder.get_object("grid1")
+        # self.notebook = self.builder.get_object("notebook1")
+        # self.info_label = self.builder.get_object("label_status")
+        # self.progress_bar = self.builder.get_object("progressbar")
+        # self.progress_bar.set_show_text(True)
+        # self.units_label = self.builder.get_object("label_units")
+        # self.toolbar = self.builder.get_object("toolbar_main")
+        #
+        # # White (transparent) background on the "Options" tab.
+        # self.builder.get_object("vp_options").override_background_color(Gtk.StateType.NORMAL,
+        #                                                                 Gdk.RGBA(1, 1, 1, 1))
+        # # Combo box to choose between project and application options.
+        # self.combo_options = self.builder.get_object("combo_options")
+        # self.combo_options.set_active(1)
+        self.ui = FlatCAMGUI()
+
+        #self.setup_project_list()  # The "Project" tab
+        self.setup_component_editor()  # The "Selected" tab
+
+        ## Setup the toolbar. Adds buttons.
+        self.setup_toolbar()
+
+        # App.log.debug("Connecting signals from builder...")
+        #### Event handling ####
+        # self.builder.connect_signals(self)
+        self.ui.menufileopengerber.connect('activate', self.on_fileopengerber)
+
+        #### Make plot area ####
+        self.plotcanvas = PlotCanvas(self.ui.plotarea)
+        self.plotcanvas.mpl_connect('button_press_event', self.on_click_over_plot)
+        self.plotcanvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
+        self.plotcanvas.mpl_connect('key_press_event', self.on_key_over_plot)
+
+        #### DATA ####
+        self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
+        self.setup_obj_classes()
+        self.mouse = None  # Mouse coordinates over plot
+        self.recent = []
+        self.collection = ObjectCollection()
+        # self.builder.get_object("box_project").pack_start(self.collection.view, False, False, 1)
+        self.ui.notebook.project_contents.pack_start(self.collection.view, False, False, 1)
+        # TODO: Do this different
+        self.collection.view.connect("row_activated", self.on_row_activated)
+
+        # Used to inhibit the on_options_update callback when
+        # the options are being changed by the program and not the user.
+        self.options_update_ignore = False
+
+        self.toggle_units_ignore = False
+
+        # self.options_box = self.builder.get_object('options_box')
+        ## Application defaults ##
+        self.defaults_form = GlobalOptionsUI()
+        self.defaults_form_fields = {
+            "units": self.defaults_form.units_radio,
+            "gerber_plot": self.defaults_form.gerber_group.plot_cb,
+            "gerber_solid": self.defaults_form.gerber_group.solid_cb,
+            "gerber_multicolored": self.defaults_form.gerber_group.multicolored_cb,
+            "gerber_isotooldia": self.defaults_form.gerber_group.iso_tool_dia_entry,
+            "gerber_isopasses": self.defaults_form.gerber_group.iso_width_entry,
+            "gerber_isooverlap": self.defaults_form.gerber_group.iso_overlap_entry,
+            "gerber_cutouttooldia": self.defaults_form.gerber_group.cutout_tooldia_entry,
+            "gerber_cutoutmargin": self.defaults_form.gerber_group.cutout_margin_entry,
+            "gerber_cutoutgapsize": self.defaults_form.gerber_group.cutout_gap_entry,
+            "gerber_gaps": self.defaults_form.gerber_group.gaps_radio,
+            "gerber_noncoppermargin": self.defaults_form.gerber_group.noncopper_margin_entry,
+            "gerber_noncopperrounded": self.defaults_form.gerber_group.noncopper_rounded_cb,
+            "gerber_bboxmargin": self.defaults_form.gerber_group.bbmargin_entry,
+            "gerber_bboxrounded": self.defaults_form.gerber_group.bbrounded_cb,
+            "excellon_plot": self.defaults_form.excellon_group.plot_cb,
+            "excellon_solid": self.defaults_form.excellon_group.solid_cb,
+            "excellon_drillz": self.defaults_form.excellon_group.cutz_entry,
+            "excellon_travelz": self.defaults_form.excellon_group.travelz_entry,
+            "excellon_feedrate": self.defaults_form.excellon_group.feedrate_entry,
+            "geometry_plot": self.defaults_form.geometry_group.plot_cb,
+            "geometry_cutz": self.defaults_form.geometry_group.cutz_entry,
+            "geometry_travelz": self.defaults_form.geometry_group.travelz_entry,
+            "geometry_feedrate": self.defaults_form.geometry_group.cncfeedrate_entry,
+            "geometry_cnctooldia": self.defaults_form.geometry_group.cnctooldia_entry,
+            "geometry_painttooldia": self.defaults_form.geometry_group.painttooldia_entry,
+            "geometry_paintoverlap": self.defaults_form.geometry_group.paintoverlap_entry,
+            "geometry_paintmargin": self.defaults_form.geometry_group.paintmargin_entry,
+            "cncjob_plot": self.defaults_form.cncjob_group.plot_cb,
+            "cncjob_tooldia": self.defaults_form.cncjob_group.tooldia_entry
+        }
+
+        self.defaults = {
+            "units": "IN",
+            "gerber_plot": True,
+            "gerber_solid": True,
+            "gerber_multicolored": False,
+            "gerber_isotooldia": 0.016,
+            "gerber_isopasses": 1,
+            "gerber_isooverlap": 0.15,
+            "gerber_cutouttooldia": 0.07,
+            "gerber_cutoutmargin": 0.1,
+            "gerber_cutoutgapsize": 0.15,
+            "gerber_gaps": "4",
+            "gerber_noncoppermargin": 0.0,
+            "gerber_noncopperrounded": False,
+            "gerber_bboxmargin": 0.0,
+            "gerber_bboxrounded": False,
+            "excellon_plot": True,
+            "excellon_solid": False,
+            "excellon_drillz": -0.1,
+            "excellon_travelz": 0.1,
+            "excellon_feedrate": 3.0,
+            "geometry_plot": True,
+            "geometry_cutz": -0.002,
+            "geometry_travelz": 0.1,
+            "geometry_feedrate": 3.0,
+            "geometry_cnctooldia": 0.016,
+            "geometry_painttooldia": 0.07,
+            "geometry_paintoverlap": 0.15,
+            "geometry_paintmargin": 0.0,
+            "cncjob_plot": True,
+            "cncjob_tooldia": 0.016
+        }
+        self.load_defaults()
+        self.defaults_write_form()
+
+        ## Current Project ##
+        self.options_form = GlobalOptionsUI()
+        self.options_form_fields = {
+            "units": self.options_form.units_radio,
+            "gerber_plot": self.options_form.gerber_group.plot_cb,
+            "gerber_solid": self.options_form.gerber_group.solid_cb,
+            "gerber_multicolored": self.options_form.gerber_group.multicolored_cb,
+            "gerber_isotooldia": self.options_form.gerber_group.iso_tool_dia_entry,
+            "gerber_isopasses": self.options_form.gerber_group.iso_width_entry,
+            "gerber_isooverlap": self.options_form.gerber_group.iso_overlap_entry,
+            "gerber_cutouttooldia": self.options_form.gerber_group.cutout_tooldia_entry,
+            "gerber_cutoutmargin": self.options_form.gerber_group.cutout_margin_entry,
+            "gerber_cutoutgapsize": self.options_form.gerber_group.cutout_gap_entry,
+            "gerber_gaps": self.options_form.gerber_group.gaps_radio,
+            "gerber_noncoppermargin": self.options_form.gerber_group.noncopper_margin_entry,
+            "gerber_noncopperrounded": self.options_form.gerber_group.noncopper_rounded_cb,
+            "gerber_bboxmargin": self.options_form.gerber_group.bbmargin_entry,
+            "gerber_bboxrounded": self.options_form.gerber_group.bbrounded_cb,
+            "excellon_plot": self.options_form.excellon_group.plot_cb,
+            "excellon_solid": self.options_form.excellon_group.solid_cb,
+            "excellon_drillz": self.options_form.excellon_group.cutz_entry,
+            "excellon_travelz": self.options_form.excellon_group.travelz_entry,
+            "excellon_feedrate": self.options_form.excellon_group.feedrate_entry,
+            "geometry_plot": self.options_form.geometry_group.plot_cb,
+            "geometry_cutz": self.options_form.geometry_group.cutz_entry,
+            "geometry_travelz": self.options_form.geometry_group.travelz_entry,
+            "geometry_feedrate": self.options_form.geometry_group.cncfeedrate_entry,
+            "geometry_cnctooldia": self.options_form.geometry_group.cnctooldia_entry,
+            "geometry_painttooldia": self.options_form.geometry_group.painttooldia_entry,
+            "geometry_paintoverlap": self.options_form.geometry_group.paintoverlap_entry,
+            "geometry_paintmargin": self.options_form.geometry_group.paintmargin_entry,
+            "cncjob_plot": self.options_form.cncjob_group.plot_cb,
+            "cncjob_tooldia": self.options_form.cncjob_group.tooldia_entry
+        }
+
+        # Project options
+        self.options = {
+            "units": "IN",
+            "gerber_plot": True,
+            "gerber_solid": True,
+            "gerber_multicolored": False,
+            "gerber_isotooldia": 0.016,
+            "gerber_isopasses": 1,
+            "gerber_isooverlap": 0.15,
+            "gerber_cutouttooldia": 0.07,
+            "gerber_cutoutmargin": 0.1,
+            "gerber_cutoutgapsize": 0.15,
+            "gerber_gaps": "4",
+            "gerber_noncoppermargin": 0.0,
+            "gerber_noncopperrounded": False,
+            "gerber_bboxmargin": 0.0,
+            "gerber_bboxrounded": False,
+            "excellon_plot": True,
+            "excellon_solid": False,
+            "excellon_drillz": -0.1,
+            "excellon_travelz": 0.1,
+            "excellon_feedrate": 3.0,
+            "geometry_plot": True,
+            "geometry_cutz": -0.002,
+            "geometry_travelz": 0.1,
+            "geometry_feedrate": 3.0,
+            "geometry_cnctooldia": 0.016,
+            "geometry_painttooldia": 0.07,
+            "geometry_paintoverlap": 0.15,
+            "geometry_paintmargin": 0.0,
+            "cncjob_plot": True,
+            "cncjob_tooldia": 0.016
+        }
+        self.options.update(self.defaults)  # Copy app defaults to project options
+        self.options_write_form()
+
+        self.project_filename = None
+
+        # Where we draw the options/defaults forms.
+        self.on_options_combo_change(None)
+        #self.options_box.pack_start(self.defaults_form, False, False, 1)
+
+        self.options_form.units_radio.group_toggle_fn = lambda x, y: self.on_toggle_units(x)
+
+        ## Event subscriptions ##
+
+        ## Tools ##
+        # self.measure = Measurement(self.builder.get_object("box39"), self.plotcanvas)
+        self.measure = Measurement(self.ui.plotarea_super, self.plotcanvas)
+        # Toolbar icon
+        # TODO: Where should I put this? Tool should have a method to add to toolbar?
+        meas_ico = Gtk.Image.new_from_file('share/measure32.png')
+        measure = Gtk.ToolButton.new(meas_ico, "")
+        measure.connect("clicked", self.measure.toggle_active)
+        measure.set_tooltip_markup("<b>Measure Tool:</b> Enable/disable tool.\n" +
+                                   "Click on point to set reference.\n" +
+                                   "(Click on plot and hit <b>m</b>)")
+        # self.toolbar.insert(measure, -1)
+        self.ui.toolbar.insert(measure, -1)
+
+        #### Initialization ####
+        # self.units_label.set_text("[" + self.options["units"] + "]")
+        self.ui.units_label.set_text("[" + self.options["units"] + "]")
+        self.setup_recent_items()
+
+        App.log.info("Starting Worker...")
+        self.worker = Worker()
+        self.worker.daemon = True
+        self.worker.start()
+
+        #### Check for updates ####
+        # Separate thread (Not worker)
+        self.version = 5
+        App.log.info("Checking for updates in backgroud (this is version %s)." % str(self.version))
+        t1 = threading.Thread(target=self.version_check)
+        t1.daemon = True
+        t1.start()
+
+        #### For debugging only ###
+        def somethreadfunc(app_obj):
+            App.log.info("Hello World!")
+
+        t = threading.Thread(target=somethreadfunc, args=(self,))
+        t.daemon = True
+        t.start()
+
+        ########################################
+        ##              START                 ##
+        ########################################
+        self.icon256 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon256.png')
+        self.icon48 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon48.png')
+        self.icon16 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon16.png')
+        Gtk.Window.set_default_icon_list([self.icon16, self.icon48, self.icon256])
+        # self.window.set_title("FlatCAM - Alpha 5")
+        # self.window.set_default_size(900, 600)
+        # self.window.show_all()
+        self.ui.show_all()
+
+        App.log.info("END of constructor. Releasing control.")
+
+    def message_dialog(self, title, message, kind="info"):
+        types = {"info": Gtk.MessageType.INFO,
+                 "warn": Gtk.MessageType.WARNING,
+                 "error": Gtk.MessageType.ERROR}
+        dlg = Gtk.MessageDialog(self.ui, 0, types[kind], Gtk.ButtonsType.OK, title)
+        dlg.format_secondary_text(message)
+
+        def lifecycle():
+            dlg.run()
+            dlg.destroy()
+
+        GLib.idle_add(lifecycle)
+
+    def question_dialog(self, title, message):
+        label = Gtk.Label(message)
+        dialog = Gtk.Dialog(title, self.window, 0,
+                            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+                             Gtk.STOCK_OK, Gtk.ResponseType.OK))
+        dialog.set_default_size(150, 100)
+        dialog.set_modal(True)
+        box = dialog.get_content_area()
+        box.set_border_width(10)
+        box.add(label)
+        dialog.show_all()
+        response = dialog.run()
+        dialog.destroy()
+        return response
+
+    def setup_toolbar(self):
+
+        # Zoom fit
+        # zf_ico = Gtk.Image.new_from_file('share/zoom_fit32.png')
+        # zoom_fit = Gtk.ToolButton.new(zf_ico, "")
+        # zoom_fit.connect("clicked", self.on_zoom_fit)
+        # zoom_fit.set_tooltip_markup("Zoom Fit.\n(Click on plot and hit <b>1</b>)")
+        # self.toolbar.insert(zoom_fit, -1)
+        self.ui.zoom_fit_btn.connect("clicked", self.on_zoom_fit)
+
+        # Zoom out
+        # zo_ico = Gtk.Image.new_from_file('share/zoom_out32.png')
+        # zoom_out = Gtk.ToolButton.new(zo_ico, "")
+        # zoom_out.connect("clicked", self.on_zoom_out)
+        # zoom_out.set_tooltip_markup("Zoom Out.\n(Click on plot and hit <b>2</b>)")
+        # self.toolbar.insert(zoom_out, -1)
+        self.ui.zoom_out_btn.connect("clicked", self.on_zoom_out)
+
+        # Zoom in
+        # zi_ico = Gtk.Image.new_from_file('share/zoom_in32.png')
+        # zoom_in = Gtk.ToolButton.new(zi_ico, "")
+        # zoom_in.connect("clicked", self.on_zoom_in)
+        # zoom_in.set_tooltip_markup("Zoom In.\n(Click on plot and hit <b>3</b>)")
+        # self.toolbar.insert(zoom_in, -1)
+        self.ui.zoom_in_btn.connect("clicked", self.on_zoom_in)
+
+        # Clear plot
+        # cp_ico = Gtk.Image.new_from_file('share/clear_plot32.png')
+        # clear_plot = Gtk.ToolButton.new(cp_ico, "")
+        # clear_plot.connect("clicked", self.on_clear_plots)
+        # clear_plot.set_tooltip_markup("Clear Plot")
+        # self.toolbar.insert(clear_plot, -1)
+        self.ui.clear_plot_btn.connect("clicked", self.on_clear_plots)
+
+        # Replot
+        # rp_ico = Gtk.Image.new_from_file('share/replot32.png')
+        # replot = Gtk.ToolButton.new(rp_ico, "")
+        # replot.connect("clicked", self.on_toolbar_replot)
+        # replot.set_tooltip_markup("Re-plot all")
+        # self.toolbar.insert(replot, -1)
+        self.ui.replot_btn.connect("clicked", self.on_toolbar_replot)
+
+        # Delete item
+        # del_ico = Gtk.Image.new_from_file('share/delete32.png')
+        # delete = Gtk.ToolButton.new(del_ico, "")
+        # delete.connect("clicked", self.on_delete)
+        # delete.set_tooltip_markup("Delete selected\nobject.")
+        # self.toolbar.insert(delete, -1)
+        self.ui.delete_btn.connect("clicked", self.on_delete)
+
+    def setup_obj_classes(self):
+        """
+        Sets up application specifics on the FlatCAMObj class.
+
+        :return: None
+        """
+        FlatCAMObj.app = self
+
+    def setup_component_editor(self):
+        """
+        Initial configuration of the component editor. Creates
+        a page titled "Selection" on the notebook on the left
+        side of the main window.
+
+        :return: None
+        """
+
+        # box_selected = self.builder.get_object("vp_selected")
+
+        # White background
+        # box_selected.override_background_color(Gtk.StateType.NORMAL,
+        #                                        Gdk.RGBA(1, 1, 1, 1))
+        self.ui.notebook.selected_contents.override_background_color(Gtk.StateType.NORMAL,
+                                                                     Gdk.RGBA(1, 1, 1, 1))
+
+        # Remove anything else in the box
+        box_children = self.ui.notebook.selected_contents.get_children()
+        for child in box_children:
+            self.ui.notebook.selected_contents.remove(child)
+
+        box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
+        label1 = Gtk.Label("Choose an item from Project")
+        box1.pack_start(label1, True, False, 1)
+        self.ui.notebook.selected_contents.add(box1)
+        box1.show()
+        label1.show()
+
+    def setup_recent_items(self):
+
+        # TODO: Move this to constructor
+        icons = {
+            "gerber": "share/flatcam_icon16.png",
+            "excellon": "share/drill16.png",
+            "cncjob": "share/cnc16.png",
+            "project": "share/project16.png"
+        }
+
+        openers = {
+            'gerber': self.open_gerber,
+            'excellon': self.open_excellon,
+            'cncjob': self.open_gcode,
+            'project': self.open_project
+        }
+
+        # Closure needed to create callbacks in a loop.
+        # Otherwise late binding occurs.
+        def make_callback(func, fname):
+            def opener(*args):
+                self.worker.add_task(func, [fname])
+            return opener
+
+        try:
+            f = open('recent.json')
+        except IOError:
+            App.log.error("Failed to load recent item list.")
+            self.info("ERROR: Failed to load recent item list.")
+            return
+
+        try:
+            self.recent = json.load(f)
+        except:
+            App.log.error("Failed to parse recent item list.")
+            self.info("ERROR: Failed to parse recent item list.")
+            f.close()
+            return
+        f.close()
+
+        recent_menu = Gtk.Menu()
+        for recent in self.recent:
+            filename = recent['filename'].split('/')[-1].split('\\')[-1]
+            item = Gtk.ImageMenuItem.new_with_label(filename)
+            im = Gtk.Image.new_from_file(icons[recent["kind"]])
+            item.set_image(im)
+
+            o = make_callback(openers[recent["kind"]], recent['filename'])
+
+            item.connect('activate', o)
+            recent_menu.append(item)
+
+        # self.builder.get_object('open_recent').set_submenu(recent_menu)
+        self.ui.menufilerecent.set_submenu(recent_menu)
+        recent_menu.show_all()
+
+    def info(self, text):
+        """
+        Show text on the status bar. This method is thread safe.
+
+        :param text: Text to display.
+        :type text: str
+        :return: None
+        """
+        GLib.idle_add(lambda: self.ui.info_label.set_text(text))
+
+    def get_radio_value(self, radio_set):
+        """
+        Returns the radio_set[key] of the radiobutton
+        whose name is key is active.
+
+        :param radio_set: A dictionary containing widget_name: value pairs.
+        :type radio_set: dict
+        :return: radio_set[key]
+        """
+
+        for name in radio_set:
+            if self.builder.get_object(name).get_active():
+                return radio_set[name]
+
+    def plot_all(self):
+        """
+        Re-generates all plots from all objects.
+
+        :return: None
+        """
+        self.plotcanvas.clear()
+        self.set_progress_bar(0.1, "Re-plotting...")
+
+        def worker_task(app_obj):
+            percentage = 0.1
+            try:
+                delta = 0.9 / len(self.collection.get_list())
+            except ZeroDivisionError:
+                GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
+                return
+            for obj in self.collection.get_list():
+                obj.plot()
+                percentage += delta
+                GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
+
+            GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
+            GLib.idle_add(lambda: self.on_zoom_fit(None))
+            GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
+
+        # Send to worker
+        self.worker.add_task(worker_task, [self])
+
+    def get_eval(self, widget_name):
+        """
+        Runs eval() on the on the text entry of name 'widget_name'
+        and returns the results.
+
+        :param widget_name: Name of Gtk.Entry
+        :type widget_name: str
+        :return: Depends on contents of the entry text.
+        """
+
+        value = self.builder.get_object(widget_name).get_text()
+        if value == "":
+            value = "None"
+        try:
+            evald = eval(value)
+            return evald
+        except:
+            self.info("Could not evaluate: " + value)
+            return None
+
+    def new_object(self, kind, name, initialize, active=True, fit=True, plot=True):
+        """
+        Creates a new specalized FlatCAMObj and attaches it to the application,
+        this is, updates the GUI accordingly, any other records and plots it.
+        This method is thread-safe.
+
+        :param kind: The kind of object to create. One of 'gerber',
+         'excellon', 'cncjob' and 'geometry'.
+        :type kind: str
+        :param name: Name for the object.
+        :type name: str
+        :param initialize: Function to run after creation of the object
+         but before it is attached to the application. The function is
+         called with 2 parameters: the new object and the App instance.
+        :type initialize: function
+        :return: None
+        :rtype: None
+        """
+
+        App.log.debug("new_object()")
+
+        # This is ok here... NO.
+        # t = Gtk.TextView()
+        # print t
+
+        ### Check for existing name
+        if name in self.collection.get_names():
+            ## Create a new name
+            # Ends with number?
+            App.log.debug("new_object(): Object name exists, changing.")
+            match = re.search(r'(.*[^\d])?(\d+)$', name)
+            if match:  # Yes: Increment the number!
+                base = match.group(1) or ''
+                num = int(match.group(2))
+                name = base + str(num + 1)
+            else:  # No: add a number!
+                name += "_1"
+
+        # App dies here!
+        # t = Gtk.TextView()
+        # print t
+
+        # Create object
+        classdict = {
+            "gerber": FlatCAMGerber,
+            "excellon": FlatCAMExcellon,
+            "cncjob": FlatCAMCNCjob,
+            "geometry": FlatCAMGeometry
+        }
+        obj = classdict[kind](name)
+        obj.units = self.options["units"]  # TODO: The constructor should look at defaults.
+
+        # Set default options from self.options
+        for option in self.options:
+            if option.find(kind + "_") == 0:
+                oname = option[len(kind)+1:]
+                obj.options[oname] = self.options[option]
+
+        # Initialize as per user request
+        # User must take care to implement initialize
+        # in a thread-safe way as is is likely that we
+        # have been invoked in a separate thread.
+        initialize(obj, self)
+
+        # Check units and convert if necessary
+        if self.options["units"].upper() != obj.units.upper():
+            GLib.idle_add(lambda: self.info("Converting units to " + self.options["units"] + "."))
+            obj.convert_units(self.options["units"])
+
+        # Add to our records
+        self.collection.append(obj, active=active)
+
+        # Show object details now.
+        # GLib.idle_add(lambda: self.notebook.set_current_page(1))
+        GLib.idle_add(lambda: self.ui.notebook.set_current_page(1))
+
+        # Plot
+        # TODO: (Thread-safe?)
+        if plot:
+            obj.plot()
+
+        if fit:
+            GLib.idle_add(lambda: self.on_zoom_fit(None))
+
+        return obj
+
+    def set_progress_bar(self, percentage, text=""):
+        """
+        Sets the application's progress bar to a given frac_digits and text.
+
+        :param percentage: The frac_digits (0.0-1.0) of the progress.
+        :type percentage: float
+        :param text: Text to display on the progress bar.
+        :type text: str
+        :return: None
+        """
+        # self.progress_bar.set_text(text)
+        # self.progress_bar.set_fraction(percentage)
+        self.ui.progress_bar.set_text(text)
+        self.ui.progress_bar.set_fraction(percentage)
+        return False
+
+    def load_defaults(self):
+        """
+        Loads the aplication's default settings from defaults.json into
+        ``self.defaults``.
+
+        :return: None
+        """
+        try:
+            f = open("defaults.json")
+            options = f.read()
+            f.close()
+        except IOError:
+            App.log.error("Could not load defaults file.")
+            self.info("ERROR: Could not load defaults file.")
+            return
+
+        try:
+            defaults = json.loads(options)
+        except:
+            e = sys.exc_info()[0]
+            App.log.error(str(e))
+            self.info("ERROR: Failed to parse defaults file.")
+            return
+        self.defaults.update(defaults)
+
+    def defaults_read_form(self):
+        for option in self.defaults_form_fields:
+            self.defaults[option] = self.defaults_form_fields[option].get_value()
+
+    def options_read_form(self):
+        for option in self.options_form_fields:
+            self.options[option] = self.options_form_fields[option].get_value()
+
+    def defaults_write_form(self):
+        for option in self.defaults_form_fields:
+            self.defaults_form_fields[option].set_value(self.defaults[option])
+
+    def options_write_form(self):
+        for option in self.options_form_fields:
+            self.options_form_fields[option].set_value(self.options[option])
+
+    def save_project(self, filename):
+        """
+        Saves the current project to the specified file.
+
+        :param filename: Name of the file in which to save.
+        :type filename: str
+        :return: None
+        """
+
+        # Capture the latest changes
+        try:
+            self.collection.get_active().read_form()
+        except:
+            pass
+
+        # Serialize the whole project
+        d = {"objs": [obj.to_dict() for obj in self.collection.get_list()],
+             "options": self.options}
+
+        try:
+            f = open(filename, 'w')
+        except IOError:
+            App.log.error("ERROR: Failed to open file for saving:", filename)
+            return
+
+        try:
+            json.dump(d, f, default=to_dict)
+        except:
+            App.log.error("ERROR: File open but failed to write:", filename)
+            f.close()
+            return
+
+        f.close()
+
+    def open_project(self, filename):
+        """
+        Loads a project from the specified file.
+
+        :param filename:  Name of the file from which to load.
+        :type filename: str
+        :return: None
+        """
+        App.log.debug("Opening project: " + filename)
+
+        try:
+            f = open(filename, 'r')
+        except IOError:
+            App.log.error("Failed to open project file: %s" % filename)
+            self.info("ERROR: Failed to open project file: %s" % filename)
+            return
+
+        try:
+            d = json.load(f, object_hook=dict2obj)
+        except:
+            App.log.error("Failed to parse project file: %s" % filename)
+            self.info("ERROR: Failed to parse project file: %s" % filename)
+            f.close()
+            return
+
+        self.register_recent("project", filename)
+
+        # Clear the current project
+        self.on_file_new(None)
+
+        # Project options
+        self.options.update(d['options'])
+        self.project_filename = filename
+        GLib.idle_add(lambda: self.units_label.set_text(self.options["units"]))
+
+        # Re create objects
+        App.log.debug("Re-creating objects...")
+        for obj in d['objs']:
+            def obj_init(obj_inst, app_inst):
+                obj_inst.from_dict(obj)
+            App.log.debug(obj['kind'] + ":  " + obj['options']['name'])
+            self.new_object(obj['kind'], obj['options']['name'], obj_init, active=False, fit=False, plot=False)
+
+        self.plot_all()
+        self.info("Project loaded from: " + filename)
+        App.log.debug("Project loaded")
+
+    def populate_objects_combo(self, combo):
+        """
+        Populates a Gtk.Comboboxtext with the list of the object in the project.
+
+        :param combo: Name or instance of the comboboxtext.
+        :type combo: str or Gtk.ComboBoxText
+        :return: None
+        """
+        App.log.debug("Populating combo!")
+        if type(combo) == str:
+            combo = self.builder.get_object(combo)
+
+        combo.remove_all()
+        for name in self.collection.get_names():
+            combo.append_text(name)
+
+    def version_check(self, *args):
+        """
+        Checks for the latest version of the program. Alerts the
+        user if theirs is outdated. This method is meant to be run
+        in a saeparate thread.
+
+        :return: None
+        """
+
+        try:
+            f = urllib.urlopen(App.version_url)
+        except:
+            App.log.warning("Failed checking for latest version. Could not connect.")
+            GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
+            return
+
+        try:
+            data = json.load(f)
+        except:
+            App.log.error("Could nor parse information about latest version.")
+            GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
+            f.close()
+            return
+
+        f.close()
+
+        if self.version >= data["version"]:
+            GLib.idle_add(lambda: self.info("FlatCAM is up to date!"))
+            return
+
+        label = Gtk.Label("There is a newer version of FlatCAM\n" +
+                          "available for download:\n\n" +
+                          data["name"] + "\n\n" + data["message"])
+        dialog = Gtk.Dialog("Newer Version Available", self.window, 0,
+                            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+                             Gtk.STOCK_OK, Gtk.ResponseType.OK))
+        dialog.set_default_size(150, 100)
+        dialog.set_modal(True)
+        box = dialog.get_content_area()
+        box.set_border_width(10)
+        box.add(label)
+
+        def do_dialog():
+            dialog.show_all()
+            response = dialog.run()
+            dialog.destroy()
+
+        GLib.idle_add(lambda: do_dialog())
+
+        return
+
+    def do_nothing(self, param):
+        return
+
+    def disable_plots(self, except_current=False):
+        """
+        Disables all plots with exception of the current object if specified.
+
+        :param except_current: Wether to skip the current object.
+        :rtype except_current: boolean
+        :return: None
+        """
+        # TODO: This method is very similar to replot_all. Try to merge.
+
+        self.set_progress_bar(0.1, "Re-plotting...")
+
+        def worker_task(app_obj):
+            percentage = 0.1
+            try:
+                delta = 0.9 / len(self.collection.get_list())
+            except ZeroDivisionError:
+                GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
+                return
+            for obj in self.collection.get_list():
+                if obj != self.collection.get_active() or not except_current:
+                    obj.options['plot'] = False
+                    obj.plot()
+                percentage += delta
+                GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
+
+            GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
+            GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
+
+        # Send to worker
+        self.worker.add_task(worker_task, [self])
+
+    def enable_all_plots(self, *args):
+        self.plotcanvas.clear()
+        self.set_progress_bar(0.1, "Re-plotting...")
+
+        def worker_task(app_obj):
+            percentage = 0.1
+            try:
+                delta = 0.9 / len(self.collection.get_list())
+            except ZeroDivisionError:
+                GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
+                return
+            for obj in self.collection.get_list():
+                obj.options['plot'] = True
+                obj.plot()
+                percentage += delta
+                GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
+
+            GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
+            GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
+
+        # Send to worker
+        self.worker.add_task(worker_task, [self])
+
+    def register_recent(self, kind, filename):
+        record = {'kind': kind, 'filename': filename}
+
+        if record in self.recent:
+            return
+
+        self.recent.insert(0, record)
+
+        if len(self.recent) > 10:  # Limit reached
+            self.recent.pop()
+
+        try:
+            f = open('recent.json', 'w')
+        except IOError:
+            App.log.error("Failed to open recent items file for writing.")
+            self.info('Failed to open recent files file for writing.')
+            return
+
+        try:
+            json.dump(self.recent, f)
+        except:
+            App.log.error("Failed to write to recent items file.")
+            self.info('ERROR: Failed to write to recent items file.')
+            f.close()
+
+        f.close()
+
+    def open_gerber(self, filename):
+        """
+        Opens a Gerber file, parses it and creates a new object for
+        it in the program. Thread-safe.
+
+        :param filename: Gerber file filename
+        :type filename: str
+        :return: None
+        """
+
+        # Fails here
+        # t = Gtk.TextView()
+        # print t
+
+        GLib.idle_add(lambda: self.set_progress_bar(0.1, "Opening Gerber ..."))
+
+        # How the object should be initialized
+        def obj_init(gerber_obj, app_obj):
+            assert isinstance(gerber_obj, FlatCAMGerber)
+
+            # Opening the file happens here
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
+            gerber_obj.parse_file(filename)
+
+            # Further parsing
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Creating Geometry ..."))
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
+
+        # Object name
+        name = filename.split('/')[-1].split('\\')[-1]
+
+        self.new_object("gerber", name, obj_init)
+
+        # New object creation and file processing
+        # try:
+        #     self.new_object("gerber", name, obj_init)
+        # except:
+        #     e = sys.exc_info()
+        #     print "ERROR:", e[0]
+        #     traceback.print_exc()
+        #     self.message_dialog("Failed to create Gerber Object",
+        #                         "Attempting to create a FlatCAM Gerber Object from " +
+        #                         "Gerber file failed during processing:\n" +
+        #                         str(e[0]) + " " + str(e[1]), kind="error")
+        #     GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
+        #     self.collection.delete_active()
+        #     return
+
+        # Register recent file
+        self.register_recent("gerber", filename)
+
+        # GUI feedback
+        self.info("Opened: " + filename)
+        GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
+        GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
+
+    def open_excellon(self, filename):
+        """
+        Opens an Excellon file, parses it and creates a new object for
+        it in the program. Thread-safe.
+
+        :param filename: Excellon file filename
+        :type filename: str
+        :return: None
+        """
+        GLib.idle_add(lambda: self.set_progress_bar(0.1, "Opening Excellon ..."))
+
+        # How the object should be initialized
+        def obj_init(excellon_obj, app_obj):
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
+            excellon_obj.parse_file(filename)
+            excellon_obj.create_geometry()
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
+
+        # Object name
+        name = filename.split('/')[-1].split('\\')[-1]
+
+        # New object creation and file processing
+        try:
+            self.new_object("excellon", name, obj_init)
+        except:
+            e = sys.exc_info()
+            App.log.error(str(e))
+            self.message_dialog("Failed to create Excellon Object",
+                                "Attempting to create a FlatCAM Excellon Object from " +
+                                "Excellon file failed during processing:\n" +
+                                str(e[0]) + " " + str(e[1]), kind="error")
+            GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
+            self.collection.delete_active()
+            return
+
+        # Register recent file
+        self.register_recent("excellon", filename)
+
+        # GUI feedback
+        self.info("Opened: " + filename)
+        GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
+        GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, ""))
+
+    def open_gcode(self, filename):
+        """
+        Opens a G-gcode file, parses it and creates a new object for
+        it in the program. Thread-safe.
+
+        :param filename: G-code file filename
+        :type filename: str
+        :return: None
+        """
+
+        # How the object should be initialized
+        def obj_init(job_obj, app_obj_):
+            """
+
+            :type app_obj_: App
+            """
+            assert isinstance(app_obj_, App)
+            GLib.idle_add(lambda: app_obj_.set_progress_bar(0.1, "Opening G-Code ..."))
+
+            f = open(filename)
+            gcode = f.read()
+            f.close()
+
+            job_obj.gcode = gcode
+
+            GLib.idle_add(lambda: app_obj_.set_progress_bar(0.2, "Parsing ..."))
+            job_obj.gcode_parse()
+
+            GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Creating geometry ..."))
+            job_obj.create_geometry()
+
+            GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Plotting ..."))
+
+        # Object name
+        name = filename.split('/')[-1].split('\\')[-1]
+
+        # New object creation and file processing
+        try:
+            self.new_object("cncjob", name, obj_init)
+        except:
+            e = sys.exc_info()
+            App.log.error(str(e))
+            self.message_dialog("Failed to create CNCJob Object",
+                                "Attempting to create a FlatCAM CNCJob Object from " +
+                                "G-Code file failed during processing:\n" +
+                                str(e[0]) + " " + str(e[1]), kind="error")
+            GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
+            self.collection.delete_active()
+            return
+
+        # Register recent file
+        self.register_recent("cncjob", filename)
+
+        # GUI feedback
+        self.info("Opened: " + filename)
+        GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
+        GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, ""))
+
+    ########################################
+    ##         EVENT HANDLERS             ##
+    ########################################
+    def on_debug_printlist(self, *args):
+        self.collection.print_list()
+
+    def on_disable_all_plots(self, widget):
+        self.disable_plots()
+
+    def on_disable_all_plots_not_current(self, widget):
+        self.disable_plots(except_current=True)
+
+    def on_about(self, widget):
+        """
+        Opens the 'About' dialog box.
+
+        :param widget: Ignored.
+        :return: None
+        """
+
+        about = self.builder.get_object("aboutdialog")
+        about.run()
+        about.hide()
+
+    def on_create_mirror(self, widget):
+        """
+        Creates a mirror image of an object to be used as a bottom layer.
+
+        :param widget: Ignored.
+        :return: None
+        """
+        # TODO: Move (some of) this to camlib!
+
+        # Object to mirror
+        obj_name = self.builder.get_object("comboboxtext_bottomlayer").get_active_text()
+        fcobj = self.collection.get_by_name(obj_name)
+
+        # For now, lets limit to Gerbers and Excellons.
+        # assert isinstance(gerb, FlatCAMGerber)
+        if not isinstance(fcobj, FlatCAMGerber) and not isinstance(fcobj, FlatCAMExcellon):
+            self.info("ERROR: Only Gerber and Excellon objects can be mirrored.")
+            return
+
+        # Mirror axis "X" or "Y
+        axis = self.get_radio_value({"rb_mirror_x": "X",
+                                     "rb_mirror_y": "Y"})
+        mode = self.get_radio_value({"rb_mirror_box": "box",
+                                     "rb_mirror_point": "point"})
+        if mode == "point":  # A single point defines the mirror axis
+            # TODO: Error handling
+            px, py = eval(self.point_entry.get_text())
+        else:  # The axis is the line dividing the box in the middle
+            name = self.box_combo.get_active_text()
+            bb_obj = self.collection.get_by_name(name)
+            xmin, ymin, xmax, ymax = bb_obj.bounds()
+            px = 0.5*(xmin+xmax)
+            py = 0.5*(ymin+ymax)
+
+        fcobj.mirror(axis, [px, py])
+        fcobj.plot()
+
+    def on_create_aligndrill(self, widget):
+        """
+        Creates alignment holes Excellon object. Creates mirror duplicates
+        of the specified holes around the specified axis.
+
+        :param widget: Ignored.
+        :return: None
+        """
+
+        # Mirror axis. Same as in on_create_mirror.
+        axis = self.get_radio_value({"rb_mirror_x": "X",
+                                     "rb_mirror_y": "Y"})
+        # TODO: Error handling
+        mode = self.get_radio_value({"rb_mirror_box": "box",
+                                     "rb_mirror_point": "point"})
+        if mode == "point":
+            px, py = eval(self.point_entry.get_text())
+        else:
+            name = self.box_combo.get_active_text()
+            bb_obj = self.collection.get_by_name(name)
+            xmin, ymin, xmax, ymax = bb_obj.bounds()
+            px = 0.5*(xmin+xmax)
+            py = 0.5*(ymin+ymax)
+        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+        # Tools
+        dia = self.get_eval("entry_dblsided_alignholediam")
+        tools = {"1": {"C": dia}}
+
+        # Parse hole list
+        # TODO: Better parsing
+        holes = self.builder.get_object("entry_dblsided_alignholes").get_text()
+        holes = eval("[" + holes + "]")
+        drills = []
+        for hole in holes:
+            point = Point(hole)
+            point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
+            drills.append({"point": point, "tool": "1"})
+            drills.append({"point": point_mirror, "tool": "1"})
+
+        def obj_init(obj_inst, app_inst):
+            obj_inst.tools = tools
+            obj_inst.drills = drills
+            obj_inst.create_geometry()
+
+        self.new_object("excellon", "Alignment Drills", obj_init)
+
+    def on_toggle_pointbox(self, widget):
+        """
+        Callback for radio selection change between point and box in the
+        Double-sided PCB tool. Updates the UI accordingly.
+
+        :param widget: Ignored.
+        :return: None
+        """
+
+        # Where the entry or combo go
+        box = self.builder.get_object("box_pointbox")
+
+        # Clear contents
+        children = box.get_children()
+        for child in children:
+            box.remove(child)
+
+        choice = self.get_radio_value({"rb_mirror_point": "point",
+                                       "rb_mirror_box": "box"})
+
+        if choice == "point":
+            self.point_entry = Gtk.Entry()
+            self.builder.get_object("box_pointbox").pack_start(self.point_entry,
+                                                               False, False, 1)
+            self.point_entry.show()
+        else:
+            self.box_combo = Gtk.ComboBoxText()
+            self.builder.get_object("box_pointbox").pack_start(self.box_combo,
+                                                               False, False, 1)
+            self.populate_objects_combo(self.box_combo)
+            self.box_combo.show()
+
+    def on_tools_doublesided(self, param):
+        """
+        Callback for menu item Tools->Double Sided PCB Tool. Launches the
+        tool placing its UI in the "Tool" tab in the notebook.
+
+        :param param: Ignored.
+        :return: None
+        """
+
+        # Were are we drawing the UI
+        box_tool = self.builder.get_object("box_tool")
+
+        # Remove anything else in the box
+        box_children = box_tool.get_children()
+        for child in box_children:
+            box_tool.remove(child)
+
+        # Get the UI
+        osw = self.builder.get_object("offscreenwindow_dblsided")
+        sw = self.builder.get_object("sw_dblsided")
+        osw.remove(sw)
+        vp = self.builder.get_object("vp_dblsided")
+        vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
+
+        # Put in the UI
+        box_tool.pack_start(sw, True, True, 0)
+
+        # INITIALIZATION
+        # Populate combo box
+        self.populate_objects_combo("comboboxtext_bottomlayer")
+
+        # Point entry
+        self.point_entry = Gtk.Entry()
+        box = self.builder.get_object("box_pointbox")
+        for child in box.get_children():
+            box.remove(child)
+        box.pack_start(self.point_entry, False, False, 1)
+
+        # Show the "Tool" tab
+        # self.notebook.set_current_page(3)
+        self.ui.notebook.set_current_page(3)
+        sw.show_all()
+
+    def on_toggle_units(self, widget):
+        """
+        Callback for the Units radio-button change in the Options tab.
+        Changes the application's default units or the current project's units.
+        If changing the project's units, the change propagates to all of
+        the objects in the project.
+
+        :param widget: Ignored.
+        :return: None
+        """
+
+        if self.toggle_units_ignore:
+            return
+
+        # Options to scale
+        dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize',
+                      'gerber_noncoppermargin', 'gerber_bboxmargin', 'excellon_drillz',
+                      'excellon_travelz', 'excellon_feedrate', 'cncjob_tooldia',
+                      'geometry_cutz', 'geometry_travelz', 'geometry_feedrate',
+                      'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap',
+                      'geometry_paintmargin']
+
+        def scale_options(sfactor):
+            for dim in dimensions:
+                self.options[dim] *= sfactor
+
+        # The scaling factor depending on choice of units.
+        factor = 1/25.4
+        if self.options_form.units_radio.get_value().upper() == 'MM':
+            factor = 25.4
+
+        # Changing project units. Warn user.
+        label = Gtk.Label("Changing the units of the project causes all geometrical \n" +
+                          "properties of all objects to be scaled accordingly. Continue?")
+        dialog = Gtk.Dialog("Changing Project Units", self.window, 0,
+                            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+                             Gtk.STOCK_OK, Gtk.ResponseType.OK))
+        dialog.set_default_size(150, 100)
+        dialog.set_modal(True)
+        box = dialog.get_content_area()
+        box.set_border_width(10)
+        box.add(label)
+        dialog.show_all()
+        response = dialog.run()
+        dialog.destroy()
+
+        if response == Gtk.ResponseType.OK:
+            self.options_read_form()
+            scale_options(factor)
+            self.options_write_form()
+            for obj in self.collection.get_list():
+                units = self.options_form.units_radio.get_value().upper()
+                obj.convert_units(units)
+            current = self.collection.get_active()
+            if current is not None:
+                current.to_form()
+            self.plot_all()
+        else:
+            # Undo toggling
+            self.toggle_units_ignore = True
+            if self.options_form.units_radio.get_value().upper() == 'MM':
+                self.options_form.units_radio.set_value('IN')
+            else:
+                self.options_form.units_radio.set_value('MM')
+            self.toggle_units_ignore = False
+
+        self.options_read_form()
+        self.info("Converted units to %s" % self.options["units"])
+        self.units_label.set_text("[" + self.options["units"] + "]")
+
+    def on_file_openproject(self, param):
+        """
+        Callback for menu item File->Open Project. Opens a file chooser and calls
+        ``self.open_project()`` after successful selection of a filename.
+
+        :param param: Ignored.
+        :return: None
+        """
+
+        def on_success(app_obj, filename):
+            app_obj.open_project(filename)
+
+        # Runs on_success on worker
+        self.file_chooser_action(on_success)
+
+    def on_file_saveproject(self, param):
+        """
+        Callback for menu item File->Save Project. Saves the project to
+        ``self.project_filename`` or calls ``self.on_file_saveprojectas()``
+        if set to None. The project is saved by calling ``self.save_project()``.
+
+        :param param: Ignored.
+        :return: None
+        """
+
+        if self.project_filename is None:
+            self.on_file_saveprojectas(None)
+        else:
+            self.save_project(self.project_filename)
+            self.register_recent("project", self.project_filename)
+            self.info("Project saved to: " + self.project_filename)
+
+    def on_file_saveprojectas(self, param):
+        """
+        Callback for menu item File->Save Project As... Opens a file
+        chooser and saves the project to the given file via
+        ``self.save_project()``.
+
+        :param param: Ignored.
+        :return: None
+        """
+
+        def on_success(app_obj, filename):
+            assert isinstance(app_obj, App)
+
+            try:
+                f = open(filename, 'r')
+                f.close()
+                exists = True
+            except IOError:
+                exists = False
+
+            msg = "File exists. Overwrite?"
+            if exists and self.question_dialog("File exists", msg) == Gtk.ResponseType.CANCEL:
+                return
+
+            app_obj.save_project(filename)
+            self.project_filename = filename
+            self.register_recent("project", filename)
+            app_obj.info("Project saved to: " + filename)
+
+        self.file_chooser_save_action(on_success)
+
+    def on_file_saveprojectcopy(self, param):
+        """
+        Callback for menu item File->Save Project Copy... Opens a file
+        chooser and saves the project to the given file via
+        ``self.save_project``. It does not update ``self.project_filename`` so
+        subsequent save requests are done on the previous known filename.
+
+        :param param: Ignore.
+        :return: None
+        """
+
+        def on_success(app_obj, filename):
+            assert isinstance(app_obj, App)
+
+            try:
+                f = open(filename, 'r')
+                f.close()
+                exists = True
+            except IOError:
+                exists = False
+
+            msg = "File exists. Overwrite?"
+            if exists and self.question_dialog("File exists", msg) == Gtk.ResponseType.CANCEL:
+                return
+
+            app_obj.save_project(filename)
+            self.register_recent("project", filename)
+            app_obj.info("Project copy saved to: " + filename)
+
+        self.file_chooser_save_action(on_success)
+
+    def on_options_app2project(self, param):
+        """
+        Callback for Options->Transfer Options->App=>Project. Copies options
+        from application defaults to project defaults.
+
+        :param param: Ignored.
+        :return: None
+        """
+
+        self.defaults_read_form()
+        self.options.update(self.defaults)
+        self.options_write_form()
+
+    def on_options_project2app(self, param):
+        """
+        Callback for Options->Transfer Options->Project=>App. Copies options
+        from project defaults to application defaults.
+
+        :param param: Ignored.
+        :return: None
+        """
+
+        self.options_read_form()
+        self.defaults.update(self.options)
+        self.defaults_write_form()
+
+    def on_options_project2object(self, param):
+        """
+        Callback for Options->Transfer Options->Project=>Object. Copies options
+        from project defaults to the currently selected object.
+
+        :param param: Ignored.
+        :return: None
+        """
+
+        self.options_read_form()
+        obj = self.collection.get_active()
+        if obj is None:
+            self.info("WARNING: No object selected.")
+            return
+        for option in self.options:
+            if option.find(obj.kind + "_") == 0:
+                oname = option[len(obj.kind)+1:]
+                obj.options[oname] = self.options[option]
+        obj.to_form()  # Update UI
+
+    def on_options_object2project(self, param):
+        """
+        Callback for Options->Transfer Options->Object=>Project. Copies options
+        from the currently selected object to project defaults.
+
+        :param param: Ignored.
+        :return: None
+        """
+
+        obj = self.collection.get_active()
+        if obj is None:
+            self.info("WARNING: No object selected.")
+            return
+        obj.read_form()
+        for option in obj.options:
+            if option in ['name']:  # TODO: Handle this better...
+                continue
+            self.options[obj.kind + "_" + option] = obj.options[option]
+        self.options_write_form()
+
+    def on_options_object2app(self, param):
+        """
+        Callback for Options->Transfer Options->Object=>App. Copies options
+        from the currently selected object to application defaults.
+
+        :param param: Ignored.
+        :return: None
+        """
+        obj = self.collection.get_active()
+        if obj is None:
+            self.info("WARNING: No object selected.")
+            return
+        obj.read_form()
+        for option in obj.options:
+            if option in ['name']:  # TODO: Handle this better...
+                continue
+            self.defaults[obj.kind + "_" + option] = obj.options[option]
+        self.defaults_write_form()
+
+    def on_options_app2object(self, param):
+        """
+        Callback for Options->Transfer Options->App=>Object. Copies options
+        from application defaults to the currently selected object.
+
+        :param param: Ignored.
+        :return: None
+        """
+
+        self.defaults_read_form()
+        obj = self.collection.get_active()
+        if obj is None:
+            self.info("WARNING: No object selected.")
+            return
+        for option in self.defaults:
+            if option.find(obj.kind + "_") == 0:
+                oname = option[len(obj.kind)+1:]
+                obj.options[oname] = self.defaults[option]
+        obj.to_form()  # Update UI
+
+    def on_file_savedefaults(self, param):
+        """
+        Callback for menu item File->Save Defaults. Saves application default options
+        ``self.defaults`` to defaults.json.
+
+        :param param: Ignored.
+        :return: None
+        """
+
+        # Read options from file
+        try:
+            f = open("defaults.json")
+            options = f.read()
+            f.close()
+        except:
+            App.log.error("Could not load defaults file.")
+            self.info("ERROR: Could not load defaults file.")
+            return
+
+        try:
+            defaults = json.loads(options)
+        except:
+            e = sys.exc_info()[0]
+            App.log.error("Failed to parse defaults file.")
+            App.log.error(str(e))
+            self.info("ERROR: Failed to parse defaults file.")
+            return
+
+        # Update options
+        self.defaults_read_form()
+        defaults.update(self.defaults)
+
+        # Save update options
+        try:
+            f = open("defaults.json", "w")
+            json.dump(defaults, f)
+            f.close()
+        except:
+            self.info("ERROR: Failed to write defaults to file.")
+            return
+
+        self.info("Defaults saved.")
+
+    def on_options_combo_change(self, widget):
+        """
+        Called when the combo box to choose between application defaults and
+        project option changes value. The corresponding variables are
+        copied to the UI.
+
+        :param widget: The widget from which this was called. Ignore.
+        :return: None
+        """
+
+        combo_sel = self.ui.notebook.combo_options.get_active()
+        App.log.debug("Options --> %s" % combo_sel)
+
+        # Remove anything else in the box
+        # box_children = self.options_box.get_children()
+        box_children = self.ui.notebook.options_contents.get_children()
+        for child in box_children:
+            self.ui.notebook.options_contents.remove(child)
+
+        form = [self.options_form, self.defaults_form][combo_sel]
+        self.ui.notebook.options_contents.pack_start(form, False, False, 1)
+        form.show_all()
+
+        # self.options2form()
+
+    def on_canvas_configure(self, widget, event):
+        """
+        Called whenever the canvas changes size. The axes are updated such
+        as to use the whole canvas.
+
+        :param widget: Ignored.
+        :param event: Ignored.
+        :return: None
+        """
+
+        self.plotcanvas.auto_adjust_axes()
+
+    def on_row_activated(self, widget, path, col):
+        """
+        Callback for selection activation (Enter or double-click) on the Project list.
+        Switches the notebook page to the object properties form. Calls
+        ``self.notebook.set_current_page(1)``.
+
+        :param widget: Ignored.
+        :param path: Ignored.
+        :param col: Ignored.
+        :return: None
+        """
+        # self.notebook.set_current_page(1)
+        self.ui.notebook.set_current_page(1)
+
+    def on_update_plot(self, widget):
+        """
+        Callback for button on form for all kinds of objects.
+        Re-plots the current object only.
+
+        :param widget: The widget from which this was called. Ignored.
+        :return: None
+        """
+
+        obj = self.collection.get_active()
+        obj.read_form()
+
+        self.set_progress_bar(0.5, "Plotting...")
+
+        def thread_func(app_obj):
+            assert isinstance(app_obj, App)
+            obj.plot()
+            GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
+
+        # Send to worker
+        self.worker.add_task(thread_func, [self])
+
+    def on_excellon_tool_choose(self, widget):
+        """
+        Callback for button on Excellon form to open up a window for
+        selecting tools.
+
+        :param widget: The widget from which this was called.
+        :return: None
+        """
+        excellon = self.collection.get_active()
+        assert isinstance(excellon, FlatCAMExcellon)
+        excellon.show_tool_chooser()
+
+    def on_entry_eval_activate(self, widget):
+        """
+        Called when an entry is activated (eg. by hitting enter) if
+        set to do so. Its text is eval()'d and set to the returned value.
+        The current object is updated.
+
+        :param widget:
+        :return:
+        """
+        self.on_eval_update(widget)
+        obj = self.collection.get_active()
+        assert isinstance(obj, FlatCAMObj)
+        obj.read_form()
+
+    def on_eval_update(self, widget):
+        """
+        Modifies the content of a Gtk.Entry by running
+        eval() on its contents and puting it back as a
+        string.
+
+        :param widget: The widget from which this was called.
+        :return: None
+        """
+        # TODO: error handling here
+        widget.set_text(str(eval(widget.get_text())))
+
+    # def on_cncjob_exportgcode(self, widget):
+    #     """
+    #     Called from button on CNCjob form to save the G-Code from the object.
+    #
+    #     :param widget: The widget from which this was called.
+    #     :return: None
+    #     """
+    #     def on_success(app_obj, filename):
+    #         cncjob = app_obj.collection.get_active()
+    #         f = open(filename, 'w')
+    #         f.write(cncjob.gcode)
+    #         f.close()
+    #         app_obj.info("Saved to: " + filename)
+    #
+    #     self.file_chooser_save_action(on_success)
+
+    def on_delete(self, widget):
+        """
+        Delete the currently selected FlatCAMObj.
+
+        :param widget: The widget from which this was called. Ignored.
+        :return: None
+        """
+
+        # Keep this for later
+        name = copy(self.collection.get_active().options["name"])
+
+        # Remove plot
+        self.plotcanvas.figure.delaxes(self.collection.get_active().axes)
+        self.plotcanvas.auto_adjust_axes()
+
+        # Clear form
+        self.setup_component_editor()
+
+        # Remove from dictionary
+        self.collection.delete_active()
+
+        self.info("Object deleted: %s" % name)
+
+    def on_toolbar_replot(self, widget):
+        """
+        Callback for toolbar button. Re-plots all objects.
+
+        :param widget: The widget from which this was called.
+        :return: None
+        """
+
+        try:
+            self.collection.get_active().read_form()
+        except AttributeError:
+            pass
+
+        self.plot_all()
+
+    def on_clear_plots(self, widget):
+        """
+        Callback for toolbar button. Clears all plots.
+
+        :param widget: The widget from which this was called.
+        :return: None
+        """
+        self.plotcanvas.clear()
+
+    def on_file_new(self, *param):
+        """
+        Callback for menu item File->New. Returns the application to its
+        startup state. This method is thread-safe.
+
+        :param param: Whatever is passed by the event. Ignore.
+        :return: None
+        """
+        # Remove everything from memory
+        App.log.debug("on_file_bew()")
+
+        # GUI things
+        def task():
+            # Clear plot
+            App.log.debug("   self.plotcanvas.clear()")
+            self.plotcanvas.clear()
+
+            # Delete data
+            App.log.debug("   self.collection.delete_all()")
+            self.collection.delete_all()
+
+            # Clear object editor
+            App.log.debug("   self.setup_component_editor()")
+            self.setup_component_editor()
+
+        GLib.idle_add(task)
+
+        # Clear project filename
+        self.project_filename = None
+
+        # Re-fresh project options
+        self.on_options_app2project(None)
+
+    def on_filequit(self, param):
+        """
+        Callback for menu item File->Quit. Closes the application.
+
+        :param param: Whatever is passed by the event. Ignore.
+        :return: None
+        """
+
+        self.window.destroy()
+        Gtk.main_quit()
+
+    def on_closewindow(self, param):
+        """
+        Callback for closing the main window.
+
+        :param param: Whatever is passed by the event. Ignore.
+        :return: None
+        """
+
+        self.window.destroy()
+        Gtk.main_quit()
+
+    def file_chooser_action(self, on_success):
+        """
+        Opens the file chooser and runs on_success on a separate thread
+        upon completion of valid file choice.
+
+        :param on_success: A function to run upon completion of a valid file
+            selection. Takes 2 parameters: The app instance and the filename.
+            Note that it is run on a separate thread, therefore it must take the
+            appropriate precautions  when accessing shared resources.
+        :type on_success: func
+        :return: None
+        """
+        dialog = Gtk.FileChooserDialog("Please choose a file", self.ui,
+                                       Gtk.FileChooserAction.OPEN,
+                                       (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+                                        Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
+        response = dialog.run()
+
+        # Works here
+        # t = Gtk.TextView()
+        # print t
+
+        if response == Gtk.ResponseType.OK:
+            filename = dialog.get_filename()
+            dialog.destroy()
+            # Send to worker.
+            self.worker.add_task(on_success, [self, filename])
+        elif response == Gtk.ResponseType.CANCEL:
+            self.info("Open cancelled.")
+            dialog.destroy()
+
+        # Works here
+        # t = Gtk.TextView()
+        # print t
+
+    def file_chooser_save_action(self, on_success):
+        """
+        Opens the file chooser and runs on_success upon completion of valid file choice.
+
+        :param on_success: A function to run upon selection of a filename. Takes 2
+            parameters: The instance of the application (App) and the chosen filename. This
+            gets run immediately in the same thread.
+        :return: None
+        """
+        dialog = Gtk.FileChooserDialog("Save file", self.window,
+                                       Gtk.FileChooserAction.SAVE,
+                                       (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+                                        Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
+        dialog.set_current_name("Untitled")
+        response = dialog.run()
+        if response == Gtk.ResponseType.OK:
+            filename = dialog.get_filename()
+            dialog.destroy()
+            on_success(self, filename)
+        elif response == Gtk.ResponseType.CANCEL:
+            self.info("Save cancelled.")  # print("Cancel clicked")
+            dialog.destroy()
+
+    def on_fileopengerber(self, param):
+        """
+        Callback for menu item File->Open Gerber. Defines a function that is then passed
+        to ``self.file_chooser_action()``. It requests the creation of a FlatCAMGerber object
+        and updates the progress bar throughout the process.
+
+        :param param: Ignore
+        :return: None
+        """
+
+        # This works here.
+        # t = Gtk.TextView()
+        # print t
+
+        self.file_chooser_action(lambda ao, filename: self.open_gerber(filename))
+
+    def on_fileopenexcellon(self, param):
+        """
+        Callback for menu item File->Open Excellon. Defines a function that is then passed
+        to ``self.file_chooser_action()``. It requests the creation of a FlatCAMExcellon object
+        and updates the progress bar throughout the process.
+
+        :param param: Ignore
+        :return: None
+        """
+
+        self.file_chooser_action(lambda ao, filename: self.open_excellon(filename))
+
+    def on_fileopengcode(self, param):
+        """
+        Callback for menu item File->Open G-Code. Defines a function that is then passed
+        to ``self.file_chooser_action()``. It requests the creation of a FlatCAMCNCjob object
+        and updates the progress bar throughout the process.
+
+        :param param: Ignore
+        :return: None
+        """
+
+        self.file_chooser_action(lambda ao, filename: self.open_gcode(filename))
+
+    def on_mouse_move_over_plot(self, event):
+        """
+        Callback for the mouse motion event over the plot. This event is generated
+        by the Matplotlib backend and has been registered in ``self.__init__()``.
+        For details, see: http://matplotlib.org/users/event_handling.html
+
+        :param event: Contains information about the event.
+        :return: None
+        """
+
+        try:  # May fail in case mouse not within axes
+            self.ui.position_label.set_label("X: %.4f   Y: %.4f" % (
+                event.xdata, event.ydata))
+            self.mouse = [event.xdata, event.ydata]
+
+            # for subscriber in self.plot_mousemove_subscribers:
+            #     self.plot_mousemove_subscribers[subscriber](event)
+
+        except:
+            self.ui.position_label.set_label("")
+            self.mouse = None
+
+    def on_click_over_plot(self, event):
+        """
+        Callback for the mouse click event over the plot. This event is generated
+        by the Matplotlib backend and has been registered in ``self.__init__()``.
+        For details, see: http://matplotlib.org/users/event_handling.html
+
+        Default actions are:
+
+        * Copy coordinates to clipboard. Ex.: (65.5473, -13.2679)
+
+        :param event: Contains information about the event, like which button
+            was clicked, the pixel coordinates and the axes coordinates.
+        :return: None
+        """
+
+        # So it can receive key presses
+        self.plotcanvas.canvas.grab_focus()
+
+        try:
+            App.log.debug('button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
+                event.button, event.x, event.y, event.xdata, event.ydata))
+
+            self.clipboard.set_text("(%.4f, %.4f)" % (event.xdata, event.ydata), -1)
+
+        except Exception, e:
+            App.log.debug("Outside plot?")
+            App.log.debug(str(e))
+
+    def on_zoom_in(self, event):
+        """
+        Callback for zoom-in request. This can be either from the corresponding
+        toolbar button or the '3' key when the canvas is focused. Calls ``self.zoom()``.
+
+        :param event: Ignored.
+        :return: None
+        """
+        self.plotcanvas.zoom(1.5)
+        return
+
+    def on_zoom_out(self, event):
+        """
+        Callback for zoom-out request. This can be either from the corresponding
+        toolbar button or the '2' key when the canvas is focused. Calls ``self.zoom()``.
+
+        :param event: Ignored.
+        :return: None
+        """
+        self.plotcanvas.zoom(1 / 1.5)
+
+    def on_zoom_fit(self, event):
+        """
+        Callback for zoom-out request. This can be either from the corresponding
+        toolbar button or the '1' key when the canvas is focused. Calls ``self.adjust_axes()``
+        with axes limits from the geometry bounds of all objects.
+
+        :param event: Ignored.
+        :return: None
+        """
+        xmin, ymin, xmax, ymax = self.collection.get_bounds()
+        width = xmax - xmin
+        height = ymax - ymin
+        xmin -= 0.05 * width
+        xmax += 0.05 * width
+        ymin -= 0.05 * height
+        ymax += 0.05 * height
+        self.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax)
+
+    def on_key_over_plot(self, event):
+        """
+        Callback for the key pressed event when the canvas is focused. Keyboard
+        shortcuts are handled here. So far, these are the shortcuts:
+
+        ==========  ============================================
+        Key         Action
+        ==========  ============================================
+        '1'         Zoom-fit. Fits the axes limits to the data.
+        '2'         Zoom-out.
+        '3'         Zoom-in.
+        'm'         Toggle on-off the measuring tool.
+        ==========  ============================================
+
+        :param event: Ignored.
+        :return: None
+        """
+
+        if event.key == '1':  # 1
+            self.on_zoom_fit(None)
+            return
+
+        if event.key == '2':  # 2
+            self.plotcanvas.zoom(1 / 1.5, self.mouse)
+            return
+
+        if event.key == '3':  # 3
+            self.plotcanvas.zoom(1.5, self.mouse)
+            return
+
+        if event.key == 'm':
+            if self.measure.toggle_active():
+                self.info("Measuring tool ON")
+            else:
+                self.info("Measuring tool OFF")
+            return
+
+
+class BaseDraw:
+    def __init__(self, plotcanvas, name=None):
+        """
+
+        :param plotcanvas: The PlotCanvas where the drawing tool will operate.
+        :type plotcanvas: PlotCanvas
+        """
+
+        self.plotcanvas = plotcanvas
+
+        # Must have unique axes
+        charset = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"
+        self.name = name or [random.choice(charset) for i in range(20)]
+        self.axes = self.plotcanvas.new_axes(self.name)
+
+
+class DrawingObject(BaseDraw):
+    def __init__(self, plotcanvas, name=None):
+        """
+        Possible objects are:
+
+        * Point
+        * Line
+        * Rectangle
+        * Circle
+        * Polygon
+        """
+
+        BaseDraw.__init__(self, plotcanvas)
+        self.properties = {}
+
+    def plot(self):
+        return
+
+    def update_plot(self):
+        self.axes.cla()
+        self.plot()
+        self.plotcanvas.auto_adjust_axes()
+
+
+class DrawingPoint(DrawingObject):
+    def __init__(self, plotcanvas, name=None, coord=None):
+        DrawingObject.__init__(self, plotcanvas)
+
+        self.properties.update({
+            "coordinate": coord
+        })
+
+    def plot(self):
+        x, y = self.properties["coordinate"]
+        self.axes.plot(x, y, 'o')
+
+
+class Measurement:
+    def __init__(self, container, plotcanvas, update=None):
+        self.update = update
+        self.container = container
+        self.frame = None
+        self.label = None
+        self.point1 = None
+        self.point2 = None
+        self.active = False
+        self.plotcanvas = plotcanvas
+        self.click_subscription = None
+        self.move_subscription = None
+
+    def toggle_active(self, *args):
+        if self.active:  # Deactivate
+            self.active = False
+            self.container.remove(self.frame)
+            if self.update is not None:
+                self.update()
+            self.plotcanvas.mpl_disconnect(self.click_subscription)
+            self.plotcanvas.mpl_disconnect(self.move_subscription)
+            return False
+        else:  # Activate
+            App.log.debug("DEBUG: Activating Measurement Tool...")
+            self.active = True
+            self.click_subscription = self.plotcanvas.mpl_connect("button_press_event", self.on_click)
+            self.move_subscription = self.plotcanvas.mpl_connect('motion_notify_event', self.on_move)
+            self.frame = Gtk.Frame()
+            self.frame.set_margin_right(5)
+            self.frame.set_margin_top(3)
+            align = Gtk.Alignment()
+            align.set(0, 0.5, 0, 0)
+            align.set_padding(4, 4, 4, 4)
+            self.label = Gtk.Label()
+            self.label.set_label("Click on a reference point...")
+            abox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 10)
+            abox.pack_start(Gtk.Image.new_from_file('share/measure16.png'), False, False, 0)
+            abox.pack_start(self.label, False, False, 0)
+            align.add(abox)
+            self.frame.add(align)
+            self.container.pack_end(self.frame, False, True, 1)
+            self.frame.show_all()
+            return True
+
+    def on_move(self, event):
+        if self.point1 is None:
+            self.label.set_label("Click on a reference point...")
+        else:
+            try:
+                dx = event.xdata - self.point1[0]
+                dy = event.ydata - self.point1[1]
+                d = sqrt(dx**2 + dy**2)
+                self.label.set_label("D = %.4f  D(x) = %.4f  D(y) = %.4f" % (d, dx, dy))
+            except TypeError:
+                pass
+        if self.update is not None:
+            self.update()
+
+    def on_click(self, event):
+            if self.point1 is None:
+                self.point1 = (event.xdata, event.ydata)
+            else:
+                self.point2 = copy(self.point1)
+                self.point1 = (event.xdata, event.ydata)
+            self.on_move(event)

+ 0 - 0
FlatCAMException.py → FlatCAM_GTK/FlatCAMException.py


+ 303 - 0
FlatCAM_GTK/FlatCAMGUI.py

@@ -0,0 +1,303 @@
+from gi.repository import Gtk
+
+from FlatCAM_GTK import FCNoteBook
+
+
+class FlatCAMGUI(Gtk.Window):
+
+    MENU = """
+    <ui>
+      <menubar name='MenuBar'>
+        <menu action='FileMenu'>
+          <menuitem action='FileNew'>
+          <separator />
+
+          <menuitem action='FileQuit' />
+        </menu>
+      </menubar>
+      <toolbar name='ToolBar'>
+        <toolitem action='FileNewStandard' />
+        <toolitem action='FileQuit' />
+      </toolbar>
+    </ui>
+    """
+
+    def __init__(self):
+        """
+
+        :return: The FlatCAM window.
+        :rtype: FlatCAM
+        """
+        Gtk.Window.__init__(self, title="FlatCAM - 0.5")
+        self.set_default_size(200, 200)
+
+        vbox1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+
+        ### Menu
+        # action_group = Gtk.ActionGroup("my_actions")
+        # self.add_file_menu_actions(action_group)
+        # #self.add_edit_menu_actions(action_group)
+        # #self.add_choices_menu_actions(action_group)
+        #
+        # uimanager = self.create_ui_manager()
+        # uimanager.insert_action_group(action_group)
+        #
+        # menubar = uimanager.get_widget("/MenuBar")
+        # vbox1.pack_start(menubar, False, False, 0)
+        #
+        # toolbar = uimanager.get_widget("/ToolBar")
+        # vbox1.pack_start(toolbar, False, False, 0)
+
+        menu = Gtk.MenuBar()
+
+        ## File
+        menufile = Gtk.MenuItem.new_with_label('File')
+        menufile_menu = Gtk.Menu()
+        menufile.set_submenu(menufile_menu)
+        # New
+        self.menufilenew = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_NEW, None)
+        menufile_menu.append(self.menufilenew)
+        menufile_menu.append(Gtk.SeparatorMenuItem())
+        # Open recent
+        self.menufilerecent = Gtk.ImageMenuItem("Open Recent", image=Gtk.Image(stock=Gtk.STOCK_OPEN))
+        menufile_menu.append(self.menufilerecent)
+        menufile_menu.append(Gtk.SeparatorMenuItem())
+        # Open Gerber ...
+        self.menufileopengerber = Gtk.ImageMenuItem("Open Gerber ...", image=Gtk.Image(stock=Gtk.STOCK_OPEN))
+        menufile_menu.append(self.menufileopengerber)
+        # Open Excellon ...
+        self.menufileopenexcellon = Gtk.ImageMenuItem("Open Excellon ...", image=Gtk.Image(stock=Gtk.STOCK_OPEN))
+        menufile_menu.append(self.menufileopenexcellon)
+        # Open G-Code ...
+        self.menufileopengcode = Gtk.ImageMenuItem("Open G-Code ...", image=Gtk.Image(stock=Gtk.STOCK_OPEN))
+        menufile_menu.append(self.menufileopengcode)
+        menufile_menu.append(Gtk.SeparatorMenuItem())
+        # Open Project ...
+        self.menufileopenproject = Gtk.ImageMenuItem("Open Project ...", image=Gtk.Image(stock=Gtk.STOCK_OPEN))
+        menufile_menu.append(self.menufileopenproject)
+        menufile_menu.append(Gtk.SeparatorMenuItem())
+        # Save Project
+        self.menufilesaveproject = Gtk.ImageMenuItem("Save Project", image=Gtk.Image(stock=Gtk.STOCK_SAVE))
+        menufile_menu.append(self.menufilesaveproject)
+        # Save Project As ...
+        self.menufilesaveprojectas = Gtk.ImageMenuItem("Save Project As ...", image=Gtk.Image(stock=Gtk.STOCK_SAVE_AS))
+        menufile_menu.append(self.menufilesaveprojectas)
+        # Save Project Copy ...
+        self.menufilesaveprojectcopy = Gtk.ImageMenuItem("Save Project Copy ...", image=Gtk.Image(stock=Gtk.STOCK_SAVE_AS))
+        menufile_menu.append(self.menufilesaveprojectcopy)
+        menufile_menu.append(Gtk.SeparatorMenuItem())
+        # Save Defaults
+        self.menufilesavedefaults = Gtk.ImageMenuItem("Save Defaults", image=Gtk.Image(stock=Gtk.STOCK_SAVE))
+        menufile_menu.append(self.menufilesavedefaults)
+        menufile_menu.append(Gtk.SeparatorMenuItem())
+        # Quit
+        self.menufilequit = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_QUIT, None)
+        menufile_menu.append(self.menufilequit)
+        menu.append(menufile)
+
+        ## Edit
+        menuedit = Gtk.MenuItem.new_with_label('Edit')
+        menu.append(menuedit)
+        menuedit_menu = Gtk.Menu()
+        menuedit.set_submenu(menuedit_menu)
+        # Delete
+        self.menueditdelete = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_DELETE, None)
+        menuedit_menu.append(self.menueditdelete)
+
+        ## View
+        menuview = Gtk.MenuItem.new_with_label('View')
+        menu.append(menuview)
+        menuview_menu = Gtk.Menu()
+        menuview.set_submenu(menuview_menu)
+        # Disable all plots
+        self.menuviewdisableall = Gtk.ImageMenuItem("Disable all plots", image=Gtk.Image.new_from_file('share/clear_plot16.png'))
+        menuview_menu.append(self.menuviewdisableall)
+        self.menuviewdisableallbutthis = Gtk.ImageMenuItem("Disable all plots but this one", image=Gtk.Image.new_from_file('share/clear_plot16.png'))
+        menuview_menu.append(self.menuviewdisableallbutthis)
+        self.menuviewenableall = Gtk.ImageMenuItem("Enable all plots", image=Gtk.Image.new_from_file('share/replot16.png'))
+        menuview_menu.append(self.menuviewenableall)
+
+        ## Options
+        menuoptions = Gtk.MenuItem.new_with_label('Options')
+        menu.append(menuoptions)
+        menuoptions_menu = Gtk.Menu()
+        menuoptions.set_submenu(menuoptions_menu)
+        # Transfer Options
+        menutransferoptions = Gtk.ImageMenuItem("Transfer Options", image=Gtk.Image.new_from_file('share/copy16.png'))
+        menuoptions_menu.append(menutransferoptions)
+        menutransferoptions_menu = Gtk.Menu()
+        menutransferoptions.set_submenu(menutransferoptions_menu)
+        self.menutransferoptions_p2a = Gtk.ImageMenuItem("Project to App", image=Gtk.Image.new_from_file('share/copy16.png'))
+        menutransferoptions_menu.append(self.menutransferoptions_p2a)
+        self.menutransferoptions_a2p = Gtk.ImageMenuItem("App to Project", image=Gtk.Image.new_from_file('share/copy16.png'))
+        menutransferoptions_menu.append(self.menutransferoptions_a2p)
+        self.menutransferoptions_o2p = Gtk.ImageMenuItem("Object to Project", image=Gtk.Image.new_from_file('share/copy16.png'))
+        menutransferoptions_menu.append(self.menutransferoptions_o2p)
+        self.menutransferoptions_o2a = Gtk.ImageMenuItem("Object to App", image=Gtk.Image.new_from_file('share/copy16.png'))
+        menutransferoptions_menu.append(self.menutransferoptions_o2a)
+        self.menutransferoptions_p2o = Gtk.ImageMenuItem("Project to Object", image=Gtk.Image.new_from_file('share/copy16.png'))
+        menutransferoptions_menu.append(self.menutransferoptions_p2o)
+        self.menutransferoptions_a2o = Gtk.ImageMenuItem("App to Object", image=Gtk.Image.new_from_file('share/copy16.png'))
+        menutransferoptions_menu.append(self.menutransferoptions_a2o)
+
+        ## Tools
+        menutools = Gtk.MenuItem.new_with_label('Tools')
+        menu.append(menutools)
+        menutools_menu = Gtk.Menu()
+        menutools.set_submenu(menutools_menu)
+        # Double Sided PCB tool
+        self.menutools_dblsided = Gtk.ImageMenuItem("Double-Sided PCB Tool", image=Gtk.Image(stock=Gtk.STOCK_PREFERENCES))
+        menutools_menu.append(self.menutools_dblsided)
+
+        ## Help
+        menuhelp = Gtk.MenuItem.new_with_label('Help')
+        menu.append(menuhelp)
+        menuhelp_menu = Gtk.Menu()
+        menuhelp.set_submenu(menuhelp_menu)
+        # About
+        self.menuhelpabout = Gtk.ImageMenuItem("About", image=Gtk.Image(stock=Gtk.STOCK_ABOUT))
+        menuhelp_menu.append(self.menuhelpabout)
+        # Updates
+        self.menuhelpupdates = Gtk.ImageMenuItem("Check for updates", image=Gtk.Image(stock=Gtk.STOCK_DIALOG_INFO))
+        menuhelp_menu.append(self.menuhelpupdates)
+
+        vbox1.pack_start(menu, False, False, 0)
+        ### End of menu
+
+        ###############
+        ### Toolbar ###
+        ###############
+        self.toolbar = Gtk.Toolbar(toolbar_style=Gtk.ToolbarStyle.ICONS)
+        vbox1.pack_start(self.toolbar, False, False, 0)
+
+        # Zoom fit
+        zf_ico = Gtk.Image.new_from_file('share/zoom_fit32.png')
+        self.zoom_fit_btn = Gtk.ToolButton.new(zf_ico, "")
+        #zoom_fit.connect("clicked", self.on_zoom_fit)
+        self.zoom_fit_btn.set_tooltip_markup("Zoom Fit.\n(Click on plot and hit <b>1</b>)")
+        self.toolbar.insert(self.zoom_fit_btn, -1)
+
+        # Zoom out
+        zo_ico = Gtk.Image.new_from_file('share/zoom_out32.png')
+        self.zoom_out_btn = Gtk.ToolButton.new(zo_ico, "")
+        #zoom_out.connect("clicked", self.on_zoom_out)
+        self.zoom_out_btn.set_tooltip_markup("Zoom Out.\n(Click on plot and hit <b>2</b>)")
+        self.toolbar.insert(self.zoom_out_btn, -1)
+
+        # Zoom in
+        zi_ico = Gtk.Image.new_from_file('share/zoom_in32.png')
+        self.zoom_in_btn = Gtk.ToolButton.new(zi_ico, "")
+        #zoom_in.connect("clicked", self.on_zoom_in)
+        self.zoom_in_btn.set_tooltip_markup("Zoom In.\n(Click on plot and hit <b>3</b>)")
+        self.toolbar.insert(self.zoom_in_btn, -1)
+
+        # Clear plot
+        cp_ico = Gtk.Image.new_from_file('share/clear_plot32.png')
+        self.clear_plot_btn = Gtk.ToolButton.new(cp_ico, "")
+        #clear_plot.connect("clicked", self.on_clear_plots)
+        self.clear_plot_btn.set_tooltip_markup("Clear Plot")
+        self.toolbar.insert(self.clear_plot_btn, -1)
+
+        # Replot
+        rp_ico = Gtk.Image.new_from_file('share/replot32.png')
+        self.replot_btn = Gtk.ToolButton.new(rp_ico, "")
+        #replot.connect("clicked", self.on_toolbar_replot)
+        self.replot_btn.set_tooltip_markup("Re-plot all")
+        self.toolbar.insert(self.replot_btn, -1)
+
+        # Delete item
+        del_ico = Gtk.Image.new_from_file('share/delete32.png')
+        self.delete_btn = Gtk.ToolButton.new(del_ico, "")
+        #delete.connect("clicked", self.on_delete)
+        self.delete_btn.set_tooltip_markup("Delete selected\nobject.")
+        self.toolbar.insert(self.delete_btn, -1)
+
+        #############
+        ### Paned ###
+        #############
+        hpane = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
+        vbox1.pack_start(hpane, expand=True, fill=True, padding=0)
+
+        ################
+        ### Notebook ###
+        ################
+        self.notebook = FCNoteBook()
+        hpane.pack1(self.notebook)
+
+        #################
+        ### Plot area ###
+        #################
+        # self.plotarea = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+        self.plotarea = Gtk.Grid()
+        self.plotarea_super = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+        self.plotarea_super.pack_start(self.plotarea, expand=True, fill=True, padding=0)
+        hpane.pack2(self.plotarea_super)
+
+        ################
+        ### Info bar ###
+        ################
+        infobox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+        vbox1.pack_start(infobox, expand=False, fill=True, padding=0)
+        ## Frame
+        frame = Gtk.Frame(margin=2, hexpand=True, halign=0)
+        infobox.pack_start(frame, expand=True, fill=True, padding=0)
+        self.info_label = Gtk.Label("Not started.", margin=2, hexpand=True)
+        frame.add(self.info_label)
+        ## Coordinate Label
+        self.position_label = Gtk.Label("X: 0.0   Y: 0.0", margin_left=4, margin_right=4)
+        infobox.pack_start(self.position_label, expand=False, fill=False, padding=0)
+        ## Units label
+        self.units_label = Gtk.Label("[in]", margin_left=4, margin_right=4)
+        infobox.pack_start(self.units_label, expand=False, fill=False, padding=0)
+        ## Progress bar
+        self.progress_bar = Gtk.ProgressBar(margin=2)
+        infobox.pack_start(self.progress_bar, expand=False, fill=False, padding=0)
+
+        self.add(vbox1)
+        self.show_all()
+
+    # def create_ui_manager(self):
+    #     uimanager = Gtk.UIManager()
+    #
+    #     # Throws exception if something went wrong
+    #     uimanager.add_ui_from_string(FlatCAM.MENU)
+    #
+    #     # Add the accelerator group to the toplevel window
+    #     accelgroup = uimanager.get_accel_group()
+    #     self.add_accel_group(accelgroup)
+    #     return uimanager
+    #
+    # def add_file_menu_actions(self, action_group):
+    #     action_filemenu = Gtk.Action("FileMenu", "File", None, None)
+    #     action_group.add_action(action_filemenu)
+    #
+    #     action_filenewmenu = Gtk.Action("FileNew", None, None, Gtk.STOCK_NEW)
+    #     action_group.add_action(action_filenewmenu)
+    #
+    #     action_new = Gtk.Action("FileNewStandard", "_New",
+    #         "Create a new file", Gtk.STOCK_NEW)
+    #     action_new.connect("activate", self.on_menu_file_new_generic)
+    #     action_group.add_action_with_accel(action_new, None)
+    #
+    #     action_group.add_actions([
+    #         ("FileNewFoo", None, "New Foo", None, "Create new foo",
+    #          self.on_menu_file_new_generic),
+    #         ("FileNewGoo", None, "_New Goo", None, "Create new goo",
+    #          self.on_menu_file_new_generic),
+    #     ])
+    #
+    #     action_filequit = Gtk.Action("FileQuit", None, None, Gtk.STOCK_QUIT)
+    #     action_filequit.connect("activate", self.on_menu_file_quit)
+    #     action_group.add_action(action_filequit)
+    #
+    # def on_menu_file_new_generic(self, widget):
+    #     print("A File|New menu item was selected.")
+    #
+    # def on_menu_file_quit(self, widget):
+    #     Gtk.main_quit()
+
+
+
+if __name__ == "__main__":
+    flatcam = FlatCAMGUI()
+    Gtk.main()

+ 1007 - 0
FlatCAM_GTK/FlatCAMObj.py

@@ -0,0 +1,1007 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://caram.cl/software/flatcam                         #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+import inspect  # TODO: Remove
+
+from gi.repository import Gtk
+from gi.repository import GLib
+from gi.repository import GObject
+
+from camlib import *
+from ObjectUI import *
+
+
+class LoudDict(dict):
+    """
+    A Dictionary with a callback for
+    item changes.
+    """
+
+    def __init__(self, *args, **kwargs):
+        super(LoudDict, self).__init__(*args, **kwargs)
+        self.callback = lambda x: None
+        self.silence = False
+
+    def set_change_callback(self, callback):
+        """
+        Assigns a function as callback on item change. The callback
+        will receive the key of the object that was changed.
+
+        :param callback: Function to call on item change.
+        :type callback: func
+        :return: None
+        """
+
+        self.callback = callback
+
+    def __setitem__(self, key, value):
+        """
+        Overridden __setitem__ method. Will call self.callback
+        if the item was changed and self.silence is False.
+        """
+        super(LoudDict, self).__setitem__(key, value)
+        try:
+            if self.__getitem__(key) == value:
+                return
+        except KeyError:
+            pass
+        if self.silence:
+            return
+        self.callback(key)
+
+
+########################################
+##            FlatCAMObj              ##
+########################################
+class FlatCAMObj(GObject.GObject, object):
+    """
+    Base type of objects handled in FlatCAM. These become interactive
+    in the GUI, can be plotted, and their options can be modified
+    by the user in their respective forms.
+    """
+
+    # Instance of the application to which these are related.
+    # The app should set this value.
+    app = None
+
+    def __init__(self, name, ui):
+        """
+
+        :param name: Name of the object given by the user.
+        :param ui: User interface to interact with the object.
+        :type ui: ObjectUI
+        :return: FlatCAMObj
+        """
+        GObject.GObject.__init__(self)
+
+        # View
+        self.ui = ui
+
+        self.options = LoudDict(name=name)
+        self.options.set_change_callback(self.on_options_change)
+
+        self.form_fields = {"name": self.ui.name_entry}
+        self.radios = {}  # Name value pairs for radio sets
+        self.radios_inv = {}  # Inverse of self.radios
+        self.axes = None  # Matplotlib axes
+        self.kind = None  # Override with proper name
+
+        self.muted_ui = False
+
+        self.ui.name_entry.connect('activate', self.on_name_activate)
+        self.ui.offset_button.connect('clicked', self.on_offset_button_click)
+        self.ui.offset_button.connect('activate', self.on_offset_button_click)
+        self.ui.scale_button.connect('clicked', self.on_scale_button_click)
+        self.ui.scale_button.connect('activate', self.on_scale_button_click)
+
+    def __str__(self):
+        return "<FlatCAMObj({:12s}): {:20s}>".format(self.kind, self.options["name"])
+
+    def on_name_activate(self, *args):
+        old_name = copy(self.options["name"])
+        new_name = self.ui.name_entry.get_text()
+        self.options["name"] = self.ui.name_entry.get_text()
+        self.app.info("Name changed from %s to %s" % (old_name, new_name))
+
+    def on_offset_button_click(self, *args):
+        self.read_form()
+        vect = self.ui.offsetvector_entry.get_value()
+        self.offset(vect)
+        self.plot()
+
+    def on_scale_button_click(self, *args):
+        self.read_form()
+        factor = self.ui.scale_entry.get_value()
+        self.scale(factor)
+        self.plot()
+
+    def on_options_change(self, key):
+        self.form_fields[key].set_value(self.options[key])
+        return
+
+    def setup_axes(self, figure):
+        """
+        1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
+        them to figure if not part of the figure. 4) Sets transparent
+        background. 5) Sets 1:1 scale aspect ratio.
+
+        :param figure: A Matplotlib.Figure on which to add/configure axes.
+        :type figure: matplotlib.figure.Figure
+        :return: None
+        :rtype: None
+        """
+
+        if self.axes is None:
+            FlatCAMApp.App.log.debug("setup_axes(): New axes")
+            self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
+                                        label=self.options["name"])
+        elif self.axes not in figure.axes:
+            FlatCAMApp.App.log.debug("setup_axes(): Clearing and attaching axes")
+            self.axes.cla()
+            figure.add_axes(self.axes)
+        else:
+            FlatCAMApp.App.log.debug("setup_axes(): Clearing Axes")
+            self.axes.cla()
+
+        # Remove all decoration. The app's axes will have
+        # the ticks and grid.
+        self.axes.set_frame_on(False)  # No frame
+        self.axes.set_xticks([])  # No tick
+        self.axes.set_yticks([])  # No ticks
+        self.axes.patch.set_visible(False)  # No background
+        self.axes.set_aspect(1)
+
+    def to_form(self):
+        """
+        Copies options to the UI form.
+
+        :return: None
+        """
+        for option in self.options:
+            self.set_form_item(option)
+
+    def read_form(self):
+        """
+        Reads form into ``self.options``.
+
+        :return: None
+        :rtype: None
+        """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()")
+        for option in self.options:
+            self.read_form_item(option)
+
+    def build_ui(self):
+        """
+        Sets up the UI/form for this object.
+
+        :return: None
+        :rtype: None
+        """
+
+        self.muted_ui = True
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.build_ui()")
+
+        # Where the UI for this object is drawn
+        # box_selected = self.app.builder.get_object("box_selected")
+        # box_selected = self.app.builder.get_object("vp_selected")
+
+        # Remove anything else in the box
+        box_children = self.app.ui.notebook.selected_contents.get_children()
+        for child in box_children:
+            self.app.ui.notebook.selected_contents.remove(child)
+
+        # Put in the UI
+        # box_selected.pack_start(sw, True, True, 0)
+        self.app.ui.notebook.selected_contents.add(self.ui)
+        self.to_form()
+        GLib.idle_add(self.app.ui.notebook.selected_contents.show_all)
+        GLib.idle_add(self.ui.show_all)
+        self.muted_ui = False
+
+    def set_form_item(self, option):
+        """
+        Copies the specified option to the UI form.
+
+        :param option: Name of the option (Key in ``self.options``).
+        :type option: str
+        :return: None
+        """
+
+        try:
+            self.form_fields[option].set_value(self.options[option])
+        except KeyError:
+            self.app.log.warn("Tried to set an option or field that does not exist: %s" % option)
+
+    def read_form_item(self, option):
+        """
+        Reads the specified option from the UI form into ``self.options``.
+
+        :param option: Name of the option.
+        :type option: str
+        :return: None
+        """
+
+        try:
+            self.options[option] = self.form_fields[option].get_value()
+        except KeyError:
+            self.app.log.warning("Failed to read option from field: %s" % option)
+
+    def plot(self):
+        """
+        Plot this object (Extend this method to implement the actual plotting).
+        Axes get created, appended to canvas and cleared before plotting.
+        Call this in descendants before doing the plotting.
+
+        :return: Whether to continue plotting or not depending on the "plot" option.
+        :rtype: bool
+        """
+
+        # Axes must exist and be attached to canvas.
+        if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes:
+            self.axes = self.app.plotcanvas.new_axes(self.options['name'])
+
+        if not self.options["plot"]:
+            self.axes.cla()
+            self.app.plotcanvas.auto_adjust_axes()
+            return False
+
+        # Clear axes or we will plot on top of them.
+        self.axes.cla()  # TODO: Thread safe?
+        # GLib.idle_add(self.axes.cla)
+        return True
+
+    def serialize(self):
+        """
+        Returns a representation of the object as a dictionary so
+        it can be later exported as JSON. Override this method.
+
+        :return: Dictionary representing the object
+        :rtype: dict
+        """
+        return
+
+    def deserialize(self, obj_dict):
+        """
+        Re-builds an object from its serialized version.
+
+        :param obj_dict: Dictionary representing a FlatCAMObj
+        :type obj_dict: dict
+        :return: None
+        """
+        return
+
+
+class FlatCAMGerber(FlatCAMObj, Gerber):
+    """
+    Represents Gerber code.
+    """
+
+    def __init__(self, name):
+        Gerber.__init__(self)
+        FlatCAMObj.__init__(self, name, GerberObjectUI())
+
+        self.kind = "gerber"
+
+        self.form_fields.update({
+            "plot": self.ui.plot_cb,
+            "multicolored": self.ui.multicolored_cb,
+            "solid": self.ui.solid_cb,
+            "isotooldia": self.ui.iso_tool_dia_entry,
+            "isopasses": self.ui.iso_width_entry,
+            "isooverlap": self.ui.iso_overlap_entry,
+            "cutouttooldia": self.ui.cutout_tooldia_entry,
+            "cutoutmargin": self.ui.cutout_margin_entry,
+            "cutoutgapsize": self.ui.cutout_gap_entry,
+            "gaps": self.ui.gaps_radio,
+            "noncoppermargin": self.ui.noncopper_margin_entry,
+            "noncopperrounded": self.ui.noncopper_rounded_cb,
+            "bboxmargin": self.ui.bbmargin_entry,
+            "bboxrounded": self.ui.bbrounded_cb
+        })
+
+        # The 'name' is already in self.options from FlatCAMObj
+        # Automatically updates the UI
+        self.options.update({
+            "plot": True,
+            "multicolored": False,
+            "solid": False,
+            "isotooldia": 0.016,
+            "isopasses": 1,
+            "isooverlap": 0.15,
+            "cutouttooldia": 0.07,
+            "cutoutmargin": 0.2,
+            "cutoutgapsize": 0.15,
+            "gaps": "tb",
+            "noncoppermargin": 0.0,
+            "noncopperrounded": False,
+            "bboxmargin": 0.0,
+            "bboxrounded": False
+        })
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from predecessors.
+        self.ser_attrs += ['options', 'kind']
+
+        assert isinstance(self.ui, GerberObjectUI)
+        self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
+        self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
+        self.ui.solid_cb.connect('clicked', self.on_solid_cb_click)
+        self.ui.solid_cb.connect('activate', self.on_solid_cb_click)
+        self.ui.multicolored_cb.connect('clicked', self.on_multicolored_cb_click)
+        self.ui.multicolored_cb.connect('activate', self.on_multicolored_cb_click)
+        self.ui.generate_iso_button.connect('clicked', self.on_iso_button_click)
+        self.ui.generate_iso_button.connect('activate', self.on_iso_button_click)
+        self.ui.generate_cutout_button.connect('clicked', self.on_generatecutout_button_click)
+        self.ui.generate_cutout_button.connect('activate', self.on_generatecutout_button_click)
+        self.ui.generate_bb_button.connect('clicked', self.on_generatebb_button_click)
+        self.ui.generate_bb_button.connect('activate', self.on_generatebb_button_click)
+        self.ui.generate_noncopper_button.connect('clicked', self.on_generatenoncopper_button_click)
+        self.ui.generate_noncopper_button.connect('activate', self.on_generatenoncopper_button_click)
+
+    def on_generatenoncopper_button_click(self, *args):
+        self.read_form()
+        name = self.options["name"] + "_noncopper"
+
+        def geo_init(geo_obj, app_obj):
+            assert isinstance(geo_obj, FlatCAMGeometry)
+            bounding_box = self.solid_geometry.envelope.buffer(self.options["noncoppermargin"])
+            if not self.options["noncopperrounded"]:
+                bounding_box = bounding_box.envelope
+            non_copper = bounding_box.difference(self.solid_geometry)
+            geo_obj.solid_geometry = non_copper
+
+        # TODO: Check for None
+        self.app.new_object("geometry", name, geo_init)
+
+    def on_generatebb_button_click(self, *args):
+        self.read_form()
+        name = self.options["name"] + "_bbox"
+
+        def geo_init(geo_obj, app_obj):
+            assert isinstance(geo_obj, FlatCAMGeometry)
+            # Bounding box with rounded corners
+            bounding_box = self.solid_geometry.envelope.buffer(self.options["bboxmargin"])
+            if not self.options["bboxrounded"]:  # Remove rounded corners
+                bounding_box = bounding_box.envelope
+            geo_obj.solid_geometry = bounding_box
+
+        self.app.new_object("geometry", name, geo_init)
+
+    def on_generatecutout_button_click(self, *args):
+        self.read_form()
+        name = self.options["name"] + "_cutout"
+
+        def geo_init(geo_obj, app_obj):
+            margin = self.options["cutoutmargin"] + self.options["cutouttooldia"]/2
+            gap_size = self.options["cutoutgapsize"] + self.options["cutouttooldia"]
+            minx, miny, maxx, maxy = self.bounds()
+            minx -= margin
+            maxx += margin
+            miny -= margin
+            maxy += margin
+            midx = 0.5 * (minx + maxx)
+            midy = 0.5 * (miny + maxy)
+            hgap = 0.5 * gap_size
+            pts = [[midx - hgap, maxy],
+                   [minx, maxy],
+                   [minx, midy + hgap],
+                   [minx, midy - hgap],
+                   [minx, miny],
+                   [midx - hgap, miny],
+                   [midx + hgap, miny],
+                   [maxx, miny],
+                   [maxx, midy - hgap],
+                   [maxx, midy + hgap],
+                   [maxx, maxy],
+                   [midx + hgap, maxy]]
+            cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
+                            [pts[6], pts[7], pts[10], pts[11]]],
+                     "lr": [[pts[9], pts[10], pts[1], pts[2]],
+                            [pts[3], pts[4], pts[7], pts[8]]],
+                     "4": [[pts[0], pts[1], pts[2]],
+                           [pts[3], pts[4], pts[5]],
+                           [pts[6], pts[7], pts[8]],
+                           [pts[9], pts[10], pts[11]]]}
+            cuts = cases[self.options['gaps']]
+            geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
+
+        # TODO: Check for None
+        self.app.new_object("geometry", name, geo_init)
+
+    def on_iso_button_click(self, *args):
+        self.read_form()
+        dia = self.options["isotooldia"]
+        passes = int(self.options["isopasses"])
+        overlap = self.options["isooverlap"] * dia
+
+        for i in range(passes):
+
+            offset = (2*i + 1)/2.0 * dia - i*overlap
+            iso_name = self.options["name"] + "_iso%d" % (i+1)
+
+            # TODO: This is ugly. Create way to pass data into init function.
+            def iso_init(geo_obj, app_obj):
+                # Propagate options
+                geo_obj.options["cnctooldia"] = self.options["isotooldia"]
+
+                geo_obj.solid_geometry = self.isolation_geometry(offset)
+                app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
+
+            # TODO: Do something if this is None. Offer changing name?
+            self.app.new_object("geometry", iso_name, iso_init)
+
+    def on_plot_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.read_form_item('plot')
+        self.plot()
+
+    def on_solid_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.read_form_item('solid')
+        self.plot()
+
+    def on_multicolored_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.read_form_item('multicolored')
+        self.plot()
+
+    def convert_units(self, units):
+        """
+        Converts the units of the object by scaling dimensions in all geometry
+        and options.
+
+        :param units: Units to which to convert the object: "IN" or "MM".
+        :type units: str
+        :return: None
+        :rtype: None
+        """
+
+        factor = Gerber.convert_units(self, units)
+
+        self.options['isotooldia'] *= factor
+        self.options['cutoutmargin'] *= factor
+        self.options['cutoutgapsize'] *= factor
+        self.options['noncoppermargin'] *= factor
+        self.options['bboxmargin'] *= factor
+
+    def plot(self):
+
+        # Does all the required setup and returns False
+        # if the 'ptint' option is set to False.
+        if not FlatCAMObj.plot(self):
+            return
+
+        # if self.options["mergepolys"]:
+        #     geometry = self.solid_geometry
+        # else:
+        #     geometry = self.buffered_paths + \
+        #                 [poly['polygon'] for poly in self.regions] + \
+        #                 self.flash_geometry
+        geometry = self.solid_geometry
+
+        # Make sure geometry is iterable.
+        try:
+            _ = iter(geometry)
+        except TypeError:
+            geometry = [geometry]
+
+        if self.options["multicolored"]:
+            linespec = '-'
+        else:
+            linespec = 'k-'
+
+        if self.options["solid"]:
+            for poly in geometry:
+                # TODO: Too many things hardcoded.
+                try:
+                    patch = PolygonPatch(poly,
+                                         facecolor="#BBF268",
+                                         edgecolor="#006E20",
+                                         alpha=0.75,
+                                         zorder=2)
+                    self.axes.add_patch(patch)
+                except AssertionError:
+                    FlatCAMApp.App.log.warning("A geometry component was not a polygon:")
+                    FlatCAMApp.App.log.warning(str(poly))
+        else:
+            for poly in geometry:
+                x, y = poly.exterior.xy
+                self.axes.plot(x, y, linespec)
+                for ints in poly.interiors:
+                    x, y = ints.coords.xy
+                    self.axes.plot(x, y, linespec)
+
+        # self.app.plotcanvas.auto_adjust_axes()
+        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
+
+    def serialize(self):
+        return {
+            "options": self.options,
+            "kind": self.kind
+        }
+
+
+class FlatCAMExcellon(FlatCAMObj, Excellon):
+    """
+    Represents Excellon/Drill code.
+    """
+
+    def __init__(self, name):
+        Excellon.__init__(self)
+        FlatCAMObj.__init__(self, name, ExcellonObjectUI())
+
+        self.kind = "excellon"
+
+        self.form_fields.update({
+            "plot": self.ui.plot_cb,
+            "solid": self.ui.solid_cb,
+            "drillz": self.ui.cutz_entry,
+            "travelz": self.ui.travelz_entry,
+            "feedrate": self.ui.feedrate_entry,
+            "toolselection": self.ui.tools_entry
+        })
+
+        self.options.update({
+            "plot": True,
+            "solid": False,
+            "drillz": -0.1,
+            "travelz": 0.1,
+            "feedrate": 5.0,
+            "toolselection": ""
+        })
+
+        # TODO: Document this.
+        self.tool_cbs = {}
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from predecessors.
+        self.ser_attrs += ['options', 'kind']
+
+        assert isinstance(self.ui, ExcellonObjectUI)
+        self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
+        self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
+        self.ui.solid_cb.connect('clicked', self.on_solid_cb_click)
+        self.ui.solid_cb.connect('activate', self.on_solid_cb_click)
+        self.ui.choose_tools_button.connect('clicked', lambda args: self.show_tool_chooser())
+        self.ui.choose_tools_button.connect('activate', lambda args: self.show_tool_chooser())
+        self.ui.generate_cnc_button.connect('clicked', self.on_create_cncjob_button_click)
+        self.ui.generate_cnc_button.connect('activate', self.on_create_cncjob_button_click)
+
+    def on_create_cncjob_button_click(self, *args):
+        self.read_form()
+        job_name = self.options["name"] + "_cnc"
+
+        # Object initialization function for app.new_object()
+        def job_init(job_obj, app_obj):
+            assert isinstance(job_obj, FlatCAMCNCjob)
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
+            job_obj.z_cut = self.options["drillz"]
+            job_obj.z_move = self.options["travelz"]
+            job_obj.feedrate = self.options["feedrate"]
+            # There could be more than one drill size...
+            # job_obj.tooldia =   # TODO: duplicate variable!
+            # job_obj.options["tooldia"] =
+            job_obj.generate_from_excellon_by_tool(self, self.options["toolselection"])
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
+            job_obj.gcode_parse()
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
+            job_obj.create_geometry()
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
+
+        # To be run in separate thread
+        def job_thread(app_obj):
+            app_obj.new_object("cncjob", job_name, job_init)
+            GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
+            GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
+
+        # Send to worker
+        self.app.worker.add_task(job_thread, [self.app])
+
+    def on_plot_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.read_form_item('plot')
+        self.plot()
+
+    def on_solid_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.read_form_item('solid')
+        self.plot()
+
+    def convert_units(self, units):
+        factor = Excellon.convert_units(self, units)
+
+        self.options['drillz'] *= factor
+        self.options['travelz'] *= factor
+        self.options['feedrate'] *= factor
+
+    def plot(self):
+
+        # Does all the required setup and returns False
+        # if the 'ptint' option is set to False.
+        if not FlatCAMObj.plot(self):
+            return
+
+        try:
+            _ = iter(self.solid_geometry)
+        except TypeError:
+            self.solid_geometry = [self.solid_geometry]
+
+        # Plot excellon (All polygons?)
+        if self.options["solid"]:
+            for geo in self.solid_geometry:
+                patch = PolygonPatch(geo,
+                                     facecolor="#C40000",
+                                     edgecolor="#750000",
+                                     alpha=0.75,
+                                     zorder=3)
+                self.axes.add_patch(patch)
+        else:
+            for geo in self.solid_geometry:
+                x, y = geo.exterior.coords.xy
+                self.axes.plot(x, y, 'r-')
+                for ints in geo.interiors:
+                    x, y = ints.coords.xy
+                    self.axes.plot(x, y, 'g-')
+
+        #self.app.plotcanvas.auto_adjust_axes()
+        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
+
+    def show_tool_chooser(self):
+        win = Gtk.Window()
+        box = Gtk.Box(spacing=2)
+        box.set_orientation(Gtk.Orientation(1))
+        win.add(box)
+        for tool in self.tools:
+            self.tool_cbs[tool] = Gtk.CheckButton(label=tool + ": " + str(self.tools[tool]))
+            box.pack_start(self.tool_cbs[tool], False, False, 1)
+        button = Gtk.Button(label="Accept")
+        box.pack_start(button, False, False, 1)
+        win.show_all()
+
+        def on_accept(widget):
+            win.destroy()
+            tool_list = []
+            for toolx in self.tool_cbs:
+                if self.tool_cbs[toolx].get_active():
+                    tool_list.append(toolx)
+            self.options["toolselection"] = ", ".join(tool_list)
+            self.to_form()
+
+        button.connect("activate", on_accept)
+        button.connect("clicked", on_accept)
+
+import time
+
+class FlatCAMCNCjob(FlatCAMObj, CNCjob):
+    """
+    Represents G-Code.
+    """
+
+    def __init__(self, name, units="in", kind="generic", z_move=0.1,
+                 feedrate=3.0, z_cut=-0.002, tooldia=0.0):
+        FlatCAMApp.App.log.debug("Creating CNCJob object...")
+        CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
+                        feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
+        ui = CNCObjectUI()
+        FlatCAMApp.App.log.debug("... UI Created")
+        time.sleep(2)
+        FlatCAMApp.App.log.debug("... 2 seconds later")
+        FlatCAMObj.__init__(self, name, ui)
+        FlatCAMApp.App.log.debug("... UI Passed to parent")
+        self.kind = "cncjob"
+
+        self.options.update({
+            "plot": True,
+            "tooldia": 0.4 / 25.4,  # 0.4mm in inches
+            #"append": ""
+        })
+
+        self.form_fields.update({
+            "plot": self.ui.plot_cb,
+            "tooldia": self.ui.tooldia_entry,
+            #"append": self.ui.append_gtext
+        })
+        FlatCAMApp.App.log.debug("... Options")
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from predecessors.
+        self.ser_attrs += ['options', 'kind']
+
+        self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
+        self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
+        self.ui.updateplot_button.connect('clicked', self.on_updateplot_button_click)
+        self.ui.updateplot_button.connect('activate', self.on_updateplot_button_click)
+        self.ui.export_gcode_button.connect('clicked', self.on_exportgcode_button_click)
+        self.ui.export_gcode_button.connect('activate', self.on_exportgcode_button_click)
+        FlatCAMApp.App.log.debug("... Callbacks. DONE")
+
+    def on_updateplot_button_click(self, *args):
+        """
+        Callback for the "Updata Plot" button. Reads the form for updates
+        and plots the object.
+        """
+        self.read_form()
+        self.plot()
+
+    def on_exportgcode_button_click(self, *args):
+        def on_success(app_obj, filename):
+            f = open(filename, 'w')
+            f.write(self.gcode)
+            f.close()
+            app_obj.info("Saved to: " + filename)
+
+        self.app.file_chooser_save_action(on_success)
+
+    def on_plot_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.read_form_item('plot')
+        self.plot()
+
+    def plot(self):
+
+        # Does all the required setup and returns False
+        # if the 'ptint' option is set to False.
+        if not FlatCAMObj.plot(self):
+            return
+
+        self.plot2(self.axes, tooldia=self.options["tooldia"])
+
+        #self.app.plotcanvas.auto_adjust_axes()
+        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
+
+    def convert_units(self, units):
+        factor = CNCjob.convert_units(self, units)
+        FlatCAMApp.App.log.debug("FlatCAMCNCjob.convert_units()")
+        self.options["tooldia"] *= factor
+
+
+class FlatCAMGeometry(FlatCAMObj, Geometry):
+    """
+    Geometric object not associated with a specific
+    format.
+    """
+
+    def __init__(self, name):
+        FlatCAMObj.__init__(self, name, GeometryObjectUI())
+        Geometry.__init__(self)
+
+        self.kind = "geometry"
+
+        self.form_fields.update({
+            "plot": self.ui.plot_cb,
+            # "solid": self.ui.sol,
+            # "multicolored": self.ui.,
+            "cutz": self.ui.cutz_entry,
+            "travelz": self.ui.travelz_entry,
+            "feedrate": self.ui.cncfeedrate_entry,
+            "cnctooldia": self.ui.cnctooldia_entry,
+            "painttooldia": self.ui.painttooldia_entry,
+            "paintoverlap": self.ui.paintoverlap_entry,
+            "paintmargin": self.ui.paintmargin_entry
+        })
+
+        self.options.update({
+            "plot": True,
+            # "solid": False,
+            # "multicolored": False,
+            "cutz": -0.002,
+            "travelz": 0.1,
+            "feedrate": 5.0,
+            "cnctooldia": 0.4 / 25.4,
+            "painttooldia": 0.0625,
+            "paintoverlap": 0.15,
+            "paintmargin": 0.01
+        })
+
+        # self.form_kinds.update({
+        #     "plot": "cb",
+        #     "solid": "cb",
+        #     "multicolored": "cb",
+        #     "cutz": "entry_eval",
+        #     "travelz": "entry_eval",
+        #     "feedrate": "entry_eval",
+        #     "cnctooldia": "entry_eval",
+        #     "painttooldia": "entry_eval",
+        #     "paintoverlap": "entry_eval",
+        #     "paintmargin": "entry_eval"
+        # })
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from predecessors.
+        self.ser_attrs += ['options', 'kind']
+
+        assert isinstance(self.ui, GeometryObjectUI)
+        self.ui.plot_cb.connect('clicked', self.on_plot_cb_click)
+        self.ui.plot_cb.connect('activate', self.on_plot_cb_click)
+        self.ui.generate_cnc_button.connect('clicked', self.on_generatecnc_button_click)
+        self.ui.generate_cnc_button.connect('activate', self.on_generatecnc_button_click)
+        self.ui.generate_paint_button.connect('clicked', self.on_paint_button_click)
+        self.ui.generate_paint_button.connect('activate', self.on_paint_button_click)
+
+    def on_paint_button_click(self, *args):
+        self.app.info("Click inside the desired polygon.")
+        self.read_form()
+        tooldia = self.options["painttooldia"]
+        overlap = self.options["paintoverlap"]
+
+        # Connection ID for the click event
+        subscription = None
+
+        # To be called after clicking on the plot.
+        def doit(event):
+            self.app.plotcanvas.mpl_disconnect(subscription)
+            point = [event.xdata, event.ydata]
+            poly = find_polygon(self.solid_geometry, point)
+
+            # Initializes the new geometry object
+            def gen_paintarea(geo_obj, app_obj):
+                assert isinstance(geo_obj, FlatCAMGeometry)
+                #assert isinstance(app_obj, App)
+                cp = clear_poly(poly.buffer(-self.options["paintmargin"]), tooldia, overlap)
+                geo_obj.solid_geometry = cp
+                geo_obj.options["cnctooldia"] = tooldia
+
+            name = self.options["name"] + "_paint"
+            self.app.new_object("geometry", name, gen_paintarea)
+
+        subscription = self.app.plotcanvas.mpl_connect('button_press_event', doit)
+
+    def on_generatecnc_button_click(self, *args):
+        self.read_form()
+        job_name = self.options["name"] + "_cnc"
+
+        # Object initialization function for app.new_object()
+        # RUNNING ON SEPARATE THREAD!
+        def job_init(job_obj, app_obj):
+            assert isinstance(job_obj, FlatCAMCNCjob)
+            # Propagate options
+            job_obj.options["tooldia"] = self.options["cnctooldia"]
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
+            job_obj.z_cut = self.options["cutz"]
+            job_obj.z_move = self.options["travelz"]
+            job_obj.feedrate = self.options["feedrate"]
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
+            # TODO: The tolerance should not be hard coded. Just for testing.
+            job_obj.generate_from_geometry(self, tolerance=0.0005)
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
+            job_obj.gcode_parse()
+
+            # TODO: job_obj.create_geometry creates stuff that is not used.
+            #GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
+            #job_obj.create_geometry()
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
+
+        # To be run in separate thread
+        def job_thread(app_obj):
+            app_obj.new_object("cncjob", job_name, job_init)
+            GLib.idle_add(lambda: app_obj.info("CNCjob created: %s" % job_name))
+            GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
+            GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, "Idle"))
+
+        # Send to worker
+        self.app.worker.add_task(job_thread, [self.app])
+
+    def on_plot_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.read_form_item('plot')
+        self.plot()
+
+    def scale(self, factor):
+        """
+        Scales all geometry by a given factor.
+
+        :param factor: Factor by which to scale the object's geometry/
+        :type factor: float
+        :return: None
+        :rtype: None
+        """
+
+        if type(self.solid_geometry) == list:
+            self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0))
+                                   for g in self.solid_geometry]
+        else:
+            self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor,
+                                                 origin=(0, 0))
+
+    def offset(self, vect):
+        """
+        Offsets all geometry by a given vector/
+
+        :param vect: (x, y) vector by which to offset the object's geometry.
+        :type vect: tuple
+        :return: None
+        :rtype: None
+        """
+
+        dx, dy = vect
+
+        if type(self.solid_geometry) == list:
+            self.solid_geometry = [affinity.translate(g, xoff=dx, yoff=dy)
+                                   for g in self.solid_geometry]
+        else:
+            self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
+
+    def convert_units(self, units):
+        factor = Geometry.convert_units(self, units)
+
+        self.options['cutz'] *= factor
+        self.options['travelz'] *= factor
+        self.options['feedrate'] *= factor
+        self.options['cnctooldia'] *= factor
+        self.options['painttooldia'] *= factor
+        self.options['paintmargin'] *= factor
+
+        return factor
+
+    def plot(self):
+        """
+        Plots the object into its axes. If None, of if the axes
+        are not part of the app's figure, it fetches new ones.
+
+        :return: None
+        """
+
+        # Does all the required setup and returns False
+        # if the 'ptint' option is set to False.
+        if not FlatCAMObj.plot(self):
+            return
+
+        # Make sure solid_geometry is iterable.
+        try:
+            _ = iter(self.solid_geometry)
+        except TypeError:
+            self.solid_geometry = [self.solid_geometry]
+
+        for geo in self.solid_geometry:
+
+            if type(geo) == Polygon:
+                x, y = geo.exterior.coords.xy
+                self.axes.plot(x, y, 'r-')
+                for ints in geo.interiors:
+                    x, y = ints.coords.xy
+                    self.axes.plot(x, y, 'r-')
+                continue
+
+            if type(geo) == LineString or type(geo) == LinearRing:
+                x, y = geo.coords.xy
+                self.axes.plot(x, y, 'r-')
+                continue
+
+            if type(geo) == MultiPolygon:
+                for poly in geo:
+                    x, y = poly.exterior.coords.xy
+                    self.axes.plot(x, y, 'r-')
+                    for ints in poly.interiors:
+                        x, y = ints.coords.xy
+                        self.axes.plot(x, y, 'r-')
+                continue
+
+            FlatCAMApp.App.log.warning("Did not plot:", str(type(geo)))
+
+        #self.app.plotcanvas.auto_adjust_axes()
+        GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)

+ 43 - 0
FlatCAM_GTK/FlatCAMWorker.py

@@ -0,0 +1,43 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://caram.cl/software/flatcam                         #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+import threading
+import Queue
+
+
+class Worker(threading.Thread):
+    """
+    Implements a queue of tasks to be carried out in order
+    in a single independent thread.
+    """
+
+    def __init__(self):
+        super(Worker, self).__init__()
+        self.queue = Queue.Queue()
+        self.stoprequest = threading.Event()
+
+    def run(self):
+        while not self.stoprequest.isSet():
+            try:
+                task = self.queue.get(True, 0.05)
+                self.do_task(task)
+            except Queue.Empty:
+                continue
+
+    @staticmethod
+    def do_task(task):
+        task['fcn'](*task['params'])
+        return
+
+    def add_task(self, target, params=list()):
+        self.queue.put({'fcn': target, 'params': params})
+        return
+
+    def join(self, timeout=None):
+        self.stoprequest.set()
+        super(Worker, self).join()

+ 249 - 0
FlatCAM_GTK/GUIElements.py

@@ -0,0 +1,249 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://caram.cl/software/flatcam                         #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+import re
+from copy import copy
+
+from gi.repository import Gtk
+
+from FlatCAM_GTK import FlatCAMApp
+
+
+class RadioSet(Gtk.Box):
+    def __init__(self, choices):
+        """
+        The choices are specified as a list of dictionaries containing:
+
+        * 'label': Shown in the UI
+        * 'value': The value returned is selected
+
+        :param choices: List of choices. See description.
+        :type choices: list
+        """
+        Gtk.Box.__init__(self)
+        self.choices = copy(choices)
+        self.group = None
+        for choice in self.choices:
+            if self.group is None:
+                choice['radio'] = Gtk.RadioButton.new_with_label(None, choice['label'])
+                self.group = choice['radio']
+            else:
+                choice['radio'] = Gtk.RadioButton.new_with_label_from_widget(self.group, choice['label'])
+            self.pack_start(choice['radio'], expand=True, fill=False, padding=2)
+            choice['radio'].connect('toggled', self.on_toggle)
+
+        self.group_toggle_fn = lambda x, y: None
+
+    def on_toggle(self, btn):
+        if btn.get_active():
+            self.group_toggle_fn(btn, self.get_value)
+        return
+
+    def get_value(self):
+        for choice in self.choices:
+            if choice['radio'].get_active():
+                return choice['value']
+        FlatCAMApp.App.log.error("No button was toggled in RadioSet.")
+        return None
+
+    def set_value(self, val):
+        for choice in self.choices:
+            if choice['value'] == val:
+                choice['radio'].set_active(True)
+                return
+        FlatCAMApp.App.log.error("Value given is not part of this RadioSet: %s" % str(val))
+
+
+class LengthEntry(Gtk.Entry):
+    """
+    A text entry that interprets its string as a
+    length, with or without specified units. When the user reads
+    the value, it is interpreted and replaced by a floating
+    point representation of the value in the default units. When
+    the entry is activated, its string is repalced by the interpreted
+    value.
+
+    Example:
+    Default units are 'IN', input is "1.0 mm", value returned
+    is 1.0/25.4 = 0.03937.
+    """
+
+    def __init__(self, output_units='IN'):
+        """
+
+        :param output_units: The default output units, 'IN' or 'MM'
+        :return: LengthEntry
+        """
+
+        Gtk.Entry.__init__(self)
+        self.output_units = output_units
+        self.format_re = re.compile(r"^([^\s]+)(?:\s([a-zA-Z]+))?$")
+
+        # Unit conversion table OUTPUT-INPUT
+        self.scales = {
+            'IN': {'IN': 1.0,
+                   'MM': 1/25.4},
+            'MM': {'IN': 25.4,
+                   'MM': 1.0}
+        }
+
+        self.connect('activate', self.on_activate)
+
+    def on_activate(self, *args):
+        """
+        Entry "activate" callback. Replaces the text in the
+        entry with the value returned by `get_value()`.
+
+        :param args: Ignored.
+        :return: None.
+        """
+        val = self.get_value()
+        if val is not None:
+            self.set_text(str(val))
+        else:
+            FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.get_text())
+
+    def get_value(self):
+        """
+        Fetches, interprets and returns the value in the entry. The text
+        is parsed to find the numerical expression and the (input) units (if any).
+        The numerical expression is interpreted and scaled acording to the
+        input and output units `self.output_units`.
+
+        :return: Floating point representation of the value in the entry.
+        :rtype: float
+        """
+
+        raw = self.get_text().strip(' ')
+        match = self.format_re.search(raw)
+        if not match:
+            return None
+        try:
+            if match.group(2) is not None and match.group(2).upper() in self.scales:
+                return float(eval(match.group(1)))*self.scales[self.output_units][match.group(2).upper()]
+            else:
+                return float(eval(match.group(1)))
+        except:
+            FlatCAMApp.App.log.warning("Could not parse value in entry: %s" % str(raw))
+            return None
+
+    def set_value(self, val):
+        self.set_text(str(val))
+
+
+class FloatEntry(Gtk.Entry):
+    def __init__(self):
+        Gtk.Entry.__init__(self)
+
+        self.connect('activate', self.on_activate)
+
+    def on_activate(self, *args):
+        val = self.get_value()
+        if val is not None:
+            self.set_text(str(val))
+        else:
+            FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.get_text())
+
+    def get_value(self):
+        raw = self.get_text().strip(' ')
+        try:
+            evaled = eval(raw)
+        except:
+            FlatCAMApp.App.log.error("Could not evaluate: %s" % str(raw))
+            return None
+
+        return float(evaled)
+
+    def set_value(self, val):
+        self.set_text(str(val))
+
+
+class IntEntry(Gtk.Entry):
+    def __init__(self):
+        Gtk.Entry.__init__(self)
+
+    def get_value(self):
+        return int(self.get_text())
+
+    def set_value(self, val):
+        self.set_text(str(val))
+
+
+class FCEntry(Gtk.Entry):
+    def __init__(self):
+        Gtk.Entry.__init__(self)
+
+    def get_value(self):
+        return self.get_text()
+
+    def set_value(self, val):
+        self.set_text(str(val))
+
+
+class EvalEntry(Gtk.Entry):
+    def __init__(self):
+        Gtk.Entry.__init__(self)
+
+    def on_activate(self, *args):
+        val = self.get_value()
+        if val is not None:
+            self.set_text(str(val))
+        else:
+            FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.get_text())
+
+    def get_value(self):
+        raw = self.get_text().strip(' ')
+        try:
+            return eval(raw)
+        except:
+            FlatCAMApp.App.log.error("Could not evaluate: %s" % str(raw))
+            return None
+
+    def set_value(self, val):
+        self.set_text(str(val))
+
+
+class FCCheckBox(Gtk.CheckButton):
+    def __init__(self, label=''):
+        Gtk.CheckButton.__init__(self, label=label)
+
+    def get_value(self):
+        return self.get_active()
+
+    def set_value(self, val):
+        self.set_active(val)
+
+
+
+
+class FCTextArea(Gtk.ScrolledWindow):
+    def __init__(self):
+        # Gtk.ScrolledWindow.__init__(self)
+        # FlatCAMApp.App.log.debug('Gtk.ScrolledWindow.__init__(self)')
+        super(FCTextArea, self).__init__()
+        FlatCAMApp.App.log.debug('super(FCTextArea, self).__init__()')
+        self.set_size_request(250, 100)
+        FlatCAMApp.App.log.debug('self.set_size_request(250, 100)')
+        textview = Gtk.TextView()
+        #print textview
+        #FlatCAMApp.App.log.debug('self.textview = Gtk.TextView()')
+        #self.textbuffer = self.textview.get_buffer()
+        #FlatCAMApp.App.log.debug('self.textbuffer = self.textview.get_buffer()')
+        #self.textbuffer.set_text("(Nothing here!)")
+        #FlatCAMApp.App.log.debug('self.textbuffer.set_text("(Nothing here!)")')
+        #self.add(self.textview)
+        #FlatCAMApp.App.log.debug('self.add(self.textview)')
+        #self.show()
+
+    def set_value(self, val):
+        #self.textbuffer.set_text(str(val))
+        return
+
+    def get_value(self):
+        #return self.textbuffer.get_text()
+        return ""

+ 261 - 0
FlatCAM_GTK/ObjectCollection.py

@@ -0,0 +1,261 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://caram.cl/software/flatcam                         #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 4/20/2014                                          #
+# MIT Licence                                              #
+############################################################
+
+import inspect  # TODO: Remove
+
+from gi.repository import Gtk, GdkPixbuf, GLib
+
+from FlatCAMObj import *
+from FlatCAM_GTK import FlatCAMApp
+
+
+class ObjectCollection:
+
+    classdict = {
+        "gerber": FlatCAMGerber,
+        "excellon": FlatCAMExcellon,
+        "cncjob": FlatCAMCNCjob,
+        "geometry": FlatCAMGeometry
+    }
+
+    icon_files = {
+        "gerber": "share/flatcam_icon16.png",
+        "excellon": "share/drill16.png",
+        "cncjob": "share/cnc16.png",
+        "geometry": "share/geometry16.png"
+    }
+
+    def __init__(self):
+
+        ### Icons for the list view
+        self.icons = {}
+        for kind in ObjectCollection.icon_files:
+            self.icons[kind] = GdkPixbuf.Pixbuf.new_from_file(ObjectCollection.icon_files[kind])
+
+        ### GUI List components
+        ## Model
+        self.store = Gtk.ListStore(FlatCAMObj)
+
+        ## View
+        self.view = Gtk.TreeView(model=self.store)
+        #self.view.connect("row_activated", self.on_row_activated)
+        self.tree_selection = self.view.get_selection()
+        self.change_subscription = self.tree_selection.connect("changed", self.on_list_selection_change)
+
+        ## Renderers
+        # Icon
+        renderer_pixbuf = Gtk.CellRendererPixbuf()
+        column_pixbuf = Gtk.TreeViewColumn("Type", renderer_pixbuf)
+
+        def _set_cell_icon(column, cell, model, it, data):
+            obj = model.get_value(it, 0)
+            cell.set_property('pixbuf', self.icons[obj.kind])
+
+        column_pixbuf.set_cell_data_func(renderer_pixbuf, _set_cell_icon)
+        self.view.append_column(column_pixbuf)
+
+        # Name
+        renderer_text = Gtk.CellRendererText()
+        column_text = Gtk.TreeViewColumn("Name", renderer_text)
+
+        def _set_cell_text(column, cell, model, it, data):
+            obj = model.get_value(it, 0)
+            cell.set_property('text', obj.options["name"])
+
+        column_text.set_cell_data_func(renderer_text, _set_cell_text)
+        self.view.append_column(column_text)
+
+    def print_list(self):
+        iterat = self.store.get_iter_first()
+        while iterat is not None:
+            obj = self.store[iterat][0]
+            print obj
+            iterat = self.store.iter_next(iterat)
+
+    def delete_all(self):
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_all()")
+        self.store.clear()
+
+    def delete_active(self):
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_active()")
+        try:
+            model, treeiter = self.tree_selection.get_selected()
+            self.store.remove(treeiter)
+        except:
+            pass
+
+    def on_list_selection_change(self, selection):
+        """
+        Callback for change in selection on the objects' list.
+        Instructs the new selection to build the UI for its options.
+
+        :param selection: Ignored.
+        :return: None
+        """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.on_list_selection_change()")
+
+        active = self.get_active()
+        active.build_ui()
+
+    def set_active(self, name):
+        """
+        Sets an object as the active object in the program. Same
+        as `set_list_selection()`.
+
+        :param name: Name of the object.
+        :type name: str
+        :return: None
+        """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.set_active()")
+        self.set_list_selection(name)
+
+    def get_active(self):
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_active()")
+        try:
+            model, treeiter = self.tree_selection.get_selected()
+            return model[treeiter][0]
+        except (TypeError, ValueError):
+            return None
+
+    def set_list_selection(self, name):
+        """
+        Sets which object should be selected in the list.
+
+        :param name: Name of the object.
+        :rtype name: str
+        :return: None
+        """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.set_list_selection()")
+        iterat = self.store.get_iter_first()
+        while iterat is not None and self.store[iterat][0].options["name"] != name:
+            iterat = self.store.iter_next(iterat)
+        self.tree_selection.select_iter(iterat)
+
+    def append(self, obj, active=False):
+        """
+        Add a FlatCAMObj the the collection. This method is thread-safe.
+
+        :param obj: FlatCAMObj to append
+        :type obj: FlatCAMObj
+        :param active: If it is to become the active object after appending
+        :type active: bool
+        :return: None
+        """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.append()")
+
+        def guitask():
+            self.store.append([obj])
+            if active:
+                self.set_list_selection(obj.options["name"])
+        GLib.idle_add(guitask)
+
+    def get_names(self):
+        """
+        Gets a list of the names of all objects in the collection.
+
+        :return: List of names.
+        :rtype: list
+        """
+
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_names()")
+
+        names = []
+        iterat = self.store.get_iter_first()
+        while iterat is not None:
+            obj = self.store[iterat][0]
+            names.append(obj.options["name"])
+            iterat = self.store.iter_next(iterat)
+        return names
+
+    def get_bounds(self):
+        """
+        Finds coordinates bounding all objects in the collection.
+
+        :return: [xmin, ymin, xmax, ymax]
+        :rtype: list
+        """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_bounds()")
+
+        # TODO: Move the operation out of here.
+
+        xmin = Inf
+        ymin = Inf
+        xmax = -Inf
+        ymax = -Inf
+
+        iterat = self.store.get_iter_first()
+        while iterat is not None:
+            obj = self.store[iterat][0]
+            try:
+                gxmin, gymin, gxmax, gymax = obj.bounds()
+                xmin = min([xmin, gxmin])
+                ymin = min([ymin, gymin])
+                xmax = max([xmax, gxmax])
+                ymax = max([ymax, gymax])
+            except:
+                FlatCAMApp.App.log.warning("DEV WARNING: Tried to get bounds of empty geometry.")
+            iterat = self.store.iter_next(iterat)
+        return [xmin, ymin, xmax, ymax]
+
+    def get_list(self):
+        """
+        Returns a list with all FlatCAMObj.
+
+        :return: List with all FlatCAMObj.
+        :rtype: list
+        """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_list()")
+        collection_list = []
+        iterat = self.store.get_iter_first()
+        while iterat is not None:
+            obj = self.store[iterat][0]
+            collection_list.append(obj)
+            iterat = self.store.iter_next(iterat)
+        return collection_list
+
+    def get_by_name(self, name):
+        """
+        Fetches the FlatCAMObj with the given `name`.
+
+        :param name: The name of the object.
+        :type name: str
+        :return: The requested object or None if no such object.
+        :rtype: FlatCAMObj or None
+        """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_by_name()")
+
+        iterat = self.store.get_iter_first()
+        while iterat is not None:
+            obj = self.store[iterat][0]
+            if obj.options["name"] == name:
+                return obj
+            iterat = self.store.iter_next(iterat)
+        return None
+
+    # def change_name(self, old_name, new_name):
+    #     """
+    #     Changes the name of `FlatCAMObj` named `old_name` to `new_name`.
+    #
+    #     :param old_name: Name of the object to change.
+    #     :type old_name: str
+    #     :param new_name: New name.
+    #     :type new_name: str
+    #     :return: True if name change succeeded, False otherwise. Will fail
+    #        if no object with `old_name` is found.
+    #     :rtype: bool
+    #     """
+    #     print inspect.stack()[1][3], "--> OC.change_name()"
+    #     iterat = self.store.get_iter_first()
+    #     while iterat is not None:
+    #         obj = self.store[iterat][0]
+    #         if obj.options["name"] == old_name:
+    #             obj.options["name"] = new_name
+    #             self.store.row_changed(0, iterat)
+    #             return True
+    #         iterat = self.store.iter_next(iterat)
+    #     return False

+ 627 - 0
FlatCAM_GTK/ObjectUI.py

@@ -0,0 +1,627 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://caram.cl/software/flatcam                         #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+from gi.repository import Gtk
+
+from FlatCAM_GTK.GUIElements import *
+
+
+class ObjectUI(Gtk.VBox):
+    """
+    Base class for the UI of FlatCAM objects. Deriving classes should
+    put UI elements in ObjectUI.custom_box (Gtk.VBox).
+    """
+
+    def __init__(self, icon_file='share/flatcam_icon32.png', title='FlatCAM Object'):
+        Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
+
+        ## Page Title box (spacing between children)
+        self.title_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 2)
+        self.pack_start(self.title_box, expand=False, fill=False, padding=2)
+
+        ## Page Title icon
+        self.icon = Gtk.Image.new_from_file(icon_file)
+        self.title_box.pack_start(self.icon, expand=False, fill=False, padding=2)
+
+        ## Title label
+        self.title_label = Gtk.Label()
+        self.title_label.set_markup("<b>" + title + "</b>")
+        self.title_label.set_justify(Gtk.Justification.CENTER)
+        self.title_box.pack_start(self.title_label, expand=False, fill=False, padding=2)
+
+        ## Object name
+        self.name_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 2)
+        self.pack_start(self.name_box, expand=False, fill=False, padding=2)
+        name_label = Gtk.Label('Name:')
+        name_label.set_justify(Gtk.Justification.RIGHT)
+        self.name_box.pack_start(name_label,
+                                 expand=False, fill=False, padding=2)
+        self.name_entry = FCEntry()
+        self.name_box.pack_start(self.name_entry, expand=True, fill=False, padding=2)
+
+        ## Box box for custom widgets
+        self.custom_box = Gtk.VBox(spacing=3, margin=0, vexpand=False)
+        self.pack_start(self.custom_box, expand=False, fill=False, padding=0)
+
+        ## Common to all objects
+        ## Scale
+        self.scale_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.scale_label.set_markup('<b>Scale:</b>')
+        self.scale_label.set_tooltip_markup(
+            "Change the size of the object."
+        )
+        self.pack_start(self.scale_label, expand=False, fill=False, padding=2)
+
+        grid5 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid5, expand=False, fill=False, padding=2)
+
+        # Factor
+        l10 = Gtk.Label('Factor:', xalign=1)
+        l10.set_tooltip_markup(
+            "Factor by which to multiply\n"
+            "geometric features of this object."
+        )
+        grid5.attach(l10, 0, 0, 1, 1)
+        self.scale_entry = FloatEntry()
+        self.scale_entry.set_text("1.0")
+        grid5.attach(self.scale_entry, 1, 0, 1, 1)
+
+        # GO Button
+        self.scale_button = Gtk.Button(label='Scale')
+        self.scale_button.set_tooltip_markup(
+            "Perform scaling operation."
+        )
+        self.pack_start(self.scale_button, expand=False, fill=False, padding=2)
+
+        ## Offset
+        self.offset_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.offset_label.set_markup('<b>Offset:</b>')
+        self.offset_label.set_tooltip_markup(
+            "Change the position of this object."
+        )
+        self.pack_start(self.offset_label, expand=False, fill=False, padding=2)
+
+        grid6 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid6, expand=False, fill=False, padding=2)
+
+        # Vector
+        l11 = Gtk.Label('Offset Vector:', xalign=1)
+        l11.set_tooltip_markup(
+            "Amount by which to move the object\n"
+            "in the x and y axes in (x, y) format."
+        )
+        grid6.attach(l11, 0, 0, 1, 1)
+        self.offsetvector_entry = EvalEntry()
+        self.offsetvector_entry.set_text("(0.0, 0.0)")
+        grid6.attach(self.offsetvector_entry, 1, 0, 1, 1)
+
+        self.offset_button = Gtk.Button(label='Scale')
+        self.offset_button.set_tooltip_markup(
+            "Perform the offset operation."
+        )
+        self.pack_start(self.offset_button, expand=False, fill=False, padding=2)
+
+    def set_field(self, name, value):
+        getattr(self, name).set_value(value)
+
+    def get_field(self, name):
+        return getattr(self, name).get_value()
+
+
+class CNCObjectUI(ObjectUI):
+    """
+    User interface for CNCJob objects.
+    """
+
+    def __init__(self):
+        ObjectUI.__init__(self, title='CNC Job Object', icon_file='share/cnc32.png')
+
+        ## Plot options
+        self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.plot_options_label.set_markup("<b>Plot Options:</b>")
+        self.custom_box.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
+
+        grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.custom_box.pack_start(grid0, expand=False, fill=False, padding=2)
+
+        # Plot CB
+        self.plot_cb = FCCheckBox(label='Plot')
+        self.plot_cb.set_tooltip_markup(
+            "Plot (show) this object."
+        )
+        grid0.attach(self.plot_cb, 0, 0, 2, 1)
+
+        # Tool dia for plot
+        l1 = Gtk.Label('Tool dia:', xalign=1)
+        l1.set_tooltip_markup(
+            "Diameter of the tool to be\n"
+            "rendered in the plot."
+        )
+        grid0.attach(l1, 0, 1, 1, 1)
+        self.tooldia_entry = LengthEntry()
+        grid0.attach(self.tooldia_entry, 1, 1, 1, 1)
+
+        # Update plot button
+        self.updateplot_button = Gtk.Button(label='Update Plot')
+        self.updateplot_button.set_tooltip_markup(
+            "Update the plot."
+        )
+        self.custom_box.pack_start(self.updateplot_button, expand=False, fill=False, padding=2)
+
+        ## Export G-Code
+        self.export_gcode_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.export_gcode_label.set_markup("<b>Export G-Code:</b>")
+        self.export_gcode_label.set_tooltip_markup(
+            "Export and save G-Code to\n"
+            "make this object to a file."
+        )
+        self.custom_box.pack_start(self.export_gcode_label, expand=False, fill=False, padding=2)
+
+        # Append text to Gerber
+        l2 = Gtk.Label('Append to G-Code:')
+        l2.set_tooltip_markup(
+            "Type here any G-Code commands you would\n"
+            "like to append to the generated file.\n"
+            "I.e.: M2 (End of program)"
+        )
+        self.custom_box.pack_start(l2, expand=False, fill=False, padding=2)
+        #self.append_gtext = FCTextArea()
+        #self.custom_box.pack_start(self.append_gtext, expand=False, fill=False, padding=2)
+
+        # GO Button
+        self.export_gcode_button = Gtk.Button(label='Export G-Code')
+        self.export_gcode_button.set_tooltip_markup(
+            "Opens dialog to save G-Code\n"
+            "file."
+        )
+        self.custom_box.pack_start(self.export_gcode_button, expand=False, fill=False, padding=2)
+
+
+class GeometryObjectUI(ObjectUI):
+    """
+    User interface for Geometry objects.
+    """
+
+    def __init__(self):
+        ObjectUI.__init__(self, title='Geometry Object', icon_file='share/geometry32.png')
+
+        ## Plot options
+        self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.plot_options_label.set_markup("<b>Plot Options:</b>")
+        self.custom_box.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
+
+        grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.custom_box.pack_start(grid0, expand=True, fill=False, padding=2)
+
+        # Plot CB
+        self.plot_cb = FCCheckBox(label='Plot')
+        self.plot_cb.set_tooltip_markup(
+            "Plot (show) this object."
+        )
+        grid0.attach(self.plot_cb, 0, 0, 1, 1)
+
+        ## Create CNC Job
+        self.cncjob_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.cncjob_label.set_markup('<b>Create CNC Job:</b>')
+        self.cncjob_label.set_tooltip_markup(
+            "Create a CNC Job object\n"
+            "tracing the contours of this\n"
+            "Geometry object."
+        )
+        self.custom_box.pack_start(self.cncjob_label, expand=True, fill=False, padding=2)
+
+        grid1 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.custom_box.pack_start(grid1, expand=True, fill=False, padding=2)
+
+        # Cut Z
+        l1 = Gtk.Label('Cut Z:', xalign=1)
+        l1.set_tooltip_markup(
+            "Cutting depth (negative)\n"
+            "below the copper surface."
+        )
+        grid1.attach(l1, 0, 0, 1, 1)
+        self.cutz_entry = LengthEntry()
+        grid1.attach(self.cutz_entry, 1, 0, 1, 1)
+
+        # Travel Z
+        l2 = Gtk.Label('Travel Z:', xalign=1)
+        l2.set_tooltip_markup(
+            "Height of the tool when\n"
+            "moving without cutting."
+        )
+        grid1.attach(l2, 0, 1, 1, 1)
+        self.travelz_entry = LengthEntry()
+        grid1.attach(self.travelz_entry, 1, 1, 1, 1)
+
+        l3 = Gtk.Label('Feed rate:', xalign=1)
+        l3.set_tooltip_markup(
+            "Cutting speed in the XY\n"
+            "plane in units per minute"
+        )
+        grid1.attach(l3, 0, 2, 1, 1)
+        self.cncfeedrate_entry = LengthEntry()
+        grid1.attach(self.cncfeedrate_entry, 1, 2, 1, 1)
+
+        l4 = Gtk.Label('Tool dia:', xalign=1)
+        l4.set_tooltip_markup(
+            "The diameter of the cutting\n"
+            "tool (just for display)."
+        )
+        grid1.attach(l4, 0, 3, 1, 1)
+        self.cnctooldia_entry = LengthEntry()
+        grid1.attach(self.cnctooldia_entry, 1, 3, 1, 1)
+
+        self.generate_cnc_button = Gtk.Button(label='Generate')
+        self.generate_cnc_button.set_tooltip_markup(
+            "Generate the CNC Job object."
+        )
+        self.custom_box.pack_start(self.generate_cnc_button, expand=True, fill=False, padding=2)
+
+        ## Paint Area
+        self.paint_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.paint_label.set_markup('<b>Paint Area:</b>')
+        self.paint_label.set_tooltip_markup(
+            "Creates tool paths to cover the\n"
+            "whole area of a polygon (remove\n"
+            "all copper). You will be asked\n"
+            "to click on the desired polygon."
+        )
+        self.custom_box.pack_start(self.paint_label, expand=True, fill=False, padding=2)
+
+        grid2 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.custom_box.pack_start(grid2, expand=True, fill=False, padding=2)
+
+        # Tool dia
+        l5 = Gtk.Label('Tool dia:', xalign=1)
+        l5.set_tooltip_markup(
+            "Diameter of the tool to\n"
+            "be used in the operation."
+        )
+        grid2.attach(l5, 0, 0, 1, 1)
+        self.painttooldia_entry = LengthEntry()
+        grid2.attach(self.painttooldia_entry, 1, 0, 1, 1)
+
+        # Overlap
+        l6 = Gtk.Label('Overlap:', xalign=1)
+        l6.set_tooltip_markup(
+            "How much (fraction) of the tool\n"
+            "width to overlap each tool pass."
+        )
+        grid2.attach(l6, 0, 1, 1, 1)
+        self.paintoverlap_entry = LengthEntry()
+        grid2.attach(self.paintoverlap_entry, 1, 1, 1, 1)
+
+        # Margin
+        l7 = Gtk.Label('Margin:', xalign=1)
+        l7.set_tooltip_markup(
+            "Distance by which to avoid\n"
+            "the edges of the polygon to\n"
+            "be painted."
+        )
+        grid2.attach(l7, 0, 2, 1, 1)
+        self.paintmargin_entry = LengthEntry()
+        grid2.attach(self.paintmargin_entry, 1, 2, 1, 1)
+
+        # GO Button
+        self.generate_paint_button = Gtk.Button(label='Generate')
+        self.generate_paint_button.set_tooltip_markup(
+            "After clicking here, click inside\n"
+            "the polygon you wish to be painted.\n"
+            "A new Geometry object with the tool\n"
+            "paths will be created."
+        )
+        self.custom_box.pack_start(self.generate_paint_button, expand=True, fill=False, padding=2)
+
+
+class ExcellonObjectUI(ObjectUI):
+    """
+    User interface for Excellon objects.
+    """
+
+    def __init__(self):
+        ObjectUI.__init__(self, title='Excellon Object', icon_file='share/drill32.png')
+
+        ## Plot options
+        self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.plot_options_label.set_markup("<b>Plot Options:</b>")
+        self.custom_box.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
+
+        grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.custom_box.pack_start(grid0, expand=True, fill=False, padding=2)
+
+        self.plot_cb = FCCheckBox(label='Plot')
+        self.plot_cb.set_tooltip_markup(
+            "Plot (show) this object."
+        )
+        grid0.attach(self.plot_cb, 0, 0, 1, 1)
+
+        self.solid_cb = FCCheckBox(label='Solid')
+        self.solid_cb.set_tooltip_markup(
+            "Solid circles."
+        )
+        grid0.attach(self.solid_cb, 1, 0, 1, 1)
+
+        ## Create CNC Job
+        self.cncjob_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.cncjob_label.set_markup('<b>Create CNC Job</b>')
+        self.cncjob_label.set_tooltip_markup(
+            "Create a CNC Job object\n"
+            "for this drill object."
+        )
+        self.custom_box.pack_start(self.cncjob_label, expand=True, fill=False, padding=2)
+
+        grid1 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.custom_box.pack_start(grid1, expand=True, fill=False, padding=2)
+
+        l1 = Gtk.Label('Cut Z:', xalign=1)
+        l1.set_tooltip_markup(
+            "Drill depth (negative)\n"
+            "below the copper surface."
+        )
+        grid1.attach(l1, 0, 0, 1, 1)
+        self.cutz_entry = LengthEntry()
+        grid1.attach(self.cutz_entry, 1, 0, 1, 1)
+
+        l2 = Gtk.Label('Travel Z:', xalign=1)
+        l2.set_tooltip_markup(
+            "Tool height when travelling\n"
+            "across the XY plane."
+        )
+        grid1.attach(l2, 0, 1, 1, 1)
+        self.travelz_entry = LengthEntry()
+        grid1.attach(self.travelz_entry, 1, 1, 1, 1)
+
+        l3 = Gtk.Label('Feed rate:', xalign=1)
+        l3.set_tooltip_markup(
+            "Tool speed while drilling\n"
+            "(in units per minute)."
+        )
+        grid1.attach(l3, 0, 2, 1, 1)
+        self.feedrate_entry = LengthEntry()
+        grid1.attach(self.feedrate_entry, 1, 2, 1, 1)
+
+        l4 = Gtk.Label('Tools:', xalign=1)
+        l4.set_tooltip_markup(
+            "Which tools to include\n"
+            "in the CNC Job."
+        )
+        grid1.attach(l4, 0, 3, 1, 1)
+        boxt = Gtk.Box()
+        grid1.attach(boxt, 1, 3, 1, 1)
+        self.tools_entry = FCEntry()
+        boxt.pack_start(self.tools_entry, expand=True, fill=False, padding=2)
+        self.choose_tools_button = Gtk.Button(label='Choose...')
+        self.choose_tools_button.set_tooltip_markup(
+            "Choose the tools\n"
+            "from a list."
+        )
+        boxt.pack_start(self.choose_tools_button, expand=True, fill=False, padding=2)
+
+        self.generate_cnc_button = Gtk.Button(label='Generate')
+        self.generate_cnc_button.set_tooltip_markup(
+            "Generate the CNC Job."
+        )
+        self.custom_box.pack_start(self.generate_cnc_button, expand=True, fill=False, padding=2)
+
+
+class GerberObjectUI(ObjectUI):
+    """
+    User interface for Gerber objects.
+    """
+
+    def __init__(self):
+        ObjectUI.__init__(self, title='Gerber Object')
+
+        ## Plot options
+        self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.plot_options_label.set_markup("<b>Plot Options:</b>")
+        self.custom_box.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
+
+        grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.custom_box.pack_start(grid0, expand=True, fill=False, padding=2)
+
+        # Plot CB
+        self.plot_cb = FCCheckBox(label='Plot')
+        self.plot_cb.set_tooltip_markup(
+            "Plot (show) this object."
+        )
+        grid0.attach(self.plot_cb, 0, 0, 1, 1)
+
+        # Solid CB
+        self.solid_cb = FCCheckBox(label='Solid')
+        self.solid_cb.set_tooltip_markup(
+            "Solid color polygons."
+        )
+        grid0.attach(self.solid_cb, 1, 0, 1, 1)
+
+        # Multicolored CB
+        self.multicolored_cb = FCCheckBox(label='Multicolored')
+        self.multicolored_cb.set_tooltip_markup(
+            "Draw polygons in different colors."
+        )
+        grid0.attach(self.multicolored_cb, 2, 0, 1, 1)
+
+        ## Isolation Routing
+        self.isolation_routing_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.isolation_routing_label.set_markup("<b>Isolation Routing:</b>")
+        self.isolation_routing_label.set_tooltip_markup(
+            "Create a Geometry object with\n"
+            "toolpaths to cut outside polygons."
+        )
+        self.custom_box.pack_start(self.isolation_routing_label, expand=True, fill=False, padding=2)
+
+        grid = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.custom_box.pack_start(grid, expand=True, fill=False, padding=2)
+
+        l1 = Gtk.Label('Tool diam:', xalign=1)
+        l1.set_tooltip_markup(
+            "Diameter of the cutting tool."
+        )
+        grid.attach(l1, 0, 0, 1, 1)
+        self.iso_tool_dia_entry = LengthEntry()
+        grid.attach(self.iso_tool_dia_entry, 1, 0, 1, 1)
+
+        l2 = Gtk.Label('Width (# passes):', xalign=1)
+        l2.set_tooltip_markup(
+            "Width of the isolation gap in\n"
+            "number (integer) of tool widths."
+        )
+        grid.attach(l2, 0, 1, 1, 1)
+        self.iso_width_entry = IntEntry()
+        grid.attach(self.iso_width_entry, 1, 1, 1, 1)
+
+        l3 = Gtk.Label('Pass overlap:', xalign=1)
+        l3.set_tooltip_markup(
+            "How much (fraction of tool width)\n"
+            "to overlap each pass."
+        )
+        grid.attach(l3, 0, 2, 1, 1)
+        self.iso_overlap_entry = FloatEntry()
+        grid.attach(self.iso_overlap_entry, 1, 2, 1, 1)
+
+        self.generate_iso_button = Gtk.Button(label='Generate Geometry')
+        self.generate_iso_button.set_tooltip_markup(
+            "Create the Geometry Object\n"
+            "for isolation routing."
+        )
+        self.custom_box.pack_start(self.generate_iso_button, expand=True, fill=False, padding=2)
+
+        ## Board cuttout
+        self.board_cutout_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.board_cutout_label.set_markup("<b>Board cutout:</b>")
+        self.board_cutout_label.set_tooltip_markup(
+            "Create toolpaths to cut around\n"
+            "the PCB and separate it from\n"
+            "the original board."
+        )
+        self.custom_box.pack_start(self.board_cutout_label, expand=True, fill=False, padding=2)
+
+        grid2 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.custom_box.pack_start(grid2, expand=True, fill=False, padding=2)
+
+        l4 = Gtk.Label('Tool dia:', xalign=1)
+        l4.set_tooltip_markup(
+            "Diameter of the cutting tool."
+        )
+        grid2.attach(l4, 0, 0, 1, 1)
+        self.cutout_tooldia_entry = LengthEntry()
+        grid2.attach(self.cutout_tooldia_entry, 1, 0, 1, 1)
+
+        l5 = Gtk.Label('Margin:', xalign=1)
+        l5.set_tooltip_markup(
+            "Distance from objects at which\n"
+            "to draw the cutout."
+        )
+        grid2.attach(l5, 0, 1, 1, 1)
+        self.cutout_margin_entry = LengthEntry()
+        grid2.attach(self.cutout_margin_entry, 1, 1, 1, 1)
+
+        l6 = Gtk.Label('Gap size:', xalign=1)
+        l6.set_tooltip_markup(
+            "Size of the gaps in the toolpath\n"
+            "that will remain to hold the\n"
+            "board in place."
+        )
+        grid2.attach(l6, 0, 2, 1, 1)
+        self.cutout_gap_entry = LengthEntry()
+        grid2.attach(self.cutout_gap_entry, 1, 2, 1, 1)
+
+        l7 = Gtk.Label('Gaps:', xalign=1)
+        l7.set_tooltip_markup(
+            "Where to place the gaps, Top/Bottom\n"
+            "Left/Rigt, or on all 4 sides."
+        )
+        grid2.attach(l7, 0, 3, 1, 1)
+        self.gaps_radio = RadioSet([{'label': '2 (T/B)', 'value': 'tb'},
+                                    {'label': '2 (L/R)', 'value': 'lr'},
+                                    {'label': '4', 'value': '4'}])
+        grid2.attach(self.gaps_radio, 1, 3, 1, 1)
+
+        self.generate_cutout_button = Gtk.Button(label='Generate Geometry')
+        self.generate_cutout_button.set_tooltip_markup(
+            "Generate the geometry for\n"
+            "the board cutout."
+        )
+        self.custom_box.pack_start(self.generate_cutout_button, expand=True, fill=False, padding=2)
+
+        ## Non-copper regions
+        self.noncopper_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.noncopper_label.set_markup("<b>Non-copper regions:</b>")
+        self.noncopper_label.set_tooltip_markup(
+            "Create polygons covering the\n"
+            "areas without copper on the PCB.\n"
+            "Equivalent to the inverse of this\n"
+            "object. Can be used to remove all\n"
+            "copper from a specified region."
+        )
+        self.custom_box.pack_start(self.noncopper_label, expand=True, fill=False, padding=2)
+
+        grid3 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.custom_box.pack_start(grid3, expand=True, fill=False, padding=2)
+
+        l8 = Gtk.Label('Boundary margin:', xalign=1)
+        l8.set_tooltip_markup(
+            "Specify the edge of the PCB\n"
+            "by drawing a box around all\n"
+            "objects with this minimum\n"
+            "distance."
+        )
+        grid3.attach(l8, 0, 0, 1, 1)
+        self.noncopper_margin_entry = LengthEntry()
+        grid3.attach(self.noncopper_margin_entry, 1, 0, 1, 1)
+
+        self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners")
+        self.noncopper_rounded_cb.set_tooltip_markup(
+            "If the boundary of the board\n"
+            "is to have rounded corners\n"
+            "their radius is equal to the margin."
+        )
+        grid3.attach(self.noncopper_rounded_cb, 0, 1, 2, 1)
+
+        self.generate_noncopper_button = Gtk.Button(label='Generate Geometry')
+        self.generate_noncopper_button.set_tooltip_markup(
+            "Creates a Geometry objects with polygons\n"
+            "covering the copper-free areas of the PCB."
+        )
+        self.custom_box.pack_start(self.generate_noncopper_button, expand=True, fill=False, padding=2)
+
+        ## Bounding box
+        self.boundingbox_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
+        self.boundingbox_label.set_markup('<b>Bounding Box:</b>')
+        self.boundingbox_label.set_tooltip_markup(
+            "Create a Geometry object with a rectangle\n"
+            "enclosing all polygons at a given distance."
+        )
+        self.custom_box.pack_start(self.boundingbox_label, expand=True, fill=False, padding=2)
+
+        grid4 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.custom_box.pack_start(grid4, expand=True, fill=False, padding=2)
+
+        l9 = Gtk.Label('Boundary Margin:', xalign=1)
+        l9.set_tooltip_markup(
+            "Distance of the edges of the box\n"
+            "to the nearest polygon."
+        )
+        grid4.attach(l9, 0, 0, 1, 1)
+        self.bbmargin_entry = LengthEntry()
+        grid4.attach(self.bbmargin_entry, 1, 0, 1, 1)
+
+        self.bbrounded_cb = FCCheckBox(label="Rounded corners")
+        self.bbrounded_cb.set_tooltip_markup(
+            "If the bounding box is \n"
+            "to have rounded corners\n"
+            "their radius is equal to\n"
+            "the margin."
+        )
+        grid4.attach(self.bbrounded_cb, 0, 1, 2, 1)
+
+        self.generate_bb_button = Gtk.Button(label='Generate Geometry')
+        self.generate_bb_button.set_tooltip_markup(
+            "Genrate the Geometry object."
+        )
+        self.custom_box.pack_start(self.generate_bb_button, expand=True, fill=False, padding=2)

+ 318 - 0
FlatCAM_GTK/PlotCanvas.py

@@ -0,0 +1,318 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://caram.cl/software/flatcam                         #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+from gi.repository import Gdk
+from matplotlib.figure import Figure
+from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
+#from FlatCAMApp import *
+from FlatCAM_GTK import FlatCAMApp
+
+
+class PlotCanvas:
+    """
+    Class handling the plotting area in the application.
+    """
+
+    def __init__(self, container):
+        """
+        The constructor configures the Matplotlib figure that
+        will contain all plots, creates the base axes and connects
+        events to the plotting area.
+
+        :param container: The parent container in which to draw plots.
+        :rtype: PlotCanvas
+        """
+        # Options
+        self.x_margin = 15  # pixels
+        self.y_margin = 25  # Pixels
+
+        # Parent container
+        self.container = container
+
+        # Plots go onto a single matplotlib.figure
+        self.figure = Figure(dpi=50)  # TODO: dpi needed?
+        self.figure.patch.set_visible(False)
+
+        # These axes show the ticks and grid. No plotting done here.
+        # New axes must have a label, otherwise mpl returns an existing one.
+        self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
+        self.axes.set_aspect(1)
+        self.axes.grid(True)
+
+        # The canvas is the top level container (Gtk.DrawingArea)
+        self.canvas = FigureCanvas(self.figure)
+        self.canvas.set_hexpand(1)
+        self.canvas.set_vexpand(1)
+        self.canvas.set_can_focus(True)  # For key press
+
+        # Attach to parent
+        self.container.attach(self.canvas, 0, 0, 600, 400)  # TODO: Height and width are num. columns??
+
+        # Events
+        self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
+        self.canvas.connect('configure-event', self.auto_adjust_axes)
+        self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
+        self.canvas.connect("scroll-event", self.on_scroll)
+        self.canvas.mpl_connect('key_press_event', self.on_key_down)
+        self.canvas.mpl_connect('key_release_event', self.on_key_up)
+
+        self.mouse = [0, 0]
+        self.key = None
+
+    def on_key_down(self, event):
+        """
+
+        :param event:
+        :return:
+        """
+        self.key = event.key
+
+    def on_key_up(self, event):
+        """
+
+        :param event:
+        :return:
+        """
+        self.key = None
+
+    def mpl_connect(self, event_name, callback):
+        """
+        Attach an event handler to the canvas through the Matplotlib interface.
+
+        :param event_name: Name of the event
+        :type event_name: str
+        :param callback: Function to call
+        :type callback: func
+        :return: Connection id
+        :rtype: int
+        """
+        return self.canvas.mpl_connect(event_name, callback)
+
+    def mpl_disconnect(self, cid):
+        """
+        Disconnect callback with the give id.
+        :param cid: Callback id.
+        :return: None
+        """
+        self.canvas.mpl_disconnect(cid)
+
+    def connect(self, event_name, callback):
+        """
+        Attach an event handler to the canvas through the native GTK interface.
+
+        :param event_name: Name of the event
+        :type event_name: str
+        :param callback: Function to call
+        :type callback: function
+        :return: Nothing
+        """
+        self.canvas.connect(event_name, callback)
+
+    def clear(self):
+        """
+        Clears axes and figure.
+
+        :return: None
+        """
+
+        # Clear
+        self.axes.cla()
+        try:
+            self.figure.clf()
+        except KeyError:
+            FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()")
+
+        # Re-build
+        self.figure.add_axes(self.axes)
+        self.axes.set_aspect(1)
+        self.axes.grid(True)
+
+        # Re-draw
+        self.canvas.queue_draw()
+
+    def adjust_axes(self, xmin, ymin, xmax, ymax):
+        """
+        Adjusts all axes while maintaining the use of the whole canvas
+        and an aspect ratio to 1:1 between x and y axes. The parameters are an original
+        request that will be modified to fit these restrictions.
+
+        :param xmin: Requested minimum value for the X axis.
+        :type xmin: float
+        :param ymin: Requested minimum value for the Y axis.
+        :type ymin: float
+        :param xmax: Requested maximum value for the X axis.
+        :type xmax: float
+        :param ymax: Requested maximum value for the Y axis.
+        :type ymax: float
+        :return: None
+        """
+
+        FlatCAMApp.App.log.debug("PC.adjust_axes()")
+
+        width = xmax - xmin
+        height = ymax - ymin
+        try:
+            r = width / height
+        except ZeroDivisionError:
+            FlatCAMApp.App.log.error("Height is %f" % height)
+            return
+        canvas_w, canvas_h = self.canvas.get_width_height()
+        canvas_r = float(canvas_w) / canvas_h
+        x_ratio = float(self.x_margin) / canvas_w
+        y_ratio = float(self.y_margin) / canvas_h
+
+        if r > canvas_r:
+            ycenter = (ymin + ymax) / 2.0
+            newheight = height * r / canvas_r
+            ymin = ycenter - newheight / 2.0
+            ymax = ycenter + newheight / 2.0
+        else:
+            xcenter = (xmax + xmin) / 2.0
+            newwidth = width * canvas_r / r
+            xmin = xcenter - newwidth / 2.0
+            xmax = xcenter + newwidth / 2.0
+
+        # Adjust axes
+        for ax in self.figure.get_axes():
+            if ax._label != 'base':
+                ax.set_frame_on(False)  # No frame
+                ax.set_xticks([])  # No tick
+                ax.set_yticks([])  # No ticks
+                ax.patch.set_visible(False)  # No background
+                ax.set_aspect(1)
+            ax.set_xlim((xmin, xmax))
+            ax.set_ylim((ymin, ymax))
+            ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
+
+        # Re-draw
+        self.canvas.queue_draw()
+
+    def auto_adjust_axes(self, *args):
+        """
+        Calls ``adjust_axes()`` using the extents of the base axes.
+
+        :rtype : None
+        :return: None
+        """
+
+        xmin, xmax = self.axes.get_xlim()
+        ymin, ymax = self.axes.get_ylim()
+        self.adjust_axes(xmin, ymin, xmax, ymax)
+
+    def zoom(self, factor, center=None):
+        """
+        Zooms the plot by factor around a given
+        center point. Takes care of re-drawing.
+
+        :param factor: Number by which to scale the plot.
+        :type factor: float
+        :param center: Coordinates [x, y] of the point around which to scale the plot.
+        :type center: list
+        :return: None
+        """
+
+        xmin, xmax = self.axes.get_xlim()
+        ymin, ymax = self.axes.get_ylim()
+        width = xmax - xmin
+        height = ymax - ymin
+
+        if center is None or center == [None, None]:
+            center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
+
+        # For keeping the point at the pointer location
+        relx = (xmax - center[0]) / width
+        rely = (ymax - center[1]) / height
+
+        new_width = width / factor
+        new_height = height / factor
+
+        xmin = center[0] - new_width * (1 - relx)
+        xmax = center[0] + new_width * relx
+        ymin = center[1] - new_height * (1 - rely)
+        ymax = center[1] + new_height * rely
+
+        # Adjust axes
+        for ax in self.figure.get_axes():
+            ax.set_xlim((xmin, xmax))
+            ax.set_ylim((ymin, ymax))
+
+        # Re-draw
+        self.canvas.queue_draw()
+
+    def pan(self, x, y):
+        xmin, xmax = self.axes.get_xlim()
+        ymin, ymax = self.axes.get_ylim()
+        width = xmax - xmin
+        height = ymax - ymin
+
+        # Adjust axes
+        for ax in self.figure.get_axes():
+            ax.set_xlim((xmin + x*width, xmax + x*width))
+            ax.set_ylim((ymin + y*height, ymax + y*height))
+
+        # Re-draw
+        self.canvas.queue_draw()
+
+    def new_axes(self, name):
+        """
+        Creates and returns an Axes object attached to this object's Figure.
+
+        :param name: Unique label for the axes.
+        :return: Axes attached to the figure.
+        :rtype: Axes
+        """
+
+        return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
+
+    def on_scroll(self, canvas, event):
+        """
+        Scroll event handler.
+
+        :param canvas: The widget generating the event. Ignored.
+        :param event: Event object containing the event information.
+        :return: None
+        """
+
+        # So it can receive key presses
+        self.canvas.grab_focus()
+
+        # Event info
+        z, direction = event.get_scroll_direction()
+
+        if self.key is None:
+
+            if direction is Gdk.ScrollDirection.UP:
+                self.zoom(1.5, self.mouse)
+            else:
+                self.zoom(1/1.5, self.mouse)
+            return
+
+        if self.key == 'shift':
+
+            if direction is Gdk.ScrollDirection.UP:
+                self.pan(0.3, 0)
+            else:
+                self.pan(-0.3, 0)
+            return
+
+        if self.key == 'ctrl+control':
+
+            if direction is Gdk.ScrollDirection.UP:
+                self.pan(0, 0.3)
+            else:
+                self.pan(0, -0.3)
+            return
+
+    def on_mouse_move(self, event):
+        """
+        Mouse movement event hadler. Stores the coordinates.
+
+        :param event: Contains information about the event.
+        :return: None
+        """
+        self.mouse = [event.xdata, event.ydata]

+ 0 - 0
setup_ubuntu.sh → FlatCAM_GTK/setup_ubuntu.sh


+ 117 - 104
GUIElements.py

@@ -1,19 +1,11 @@
-############################################################
-# FlatCAM: 2D Post-processing for Manufacturing            #
-# http://caram.cl/software/flatcam                         #
-# Author: Juan Pablo Caram (c)                             #
-# Date: 2/5/2014                                           #
-# MIT Licence                                              #
-############################################################
-
-from gi.repository import Gtk
-import re
+from PyQt4 import QtGui, QtCore
 from copy import copy
 import FlatCAMApp
+import re
 
 
-class RadioSet(Gtk.Box):
-    def __init__(self, choices):
+class RadioSet(QtGui.QWidget):
+    def __init__(self, choices, orientation='horizontal', parent=None):
         """
         The choices are specified as a list of dictionaries containing:
 
@@ -23,28 +15,37 @@ class RadioSet(Gtk.Box):
         :param choices: List of choices. See description.
         :type choices: list
         """
-        Gtk.Box.__init__(self)
+        super(RadioSet, self).__init__(parent)
         self.choices = copy(choices)
-        self.group = None
+
+        if orientation == 'horizontal':
+            layout = QtGui.QHBoxLayout()
+        else:
+            layout = QtGui.QVBoxLayout()
+
+        group = QtGui.QButtonGroup(self)
+
         for choice in self.choices:
-            if self.group is None:
-                choice['radio'] = Gtk.RadioButton.new_with_label(None, choice['label'])
-                self.group = choice['radio']
-            else:
-                choice['radio'] = Gtk.RadioButton.new_with_label_from_widget(self.group, choice['label'])
-            self.pack_start(choice['radio'], expand=True, fill=False, padding=2)
-            choice['radio'].connect('toggled', self.on_toggle)
+            choice['radio'] = QtGui.QRadioButton(choice['label'])
+            group.addButton(choice['radio'])
+            layout.addWidget(choice['radio'], stretch=0)
+            choice['radio'].toggled.connect(self.on_toggle)
+
+        layout.addStretch()
+        self.setLayout(layout)
 
-        self.group_toggle_fn = lambda x, y: None
+        self.group_toggle_fn = lambda: None
 
-    def on_toggle(self, btn):
-        if btn.get_active():
-            self.group_toggle_fn(btn, self.get_value)
+    def on_toggle(self):
+        FlatCAMApp.App.log.debug("Radio toggled")
+        radio = self.sender()
+        if radio.isChecked():
+            self.group_toggle_fn()
         return
 
     def get_value(self):
         for choice in self.choices:
-            if choice['radio'].get_active():
+            if choice['radio'].isChecked():
                 return choice['value']
         FlatCAMApp.App.log.error("No button was toggled in RadioSet.")
         return None
@@ -52,33 +53,15 @@ class RadioSet(Gtk.Box):
     def set_value(self, val):
         for choice in self.choices:
             if choice['value'] == val:
-                choice['radio'].set_active(True)
+                choice['radio'].setChecked(True)
                 return
         FlatCAMApp.App.log.error("Value given is not part of this RadioSet: %s" % str(val))
 
 
-class LengthEntry(Gtk.Entry):
-    """
-    A text entry that interprets its string as a
-    length, with or without specified units. When the user reads
-    the value, it is interpreted and replaced by a floating
-    point representation of the value in the default units. When
-    the entry is activated, its string is repalced by the interpreted
-    value.
-
-    Example:
-    Default units are 'IN', input is "1.0 mm", value returned
-    is 1.0/25.4 = 0.03937.
-    """
-
-    def __init__(self, output_units='IN'):
-        """
-
-        :param output_units: The default output units, 'IN' or 'MM'
-        :return: LengthEntry
-        """
+class LengthEntry(QtGui.QLineEdit):
+    def __init__(self, output_units='IN', parent=None):
+        super(LengthEntry, self).__init__(parent)
 
-        Gtk.Entry.__init__(self)
         self.output_units = output_units
         self.format_re = re.compile(r"^([^\s]+)(?:\s([a-zA-Z]+))?$")
 
@@ -90,40 +73,22 @@ class LengthEntry(Gtk.Entry):
                    'MM': 1.0}
         }
 
-        self.connect('activate', self.on_activate)
-
-    def on_activate(self, *args):
-        """
-        Entry "activate" callback. Replaces the text in the
-        entry with the value returned by `get_value()`.
-
-        :param args: Ignored.
-        :return: None.
-        """
+    def returnPressed(self, *args, **kwargs):
         val = self.get_value()
         if val is not None:
-            self.set_text(str(val))
+            self.set_text(QtCore.QString(str(val)))
         else:
             FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.get_text())
 
     def get_value(self):
-        """
-        Fetches, interprets and returns the value in the entry. The text
-        is parsed to find the numerical expression and the (input) units (if any).
-        The numerical expression is interpreted and scaled acording to the
-        input and output units `self.output_units`.
-
-        :return: Floating point representation of the value in the entry.
-        :rtype: float
-        """
-
-        raw = self.get_text().strip(' ')
+        raw = str(self.text()).strip(' ')
         match = self.format_re.search(raw)
+
         if not match:
             return None
         try:
             if match.group(2) is not None and match.group(2).upper() in self.scales:
-                return float(eval(match.group(1)))*self.scales[self.output_units][match.group(2).upper()]
+                return float(eval(match.group(1)))*float(self.scales[self.output_units][match.group(2).upper()])
             else:
                 return float(eval(match.group(1)))
         except:
@@ -131,24 +96,22 @@ class LengthEntry(Gtk.Entry):
             return None
 
     def set_value(self, val):
-        self.set_text(str(val))
+        self.setText(QtCore.QString(str(val)))
 
 
-class FloatEntry(Gtk.Entry):
-    def __init__(self):
-        Gtk.Entry.__init__(self)
+class FloatEntry(QtGui.QLineEdit):
+    def __init__(self, parent=None):
+        super(FloatEntry, self).__init__(parent)
 
-        self.connect('activate', self.on_activate)
-
-    def on_activate(self, *args):
+    def returnPressed(self, *args, **kwargs):
         val = self.get_value()
         if val is not None:
-            self.set_text(str(val))
+            self.set_text(QtCore.QString(str(val)))
         else:
-            FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.get_text())
+            FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.text())
 
     def get_value(self):
-        raw = self.get_text().strip(' ')
+        raw = str(self.text()).strip(' ')
         try:
             evaled = eval(raw)
         except:
@@ -158,44 +121,44 @@ class FloatEntry(Gtk.Entry):
         return float(evaled)
 
     def set_value(self, val):
-        self.set_text(str(val))
+        self.setText("%.6f"%val)
 
 
-class IntEntry(Gtk.Entry):
-    def __init__(self):
-        Gtk.Entry.__init__(self)
+class IntEntry(QtGui.QLineEdit):
+    def __init__(self, parent=None):
+        super(IntEntry, self).__init__(parent)
 
     def get_value(self):
-        return int(self.get_text())
+        return int(self.text())
 
     def set_value(self, val):
-        self.set_text(str(val))
+        self.setText(QtCore.QString(str(val)))
 
 
-class FCEntry(Gtk.Entry):
-    def __init__(self):
-        Gtk.Entry.__init__(self)
+class FCEntry(QtGui.QLineEdit):
+    def __init__(self, parent=None):
+        super(FCEntry, self).__init__(parent)
 
     def get_value(self):
-        return self.get_text()
+        return str(self.text())
 
     def set_value(self, val):
-        self.set_text(str(val))
+        self.setText(QtCore.QString(str(val)))
 
 
-class EvalEntry(Gtk.Entry):
-    def __init__(self):
-        Gtk.Entry.__init__(self)
+class EvalEntry(QtGui.QLineEdit):
+    def __init__(self, parent=None):
+        super(EvalEntry, self).__init__(parent)
 
-    def on_activate(self, *args):
+    def returnPressed(self, *args, **kwargs):
         val = self.get_value()
         if val is not None:
-            self.set_text(str(val))
+            self.setText(QtCore.QString(str(val)))
         else:
             FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.get_text())
 
     def get_value(self):
-        raw = self.get_text().strip(' ')
+        raw = str(self.text()).strip(' ')
         try:
             return eval(raw)
         except:
@@ -203,15 +166,65 @@ class EvalEntry(Gtk.Entry):
             return None
 
     def set_value(self, val):
-        self.set_text(str(val))
+        self.setText(QtCore.QString(str(val)))
 
 
-class FCCheckBox(Gtk.CheckButton):
-    def __init__(self, label=''):
-        Gtk.CheckButton.__init__(self, label=label)
+class FCCheckBox(QtGui.QCheckBox):
+    def __init__(self, label='', parent=None):
+        super(FCCheckBox, self).__init__(QtCore.QString(label), parent)
 
     def get_value(self):
-        return self.get_active()
+        return self.isChecked()
 
     def set_value(self, val):
-        self.set_active(val)
+        self.setChecked(val)
+
+
+class FCTextArea(QtGui.QPlainTextEdit):
+    def __init__(self, parent=None):
+        super(FCTextArea, self).__init__(parent)
+
+    def set_value(self, val):
+        self.setPlainText(val)
+
+    def get_value(self):
+        return str(self.toPlainText())
+
+
+class VerticalScrollArea(QtGui.QScrollArea):
+    """
+    This widget extends QtGui.QScrollArea to make a vertical-only
+    scroll area that also expands horizontally to accomodate
+    its contents.
+    """
+    def __init__(self, parent=None):
+        QtGui.QScrollArea.__init__(self, parent=parent)
+        self.setWidgetResizable(True)
+        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+
+    def eventFilter(self, source, event):
+        """
+        The event filter gets automatically installed when setWidget()
+        is called.
+
+        :param source:
+        :param event:
+        :return:
+        """
+        if event.type() == QtCore.QEvent.Resize and source == self.widget():
+            # FlatCAMApp.App.log.debug("VerticalScrollArea: Widget resized:")
+            # FlatCAMApp.App.log.debug(" minimumSizeHint().width() = %d" % self.widget().minimumSizeHint().width())
+            # FlatCAMApp.App.log.debug(" verticalScrollBar().width() = %d" % self.verticalScrollBar().width())
+
+            self.setMinimumWidth(self.widget().sizeHint().width() +
+                                 self.verticalScrollBar().sizeHint().width())
+
+            # if self.verticalScrollBar().isVisible():
+            #     FlatCAMApp.App.log.debug(" Scroll bar visible")
+            #     self.setMinimumWidth(self.widget().minimumSizeHint().width() +
+            #                          self.verticalScrollBar().width())
+            # else:
+            #     FlatCAMApp.App.log.debug(" Scroll bar hidden")
+            #     self.setMinimumWidth(self.widget().minimumSizeHint().width())
+        return QtGui.QWidget.eventFilter(self, source, event)

+ 88 - 173
ObjectCollection.py

@@ -1,18 +1,11 @@
-############################################################
-# FlatCAM: 2D Post-processing for Manufacturing            #
-# http://caram.cl/software/flatcam                         #
-# Author: Juan Pablo Caram (c)                             #
-# Date: 4/20/2014                                           #
-# MIT Licence                                              #
-############################################################
-
+from PyQt4.QtCore import QModelIndex
 from FlatCAMObj import *
-from gi.repository import Gtk, GdkPixbuf
 import inspect  # TODO: Remove
 import FlatCAMApp
+from PyQt4 import Qt, QtGui, QtCore
 
 
-class ObjectCollection:
+class ObjectCollection(QtCore.QAbstractListModel):
 
     classdict = {
         "gerber": FlatCAMGerber,
@@ -28,130 +21,51 @@ class ObjectCollection:
         "geometry": "share/geometry16.png"
     }
 
-    def __init__(self):
-
+    def __init__(self, parent=None):
+        QtCore.QAbstractListModel.__init__(self, parent=parent)
         ### Icons for the list view
         self.icons = {}
         for kind in ObjectCollection.icon_files:
-            self.icons[kind] = GdkPixbuf.Pixbuf.new_from_file(ObjectCollection.icon_files[kind])
-
-        ### GUI List components
-        ## Model
-        self.store = Gtk.ListStore(FlatCAMObj)
-
-        ## View
-        self.view = Gtk.TreeView(model=self.store)
-        #self.view.connect("row_activated", self.on_row_activated)
-        self.tree_selection = self.view.get_selection()
-        self.change_subscription = self.tree_selection.connect("changed", self.on_list_selection_change)
+            self.icons[kind] = QtGui.QPixmap(ObjectCollection.icon_files[kind])
 
-        ## Renderers
-        # Icon
-        renderer_pixbuf = Gtk.CellRendererPixbuf()
-        column_pixbuf = Gtk.TreeViewColumn("Type", renderer_pixbuf)
+        self.object_list = []
 
-        def _set_cell_icon(column, cell, model, it, data):
-            obj = model.get_value(it, 0)
-            cell.set_property('pixbuf', self.icons[obj.kind])
+        self.view = QtGui.QListView()
+        self.view.setModel(self)
+        self.view.selectionModel().selectionChanged.connect(self.on_list_selection_change)
+        self.view.activated.connect(self.on_item_activated)
 
-        column_pixbuf.set_cell_data_func(renderer_pixbuf, _set_cell_icon)
-        self.view.append_column(column_pixbuf)
+    def rowCount(self, parent=QtCore.QModelIndex(), *args, **kwargs):
+        return len(self.object_list)
 
-        # Name
-        renderer_text = Gtk.CellRendererText()
-        column_text = Gtk.TreeViewColumn("Name", renderer_text)
+    def columnCount(self, *args, **kwargs):
+        return 1
 
-        def _set_cell_text(column, cell, model, it, data):
-            obj = model.get_value(it, 0)
-            cell.set_property('text', obj.options["name"])
-
-        column_text.set_cell_data_func(renderer_text, _set_cell_text)
-        self.view.append_column(column_text)
+    def data(self, index, role=Qt.Qt.DisplayRole):
+        if not index.isValid() or not 0 <= index.row() < self.rowCount():
+            return QtCore.QVariant()
+        row = index.row()
+        if role == Qt.Qt.DisplayRole:
+            return self.object_list[row].options["name"]
+        if role == Qt.Qt.DecorationRole:
+            return self.icons[self.object_list[row].kind]
 
     def print_list(self):
-        iterat = self.store.get_iter_first()
-        while iterat is not None:
-            obj = self.store[iterat][0]
+        for obj in self.object_list:
             print obj
-            iterat = self.store.iter_next(iterat)
-
-    def delete_all(self):
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_all()")
-        self.store.clear()
-
-    def delete_active(self):
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_active()")
-        try:
-            model, treeiter = self.tree_selection.get_selected()
-            self.store.remove(treeiter)
-        except:
-            pass
-
-    def on_list_selection_change(self, selection):
-        """
-        Callback for change in selection on the objects' list.
-        Instructs the new selection to build the UI for its options.
-
-        :param selection: Ignored.
-        :return: None
-        """
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.on_list_selection_change()")
-        try:
-            self.get_active().build_ui()
-        except AttributeError:  # For None being active
-            pass
 
-    def set_active(self, name):
-        """
-        Sets an object as the active object in the program. Same
-        as `set_list_selection()`.
-
-        :param name: Name of the object.
-        :type name: str
-        :return: None
-        """
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.set_active()")
-        self.set_list_selection(name)
-
-    def get_active(self):
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_active()")
-        try:
-            model, treeiter = self.tree_selection.get_selected()
-            return model[treeiter][0]
-        except (TypeError, ValueError):
-            return None
+    def append(self, obj, active=False):
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.append()")
 
-    def set_list_selection(self, name):
-        """
-        Sets which object should be selected in the list.
+        obj.set_ui(obj.ui_type())
 
-        :param name: Name of the object.
-        :rtype name: str
-        :return: None
-        """
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.set_list_selection()")
-        iterat = self.store.get_iter_first()
-        while iterat is not None and self.store[iterat][0].options["name"] != name:
-            iterat = self.store.iter_next(iterat)
-        self.tree_selection.select_iter(iterat)
-
-    def append(self, obj, active=False):
-        """
-        Add a FlatCAMObj the the collection. This method is thread-safe.
+        # Required before appending
+        self.beginInsertRows(QtCore.QModelIndex(), len(self.object_list), len(self.object_list))
 
-        :param obj: FlatCAMObj to append
-        :type obj: FlatCAMObj
-        :param active: If it is to become the active object after appending
-        :type active: bool
-        :return: None
-        """
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.append()")
+        self.object_list.append(obj)
 
-        def guitask():
-            self.store.append([obj])
-            if active:
-                self.set_list_selection(obj.options["name"])
-        GLib.idle_add(guitask)
+        # Required after appending
+        self.endInsertRows()
 
     def get_names(self):
         """
@@ -160,14 +74,9 @@ class ObjectCollection:
         :return: List of names.
         :rtype: list
         """
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_names()")
-        names = []
-        iterat = self.store.get_iter_first()
-        while iterat is not None:
-            obj = self.store[iterat][0]
-            names.append(obj.options["name"])
-            iterat = self.store.iter_next(iterat)
-        return names
+
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.get_names()")
+        return [x.options['name'] for x in self.object_list]
 
     def get_bounds(self):
         """
@@ -185,9 +94,7 @@ class ObjectCollection:
         xmax = -Inf
         ymax = -Inf
 
-        iterat = self.store.get_iter_first()
-        while iterat is not None:
-            obj = self.store[iterat][0]
+        for obj in self.object_list:
             try:
                 gxmin, gymin, gxmax, gymax = obj.bounds()
                 xmin = min([xmin, gxmin])
@@ -196,24 +103,8 @@ class ObjectCollection:
                 ymax = max([ymax, gymax])
             except:
                 FlatCAMApp.App.log.warning("DEV WARNING: Tried to get bounds of empty geometry.")
-            iterat = self.store.iter_next(iterat)
-        return [xmin, ymin, xmax, ymax]
-
-    def get_list(self):
-        """
-        Returns a list with all FlatCAMObj.
 
-        :return: List with all FlatCAMObj.
-        :rtype: list
-        """
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_list()")
-        collection_list = []
-        iterat = self.store.get_iter_first()
-        while iterat is not None:
-            obj = self.store[iterat][0]
-            collection_list.append(obj)
-            iterat = self.store.iter_next(iterat)
-        return collection_list
+        return [xmin, ymin, xmax, ymax]
 
     def get_by_name(self, name):
         """
@@ -226,33 +117,57 @@ class ObjectCollection:
         """
         FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_by_name()")
 
-        iterat = self.store.get_iter_first()
-        while iterat is not None:
-            obj = self.store[iterat][0]
-            if obj.options["name"] == name:
+        for obj in self.object_list:
+            if obj.options['name'] == name:
                 return obj
-            iterat = self.store.iter_next(iterat)
         return None
 
-    # def change_name(self, old_name, new_name):
-    #     """
-    #     Changes the name of `FlatCAMObj` named `old_name` to `new_name`.
-    #
-    #     :param old_name: Name of the object to change.
-    #     :type old_name: str
-    #     :param new_name: New name.
-    #     :type new_name: str
-    #     :return: True if name change succeeded, False otherwise. Will fail
-    #        if no object with `old_name` is found.
-    #     :rtype: bool
-    #     """
-    #     print inspect.stack()[1][3], "--> OC.change_name()"
-    #     iterat = self.store.get_iter_first()
-    #     while iterat is not None:
-    #         obj = self.store[iterat][0]
-    #         if obj.options["name"] == old_name:
-    #             obj.options["name"] = new_name
-    #             self.store.row_changed(0, iterat)
-    #             return True
-    #         iterat = self.store.iter_next(iterat)
-    #     return False
+    def delete_active(self):
+        selections = self.view.selectedIndexes()
+        if len(selections) == 0:
+            return
+        row = selections[0].row()
+
+        self.beginRemoveRows(QtCore.QModelIndex(), row, row)
+
+        self.object_list.pop(row)
+
+        self.endRemoveRows()
+
+    def get_active(self):
+        selections = self.view.selectedIndexes()
+        if len(selections) == 0:
+            return None
+        row = selections[0].row()
+        return self.object_list[row]
+
+    def set_active(self, name):
+        iobj = self.createIndex(self.get_names().index(name))
+        self.view.selectionModel().select(iobj, QtGui.QItemSelectionModel)
+
+    def on_list_selection_change(self, current, previous):
+        FlatCAMApp.App.log.debug("on_list_selection_change()")
+        FlatCAMApp.App.log.debug("Current: %s, Previous %s" % (str(current), str(previous)))
+        try:
+            selection_index = current.indexes()[0].row()
+        except IndexError:
+            FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
+            return
+
+        self.object_list[selection_index].build_ui()
+
+    def on_item_activated(self, index):
+        self.object_list[index.row()].build_ui()
+
+    def delete_all(self):
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_all()")
+
+        self.beginResetModel()
+
+        self.object_list = []
+
+        self.endResetModel()
+
+    def get_list(self):
+        return self.object_list
+

+ 285 - 298
ObjectUI.py

@@ -1,114 +1,103 @@
-############################################################
-# FlatCAM: 2D Post-processing for Manufacturing            #
-# http://caram.cl/software/flatcam                         #
-# Author: Juan Pablo Caram (c)                             #
-# Date: 2/5/2014                                           #
-# MIT Licence                                              #
-############################################################
-
-from gi.repository import Gtk
+import sys
+from PyQt4 import QtGui, QtCore
 from GUIElements import *
 
 
-class ObjectUI(Gtk.VBox):
+class ObjectUI(QtGui.QWidget):
     """
-    Base class for the UI of FlatCAM objects.
+    Base class for the UI of FlatCAM objects. Deriving classes should
+    put UI elements in ObjectUI.custom_box (QtGui.QLayout).
     """
 
-    def __init__(self, icon_file='share/flatcam_icon32.png', title='FlatCAM Object'):
-        Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
+    def __init__(self, icon_file='share/flatcam_icon32.png', title='FlatCAM Object', parent=None):
+        QtGui.QWidget.__init__(self, parent=parent)
+
+        layout = QtGui.QVBoxLayout()
+        self.setLayout(layout)
 
         ## Page Title box (spacing between children)
-        self.title_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 2)
-        self.pack_start(self.title_box, expand=False, fill=False, padding=2)
+        self.title_box = QtGui.QHBoxLayout()
+        layout.addLayout(self.title_box)
 
         ## Page Title icon
-        self.icon = Gtk.Image.new_from_file(icon_file)
-        self.title_box.pack_start(self.icon, expand=False, fill=False, padding=2)
+        pixmap = QtGui.QPixmap(icon_file)
+        self.icon = QtGui.QLabel()
+        self.icon.setPixmap(pixmap)
+        self.title_box.addWidget(self.icon, stretch=0)
 
         ## Title label
-        self.title_label = Gtk.Label()
-        self.title_label.set_markup("<b>" + title + "</b>")
-        self.title_label.set_justify(Gtk.Justification.CENTER)
-        self.title_box.pack_start(self.title_label, expand=False, fill=False, padding=2)
+        self.title_label = QtGui.QLabel("<font size=5><b>" + title + "</b></font>")
+        self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.title_box.addWidget(self.title_label, stretch=1)
 
         ## Object name
-        self.name_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 2)
-        self.pack_start(self.name_box, expand=False, fill=False, padding=2)
-        name_label = Gtk.Label('Name:')
-        name_label.set_justify(Gtk.Justification.RIGHT)
-        self.name_box.pack_start(name_label,
-                                 expand=False, fill=False, padding=2)
+        self.name_box = QtGui.QHBoxLayout()
+        layout.addLayout(self.name_box)
+        name_label = QtGui.QLabel("Name:")
+        self.name_box.addWidget(name_label)
         self.name_entry = FCEntry()
-        self.name_box.pack_start(self.name_entry, expand=True, fill=False, padding=2)
+        self.name_box.addWidget(self.name_entry)
 
         ## Box box for custom widgets
-        self.custom_box = Gtk.VBox(spacing=3, margin=0, vexpand=False)
-        self.pack_start(self.custom_box, expand=False, fill=False, padding=0)
+        self.custom_box = QtGui.QVBoxLayout()
+        layout.addLayout(self.custom_box)
 
         ## Common to all objects
         ## Scale
-        self.scale_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.scale_label.set_markup('<b>Scale:</b>')
-        self.scale_label.set_tooltip_markup(
+        self.scale_label = QtGui.QLabel('<b>Scale:</b>')
+        self.scale_label.setToolTip(
             "Change the size of the object."
         )
-        self.pack_start(self.scale_label, expand=False, fill=False, padding=2)
+        layout.addWidget(self.scale_label)
 
-        grid5 = Gtk.Grid(column_spacing=3, row_spacing=2)
-        self.pack_start(grid5, expand=False, fill=False, padding=2)
+        grid1 = QtGui.QGridLayout()
+        layout.addLayout(grid1)
 
         # Factor
-        l10 = Gtk.Label('Factor:', xalign=1)
-        l10.set_tooltip_markup(
+        faclabel = QtGui.QLabel('Factor:')
+        faclabel.setToolTip(
             "Factor by which to multiply\n"
             "geometric features of this object."
         )
-        grid5.attach(l10, 0, 0, 1, 1)
+        grid1.addWidget(faclabel, 0, 0)
         self.scale_entry = FloatEntry()
-        self.scale_entry.set_text("1.0")
-        grid5.attach(self.scale_entry, 1, 0, 1, 1)
+        self.scale_entry.set_value(1.0)
+        grid1.addWidget(self.scale_entry, 0, 1)
 
         # GO Button
-        self.scale_button = Gtk.Button(label='Scale')
-        self.scale_button.set_tooltip_markup(
+        self.scale_button = QtGui.QPushButton('Scale')
+        self.scale_button.setToolTip(
             "Perform scaling operation."
         )
-        self.pack_start(self.scale_button, expand=False, fill=False, padding=2)
+        layout.addWidget(self.scale_button)
 
         ## Offset
-        self.offset_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.offset_label.set_markup('<b>Offset:</b>')
-        self.offset_label.set_tooltip_markup(
+        self.offset_label = QtGui.QLabel('<b>Offset:</b>')
+        self.offset_label.setToolTip(
             "Change the position of this object."
         )
-        self.pack_start(self.offset_label, expand=False, fill=False, padding=2)
+        layout.addWidget(self.offset_label)
 
-        grid6 = Gtk.Grid(column_spacing=3, row_spacing=2)
-        self.pack_start(grid6, expand=False, fill=False, padding=2)
+        grid2 = QtGui.QGridLayout()
+        layout.addLayout(grid2)
 
-        # Vector
-        l11 = Gtk.Label('Offset Vector:', xalign=1)
-        l11.set_tooltip_markup(
+        self.offset_label = QtGui.QLabel('Vector:')
+        self.offset_label.setToolTip(
             "Amount by which to move the object\n"
             "in the x and y axes in (x, y) format."
         )
-        grid6.attach(l11, 0, 0, 1, 1)
+        grid2.addWidget(self.offset_label, 0, 0)
         self.offsetvector_entry = EvalEntry()
-        self.offsetvector_entry.set_text("(0.0, 0.0)")
-        grid6.attach(self.offsetvector_entry, 1, 0, 1, 1)
+        self.offsetvector_entry.setText("(0.0, 0.0)")
+        grid2.addWidget(self.offsetvector_entry, 0, 1)
 
-        self.offset_button = Gtk.Button(label='Scale')
-        self.offset_button.set_tooltip_markup(
+        self.offset_button = QtGui.QPushButton('Offset')
+        self.offset_button.setToolTip(
             "Perform the offset operation."
         )
-        self.pack_start(self.offset_button, expand=False, fill=False, padding=2)
-
-    def set_field(self, name, value):
-        getattr(self, name).set_value(value)
+        layout.addWidget(self.offset_button)
 
-    def get_field(self, name):
-        return getattr(self, name).get_value()
+        layout.addStretch()
 
 
 class CNCObjectUI(ObjectUI):
@@ -116,57 +105,68 @@ class CNCObjectUI(ObjectUI):
     User interface for CNCJob objects.
     """
 
-    def __init__(self):
-        ObjectUI.__init__(self, title='CNC Job Object', icon_file='share/cnc32.png')
+    def __init__(self, parent=None):
+        ObjectUI.__init__(self, title='CNC Job Object', icon_file='share/cnc32.png', parent=parent)
 
         ## Plot options
-        self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.plot_options_label.set_markup("<b>Plot Options:</b>")
-        self.custom_box.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
+        self.plot_options_label = QtGui.QLabel("<b>Plot Options:</b>")
+        self.custom_box.addWidget(self.plot_options_label)
 
-        grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
-        self.custom_box.pack_start(grid0, expand=False, fill=False, padding=2)
+        grid0 = QtGui.QGridLayout()
+        self.custom_box.addLayout(grid0)
 
         # Plot CB
-        self.plot_cb = FCCheckBox(label='Plot')
-        self.plot_cb.set_tooltip_markup(
+        # self.plot_cb = QtGui.QCheckBox('Plot')
+        self.plot_cb = FCCheckBox('Plot')
+        self.plot_cb.setToolTip(
             "Plot (show) this object."
         )
-        grid0.attach(self.plot_cb, 0, 0, 2, 1)
+        grid0.addWidget(self.plot_cb, 0, 0)
 
         # Tool dia for plot
-        l1 = Gtk.Label('Tool dia:', xalign=1)
-        l1.set_tooltip_markup(
+        tdlabel = QtGui.QLabel('Tool dia:')
+        tdlabel.setToolTip(
             "Diameter of the tool to be\n"
             "rendered in the plot."
         )
-        grid0.attach(l1, 0, 1, 1, 1)
+        grid0.addWidget(tdlabel, 1, 0)
         self.tooldia_entry = LengthEntry()
-        grid0.attach(self.tooldia_entry, 1, 1, 1, 1)
+        grid0.addWidget(self.tooldia_entry, 1, 1)
 
         # Update plot button
-        self.updateplot_button = Gtk.Button(label='Update Plot')
-        self.updateplot_button.set_tooltip_markup(
+        self.updateplot_button = QtGui.QPushButton('Update Plot')
+        self.updateplot_button.setToolTip(
             "Update the plot."
         )
-        self.custom_box.pack_start(self.updateplot_button, expand=False, fill=False, padding=2)
+        self.custom_box.addWidget(self.updateplot_button)
 
         ## Export G-Code
-        self.export_gcode_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.export_gcode_label.set_markup("<b>Export G-Code:</b>")
-        self.export_gcode_label.set_tooltip_markup(
+        self.export_gcode_label = QtGui.QLabel("<b>Export G-Code:</b>")
+        self.export_gcode_label.setToolTip(
             "Export and save G-Code to\n"
             "make this object to a file."
         )
-        self.custom_box.pack_start(self.export_gcode_label, expand=False, fill=False, padding=2)
+        self.custom_box.addWidget(self.export_gcode_label)
+
+        # Append text to Gerber
+        appendlabel = QtGui.QLabel('Append to G-Code:')
+        appendlabel.setToolTip(
+            "Type here any G-Code commands you would\n"
+            "like to append to the generated file.\n"
+            "I.e.: M2 (End of program)"
+        )
+        self.custom_box.addWidget(appendlabel)
+
+        self.append_text = FCTextArea()
+        self.custom_box.addWidget(self.append_text)
 
         # GO Button
-        self.export_gcode_button = Gtk.Button(label='Export G-Code')
-        self.export_gcode_button.set_tooltip_markup(
+        self.export_gcode_button = QtGui.QPushButton('Export G-Code')
+        self.export_gcode_button.setToolTip(
             "Opens dialog to save G-Code\n"
             "file."
         )
-        self.custom_box.pack_start(self.export_gcode_button, expand=False, fill=False, padding=2)
+        self.custom_box.addWidget(self.export_gcode_button)
 
 
 class GeometryObjectUI(ObjectUI):
@@ -174,135 +174,131 @@ class GeometryObjectUI(ObjectUI):
     User interface for Geometry objects.
     """
 
-    def __init__(self):
-        ObjectUI.__init__(self, title='Geometry Object', icon_file='share/geometry32.png')
+    def __init__(self, parent=None):
+        super(GeometryObjectUI, self).__init__(title='Geometry Object', icon_file='share/geometry32.png', parent=parent)
 
         ## Plot options
-        self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.plot_options_label.set_markup("<b>Plot Options:</b>")
-        self.custom_box.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
-
-        grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
-        self.custom_box.pack_start(grid0, expand=True, fill=False, padding=2)
+        self.plot_options_label = QtGui.QLabel("<b>Plot Options:</b>")
+        self.custom_box.addWidget(self.plot_options_label)
 
         # Plot CB
         self.plot_cb = FCCheckBox(label='Plot')
-        self.plot_cb.set_tooltip_markup(
+        self.plot_cb.setToolTip(
             "Plot (show) this object."
         )
-        grid0.attach(self.plot_cb, 0, 0, 1, 1)
+        self.custom_box.addWidget(self.plot_cb)
 
         ## Create CNC Job
-        self.cncjob_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.cncjob_label.set_markup('<b>Create CNC Job:</b>')
-        self.cncjob_label.set_tooltip_markup(
+        self.cncjob_label = QtGui.QLabel('<b>Create CNC Job:</b>')
+        self.cncjob_label.setToolTip(
             "Create a CNC Job object\n"
             "tracing the contours of this\n"
             "Geometry object."
         )
-        self.custom_box.pack_start(self.cncjob_label, expand=True, fill=False, padding=2)
+        self.custom_box.addWidget(self.cncjob_label)
 
-        grid1 = Gtk.Grid(column_spacing=3, row_spacing=2)
-        self.custom_box.pack_start(grid1, expand=True, fill=False, padding=2)
+        grid1 = QtGui.QGridLayout()
+        self.custom_box.addLayout(grid1)
 
-        # Cut Z
-        l1 = Gtk.Label('Cut Z:', xalign=1)
-        l1.set_tooltip_markup(
+        cutzlabel = QtGui.QLabel('Cut Z:')
+        cutzlabel.setToolTip(
             "Cutting depth (negative)\n"
             "below the copper surface."
         )
-        grid1.attach(l1, 0, 0, 1, 1)
+        grid1.addWidget(cutzlabel, 0, 0)
         self.cutz_entry = LengthEntry()
-        grid1.attach(self.cutz_entry, 1, 0, 1, 1)
+        grid1.addWidget(self.cutz_entry, 0, 1)
 
         # Travel Z
-        l2 = Gtk.Label('Travel Z:', xalign=1)
-        l2.set_tooltip_markup(
+        travelzlabel = QtGui.QLabel('Travel Z:')
+        travelzlabel.setToolTip(
             "Height of the tool when\n"
             "moving without cutting."
         )
-        grid1.attach(l2, 0, 1, 1, 1)
+        grid1.addWidget(travelzlabel, 1, 0)
         self.travelz_entry = LengthEntry()
-        grid1.attach(self.travelz_entry, 1, 1, 1, 1)
+        grid1.addWidget(self.travelz_entry, 1, 1)
 
-        l3 = Gtk.Label('Feed rate:', xalign=1)
-        l3.set_tooltip_markup(
+        # Feedrate
+        frlabel = QtGui.QLabel('Feed Rate:')
+        frlabel.setToolTip(
             "Cutting speed in the XY\n"
             "plane in units per minute"
         )
-        grid1.attach(l3, 0, 2, 1, 1)
+        grid1.addWidget(frlabel, 2, 0)
         self.cncfeedrate_entry = LengthEntry()
-        grid1.attach(self.cncfeedrate_entry, 1, 2, 1, 1)
+        grid1.addWidget(self.cncfeedrate_entry, 2, 1)
 
-        l4 = Gtk.Label('Tool dia:', xalign=1)
-        l4.set_tooltip_markup(
+        # Tooldia
+        tdlabel = QtGui.QLabel('Tool dia:')
+        tdlabel.setToolTip(
             "The diameter of the cutting\n"
             "tool (just for display)."
         )
-        grid1.attach(l4, 0, 3, 1, 1)
+        grid1.addWidget(tdlabel, 3, 0)
         self.cnctooldia_entry = LengthEntry()
-        grid1.attach(self.cnctooldia_entry, 1, 3, 1, 1)
+        grid1.addWidget(self.cnctooldia_entry, 3, 1)
 
-        self.generate_cnc_button = Gtk.Button(label='Generate')
-        self.generate_cnc_button.set_tooltip_markup(
+        self.generate_cnc_button = QtGui.QPushButton('Generate')
+        self.generate_cnc_button.setToolTip(
             "Generate the CNC Job object."
         )
-        self.custom_box.pack_start(self.generate_cnc_button, expand=True, fill=False, padding=2)
+        self.custom_box.addWidget(self.generate_cnc_button)
 
-        ## Paint Area
-        self.paint_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.paint_label.set_markup('<b>Paint Area:</b>')
-        self.paint_label.set_tooltip_markup(
+        ## Paint area
+        self.paint_label = QtGui.QLabel('<b>Paint Area:</b>')
+        self.paint_label.setToolTip(
             "Creates tool paths to cover the\n"
             "whole area of a polygon (remove\n"
             "all copper). You will be asked\n"
             "to click on the desired polygon."
         )
-        self.custom_box.pack_start(self.paint_label, expand=True, fill=False, padding=2)
+        self.custom_box.addWidget(self.paint_label)
 
-        grid2 = Gtk.Grid(column_spacing=3, row_spacing=2)
-        self.custom_box.pack_start(grid2, expand=True, fill=False, padding=2)
+        grid2 = QtGui.QGridLayout()
+        self.custom_box.addLayout(grid2)
 
         # Tool dia
-        l5 = Gtk.Label('Tool dia:', xalign=1)
-        l5.set_tooltip_markup(
+        ptdlabel = QtGui.QLabel('Tool dia:')
+        ptdlabel.setToolTip(
             "Diameter of the tool to\n"
             "be used in the operation."
         )
-        grid2.attach(l5, 0, 0, 1, 1)
+        grid2.addWidget(ptdlabel, 0, 0)
+
         self.painttooldia_entry = LengthEntry()
-        grid2.attach(self.painttooldia_entry, 1, 0, 1, 1)
+        grid2.addWidget(self.painttooldia_entry, 0, 1)
 
         # Overlap
-        l6 = Gtk.Label('Overlap:', xalign=1)
-        l6.set_tooltip_markup(
+        ovlabel = QtGui.QLabel('Overlap:')
+        ovlabel.setToolTip(
             "How much (fraction) of the tool\n"
             "width to overlap each tool pass."
         )
-        grid2.attach(l6, 0, 1, 1, 1)
+        grid2.addWidget(ovlabel, 1, 0)
         self.paintoverlap_entry = LengthEntry()
-        grid2.attach(self.paintoverlap_entry, 1, 1, 1, 1)
+        grid2.addWidget(self.paintoverlap_entry, 1, 1)
 
         # Margin
-        l7 = Gtk.Label('Margin:', xalign=1)
-        l7.set_tooltip_markup(
+        marginlabel = QtGui.QLabel('Margin:')
+        marginlabel.setToolTip(
             "Distance by which to avoid\n"
             "the edges of the polygon to\n"
             "be painted."
         )
-        grid2.attach(l7, 0, 2, 1, 1)
+        grid2.addWidget(marginlabel, 2, 0)
         self.paintmargin_entry = LengthEntry()
-        grid2.attach(self.paintmargin_entry, 1, 2, 1, 1)
+        grid2.addWidget(self.paintmargin_entry)
 
         # GO Button
-        self.generate_paint_button = Gtk.Button(label='Generate')
-        self.generate_paint_button.set_tooltip_markup(
+        self.generate_paint_button = QtGui.QPushButton('Generate')
+        self.generate_paint_button.setToolTip(
             "After clicking here, click inside\n"
             "the polygon you wish to be painted.\n"
             "A new Geometry object with the tool\n"
             "paths will be created."
         )
-        self.custom_box.pack_start(self.generate_paint_button, expand=True, fill=False, padding=2)
+        self.custom_box.addWidget(self.generate_paint_button)
 
 
 class ExcellonObjectUI(ObjectUI):
@@ -310,90 +306,85 @@ class ExcellonObjectUI(ObjectUI):
     User interface for Excellon objects.
     """
 
-    def __init__(self):
-        ObjectUI.__init__(self, title='Excellon Object', icon_file='share/drill32.png')
+    def __init__(self, parent=None):
+        ObjectUI.__init__(self, title='Excellon Object', icon_file='share/drill32.png', parent=parent)
 
         ## Plot options
-        self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.plot_options_label.set_markup("<b>Plot Options:</b>")
-        self.custom_box.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
-
-        grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
-        self.custom_box.pack_start(grid0, expand=True, fill=False, padding=2)
+        self.plot_options_label = QtGui.QLabel("<b>Plot Options:</b>")
+        self.custom_box.addWidget(self.plot_options_label)
 
+        grid0 = QtGui.QGridLayout()
+        self.custom_box.addLayout(grid0)
         self.plot_cb = FCCheckBox(label='Plot')
-        self.plot_cb.set_tooltip_markup(
+        self.plot_cb.setToolTip(
             "Plot (show) this object."
         )
-        grid0.attach(self.plot_cb, 0, 0, 1, 1)
-
+        grid0.addWidget(self.plot_cb, 0, 0)
         self.solid_cb = FCCheckBox(label='Solid')
-        self.solid_cb.set_tooltip_markup(
+        self.solid_cb.setToolTip(
             "Solid circles."
         )
-        grid0.attach(self.solid_cb, 1, 0, 1, 1)
+        grid0.addWidget(self.solid_cb, 0, 1)
+
+        ## Tools
+        self.tools_table_label = QtGui.QLabel('<b>Tools</b>')
+        self.tools_table_label.setToolTip(
+            "Tools in this Excellon object."
+        )
+        self.custom_box.addWidget(self.tools_table_label)
+        self.tools_table = QtGui.QTableWidget()
+        self.tools_table.setFixedHeight(100)
+        self.custom_box.addWidget(self.tools_table)
 
         ## Create CNC Job
-        self.cncjob_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.cncjob_label.set_markup('<b>Create CNC Job</b>')
-        self.cncjob_label.set_tooltip_markup(
+        self.cncjob_label = QtGui.QLabel('<b>Create CNC Job</b>')
+        self.cncjob_label.setToolTip(
             "Create a CNC Job object\n"
             "for this drill object."
         )
-        self.custom_box.pack_start(self.cncjob_label, expand=True, fill=False, padding=2)
+        self.custom_box.addWidget(self.cncjob_label)
 
-        grid1 = Gtk.Grid(column_spacing=3, row_spacing=2)
-        self.custom_box.pack_start(grid1, expand=True, fill=False, padding=2)
+        grid1 = QtGui.QGridLayout()
+        self.custom_box.addLayout(grid1)
 
-        l1 = Gtk.Label('Cut Z:', xalign=1)
-        l1.set_tooltip_markup(
+        cutzlabel = QtGui.QLabel('Cut Z:')
+        cutzlabel.setToolTip(
             "Drill depth (negative)\n"
             "below the copper surface."
         )
-        grid1.attach(l1, 0, 0, 1, 1)
+        grid1.addWidget(cutzlabel, 0, 0)
         self.cutz_entry = LengthEntry()
-        grid1.attach(self.cutz_entry, 1, 0, 1, 1)
+        grid1.addWidget(self.cutz_entry, 0, 1)
 
-        l2 = Gtk.Label('Travel Z:', xalign=1)
-        l2.set_tooltip_markup(
+        travelzlabel = QtGui.QLabel('Travel Z:')
+        travelzlabel.setToolTip(
             "Tool height when travelling\n"
             "across the XY plane."
         )
-        grid1.attach(l2, 0, 1, 1, 1)
+        grid1.addWidget(travelzlabel, 1, 0)
         self.travelz_entry = LengthEntry()
-        grid1.attach(self.travelz_entry, 1, 1, 1, 1)
+        grid1.addWidget(self.travelz_entry, 1, 1)
 
-        l3 = Gtk.Label('Feed rate:', xalign=1)
-        l3.set_tooltip_markup(
+        frlabel = QtGui.QLabel('Feed rate:')
+        frlabel.setToolTip(
             "Tool speed while drilling\n"
             "(in units per minute)."
         )
-        grid1.attach(l3, 0, 2, 1, 1)
+        grid1.addWidget(frlabel, 2, 0)
         self.feedrate_entry = LengthEntry()
-        grid1.attach(self.feedrate_entry, 1, 2, 1, 1)
-
-        l4 = Gtk.Label('Tools:', xalign=1)
-        l4.set_tooltip_markup(
-            "Which tools to include\n"
-            "in the CNC Job."
-        )
-        grid1.attach(l4, 0, 3, 1, 1)
-        boxt = Gtk.Box()
-        grid1.attach(boxt, 1, 3, 1, 1)
-        self.tools_entry = FCEntry()
-        boxt.pack_start(self.tools_entry, expand=True, fill=False, padding=2)
-        self.choose_tools_button = Gtk.Button(label='Choose...')
-        self.choose_tools_button.set_tooltip_markup(
-            "Choose the tools\n"
-            "from a list."
-        )
-        boxt.pack_start(self.choose_tools_button, expand=True, fill=False, padding=2)
-
-        self.generate_cnc_button = Gtk.Button(label='Generate')
-        self.generate_cnc_button.set_tooltip_markup(
+        grid1.addWidget(self.feedrate_entry, 2, 1)
+
+        choose_tools_label = QtGui.QLabel(
+            "Select from the tools section above\n"
+            "the tools you want to include."
+        )
+        self.custom_box.addWidget(choose_tools_label)
+
+        self.generate_cnc_button = QtGui.QPushButton('Generate')
+        self.generate_cnc_button.setToolTip(
             "Generate the CNC Job."
         )
-        self.custom_box.pack_start(self.generate_cnc_button, expand=True, fill=False, padding=2)
+        self.custom_box.addWidget(self.generate_cnc_button)
 
 
 class GerberObjectUI(ObjectUI):
@@ -401,214 +392,210 @@ class GerberObjectUI(ObjectUI):
     User interface for Gerber objects.
     """
 
-    def __init__(self):
-        ObjectUI.__init__(self, title='Gerber Object')
+    def __init__(self, parent=None):
+        ObjectUI.__init__(self, title='Gerber Object', parent=parent)
 
         ## Plot options
-        self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.plot_options_label.set_markup("<b>Plot Options:</b>")
-        self.custom_box.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
-
-        grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
-        self.custom_box.pack_start(grid0, expand=True, fill=False, padding=2)
+        self.plot_options_label = QtGui.QLabel("<b>Plot Options:</b>")
+        self.custom_box.addWidget(self.plot_options_label)
 
+        grid0 = QtGui.QGridLayout()
+        self.custom_box.addLayout(grid0)
         # Plot CB
         self.plot_cb = FCCheckBox(label='Plot')
-        self.plot_cb.set_tooltip_markup(
+        self.plot_options_label.setToolTip(
             "Plot (show) this object."
         )
-        grid0.attach(self.plot_cb, 0, 0, 1, 1)
+        grid0.addWidget(self.plot_cb, 0, 0)
 
         # Solid CB
         self.solid_cb = FCCheckBox(label='Solid')
-        self.solid_cb.set_tooltip_markup(
+        self.solid_cb.setToolTip(
             "Solid color polygons."
         )
-        grid0.attach(self.solid_cb, 1, 0, 1, 1)
+        grid0.addWidget(self.solid_cb, 0, 1)
 
         # Multicolored CB
         self.multicolored_cb = FCCheckBox(label='Multicolored')
-        self.multicolored_cb.set_tooltip_markup(
+        self.multicolored_cb.setToolTip(
             "Draw polygons in different colors."
         )
-        grid0.attach(self.multicolored_cb, 2, 0, 1, 1)
+        grid0.addWidget(self.multicolored_cb, 0, 2)
 
         ## Isolation Routing
-        self.isolation_routing_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.isolation_routing_label.set_markup("<b>Isolation Routing:</b>")
-        self.isolation_routing_label.set_tooltip_markup(
+        self.isolation_routing_label = QtGui.QLabel("<b>Isolation Routing:</b>")
+        self.isolation_routing_label.setToolTip(
             "Create a Geometry object with\n"
             "toolpaths to cut outside polygons."
         )
-        self.custom_box.pack_start(self.isolation_routing_label, expand=True, fill=False, padding=2)
+        self.custom_box.addWidget(self.isolation_routing_label)
 
-        grid = Gtk.Grid(column_spacing=3, row_spacing=2)
-        self.custom_box.pack_start(grid, expand=True, fill=False, padding=2)
-
-        l1 = Gtk.Label('Tool diam:', xalign=1)
-        l1.set_tooltip_markup(
+        grid1 = QtGui.QGridLayout()
+        self.custom_box.addLayout(grid1)
+        tdlabel = QtGui.QLabel('Tool dia:')
+        tdlabel.setToolTip(
             "Diameter of the cutting tool."
         )
-        grid.attach(l1, 0, 0, 1, 1)
+        grid1.addWidget(tdlabel, 0, 0)
         self.iso_tool_dia_entry = LengthEntry()
-        grid.attach(self.iso_tool_dia_entry, 1, 0, 1, 1)
+        grid1.addWidget(self.iso_tool_dia_entry, 0, 1)
 
-        l2 = Gtk.Label('Width (# passes):', xalign=1)
-        l2.set_tooltip_markup(
+        passlabel = QtGui.QLabel('Width (# passes):')
+        passlabel.setToolTip(
             "Width of the isolation gap in\n"
             "number (integer) of tool widths."
         )
-        grid.attach(l2, 0, 1, 1, 1)
+        grid1.addWidget(passlabel, 1, 0)
         self.iso_width_entry = IntEntry()
-        grid.attach(self.iso_width_entry, 1, 1, 1, 1)
+        grid1.addWidget(self.iso_width_entry, 1, 1)
 
-        l3 = Gtk.Label('Pass overlap:', xalign=1)
-        l3.set_tooltip_markup(
+        overlabel = QtGui.QLabel('Pass overlap:')
+        overlabel.setToolTip(
             "How much (fraction of tool width)\n"
             "to overlap each pass."
         )
-        grid.attach(l3, 0, 2, 1, 1)
+        grid1.addWidget(overlabel, 2, 0)
         self.iso_overlap_entry = FloatEntry()
-        grid.attach(self.iso_overlap_entry, 1, 2, 1, 1)
+        grid1.addWidget(self.iso_overlap_entry, 2, 1)
 
-        self.generate_iso_button = Gtk.Button(label='Generate Geometry')
-        self.generate_iso_button.set_tooltip_markup(
+        self.generate_iso_button = QtGui.QPushButton('Generate Geometry')
+        self.generate_iso_button.setToolTip(
             "Create the Geometry Object\n"
             "for isolation routing."
         )
-        self.custom_box.pack_start(self.generate_iso_button, expand=True, fill=False, padding=2)
+        self.custom_box.addWidget(self.generate_iso_button)
 
         ## Board cuttout
-        self.board_cutout_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.board_cutout_label.set_markup("<b>Board cutout:</b>")
-        self.board_cutout_label.set_tooltip_markup(
+        self.board_cutout_label = QtGui.QLabel("<b>Board cutout:</b>")
+        self.board_cutout_label.setToolTip(
             "Create toolpaths to cut around\n"
             "the PCB and separate it from\n"
             "the original board."
         )
-        self.custom_box.pack_start(self.board_cutout_label, expand=True, fill=False, padding=2)
-
-        grid2 = Gtk.Grid(column_spacing=3, row_spacing=2)
-        self.custom_box.pack_start(grid2, expand=True, fill=False, padding=2)
+        self.custom_box.addWidget(self.board_cutout_label)
 
-        l4 = Gtk.Label('Tool dia:', xalign=1)
-        l4.set_tooltip_markup(
+        grid2 = QtGui.QGridLayout()
+        self.custom_box.addLayout(grid2)
+        tdclabel = QtGui.QLabel('Tool dia:')
+        tdclabel.setToolTip(
             "Diameter of the cutting tool."
         )
-        grid2.attach(l4, 0, 0, 1, 1)
+        grid2.addWidget(tdclabel, 0, 0)
         self.cutout_tooldia_entry = LengthEntry()
-        grid2.attach(self.cutout_tooldia_entry, 1, 0, 1, 1)
+        grid2.addWidget(self.cutout_tooldia_entry, 0, 1)
 
-        l5 = Gtk.Label('Margin:', xalign=1)
-        l5.set_tooltip_markup(
+        marginlabel = QtGui.QLabel('Margin:')
+        marginlabel.setToolTip(
             "Distance from objects at which\n"
             "to draw the cutout."
         )
-        grid2.attach(l5, 0, 1, 1, 1)
+        grid2.addWidget(marginlabel, 1, 0)
         self.cutout_margin_entry = LengthEntry()
-        grid2.attach(self.cutout_margin_entry, 1, 1, 1, 1)
+        grid2.addWidget(self.cutout_margin_entry, 1, 1)
 
-        l6 = Gtk.Label('Gap size:', xalign=1)
-        l6.set_tooltip_markup(
+        gaplabel = QtGui.QLabel('Gap size:')
+        gaplabel.setToolTip(
             "Size of the gaps in the toolpath\n"
             "that will remain to hold the\n"
             "board in place."
         )
-        grid2.attach(l6, 0, 2, 1, 1)
+        grid2.addWidget(gaplabel, 2, 0)
         self.cutout_gap_entry = LengthEntry()
-        grid2.attach(self.cutout_gap_entry, 1, 2, 1, 1)
+        grid2.addWidget(self.cutout_gap_entry, 2, 1)
 
-        l7 = Gtk.Label('Gaps:', xalign=1)
-        l7.set_tooltip_markup(
+        gapslabel = QtGui.QLabel('Gaps:')
+        gapslabel.setToolTip(
             "Where to place the gaps, Top/Bottom\n"
             "Left/Rigt, or on all 4 sides."
         )
-        grid2.attach(l7, 0, 3, 1, 1)
+        grid2.addWidget(gapslabel, 3, 0)
         self.gaps_radio = RadioSet([{'label': '2 (T/B)', 'value': 'tb'},
                                     {'label': '2 (L/R)', 'value': 'lr'},
                                     {'label': '4', 'value': '4'}])
-        grid2.attach(self.gaps_radio, 1, 3, 1, 1)
+        grid2.addWidget(self.gaps_radio, 3, 1)
 
-        self.generate_cutout_button = Gtk.Button(label='Generate Geometry')
-        self.generate_cutout_button.set_tooltip_markup(
+        self.generate_cutout_button = QtGui.QPushButton('Generate Geometry')
+        self.generate_cutout_button.setToolTip(
             "Generate the geometry for\n"
             "the board cutout."
         )
-        self.custom_box.pack_start(self.generate_cutout_button, expand=True, fill=False, padding=2)
+        self.custom_box.addWidget(self.generate_cutout_button)
 
         ## Non-copper regions
-        self.noncopper_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.noncopper_label.set_markup("<b>Non-copper regions:</b>")
-        self.noncopper_label.set_tooltip_markup(
+        self.noncopper_label = QtGui.QLabel("<b>Non-copper regions:</b>")
+        self.noncopper_label.setToolTip(
             "Create polygons covering the\n"
             "areas without copper on the PCB.\n"
             "Equivalent to the inverse of this\n"
             "object. Can be used to remove all\n"
             "copper from a specified region."
         )
-        self.custom_box.pack_start(self.noncopper_label, expand=True, fill=False, padding=2)
+        self.custom_box.addWidget(self.noncopper_label)
 
-        grid3 = Gtk.Grid(column_spacing=3, row_spacing=2)
-        self.custom_box.pack_start(grid3, expand=True, fill=False, padding=2)
+        grid3 = QtGui.QGridLayout()
+        self.custom_box.addLayout(grid3)
 
-        l8 = Gtk.Label('Boundary margin:', xalign=1)
-        l8.set_tooltip_markup(
+        # Margin
+        bmlabel = QtGui.QLabel('Boundary Margin:')
+        bmlabel.setToolTip(
             "Specify the edge of the PCB\n"
             "by drawing a box around all\n"
             "objects with this minimum\n"
             "distance."
         )
-        grid3.attach(l8, 0, 0, 1, 1)
+        grid3.addWidget(bmlabel, 0, 0)
         self.noncopper_margin_entry = LengthEntry()
-        grid3.attach(self.noncopper_margin_entry, 1, 0, 1, 1)
+        grid3.addWidget(self.noncopper_margin_entry, 0, 1)
 
+        # Rounded corners
         self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners")
-        self.noncopper_rounded_cb.set_tooltip_markup(
-            "If the boundary of the board\n"
-            "is to have rounded corners\n"
-            "their radius is equal to the margin."
-        )
-        grid3.attach(self.noncopper_rounded_cb, 0, 1, 2, 1)
-
-        self.generate_noncopper_button = Gtk.Button(label='Generate Geometry')
-        self.generate_noncopper_button.set_tooltip_markup(
+        self.noncopper_rounded_cb.setToolTip(
             "Creates a Geometry objects with polygons\n"
             "covering the copper-free areas of the PCB."
         )
-        self.custom_box.pack_start(self.generate_noncopper_button, expand=True, fill=False, padding=2)
+        grid3.addWidget(self.noncopper_rounded_cb, 1, 0, 1, 2)
+
+        self.generate_noncopper_button = QtGui.QPushButton('Generate Geometry')
+        self.custom_box.addWidget(self.generate_noncopper_button)
 
         ## Bounding box
-        self.boundingbox_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
-        self.boundingbox_label.set_markup('<b>Bounding Box:</b>')
-        self.boundingbox_label.set_tooltip_markup(
-            "Create a Geometry object with a rectangle\n"
-            "enclosing all polygons at a given distance."
-        )
-        self.custom_box.pack_start(self.boundingbox_label, expand=True, fill=False, padding=2)
+        self.boundingbox_label = QtGui.QLabel('<b>Bounding Box:</b>')
+        self.custom_box.addWidget(self.boundingbox_label)
 
-        grid4 = Gtk.Grid(column_spacing=3, row_spacing=2)
-        self.custom_box.pack_start(grid4, expand=True, fill=False, padding=2)
+        grid4 = QtGui.QGridLayout()
+        self.custom_box.addLayout(grid4)
 
-        l9 = Gtk.Label('Boundary Margin:', xalign=1)
-        l9.set_tooltip_markup(
+        bbmargin = QtGui.QLabel('Boundary Margin:')
+        bbmargin.setToolTip(
             "Distance of the edges of the box\n"
             "to the nearest polygon."
         )
-        grid4.attach(l9, 0, 0, 1, 1)
+        grid4.addWidget(bbmargin, 0, 0)
         self.bbmargin_entry = LengthEntry()
-        grid4.attach(self.bbmargin_entry, 1, 0, 1, 1)
+        grid4.addWidget(self.bbmargin_entry, 0, 1)
 
         self.bbrounded_cb = FCCheckBox(label="Rounded corners")
-        self.bbrounded_cb.set_tooltip_markup(
+        self.bbrounded_cb.setToolTip(
             "If the bounding box is \n"
             "to have rounded corners\n"
             "their radius is equal to\n"
             "the margin."
         )
-        grid4.attach(self.bbrounded_cb, 0, 1, 2, 1)
+        grid4.addWidget(self.bbrounded_cb, 1, 0, 1, 2)
 
-        self.generate_bb_button = Gtk.Button(label='Generate Geometry')
-        self.generate_bb_button.set_tooltip_markup(
+        self.generate_bb_button = QtGui.QPushButton('Generate Geometry')
+        self.generate_bb_button.setToolTip(
             "Genrate the Geometry object."
         )
-        self.custom_box.pack_start(self.generate_bb_button, expand=True, fill=False, padding=2)
+        self.custom_box.addWidget(self.generate_bb_button)
+
+
+# def main():
+#
+#     app = QtGui.QApplication(sys.argv)
+#     fc = GerberObjectUI()
+#     sys.exit(app.exec_())
+#
+#
+# if __name__ == '__main__':
+#     main()

+ 44 - 23
PlotCanvas.py

@@ -1,10 +1,26 @@
-from gi.repository import Gdk
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://caram.cl/software/flatcam                         #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+from PyQt4 import QtGui, QtCore
 from matplotlib.figure import Figure
-from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
-#from FlatCAMApp import *
+from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
 import FlatCAMApp
 
 
+# class FCFigureCanvas(FigureCanvas):
+#
+#     resized = QtCore.pyqtSignal()
+#
+#     def resizeEvent(self, event):
+#         FigureCanvas.resizeEvent(self, event)
+#         self.emit(QtCore.SIGNAL("resized"))
+#         print self.geometry()
+
 class PlotCanvas:
     """
     Class handling the plotting area in the application.
@@ -38,18 +54,21 @@ class PlotCanvas:
 
         # The canvas is the top level container (Gtk.DrawingArea)
         self.canvas = FigureCanvas(self.figure)
-        self.canvas.set_hexpand(1)
-        self.canvas.set_vexpand(1)
-        self.canvas.set_can_focus(True)  # For key press
+        #self.canvas.set_hexpand(1)
+        #self.canvas.set_vexpand(1)
+        #self.canvas.set_can_focus(True)  # For key press
 
         # Attach to parent
-        self.container.attach(self.canvas, 0, 0, 600, 400)  # TODO: Height and width are num. columns??
+        #self.container.attach(self.canvas, 0, 0, 600, 400)  # TODO: Height and width are num. columns??
+        self.container.addWidget(self.canvas)  # Qt
 
         # Events
         self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
-        self.canvas.connect('configure-event', self.auto_adjust_axes)
-        self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
-        self.canvas.connect("scroll-event", self.on_scroll)
+        #self.canvas.connect('configure-event', self.auto_adjust_axes)
+        self.canvas.mpl_connect('resize_event', self.auto_adjust_axes)
+        #self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
+        #self.canvas.connect("scroll-event", self.on_scroll)
+        self.canvas.mpl_connect('scroll_event', self.on_scroll)
         self.canvas.mpl_connect('key_press_event', self.on_key_down)
         self.canvas.mpl_connect('key_release_event', self.on_key_up)
 
@@ -62,6 +81,7 @@ class PlotCanvas:
         :param event:
         :return:
         """
+        FlatCAMApp.App.log.debug('on_key_down(): ' + str(event.key))
         self.key = event.key
 
     def on_key_up(self, event):
@@ -125,7 +145,7 @@ class PlotCanvas:
         self.axes.grid(True)
 
         # Re-draw
-        self.canvas.queue_draw()
+        self.canvas.draw()
 
     def adjust_axes(self, xmin, ymin, xmax, ymax):
         """
@@ -144,7 +164,7 @@ class PlotCanvas:
         :return: None
         """
 
-        FlatCAMApp.App.log.debug("PC.adjust_axes()")
+        # FlatCAMApp.App.log.debug("PC.adjust_axes()")
 
         width = xmax - xmin
         height = ymax - ymin
@@ -182,7 +202,7 @@ class PlotCanvas:
             ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
 
         # Re-draw
-        self.canvas.queue_draw()
+        self.canvas.draw()
 
     def auto_adjust_axes(self, *args):
         """
@@ -234,7 +254,7 @@ class PlotCanvas:
             ax.set_ylim((ymin, ymax))
 
         # Re-draw
-        self.canvas.queue_draw()
+        self.canvas.draw()
 
     def pan(self, x, y):
         xmin, xmax = self.axes.get_xlim()
@@ -248,7 +268,7 @@ class PlotCanvas:
             ax.set_ylim((ymin + y*height, ymax + y*height))
 
         # Re-draw
-        self.canvas.queue_draw()
+        self.canvas.draw()
 
     def new_axes(self, name):
         """
@@ -261,24 +281,24 @@ class PlotCanvas:
 
         return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
 
-    def on_scroll(self, canvas, event):
+    def on_scroll(self, event):
         """
         Scroll event handler.
 
-        :param canvas: The widget generating the event. Ignored.
         :param event: Event object containing the event information.
         :return: None
         """
 
         # So it can receive key presses
-        self.canvas.grab_focus()
+        # self.canvas.grab_focus()
+        self.canvas.setFocus()
 
         # Event info
-        z, direction = event.get_scroll_direction()
+        # z, direction = event.get_scroll_direction()
 
         if self.key is None:
 
-            if direction is Gdk.ScrollDirection.UP:
+            if event.button == 'up':
                 self.zoom(1.5, self.mouse)
             else:
                 self.zoom(1/1.5, self.mouse)
@@ -286,15 +306,15 @@ class PlotCanvas:
 
         if self.key == 'shift':
 
-            if direction is Gdk.ScrollDirection.UP:
+            if event.button == 'up':
                 self.pan(0.3, 0)
             else:
                 self.pan(-0.3, 0)
             return
 
-        if self.key == 'ctrl+control':
+        if self.key == 'control':
 
-            if direction is Gdk.ScrollDirection.UP:
+            if event.button == 'up':
                 self.pan(0, 0.3)
             else:
                 self.pan(0, -0.3)
@@ -308,3 +328,4 @@ class PlotCanvas:
         :return: None
         """
         self.mouse = [event.xdata, event.ydata]
+

+ 1 - 1
defaults.json

@@ -1 +1 @@
-{"gerber_noncopperrounded": false, "geometry_paintoverlap": 0.15, "geometry_plot": true, "excellon_feedrate": 5.0, "gerber_plot": true, "excellon_drillz": -0.1, "geometry_feedrate": 3.0, "units": "IN", "excellon_travelz": 0.1, "gerber_multicolored": false, "gerber_solid": true, "gerber_isopasses": 1, "excellon_plot": true, "gerber_isotooldia": 0.016, "gerber_bboxmargin": 0.0, "cncjob_tooldia": 0.016, "geometry_travelz": 0.1, "gerber_cutoutmargin": 0.2, "excellon_solid": false, "geometry_paintmargin": 0.01, "geometry_cutz": -0.002, "geometry_cnctooldia": 0.016, "gerber_cutouttooldia": 0.07, "gerber_gaps": "4", "geometry_painttooldia": 0.0625, "cncjob_plot": true, "gerber_cutoutgapsize": 0.15, "gerber_isooverlap": 0.15, "gerber_bboxrounded": false, "gerber_noncoppermargin": 0.0}
+{"gerber_cutoutgapsize": 0.15, "gerber_noncopperrounded": false, "geometry_paintoverlap": 0.15, "excellon_plot": true, "gerber_isotooldia": 0.016, "gerber_plot": true, "excellon_drillz": -0.1, "geometry_feedrate": 3.0, "units": "IN", "excellon_travelz": 0.1, "gerber_multicolored": false, "gerber_solid": true, "gerber_isopasses": 1, "cncjob_append": "", "excellon_feedrate": 3.0, "cncjob_tooldia": 0.016, "geometry_travelz": 0.1, "gerber_cutoutmargin": 0.1, "excellon_solid": true, "geometry_paintmargin": 0.0, "geometry_cutz": -0.002, "gerber_noncoppermargin": 0.0, "gerber_cutouttooldia": 0.07, "gerber_gaps": "4", "gerber_bboxmargin": 0.0, "cncjob_plot": true, "geometry_plot": true, "gerber_isooverlap": 0.15, "gerber_bboxrounded": false, "geometry_cnctooldia": 0.016, "geometry_painttooldia": 0.07}

+ 1 - 1
recent.json

@@ -1 +1 @@
-[{"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\bedini 7 coils capacitor discharge.gbr"}, {"kind": "cncjob", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\test.cnc"}, {"kind": "project", "filename": "C:\\Users\\jpcaram\\Dropbox\\VNA\\KiCad_Bridge2\\Bridge2_test.fcproj"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\Controller_Board_Shield\\CBS-B_Cu.gbl"}, {"kind": "cncjob", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\squarerpcb\\KiCad_Squarer.gcode"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\VNA\\KiCad_Squarer\\KiCad_Squarer.drl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\VNA\\KiCad_Squarer\\KiCad_Squarer-F_Cu.gtl"}, {"kind": "project", "filename": "C:\\Users\\jpcaram\\Dropbox\\VNA\\KiCad_Bridge2\\Bridge2.fcproj"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\Kenney\\Project Outputs for AnalogPredistortion1\\apd.TXT"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\PhD\\Kenney\\Project Outputs for AnalogPredistortion1\\apd.GTL"}]
+[{"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/FlatCam_Drilling_Test/FlatCam_Drilling_Test.drl"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Excellon_Planck/X-Y CONTROLLER - Drill Data - Through Hole.drl"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/7V-PSU/7V PSU.GTL"}, {"kind": "cncjob", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/output.gcode"}, {"kind": "project", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/project_copy.fcproj"}, {"kind": "project", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/a_project.fcproj"}, {"kind": "project", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/FlatCAM_TestProject.fcproj"}, {"kind": "cncjob", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/FlatCAM_TestGCode.gcode"}, {"kind": "cncjob", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/CBS-B_Cu_ISOLATION_GCODE.ngc"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/FlatCAM_TestDrill.drl"}]

+ 0 - 783
tests/KiCad_Squarer-F_Cu.gtl

@@ -1,783 +0,0 @@
-G04 (created by PCBNEW (2013-07-07 BZR 4022)-stable) date 11/20/2013 5:37:08 PM*
-%MOIN*%
-G04 Gerber Fmt 3.4, Leading zero omitted, Abs format*
-%FSLAX34Y34*%
-G01*
-G70*
-G90*
-G04 APERTURE LIST*
-%ADD10C,0.00590551*%
-%ADD11C,0.015*%
-%ADD12R,0.0393X0.0629*%
-%ADD13R,0.2125X0.244*%
-%ADD14R,0.02X0.03*%
-%ADD15R,0.144X0.08*%
-%ADD16R,0.04X0.08*%
-%ADD17R,0.0394X0.0354*%
-%ADD18R,0.0354X0.0394*%
-%ADD19R,0.063X0.071*%
-%ADD20R,0.071X0.063*%
-%ADD21C,0.1*%
-%ADD22R,0.08X0.08*%
-%ADD23C,0.08*%
-%ADD24C,0.04*%
-%ADD25C,0.02*%
-G04 APERTURE END LIST*
-G54D10*
-G54D11*
-X27233Y12376D02*
-X27728Y12376D01*
-X27461Y12071D01*
-X27576Y12071D01*
-X27652Y12033D01*
-X27690Y11995D01*
-X27728Y11919D01*
-X27728Y11728D01*
-X27690Y11652D01*
-X27652Y11614D01*
-X27576Y11576D01*
-X27347Y11576D01*
-X27271Y11614D01*
-X27233Y11652D01*
-X24021Y12300D02*
-X24059Y12338D01*
-X24135Y12376D01*
-X24326Y12376D01*
-X24402Y12338D01*
-X24440Y12300D01*
-X24478Y12223D01*
-X24478Y12147D01*
-X24440Y12033D01*
-X23983Y11576D01*
-X24478Y11576D01*
-X4578Y11701D02*
-X4121Y11701D01*
-X4350Y11701D02*
-X4350Y12501D01*
-X4273Y12386D01*
-X4197Y12310D01*
-X4121Y12272D01*
-X569Y1526D02*
-X1064Y1526D01*
-X797Y1221D01*
-X911Y1221D01*
-X988Y1183D01*
-X1026Y1145D01*
-X1064Y1069D01*
-X1064Y878D01*
-X1026Y802D01*
-X988Y764D01*
-X911Y726D01*
-X683Y726D01*
-X607Y764D01*
-X569Y802D01*
-X1407Y802D02*
-X1445Y764D01*
-X1407Y726D01*
-X1369Y764D01*
-X1407Y802D01*
-X1407Y726D01*
-X1711Y1526D02*
-X2207Y1526D01*
-X1940Y1221D01*
-X2054Y1221D01*
-X2130Y1183D01*
-X2169Y1145D01*
-X2207Y1069D01*
-X2207Y878D01*
-X2169Y802D01*
-X2130Y764D01*
-X2054Y726D01*
-X1826Y726D01*
-X1750Y764D01*
-X1711Y802D01*
-X2435Y1526D02*
-X2702Y726D01*
-X2969Y1526D01*
-X1672Y14551D02*
-X1291Y14551D01*
-X1253Y14170D01*
-X1291Y14208D01*
-X1367Y14246D01*
-X1558Y14246D01*
-X1634Y14208D01*
-X1672Y14170D01*
-X1710Y14094D01*
-X1710Y13903D01*
-X1672Y13827D01*
-X1634Y13789D01*
-X1558Y13751D01*
-X1367Y13751D01*
-X1291Y13789D01*
-X1253Y13827D01*
-X1939Y14551D02*
-X2205Y13751D01*
-X2472Y14551D01*
-X26525Y14651D02*
-X26677Y14651D01*
-X26753Y14613D01*
-X26829Y14536D01*
-X26867Y14384D01*
-X26867Y14117D01*
-X26829Y13965D01*
-X26753Y13889D01*
-X26677Y13851D01*
-X26525Y13851D01*
-X26448Y13889D01*
-X26372Y13965D01*
-X26334Y14117D01*
-X26334Y14384D01*
-X26372Y14536D01*
-X26448Y14613D01*
-X26525Y14651D01*
-X27210Y14651D02*
-X27210Y14003D01*
-X27248Y13927D01*
-X27286Y13889D01*
-X27363Y13851D01*
-X27515Y13851D01*
-X27591Y13889D01*
-X27629Y13927D01*
-X27667Y14003D01*
-X27667Y14651D01*
-X27934Y14651D02*
-X28391Y14651D01*
-X28163Y13851D02*
-X28163Y14651D01*
-X17800Y13451D02*
-X17952Y13451D01*
-X18028Y13413D01*
-X18104Y13336D01*
-X18142Y13184D01*
-X18142Y12917D01*
-X18104Y12765D01*
-X18028Y12689D01*
-X17952Y12651D01*
-X17800Y12651D01*
-X17723Y12689D01*
-X17647Y12765D01*
-X17609Y12917D01*
-X17609Y13184D01*
-X17647Y13336D01*
-X17723Y13413D01*
-X17800Y13451D01*
-X18485Y13451D02*
-X18485Y12803D01*
-X18523Y12727D01*
-X18561Y12689D01*
-X18638Y12651D01*
-X18790Y12651D01*
-X18866Y12689D01*
-X18904Y12727D01*
-X18942Y12803D01*
-X18942Y13451D01*
-X19209Y13451D02*
-X19666Y13451D01*
-X19438Y12651D02*
-X19438Y13451D01*
-X8900Y13326D02*
-X9052Y13326D01*
-X9128Y13288D01*
-X9204Y13211D01*
-X9242Y13059D01*
-X9242Y12792D01*
-X9204Y12640D01*
-X9128Y12564D01*
-X9052Y12526D01*
-X8900Y12526D01*
-X8823Y12564D01*
-X8747Y12640D01*
-X8709Y12792D01*
-X8709Y13059D01*
-X8747Y13211D01*
-X8823Y13288D01*
-X8900Y13326D01*
-X9585Y13326D02*
-X9585Y12678D01*
-X9623Y12602D01*
-X9661Y12564D01*
-X9738Y12526D01*
-X9890Y12526D01*
-X9966Y12564D01*
-X10004Y12602D01*
-X10042Y12678D01*
-X10042Y13326D01*
-X10309Y13326D02*
-X10766Y13326D01*
-X10538Y12526D02*
-X10538Y13326D01*
-X28880Y726D02*
-X28880Y1526D01*
-X29261Y726D02*
-X29261Y1526D01*
-X29719Y726D01*
-X29719Y1526D01*
-X23705Y2126D02*
-X23705Y2926D01*
-X24086Y2126D02*
-X24086Y2926D01*
-X24544Y2126D01*
-X24544Y2926D01*
-X3355Y3126D02*
-X3355Y3926D01*
-X3736Y3126D02*
-X3736Y3926D01*
-X4194Y3126D01*
-X4194Y3926D01*
-G54D12*
-X12278Y8961D03*
-X14072Y8961D03*
-G54D13*
-X13175Y11834D03*
-G54D14*
-X5850Y7450D03*
-X6600Y7450D03*
-X5850Y8450D03*
-X6225Y7450D03*
-X6600Y8450D03*
-X21075Y7400D03*
-X21825Y7400D03*
-X21075Y8400D03*
-X21450Y7400D03*
-X21825Y8400D03*
-G54D15*
-X15962Y2837D03*
-G54D16*
-X15962Y5437D03*
-X15062Y5437D03*
-X16862Y5437D03*
-G54D17*
-X23150Y6429D03*
-X23150Y7021D03*
-G54D18*
-X20546Y5800D03*
-X19954Y5800D03*
-G54D17*
-X22825Y7829D03*
-X22825Y8421D03*
-X21825Y9404D03*
-X21825Y9996D03*
-G54D18*
-X19579Y9325D03*
-X20171Y9325D03*
-X4454Y9325D03*
-X5046Y9325D03*
-G54D17*
-X6600Y9629D03*
-X6600Y10221D03*
-X7600Y8004D03*
-X7600Y8596D03*
-X8000Y6429D03*
-X8000Y7021D03*
-G54D18*
-X5171Y5850D03*
-X4579Y5850D03*
-G54D19*
-X12650Y7900D03*
-X13750Y7900D03*
-G54D20*
-X11025Y9750D03*
-X11025Y10850D03*
-X13737Y4387D03*
-X13737Y5487D03*
-G54D19*
-X16962Y6787D03*
-X15862Y6787D03*
-G54D17*
-X20175Y7804D03*
-X20175Y8396D03*
-X6225Y5854D03*
-X6225Y6446D03*
-G54D18*
-X16608Y7712D03*
-X16016Y7712D03*
-X23421Y9375D03*
-X22829Y9375D03*
-X24296Y7850D03*
-X23704Y7850D03*
-G54D17*
-X21450Y5804D03*
-X21450Y6396D03*
-G54D18*
-X9146Y8000D03*
-X8554Y8000D03*
-X8196Y9625D03*
-X7604Y9625D03*
-G54D17*
-X10100Y10104D03*
-X10100Y10696D03*
-X5050Y7854D03*
-X5050Y8446D03*
-G54D14*
-X28950Y6850D03*
-X29700Y6850D03*
-X28950Y7850D03*
-X29325Y6850D03*
-X29700Y7850D03*
-G54D21*
-X21825Y11925D03*
-X22825Y12925D03*
-X20825Y12925D03*
-X20825Y10925D03*
-X22825Y10925D03*
-X21450Y3475D03*
-X22450Y4475D03*
-X20450Y4475D03*
-X20450Y2475D03*
-X22450Y2475D03*
-X29325Y3475D03*
-X30325Y4475D03*
-X28325Y4475D03*
-X28325Y2475D03*
-X30325Y2475D03*
-X29700Y11925D03*
-X30700Y12925D03*
-X28700Y12925D03*
-X28700Y10925D03*
-X30700Y10925D03*
-G54D17*
-X27425Y8571D03*
-X27425Y7979D03*
-G54D22*
-X1375Y8325D03*
-G54D23*
-X1375Y9325D03*
-X1375Y10325D03*
-G54D22*
-X16375Y9625D03*
-G54D23*
-X16375Y10625D03*
-X16375Y11625D03*
-G54D22*
-X26375Y8425D03*
-G54D23*
-X26375Y9425D03*
-X26375Y10425D03*
-G54D22*
-X10225Y2475D03*
-G54D23*
-X11225Y2475D03*
-G54D22*
-X28375Y8850D03*
-G54D21*
-X10400Y6275D03*
-X25125Y5450D03*
-X6600Y11925D03*
-X7600Y12925D03*
-X5600Y12925D03*
-X5600Y10925D03*
-X7600Y10925D03*
-X6225Y3475D03*
-X7225Y4475D03*
-X5225Y4475D03*
-X5225Y2475D03*
-X7225Y2475D03*
-G54D17*
-X28225Y7254D03*
-X28225Y7846D03*
-G54D23*
-X12375Y2475D03*
-X30075Y6475D03*
-X28225Y6775D03*
-X20350Y7075D03*
-X22300Y6950D03*
-X7100Y6900D03*
-X19475Y5175D03*
-X24325Y9375D03*
-X25175Y7850D03*
-X17525Y7600D03*
-X13625Y3325D03*
-X11025Y8900D03*
-X10000Y8000D03*
-X8875Y9625D03*
-X4950Y7275D03*
-X3825Y5850D03*
-G54D24*
-X15962Y2837D02*
-X15962Y1162D01*
-X15962Y1162D02*
-X15975Y1150D01*
-X16375Y9625D02*
-X17450Y9625D01*
-X18650Y8425D02*
-X18650Y1150D01*
-X17450Y9625D02*
-X18650Y8425D01*
-X26375Y8425D02*
-X26375Y7775D01*
-X26350Y7800D02*
-X26375Y7775D01*
-X26375Y7775D02*
-X26975Y7175D01*
-X25700Y1150D02*
-X18650Y1150D01*
-X18650Y1150D02*
-X16925Y1150D01*
-X15975Y1150D02*
-X16925Y1150D01*
-X26975Y2425D02*
-X25700Y1150D01*
-X26975Y7175D02*
-X26975Y2425D01*
-X15962Y2837D02*
-X15962Y5437D01*
-X13175Y1150D02*
-X13550Y1150D01*
-X1375Y8325D02*
-X1375Y3774D01*
-X4000Y1150D02*
-X13175Y1150D01*
-X1375Y3774D02*
-X4000Y1150D01*
-X13550Y1150D02*
-X15975Y1150D01*
-G54D25*
-X16016Y7712D02*
-X15862Y7558D01*
-X15862Y7558D02*
-X15862Y6787D01*
-G54D24*
-X15962Y5437D02*
-X15962Y6112D01*
-X15862Y6212D02*
-X15862Y6787D01*
-X15962Y6112D02*
-X15862Y6212D01*
-X13175Y11834D02*
-X13175Y14200D01*
-X13150Y14225D02*
-X13150Y14200D01*
-X13175Y14200D02*
-X13150Y14225D01*
-X16375Y11625D02*
-X16400Y11650D01*
-X16400Y14175D02*
-X16400Y14200D01*
-X16400Y11650D02*
-X16400Y14175D01*
-X26375Y10425D02*
-X26375Y12750D01*
-X9775Y14200D02*
-X13150Y14200D01*
-X1375Y11475D02*
-X4100Y14200D01*
-X4100Y14200D02*
-X9775Y14200D01*
-X1375Y10325D02*
-X1375Y11475D01*
-X13150Y14200D02*
-X15025Y14200D01*
-X26375Y12750D02*
-X24925Y14200D01*
-X24925Y14200D02*
-X16400Y14200D01*
-X16400Y14200D02*
-X14825Y14200D01*
-X11025Y10850D02*
-X12191Y10850D01*
-X12191Y10850D02*
-X13175Y11834D01*
-G54D25*
-X10100Y10696D02*
-X10871Y10696D01*
-X10871Y10696D02*
-X11025Y10850D01*
-X6225Y3399D02*
-X6225Y5854D01*
-X6225Y5854D02*
-X5175Y5854D01*
-X5175Y5854D02*
-X5171Y5850D01*
-X21450Y3799D02*
-X21450Y5804D01*
-X20546Y5800D02*
-X21446Y5800D01*
-X21446Y5800D02*
-X21450Y5804D01*
-X6600Y10221D02*
-X6600Y11575D01*
-X21825Y11525D02*
-X21825Y9996D01*
-X8000Y7021D02*
-X8000Y7200D01*
-X7600Y7600D02*
-X7600Y8004D01*
-X8000Y7200D02*
-X7600Y7600D01*
-X7600Y8004D02*
-X8550Y8004D01*
-X8550Y8004D02*
-X8554Y8000D01*
-X10400Y6275D02*
-X9875Y6275D01*
-X9721Y6429D02*
-X9875Y6275D01*
-X9721Y6429D02*
-X8000Y6429D01*
-X6225Y6446D02*
-X6554Y6446D01*
-X7579Y6429D02*
-X8000Y6429D01*
-X7375Y6225D02*
-X7579Y6429D01*
-X6775Y6225D02*
-X7375Y6225D01*
-X6554Y6446D02*
-X6775Y6225D01*
-X8000Y6429D02*
-X8000Y6275D01*
-X8000Y6275D02*
-X8000Y6429D01*
-X6225Y6446D02*
-X6225Y7450D01*
-X6600Y9629D02*
-X6600Y8450D01*
-X7604Y9625D02*
-X6604Y9625D01*
-X6604Y9625D02*
-X6600Y9629D01*
-X7600Y8596D02*
-X7600Y9621D01*
-X7600Y9621D02*
-X7604Y9625D01*
-X23150Y7021D02*
-X23150Y7275D01*
-X22825Y7600D02*
-X22825Y7829D01*
-X23150Y7275D02*
-X22825Y7600D01*
-X22825Y7829D02*
-X23683Y7829D01*
-X23683Y7829D02*
-X23704Y7850D01*
-X24725Y6050D02*
-X25125Y5650D01*
-X24346Y6429D02*
-X24725Y6050D01*
-X23150Y6429D02*
-X24346Y6429D01*
-X25125Y5650D02*
-X25125Y5450D01*
-X21450Y6396D02*
-X21679Y6396D01*
-X23150Y6425D02*
-X23150Y6429D01*
-X22775Y6425D02*
-X23150Y6425D01*
-X22600Y6250D02*
-X22775Y6425D01*
-X21825Y6250D02*
-X22600Y6250D01*
-X21679Y6396D02*
-X21825Y6250D01*
-X21450Y7400D02*
-X21450Y6396D01*
-X21825Y8400D02*
-X21825Y9404D01*
-X21825Y9404D02*
-X21854Y9375D01*
-X21854Y9375D02*
-X22829Y9375D01*
-X22829Y9375D02*
-X22825Y9371D01*
-X22825Y9371D02*
-X22825Y8421D01*
-G54D24*
-X1375Y9325D02*
-X4454Y9325D01*
-X5050Y8725D02*
-X5050Y9321D01*
-X5050Y9321D02*
-X5046Y9325D01*
-X5050Y8446D02*
-X5050Y8725D01*
-X5850Y8725D02*
-X5850Y8450D01*
-X5050Y8725D02*
-X5850Y8725D01*
-X19579Y9325D02*
-X19400Y9325D01*
-X18100Y10625D02*
-X19400Y9325D01*
-X18100Y10625D02*
-X16375Y10625D01*
-X20171Y9325D02*
-X20171Y8400D01*
-X20171Y8400D02*
-X21075Y8400D01*
-X26375Y9425D02*
-X27050Y9425D01*
-X27425Y8625D02*
-X27425Y8571D01*
-X27425Y9050D02*
-X27425Y8625D01*
-X27050Y9425D02*
-X27425Y9050D01*
-G54D25*
-X28375Y8850D02*
-X28375Y7925D01*
-X28375Y7925D02*
-X28225Y7846D01*
-G54D24*
-X28225Y7846D02*
-X28921Y7846D01*
-X28925Y7850D02*
-X28950Y7850D01*
-X28921Y7846D02*
-X28925Y7850D01*
-X28225Y7846D02*
-X27454Y7846D01*
-X27425Y7875D02*
-X27425Y7979D01*
-X27454Y7846D02*
-X27425Y7875D01*
-X10225Y2475D02*
-X10225Y2950D01*
-X12175Y5550D02*
-X12162Y5550D01*
-X12175Y4900D02*
-X12175Y5550D01*
-X10225Y2950D02*
-X12175Y4900D01*
-X13750Y7900D02*
-X14072Y8222D01*
-X12162Y5487D02*
-X12162Y5550D01*
-X12162Y5550D02*
-X12162Y6362D01*
-X14072Y7147D02*
-X14072Y8961D01*
-X13650Y6725D02*
-X14072Y7147D01*
-X12525Y6725D02*
-X13650Y6725D01*
-X12162Y6362D02*
-X12525Y6725D01*
-X12162Y5487D02*
-X13737Y5487D01*
-X15062Y5437D02*
-X13787Y5437D01*
-X13787Y5437D02*
-X13737Y5487D01*
-X14072Y8222D02*
-X14072Y8961D01*
-X11225Y2475D02*
-X12375Y2475D01*
-G54D25*
-X29700Y6850D02*
-X29800Y6750D01*
-X29800Y6750D02*
-X29700Y6850D01*
-G54D24*
-X29825Y6725D02*
-X30100Y6450D01*
-X28225Y6800D02*
-X28800Y6800D01*
-X28850Y6800D02*
-X28825Y6775D01*
-X28800Y6800D02*
-X28850Y6800D01*
-X28225Y7254D02*
-X28225Y6800D01*
-X28225Y6800D02*
-X28225Y6800D01*
-X28225Y6800D02*
-X28225Y6775D01*
-G54D25*
-X21075Y7400D02*
-X20900Y7400D01*
-G54D24*
-X20900Y7400D02*
-X20625Y7400D01*
-G54D25*
-X5850Y7450D02*
-X5725Y7450D01*
-G54D24*
-X5050Y7475D02*
-X5075Y7450D01*
-X5075Y7450D02*
-X5725Y7450D01*
-X5050Y7475D02*
-X5050Y7854D01*
-X20625Y7400D02*
-X20350Y7125D01*
-X20350Y7125D02*
-X20350Y7075D01*
-G54D25*
-X21825Y7400D02*
-X21850Y7400D01*
-X21850Y7400D02*
-X22300Y6950D01*
-X6600Y7450D02*
-X6625Y7450D01*
-X7125Y6925D02*
-X7100Y6900D01*
-X7150Y6925D02*
-X7125Y6925D01*
-X6625Y7450D02*
-X7150Y6925D01*
-G54D24*
-X19954Y5800D02*
-X19479Y5179D01*
-X19479Y5179D02*
-X19475Y5175D01*
-X23421Y9375D02*
-X24325Y9375D01*
-X24296Y7850D02*
-X25175Y7850D01*
-X16962Y6787D02*
-X16962Y7037D01*
-X16962Y7037D02*
-X17525Y7600D01*
-X13737Y4387D02*
-X13737Y3437D01*
-X13737Y3437D02*
-X13625Y3325D01*
-X11025Y9750D02*
-X11025Y8900D01*
-X9146Y8000D02*
-X10000Y8000D01*
-X8196Y9625D02*
-X8875Y9625D01*
-X5050Y7854D02*
-X5050Y7375D01*
-X5050Y7375D02*
-X4950Y7275D01*
-X4579Y5850D02*
-X3825Y5850D01*
-G54D25*
-X16608Y7712D02*
-X16962Y7358D01*
-X16962Y7358D02*
-X16962Y6787D01*
-G54D24*
-X16862Y5437D02*
-X16962Y5537D01*
-X16962Y5537D02*
-X16962Y6787D01*
-X12278Y8961D02*
-X12278Y8272D01*
-X12278Y8272D02*
-X12650Y7900D01*
-X11025Y9750D02*
-X11489Y9750D01*
-X11489Y9750D02*
-X12278Y8961D01*
-G54D25*
-X10100Y10104D02*
-X10454Y9750D01*
-X10454Y9750D02*
-X11025Y9750D01*
-G54D24*
-X20175Y7804D02*
-X20175Y7750D01*
-X20525Y7400D02*
-X20625Y7400D01*
-X20175Y7750D02*
-X20525Y7400D01*
-G54D25*
-X29325Y6850D02*
-X29325Y3475D01*
-X29700Y11925D02*
-X29700Y7850D01*
-M02*

+ 0 - 77
tests/KiCad_Squarer.drl

@@ -1,77 +0,0 @@
-M48
-;DRILL file {Pcbnew (2013-07-07 BZR 4022)-stable} date 11/20/2013 1:52:19 PM
-;FORMAT={2:4/ absolute / inch / keep zeros}
-FMAT,2
-INCH,TZ
-T1C0.030
-T2C0.040
-T3C0.060
-%
-G90
-G05
-M72
-T1
-X010400Y006275
-X025125Y005450
-T2
-X001375Y010325
-X001375Y009325
-X001375Y008325
-X003825Y005850
-X004950Y007275
-X007100Y006900
-X008875Y009625
-X010000Y008000
-X010225Y002475
-X011025Y008900
-X011225Y002475
-X012375Y002475
-X013625Y003325
-X016375Y011625
-X016375Y010625
-X016375Y009625
-X017525Y007600
-X019475Y005175
-X020350Y007075
-X022300Y006950
-X024325Y009375
-X025175Y007850
-X026375Y010425
-X026375Y009425
-X026375Y008425
-X028225Y006775
-X028375Y008850
-X030075Y006475
-T3
-X005225Y004475
-X005225Y002475
-X005600Y012925
-X005600Y010925
-X006225Y003475
-X006600Y011925
-X007225Y004475
-X007225Y002475
-X007600Y012925
-X007600Y010925
-X020450Y004475
-X020450Y002475
-X020825Y012925
-X020825Y010925
-X021450Y003475
-X021825Y011925
-X022450Y004475
-X022450Y002475
-X022825Y012925
-X022825Y010925
-X028325Y004475
-X028325Y002475
-X028700Y012925
-X028700Y010925
-X029325Y003475
-X029700Y011925
-X030325Y004475
-X030325Y002475
-X030700Y012925
-X030700Y010925
-T0
-M30

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