Kaynağa Gözat

Convertion to Qt. Major refactoring.

Juan Pablo Caram 11 yıl önce
ebeveyn
işleme
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_())

Dosya farkı çok büyük olduğundan ihmal edildi
+ 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 FlatCAMApp
+import inspect  # TODO: For debugging only.
 from camlib import *
 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              ##
 ##            FlatCAMObj              ##
 ########################################
 ########################################
-class FlatCAMObj(GObject.GObject, object):
+class FlatCAMObj(QtCore.QObject):
     """
     """
     Base type of objects handled in FlatCAM. These become interactive
     Base type of objects handled in FlatCAM. These become interactive
     in the GUI, can be plotted, and their options can be modified
     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.
     # The app should set this value.
     app = None
     app = None
 
 
-    def __init__(self, name, ui):
+    def __init__(self, name):
         """
         """
 
 
         :param name: Name of the object given by the user.
         :param name: Name of the object given by the user.
@@ -79,53 +28,60 @@ class FlatCAMObj(GObject.GObject, object):
         :type ui: ObjectUI
         :type ui: ObjectUI
         :return: FlatCAMObj
         :return: FlatCAMObj
         """
         """
-        GObject.GObject.__init__(self)
+        QtCore.QObject.__init__(self)
 
 
         # View
         # View
-        self.ui = ui
+        self.ui = None
 
 
         self.options = LoudDict(name=name)
         self.options = LoudDict(name=name)
         self.options.set_change_callback(self.on_options_change)
         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.axes = None  # Matplotlib axes
         self.kind = None  # Override with proper name
         self.kind = None  # Override with proper name
 
 
         self.muted_ui = False
         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):
     def __str__(self):
         return "<FlatCAMObj({:12s}): {:20s}>".format(self.kind, self.options["name"])
         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"])
         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))
         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()
         self.read_form()
         vect = self.ui.offsetvector_entry.get_value()
         vect = self.ui.offsetvector_entry.get_value()
         self.offset(vect)
         self.offset(vect)
         self.plot()
         self.plot()
 
 
-    def on_scale_button_click(self, *args):
+    def on_scale_button_click(self):
         self.read_form()
         self.read_form()
         factor = self.ui.scale_entry.get_value()
         factor = self.ui.scale_entry.get_value()
         self.scale(factor)
         self.scale(factor)
         self.plot()
         self.plot()
 
 
-    def on_options_change(self, key):
-        self.form_fields[key].set_value(self.options[key])
-        return
-
     def setup_axes(self, figure):
     def setup_axes(self, figure):
         """
         """
         1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
         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
         self.muted_ui = True
         FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.build_ui()")
         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
         # 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
         # Put in the UI
         # box_selected.pack_start(sw, True, True, 0)
         # 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()
         self.to_form()
-        GLib.idle_add(box_selected.show_all)
-        GLib.idle_add(self.ui.show_all)
+
         self.muted_ui = False
         self.muted_ui = False
 
 
     def set_form_item(self, option):
     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.
         :return: Whether to continue plotting or not depending on the "plot" option.
         :rtype: bool
         :rtype: bool
         """
         """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.plot()")
 
 
         # Axes must exist and be attached to canvas.
         # Axes must exist and be attached to canvas.
         if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes:
         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
             return False
 
 
         # Clear axes or we will plot on top of them.
         # 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)
         # GLib.idle_add(self.axes.cla)
         return True
         return True
 
 
@@ -284,29 +244,14 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
     Represents Gerber code.
     Represents Gerber code.
     """
     """
 
 
+    ui_type = GerberObjectUI
+
     def __init__(self, name):
     def __init__(self, name):
         Gerber.__init__(self)
         Gerber.__init__(self)
-        FlatCAMObj.__init__(self, name, GerberObjectUI())
+        FlatCAMObj.__init__(self, name)
 
 
         self.kind = "gerber"
         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
         # The 'name' is already in self.options from FlatCAMObj
         # Automatically updates the UI
         # Automatically updates the UI
         self.options.update({
         self.options.update({
@@ -331,21 +276,45 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         # from predecessors.
         # from predecessors.
         self.ser_attrs += ['options', 'kind']
         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)
         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):
     def on_generatenoncopper_button_click(self, *args):
         self.read_form()
         self.read_form()
@@ -478,17 +447,13 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
     def plot(self):
     def plot(self):
 
 
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMGerber.plot()")
+
         # Does all the required setup and returns False
         # Does all the required setup and returns False
         # if the 'ptint' option is set to False.
         # if the 'ptint' option is set to False.
         if not FlatCAMObj.plot(self):
         if not FlatCAMObj.plot(self):
             return
             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
         geometry = self.solid_geometry
 
 
         # Make sure geometry is iterable.
         # Make sure geometry is iterable.
@@ -523,8 +488,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     x, y = ints.coords.xy
                     x, y = ints.coords.xy
                     self.axes.plot(x, y, linespec)
                     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):
     def serialize(self):
         return {
         return {
@@ -538,29 +504,21 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
     Represents Excellon/Drill code.
     Represents Excellon/Drill code.
     """
     """
 
 
+    ui_type = ExcellonObjectUI
+
     def __init__(self, name):
     def __init__(self, name):
         Excellon.__init__(self)
         Excellon.__init__(self)
-        FlatCAMObj.__init__(self, name, ExcellonObjectUI())
+        FlatCAMObj.__init__(self, name)
 
 
         self.kind = "excellon"
         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({
         self.options.update({
             "plot": True,
             "plot": True,
             "solid": False,
             "solid": False,
             "drillz": -0.1,
             "drillz": -0.1,
             "travelz": 0.1,
             "travelz": 0.1,
             "feedrate": 5.0,
             "feedrate": 5.0,
-            "toolselection": ""
+            # "toolselection": ""
         })
         })
 
 
         # TODO: Document this.
         # TODO: Document this.
@@ -571,49 +529,100 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         # from predecessors.
         # from predecessors.
         self.ser_attrs += ['options', 'kind']
         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)
         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):
     def on_create_cncjob_button_click(self, *args):
         self.read_form()
         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"
         job_name = self.options["name"] + "_cnc"
 
 
         # Object initialization function for app.new_object()
         # Object initialization function for app.new_object()
         def job_init(job_obj, app_obj):
         def job_init(job_obj, app_obj):
             assert isinstance(job_obj, FlatCAMCNCjob)
             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_cut = self.options["drillz"]
             job_obj.z_move = self.options["travelz"]
             job_obj.z_move = self.options["travelz"]
             job_obj.feedrate = self.options["feedrate"]
             job_obj.feedrate = self.options["feedrate"]
             # There could be more than one drill size...
             # There could be more than one drill size...
             # job_obj.tooldia =   # TODO: duplicate variable!
             # job_obj.tooldia =   # TODO: duplicate variable!
             # job_obj.options["tooldia"] =
             # 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()
             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()
             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
         # To be run in separate thread
         def job_thread(app_obj):
         def job_thread(app_obj):
             app_obj.new_object("cncjob", job_name, job_init)
             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
         # 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):
     def on_plot_cb_click(self, *args):
         if self.muted_ui:
         if self.muted_ui:
@@ -663,32 +672,34 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                     x, y = ints.coords.xy
                     x, y = ints.coords.xy
                     self.axes.plot(x, y, 'g-')
                     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):
     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):
 class FlatCAMCNCjob(FlatCAMObj, CNCjob):
@@ -696,23 +707,21 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
     Represents G-Code.
     Represents G-Code.
     """
     """
 
 
+    ui_type = CNCObjectUI
+
     def __init__(self, name, units="in", kind="generic", z_move=0.1,
     def __init__(self, name, units="in", kind="generic", z_move=0.1,
                  feedrate=3.0, z_cut=-0.002, tooldia=0.0):
                  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,
         CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
                         feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
                         feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
-        FlatCAMObj.__init__(self, name, CNCObjectUI())
+        FlatCAMObj.__init__(self, name)
 
 
         self.kind = "cncjob"
         self.kind = "cncjob"
 
 
         self.options.update({
         self.options.update({
             "plot": True,
             "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
         # Attributes to be included in serialization
@@ -720,25 +729,47 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         # from predecessors.
         # from predecessors.
         self.ser_attrs += ['options', 'kind']
         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):
     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.read_form()
         self.plot()
         self.plot()
 
 
     def on_exportgcode_button_click(self, *args):
     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):
     def on_plot_cb_click(self, *args):
         if self.muted_ui:
         if self.muted_ui:
@@ -755,8 +786,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
 
 
         self.plot2(self.axes, tooldia=self.options["tooldia"])
         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):
     def convert_units(self, units):
         factor = CNCjob.convert_units(self, units)
         factor = CNCjob.convert_units(self, units)
@@ -770,26 +800,14 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
     format.
     format.
     """
     """
 
 
+    ui_type = GeometryObjectUI
+
     def __init__(self, name):
     def __init__(self, name):
-        FlatCAMObj.__init__(self, name, GeometryObjectUI())
+        FlatCAMObj.__init__(self, name)
         Geometry.__init__(self)
         Geometry.__init__(self)
 
 
         self.kind = "geometry"
         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({
         self.options.update({
             "plot": True,
             "plot": True,
             # "solid": False,
             # "solid": False,
@@ -803,31 +821,34 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             "paintmargin": 0.01
             "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
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # Always append to it because it carries contents
         # from predecessors.
         # from predecessors.
         self.ser_attrs += ['options', 'kind']
         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)
         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):
     def on_paint_button_click(self, *args):
         self.app.info("Click inside the desired polygon.")
         self.app.info("Click inside the desired polygon.")
@@ -868,35 +889,41 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             # Propagate options
             # Propagate options
             job_obj.options["tooldia"] = self.options["cnctooldia"]
             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_cut = self.options["cutz"]
             job_obj.z_move = self.options["travelz"]
             job_obj.z_move = self.options["travelz"]
             job_obj.feedrate = self.options["feedrate"]
             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.
             # TODO: The tolerance should not be hard coded. Just for testing.
             job_obj.generate_from_geometry(self, tolerance=0.0005)
             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()
             job_obj.gcode_parse()
 
 
             # TODO: job_obj.create_geometry creates stuff that is not used.
             # 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..."))
             #GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
             #job_obj.create_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
         # To be run in separate thread
         def job_thread(app_obj):
         def job_thread(app_obj):
             app_obj.new_object("cncjob", job_name, job_init)
             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
         # 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:
         if self.muted_ui:
             return
             return
         self.read_form_item('plot')
         self.read_form_item('plot')
@@ -994,5 +1021,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
 
             FlatCAMApp.App.log.warning("Did not plot:", str(type(geo)))
             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
     Implements a queue of tasks to be carried out in order
     in a single independent thread.
     in a single independent thread.
     """
     """
 
 
-    def __init__(self):
+    def __init__(self, app, name=None):
         super(Worker, self).__init__()
         super(Worker, self).__init__()
-        self.queue = Queue.Queue()
-        self.stoprequest = threading.Event()
+        self.app = app
+        self.name = name
 
 
     def run(self):
     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
 from copy import copy
 import FlatCAMApp
 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:
         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.
         :param choices: List of choices. See description.
         :type choices: list
         :type choices: list
         """
         """
-        Gtk.Box.__init__(self)
+        super(RadioSet, self).__init__(parent)
         self.choices = copy(choices)
         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:
         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
         return
 
 
     def get_value(self):
     def get_value(self):
         for choice in self.choices:
         for choice in self.choices:
-            if choice['radio'].get_active():
+            if choice['radio'].isChecked():
                 return choice['value']
                 return choice['value']
         FlatCAMApp.App.log.error("No button was toggled in RadioSet.")
         FlatCAMApp.App.log.error("No button was toggled in RadioSet.")
         return None
         return None
@@ -52,33 +53,15 @@ class RadioSet(Gtk.Box):
     def set_value(self, val):
     def set_value(self, val):
         for choice in self.choices:
         for choice in self.choices:
             if choice['value'] == val:
             if choice['value'] == val:
-                choice['radio'].set_active(True)
+                choice['radio'].setChecked(True)
                 return
                 return
         FlatCAMApp.App.log.error("Value given is not part of this RadioSet: %s" % str(val))
         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.output_units = output_units
         self.format_re = re.compile(r"^([^\s]+)(?:\s([a-zA-Z]+))?$")
         self.format_re = re.compile(r"^([^\s]+)(?:\s([a-zA-Z]+))?$")
 
 
@@ -90,40 +73,22 @@ class LengthEntry(Gtk.Entry):
                    'MM': 1.0}
                    '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()
         val = self.get_value()
         if val is not None:
         if val is not None:
-            self.set_text(str(val))
+            self.set_text(QtCore.QString(str(val)))
         else:
         else:
             FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.get_text())
             FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.get_text())
 
 
     def get_value(self):
     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)
         match = self.format_re.search(raw)
+
         if not match:
         if not match:
             return None
             return None
         try:
         try:
             if match.group(2) is not None and match.group(2).upper() in self.scales:
             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:
             else:
                 return float(eval(match.group(1)))
                 return float(eval(match.group(1)))
         except:
         except:
@@ -131,24 +96,22 @@ class LengthEntry(Gtk.Entry):
             return None
             return None
 
 
     def set_value(self, val):
     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()
         val = self.get_value()
         if val is not None:
         if val is not None:
-            self.set_text(str(val))
+            self.set_text(QtCore.QString(str(val)))
         else:
         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):
     def get_value(self):
-        raw = self.get_text().strip(' ')
+        raw = str(self.text()).strip(' ')
         try:
         try:
             evaled = eval(raw)
             evaled = eval(raw)
         except:
         except:
@@ -158,44 +121,44 @@ class FloatEntry(Gtk.Entry):
         return float(evaled)
         return float(evaled)
 
 
     def set_value(self, val):
     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):
     def get_value(self):
-        return int(self.get_text())
+        return int(self.text())
 
 
     def set_value(self, val):
     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):
     def get_value(self):
-        return self.get_text()
+        return str(self.text())
 
 
     def set_value(self, val):
     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()
         val = self.get_value()
         if val is not None:
         if val is not None:
-            self.set_text(str(val))
+            self.setText(QtCore.QString(str(val)))
         else:
         else:
             FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.get_text())
             FlatCAMApp.App.log.warning("Could not interpret entry: %s" % self.get_text())
 
 
     def get_value(self):
     def get_value(self):
-        raw = self.get_text().strip(' ')
+        raw = str(self.text()).strip(' ')
         try:
         try:
             return eval(raw)
             return eval(raw)
         except:
         except:
@@ -203,15 +166,65 @@ class EvalEntry(Gtk.Entry):
             return None
             return None
 
 
     def set_value(self, val):
     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):
     def get_value(self):
-        return self.get_active()
+        return self.isChecked()
 
 
     def set_value(self, val):
     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 FlatCAMObj import *
-from gi.repository import Gtk, GdkPixbuf
 import inspect  # TODO: Remove
 import inspect  # TODO: Remove
 import FlatCAMApp
 import FlatCAMApp
+from PyQt4 import Qt, QtGui, QtCore
 
 
 
 
-class ObjectCollection:
+class ObjectCollection(QtCore.QAbstractListModel):
 
 
     classdict = {
     classdict = {
         "gerber": FlatCAMGerber,
         "gerber": FlatCAMGerber,
@@ -28,130 +21,51 @@ class ObjectCollection:
         "geometry": "share/geometry16.png"
         "geometry": "share/geometry16.png"
     }
     }
 
 
-    def __init__(self):
-
+    def __init__(self, parent=None):
+        QtCore.QAbstractListModel.__init__(self, parent=parent)
         ### Icons for the list view
         ### Icons for the list view
         self.icons = {}
         self.icons = {}
         for kind in ObjectCollection.icon_files:
         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):
     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
             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):
     def get_names(self):
         """
         """
@@ -160,14 +74,9 @@ class ObjectCollection:
         :return: List of names.
         :return: List of names.
         :rtype: list
         :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):
     def get_bounds(self):
         """
         """
@@ -185,9 +94,7 @@ class ObjectCollection:
         xmax = -Inf
         xmax = -Inf
         ymax = -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:
             try:
                 gxmin, gymin, gxmax, gymax = obj.bounds()
                 gxmin, gymin, gxmax, gymax = obj.bounds()
                 xmin = min([xmin, gxmin])
                 xmin = min([xmin, gxmin])
@@ -196,24 +103,8 @@ class ObjectCollection:
                 ymax = max([ymax, gymax])
                 ymax = max([ymax, gymax])
             except:
             except:
                 FlatCAMApp.App.log.warning("DEV WARNING: Tried to get bounds of empty geometry.")
                 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):
     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()")
         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
                 return obj
-            iterat = self.store.iter_next(iterat)
         return None
         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 *
 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)
         ## 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
         ## 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
         ## 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
         ## 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_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
         ## 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
         ## Common to all objects
         ## Scale
         ## 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."
             "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
         # Factor
-        l10 = Gtk.Label('Factor:', xalign=1)
-        l10.set_tooltip_markup(
+        faclabel = QtGui.QLabel('Factor:')
+        faclabel.setToolTip(
             "Factor by which to multiply\n"
             "Factor by which to multiply\n"
             "geometric features of this object."
             "geometric features of this object."
         )
         )
-        grid5.attach(l10, 0, 0, 1, 1)
+        grid1.addWidget(faclabel, 0, 0)
         self.scale_entry = FloatEntry()
         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
         # 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."
             "Perform scaling operation."
         )
         )
-        self.pack_start(self.scale_button, expand=False, fill=False, padding=2)
+        layout.addWidget(self.scale_button)
 
 
         ## Offset
         ## 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."
             "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"
             "Amount by which to move the object\n"
             "in the x and y axes in (x, y) format."
             "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 = 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."
             "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):
 class CNCObjectUI(ObjectUI):
@@ -116,57 +105,68 @@ class CNCObjectUI(ObjectUI):
     User interface for CNCJob objects.
     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
         ## 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
         # 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."
             "Plot (show) this object."
         )
         )
-        grid0.attach(self.plot_cb, 0, 0, 2, 1)
+        grid0.addWidget(self.plot_cb, 0, 0)
 
 
         # Tool dia for plot
         # 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"
             "Diameter of the tool to be\n"
             "rendered in the plot."
             "rendered in the plot."
         )
         )
-        grid0.attach(l1, 0, 1, 1, 1)
+        grid0.addWidget(tdlabel, 1, 0)
         self.tooldia_entry = LengthEntry()
         self.tooldia_entry = LengthEntry()
-        grid0.attach(self.tooldia_entry, 1, 1, 1, 1)
+        grid0.addWidget(self.tooldia_entry, 1, 1)
 
 
         # Update plot button
         # 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."
             "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
         ## 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"
             "Export and save G-Code to\n"
             "make this object to a file."
             "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
         # 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"
             "Opens dialog to save G-Code\n"
             "file."
             "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):
 class GeometryObjectUI(ObjectUI):
@@ -174,135 +174,131 @@ class GeometryObjectUI(ObjectUI):
     User interface for Geometry objects.
     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
         ## 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
         # Plot CB
         self.plot_cb = FCCheckBox(label='Plot')
         self.plot_cb = FCCheckBox(label='Plot')
-        self.plot_cb.set_tooltip_markup(
+        self.plot_cb.setToolTip(
             "Plot (show) this object."
             "Plot (show) this object."
         )
         )
-        grid0.attach(self.plot_cb, 0, 0, 1, 1)
+        self.custom_box.addWidget(self.plot_cb)
 
 
         ## Create CNC Job
         ## 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"
             "Create a CNC Job object\n"
             "tracing the contours of this\n"
             "tracing the contours of this\n"
             "Geometry object."
             "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"
             "Cutting depth (negative)\n"
             "below the copper surface."
             "below the copper surface."
         )
         )
-        grid1.attach(l1, 0, 0, 1, 1)
+        grid1.addWidget(cutzlabel, 0, 0)
         self.cutz_entry = LengthEntry()
         self.cutz_entry = LengthEntry()
-        grid1.attach(self.cutz_entry, 1, 0, 1, 1)
+        grid1.addWidget(self.cutz_entry, 0, 1)
 
 
         # Travel Z
         # 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"
             "Height of the tool when\n"
             "moving without cutting."
             "moving without cutting."
         )
         )
-        grid1.attach(l2, 0, 1, 1, 1)
+        grid1.addWidget(travelzlabel, 1, 0)
         self.travelz_entry = LengthEntry()
         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"
             "Cutting speed in the XY\n"
             "plane in units per minute"
             "plane in units per minute"
         )
         )
-        grid1.attach(l3, 0, 2, 1, 1)
+        grid1.addWidget(frlabel, 2, 0)
         self.cncfeedrate_entry = LengthEntry()
         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"
             "The diameter of the cutting\n"
             "tool (just for display)."
             "tool (just for display)."
         )
         )
-        grid1.attach(l4, 0, 3, 1, 1)
+        grid1.addWidget(tdlabel, 3, 0)
         self.cnctooldia_entry = LengthEntry()
         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."
             "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"
             "Creates tool paths to cover the\n"
             "whole area of a polygon (remove\n"
             "whole area of a polygon (remove\n"
             "all copper). You will be asked\n"
             "all copper). You will be asked\n"
             "to click on the desired polygon."
             "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
         # 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"
             "Diameter of the tool to\n"
             "be used in the operation."
             "be used in the operation."
         )
         )
-        grid2.attach(l5, 0, 0, 1, 1)
+        grid2.addWidget(ptdlabel, 0, 0)
+
         self.painttooldia_entry = LengthEntry()
         self.painttooldia_entry = LengthEntry()
-        grid2.attach(self.painttooldia_entry, 1, 0, 1, 1)
+        grid2.addWidget(self.painttooldia_entry, 0, 1)
 
 
         # Overlap
         # Overlap
-        l6 = Gtk.Label('Overlap:', xalign=1)
-        l6.set_tooltip_markup(
+        ovlabel = QtGui.QLabel('Overlap:')
+        ovlabel.setToolTip(
             "How much (fraction) of the tool\n"
             "How much (fraction) of the tool\n"
             "width to overlap each tool pass."
             "width to overlap each tool pass."
         )
         )
-        grid2.attach(l6, 0, 1, 1, 1)
+        grid2.addWidget(ovlabel, 1, 0)
         self.paintoverlap_entry = LengthEntry()
         self.paintoverlap_entry = LengthEntry()
-        grid2.attach(self.paintoverlap_entry, 1, 1, 1, 1)
+        grid2.addWidget(self.paintoverlap_entry, 1, 1)
 
 
         # Margin
         # Margin
-        l7 = Gtk.Label('Margin:', xalign=1)
-        l7.set_tooltip_markup(
+        marginlabel = QtGui.QLabel('Margin:')
+        marginlabel.setToolTip(
             "Distance by which to avoid\n"
             "Distance by which to avoid\n"
             "the edges of the polygon to\n"
             "the edges of the polygon to\n"
             "be painted."
             "be painted."
         )
         )
-        grid2.attach(l7, 0, 2, 1, 1)
+        grid2.addWidget(marginlabel, 2, 0)
         self.paintmargin_entry = LengthEntry()
         self.paintmargin_entry = LengthEntry()
-        grid2.attach(self.paintmargin_entry, 1, 2, 1, 1)
+        grid2.addWidget(self.paintmargin_entry)
 
 
         # GO Button
         # 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"
             "After clicking here, click inside\n"
             "the polygon you wish to be painted.\n"
             "the polygon you wish to be painted.\n"
             "A new Geometry object with the tool\n"
             "A new Geometry object with the tool\n"
             "paths will be created."
             "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):
 class ExcellonObjectUI(ObjectUI):
@@ -310,90 +306,85 @@ class ExcellonObjectUI(ObjectUI):
     User interface for Excellon objects.
     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
         ## 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 = FCCheckBox(label='Plot')
-        self.plot_cb.set_tooltip_markup(
+        self.plot_cb.setToolTip(
             "Plot (show) this object."
             "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 = FCCheckBox(label='Solid')
-        self.solid_cb.set_tooltip_markup(
+        self.solid_cb.setToolTip(
             "Solid circles."
             "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
         ## 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"
             "Create a CNC Job object\n"
             "for this drill object."
             "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"
             "Drill depth (negative)\n"
             "below the copper surface."
             "below the copper surface."
         )
         )
-        grid1.attach(l1, 0, 0, 1, 1)
+        grid1.addWidget(cutzlabel, 0, 0)
         self.cutz_entry = LengthEntry()
         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"
             "Tool height when travelling\n"
             "across the XY plane."
             "across the XY plane."
         )
         )
-        grid1.attach(l2, 0, 1, 1, 1)
+        grid1.addWidget(travelzlabel, 1, 0)
         self.travelz_entry = LengthEntry()
         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"
             "Tool speed while drilling\n"
             "(in units per minute)."
             "(in units per minute)."
         )
         )
-        grid1.attach(l3, 0, 2, 1, 1)
+        grid1.addWidget(frlabel, 2, 0)
         self.feedrate_entry = LengthEntry()
         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."
             "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):
 class GerberObjectUI(ObjectUI):
@@ -401,214 +392,210 @@ class GerberObjectUI(ObjectUI):
     User interface for Gerber objects.
     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
         ## 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
         # Plot CB
         self.plot_cb = FCCheckBox(label='Plot')
         self.plot_cb = FCCheckBox(label='Plot')
-        self.plot_cb.set_tooltip_markup(
+        self.plot_options_label.setToolTip(
             "Plot (show) this object."
             "Plot (show) this object."
         )
         )
-        grid0.attach(self.plot_cb, 0, 0, 1, 1)
+        grid0.addWidget(self.plot_cb, 0, 0)
 
 
         # Solid CB
         # Solid CB
         self.solid_cb = FCCheckBox(label='Solid')
         self.solid_cb = FCCheckBox(label='Solid')
-        self.solid_cb.set_tooltip_markup(
+        self.solid_cb.setToolTip(
             "Solid color polygons."
             "Solid color polygons."
         )
         )
-        grid0.attach(self.solid_cb, 1, 0, 1, 1)
+        grid0.addWidget(self.solid_cb, 0, 1)
 
 
         # Multicolored CB
         # Multicolored CB
         self.multicolored_cb = FCCheckBox(label='Multicolored')
         self.multicolored_cb = FCCheckBox(label='Multicolored')
-        self.multicolored_cb.set_tooltip_markup(
+        self.multicolored_cb.setToolTip(
             "Draw polygons in different colors."
             "Draw polygons in different colors."
         )
         )
-        grid0.attach(self.multicolored_cb, 2, 0, 1, 1)
+        grid0.addWidget(self.multicolored_cb, 0, 2)
 
 
         ## Isolation Routing
         ## 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"
             "Create a Geometry object with\n"
             "toolpaths to cut outside polygons."
             "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."
             "Diameter of the cutting tool."
         )
         )
-        grid.attach(l1, 0, 0, 1, 1)
+        grid1.addWidget(tdlabel, 0, 0)
         self.iso_tool_dia_entry = LengthEntry()
         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"
             "Width of the isolation gap in\n"
             "number (integer) of tool widths."
             "number (integer) of tool widths."
         )
         )
-        grid.attach(l2, 0, 1, 1, 1)
+        grid1.addWidget(passlabel, 1, 0)
         self.iso_width_entry = IntEntry()
         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"
             "How much (fraction of tool width)\n"
             "to overlap each pass."
             "to overlap each pass."
         )
         )
-        grid.attach(l3, 0, 2, 1, 1)
+        grid1.addWidget(overlabel, 2, 0)
         self.iso_overlap_entry = FloatEntry()
         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"
             "Create the Geometry Object\n"
             "for isolation routing."
             "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
         ## 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"
             "Create toolpaths to cut around\n"
             "the PCB and separate it from\n"
             "the PCB and separate it from\n"
             "the original board."
             "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."
             "Diameter of the cutting tool."
         )
         )
-        grid2.attach(l4, 0, 0, 1, 1)
+        grid2.addWidget(tdclabel, 0, 0)
         self.cutout_tooldia_entry = LengthEntry()
         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"
             "Distance from objects at which\n"
             "to draw the cutout."
             "to draw the cutout."
         )
         )
-        grid2.attach(l5, 0, 1, 1, 1)
+        grid2.addWidget(marginlabel, 1, 0)
         self.cutout_margin_entry = LengthEntry()
         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"
             "Size of the gaps in the toolpath\n"
             "that will remain to hold the\n"
             "that will remain to hold the\n"
             "board in place."
             "board in place."
         )
         )
-        grid2.attach(l6, 0, 2, 1, 1)
+        grid2.addWidget(gaplabel, 2, 0)
         self.cutout_gap_entry = LengthEntry()
         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"
             "Where to place the gaps, Top/Bottom\n"
             "Left/Rigt, or on all 4 sides."
             "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'},
         self.gaps_radio = RadioSet([{'label': '2 (T/B)', 'value': 'tb'},
                                     {'label': '2 (L/R)', 'value': 'lr'},
                                     {'label': '2 (L/R)', 'value': 'lr'},
                                     {'label': '4', 'value': '4'}])
                                     {'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"
             "Generate the geometry for\n"
             "the board cutout."
             "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
         ## 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"
             "Create polygons covering the\n"
             "areas without copper on the PCB.\n"
             "areas without copper on the PCB.\n"
             "Equivalent to the inverse of this\n"
             "Equivalent to the inverse of this\n"
             "object. Can be used to remove all\n"
             "object. Can be used to remove all\n"
             "copper from a specified region."
             "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"
             "Specify the edge of the PCB\n"
             "by drawing a box around all\n"
             "by drawing a box around all\n"
             "objects with this minimum\n"
             "objects with this minimum\n"
             "distance."
             "distance."
         )
         )
-        grid3.attach(l8, 0, 0, 1, 1)
+        grid3.addWidget(bmlabel, 0, 0)
         self.noncopper_margin_entry = LengthEntry()
         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 = 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"
             "Creates a Geometry objects with polygons\n"
             "covering the copper-free areas of the PCB."
             "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
         ## 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"
             "Distance of the edges of the box\n"
             "to the nearest polygon."
             "to the nearest polygon."
         )
         )
-        grid4.attach(l9, 0, 0, 1, 1)
+        grid4.addWidget(bbmargin, 0, 0)
         self.bbmargin_entry = LengthEntry()
         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 = FCCheckBox(label="Rounded corners")
-        self.bbrounded_cb.set_tooltip_markup(
+        self.bbrounded_cb.setToolTip(
             "If the bounding box is \n"
             "If the bounding box is \n"
             "to have rounded corners\n"
             "to have rounded corners\n"
             "their radius is equal to\n"
             "their radius is equal to\n"
             "the margin."
             "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."
             "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.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
 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 PlotCanvas:
     """
     """
     Class handling the plotting area in the application.
     Class handling the plotting area in the application.
@@ -38,18 +54,21 @@ class PlotCanvas:
 
 
         # The canvas is the top level container (Gtk.DrawingArea)
         # The canvas is the top level container (Gtk.DrawingArea)
         self.canvas = FigureCanvas(self.figure)
         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
         # 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
         # Events
         self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
         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_press_event', self.on_key_down)
         self.canvas.mpl_connect('key_release_event', self.on_key_up)
         self.canvas.mpl_connect('key_release_event', self.on_key_up)
 
 
@@ -62,6 +81,7 @@ class PlotCanvas:
         :param event:
         :param event:
         :return:
         :return:
         """
         """
+        FlatCAMApp.App.log.debug('on_key_down(): ' + str(event.key))
         self.key = event.key
         self.key = event.key
 
 
     def on_key_up(self, event):
     def on_key_up(self, event):
@@ -125,7 +145,7 @@ class PlotCanvas:
         self.axes.grid(True)
         self.axes.grid(True)
 
 
         # Re-draw
         # Re-draw
-        self.canvas.queue_draw()
+        self.canvas.draw()
 
 
     def adjust_axes(self, xmin, ymin, xmax, ymax):
     def adjust_axes(self, xmin, ymin, xmax, ymax):
         """
         """
@@ -144,7 +164,7 @@ class PlotCanvas:
         :return: None
         :return: None
         """
         """
 
 
-        FlatCAMApp.App.log.debug("PC.adjust_axes()")
+        # FlatCAMApp.App.log.debug("PC.adjust_axes()")
 
 
         width = xmax - xmin
         width = xmax - xmin
         height = ymax - ymin
         height = ymax - ymin
@@ -182,7 +202,7 @@ class PlotCanvas:
             ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
             ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
 
 
         # Re-draw
         # Re-draw
-        self.canvas.queue_draw()
+        self.canvas.draw()
 
 
     def auto_adjust_axes(self, *args):
     def auto_adjust_axes(self, *args):
         """
         """
@@ -234,7 +254,7 @@ class PlotCanvas:
             ax.set_ylim((ymin, ymax))
             ax.set_ylim((ymin, ymax))
 
 
         # Re-draw
         # Re-draw
-        self.canvas.queue_draw()
+        self.canvas.draw()
 
 
     def pan(self, x, y):
     def pan(self, x, y):
         xmin, xmax = self.axes.get_xlim()
         xmin, xmax = self.axes.get_xlim()
@@ -248,7 +268,7 @@ class PlotCanvas:
             ax.set_ylim((ymin + y*height, ymax + y*height))
             ax.set_ylim((ymin + y*height, ymax + y*height))
 
 
         # Re-draw
         # Re-draw
-        self.canvas.queue_draw()
+        self.canvas.draw()
 
 
     def new_axes(self, name):
     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)
         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.
         Scroll event handler.
 
 
-        :param canvas: The widget generating the event. Ignored.
         :param event: Event object containing the event information.
         :param event: Event object containing the event information.
         :return: None
         :return: None
         """
         """
 
 
         # So it can receive key presses
         # So it can receive key presses
-        self.canvas.grab_focus()
+        # self.canvas.grab_focus()
+        self.canvas.setFocus()
 
 
         # Event info
         # Event info
-        z, direction = event.get_scroll_direction()
+        # z, direction = event.get_scroll_direction()
 
 
         if self.key is None:
         if self.key is None:
 
 
-            if direction is Gdk.ScrollDirection.UP:
+            if event.button == 'up':
                 self.zoom(1.5, self.mouse)
                 self.zoom(1.5, self.mouse)
             else:
             else:
                 self.zoom(1/1.5, self.mouse)
                 self.zoom(1/1.5, self.mouse)
@@ -286,15 +306,15 @@ class PlotCanvas:
 
 
         if self.key == 'shift':
         if self.key == 'shift':
 
 
-            if direction is Gdk.ScrollDirection.UP:
+            if event.button == 'up':
                 self.pan(0.3, 0)
                 self.pan(0.3, 0)
             else:
             else:
                 self.pan(-0.3, 0)
                 self.pan(-0.3, 0)
             return
             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)
                 self.pan(0, 0.3)
             else:
             else:
                 self.pan(0, -0.3)
                 self.pan(0, -0.3)
@@ -308,3 +328,4 @@ class PlotCanvas:
         :return: None
         :return: None
         """
         """
         self.mouse = [event.xdata, event.ydata]
         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

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor