Przeglądaj źródła

Major modifications to data/gui interactions. In progress.

Juan Pablo Caram 11 lat temu
rodzic
commit
0bdc3b19f0
12 zmienionych plików z 4233 dodań i 5740 usunięć
  1. 2 2963
      FlatCAM.py
  2. 0 2588
      FlatCAM.ui
  3. 3025 0
      FlatCAMApp.py
  4. 401 94
      FlatCAMObj.py
  5. 143 0
      GUIElements.py
  6. 254 0
      ObjectCollection.py
  7. 375 0
      ObjectUI.py
  8. 28 90
      camlib.py
  9. 1 1
      defaults.json
  10. 2 2
      doc/build/app.html
  11. 1 1
      doc/build/genindex.html
  12. 1 1
      recent.json

+ 2 - 2963
FlatCAM.py

@@ -6,2969 +6,8 @@
 # MIT Licence                                              #
 ############################################################
 
-import threading
-import traceback
-import sys
-import urllib
-import copy
-import random
-
-from gi.repository import Gtk, GdkPixbuf
-from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
-from shapely import speedups
-
-
-########################################
-##      Imports part of FlatCAM       ##
-########################################
-from FlatCAMObj import *
-from FlatCAMWorker import Worker
-
-
-########################################
-##                App                 ##
-########################################
-class App:
-    """
-    The main application class. The constructor starts the GUI.
-    """
-
-    version_url = "http://caram.cl/flatcam/VERSION"
-
-    def __init__(self):
-        """
-        Starts the application. Takes no parameters.
-
-        :return: app
-        :rtype: App
-        """
-
-        if speedups.available:
-            speedups.enable()
-
-        # Needed to interact with the GUI from other threads.
-        GObject.threads_init()
-
-        # GLib.log_set_handler()
-
-        #### GUI ####
-        # Glade init
-        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.setup_project_list()  # The "Project" tab
-        self.setup_component_editor()  # The "Selected" tab
-
-        ## Setup the toolbar. Adds buttons.
-        self.setup_toolbar()
-
-        #### Event handling ####
-        self.builder.connect_signals(self)
-
-        #### Make plot area ####
-        self.plotcanvas = PlotCanvas(self.grid)
-        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)
-
-        self.setup_tooltips()
-
-        # TODO: Hardcoded list
-        for kind in ['gerber', 'excellon', 'geometry', 'cncjob']:
-            entry_name = self.builder.get_object("entry_text_" + kind + "_name")
-            entry_name.connect("activate", self.on_activate_name)
-
-        #### 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)
-        # 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.defaults = {
-            "units": "in"
-        }  # Application defaults
-
-        ## Current Project ##
-        self.options = {}  # Project options
-        self.project_filename = None
-
-        self.form_kinds = {
-            "units": "radio"
-        }
-
-        self.radios = {"units": {"rb_inch": "IN", "rb_mm": "MM"},
-                       "gerber_gaps": {"rb_app_2tb": "tb", "rb_app_2lr": "lr", "rb_app_4": "4"}}
-        self.radios_inv = {"units": {"IN": "rb_inch", "MM": "rb_mm"},
-                           "gerber_gaps": {"tb": "rb_app_2tb", "lr": "rb_app_2lr", "4": "rb_app_4"}}
-
-        # Options for each kind of FlatCAMObj.
-        # Example: 'gerber_plot': 'cb'. The widget name would be: 'cb_app_gerber_plot'
-        for FlatCAMClass in [FlatCAMExcellon, FlatCAMGeometry, FlatCAMGerber, FlatCAMCNCjob]:
-            obj = FlatCAMClass("no_name")
-            for option in obj.form_kinds:
-                self.form_kinds[obj.kind + "_" + option] = obj.form_kinds[option]
-                # if obj.form_kinds[option] == "radio":
-                #     self.radios.update({obj.kind + "_" + option: obj.radios[option]})
-                #     self.radios_inv.update({obj.kind + "_" + option: obj.radios_inv[option]})
-
-        ## Event subscriptions ##
-
-        ## Tools ##
-        self.measure = Measurement(self.builder.get_object("box39"), 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)
-
-        #### Initialization ####
-        self.load_defaults()
-        self.options.update(self.defaults)  # Copy app defaults to project options
-        self.options2form()  # Populate the app defaults form
-        self.units_label.set_text("[" + self.options["units"] + "]")
-        self.setup_recent_items()
-
-        self.worker = Worker()
-        self.worker.daemon = True
-        self.worker.start()
-
-        #### Check for updates ####
-        # Separate thread (Not worker)
-        self.version = 4
-        t1 = threading.Thread(target=self.versionCheck)
-        t1.daemon = True
-        t1.start()
-
-        #### For debugging only ###
-        def somethreadfunc(app_obj):
-            print "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 4 UNSTABLE")
-        self.window.set_default_size(900, 600)
-        self.window.show_all()
-
-    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.window, 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)
-
-        # 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)
-
-        # 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)
-
-        # 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)
-
-        # 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)
-
-        # 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)
-
-    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("box_selected")
-
-        # Remove anything else in the box
-        box_children = box_selected.get_children()
-        for child in box_children:
-            box_selected.remove(child)
-
-        box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
-        label1 = Gtk.Label("Choose an item from Project")
-        box1.pack_start(label1, True, False, 1)
-        box_selected.pack_start(box1, True, True, 0)
-        #box_selected.show()
-        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:
-            print "ERROR: Failed to load recent item list."
-            self.info("Failed to load recent item list.")
-            return
-
-        try:
-            self.recent = json.load(f)
-        except:
-            print "ERROR: Failed to parse recent item list."
-            self.info("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)
-        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.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):
-        """
-        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
-        """
-
-        print "new_object()"
-
-        ### Check for existing name
-        if name in self.collection.get_names():
-            ## Create a new name
-            # Ends with number?
-            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"
-
-        # 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=True)
-
-        # Show object details now.
-        GLib.idle_add(lambda: self.notebook.set_current_page(1))
-
-        # Plot
-        # TODO: (Thread-safe?)
-        obj.plot()
-
-        GLib.idle_add(lambda: self.on_zoom_fit(None))
-        #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)
-        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:
-            self.info("ERROR: Could not load defaults file.")
-            return
-
-        try:
-            defaults = json.loads(options)
-        except:
-            e = sys.exc_info()[0]
-            print e
-            self.info("ERROR: Failed to parse defaults file.")
-            return
-        self.defaults.update(defaults)
-
-    def read_form(self):
-        """
-        Reads the options form into self.defaults/self.options.
-
-        :return: None
-        :rtype: None
-        """
-        combo_sel = self.combo_options.get_active()
-        options_set = [self.options, self.defaults][combo_sel]
-        for option in options_set:
-            self.read_form_item(option, options_set)
-
-    def read_form_item(self, name, dest):
-        """
-        Reads the value of a form item in the defaults/options form and
-        saves it to the corresponding dictionary.
-
-        :param name: Name of the form item. A key in ``self.defaults`` or
-            ``self.options``.
-        :type name: str
-        :param dest: Dictionary to which to save the value.
-        :type dest: dict
-        :return: None
-        """
-        fkind = self.form_kinds[name]
-        fname = fkind + "_" + "app" + "_" + name
-
-        if fkind == 'entry_text':
-            dest[name] = self.builder.get_object(fname).get_text()
-            return
-        if fkind == 'entry_eval':
-            dest[name] = self.get_eval(fname)
-            return
-        if fkind == 'cb':
-            dest[name] = self.builder.get_object(fname).get_active()
-            return
-        if fkind == 'radio':
-            dest[name] = self.get_radio_value(self.radios[name])
-            return
-        print "Unknown kind of form item:", fkind
-
-    def options2form(self):
-        """
-        Sets the 'Project Options' or 'Application Defaults' form with values from
-        ``self.options`` or ``self.defaults``.
-
-        :return: None
-        :rtype: None
-        """
-
-        # Set the on-change callback to do nothing while we do the changes.
-        self.options_update_ignore = True
-        self.toggle_units_ignore = True
-
-        combo_sel = self.combo_options.get_active()
-        options_set = [self.options, self.defaults][combo_sel]
-        for option in options_set:
-            self.set_form_item(option, options_set[option])
-
-        self.options_update_ignore = False
-        self.toggle_units_ignore = False
-
-    def set_form_item(self, name, value):
-        """
-        Sets a form item 'name' in the GUI with the given 'value'. The syntax of
-        form names in the GUI is <kind>_app_<name>, where kind is one of: rb (radio button),
-        cb (check button), entry_eval or entry_text (entry), combo (combo box). name is
-        whatever name it's been given. For self.defaults, name is a key in the dictionary.
-
-        :param name: Name of the form field.
-        :type name: str
-        :param value: The value to set the form field to.
-        :type value: Depends on field kind.
-        :return: None
-        """
-        if name not in self.form_kinds:
-            print "WARNING: Tried to set unknown option/form item:", name
-            return
-        fkind = self.form_kinds[name]
-        fname = fkind + "_" + "app" + "_" + name
-        if fkind == 'entry_eval' or fkind == 'entry_text':
-            try:
-                self.builder.get_object(fname).set_text(str(value))
-            except:
-                print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
-            return
-        if fkind == 'cb':
-            try:
-                self.builder.get_object(fname).set_active(value)
-            except:
-                print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
-            return
-        if fkind == 'radio':
-            try:
-                self.builder.get_object(self.radios_inv[name][value]).set_active(True)
-            except:
-                print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
-            return
-        print "Unknown kind of form item:", fkind
-
-    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:
-            print "ERROR: Failed to open file for saving:", filename
-            return
-
-        try:
-            json.dump(d, f, default=to_dict)
-        except:
-            print "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
-        """
-
-        try:
-            f = open(filename, 'r')
-        except:
-            self.info("ERROR: Failed to open project file: %s" % filename)
-            return
-
-        try:
-            d = json.load(f, object_hook=dict2obj)
-        except:
-            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
-        for obj in d['objs']:
-            def obj_init(obj_inst, app_inst):
-                obj_inst.from_dict(obj)
-            self.new_object(obj['kind'], obj['options']['name'], obj_init)
-
-        self.info("Project loaded from: " + filename)
-
-    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
-        """
-        print "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 versionCheck(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:
-            GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
-            return
-
-        try:
-            data = json.load(f)
-        except:
-            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 setup_tooltips(self):
-        tooltips = {
-            "cb_gerber_plot": "Plot this object on the main window.",
-            "cb_gerber_mergepolys": "Show overlapping polygons as single.",
-            "cb_gerber_solid": "Paint inside polygons.",
-            "cb_gerber_multicolored": "Draw polygons with different colors."
-        }
-
-        for widget in tooltips:
-            self.builder.get_object(widget).set_tooltip_markup(tooltips[widget])
-
-    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 i != app_obj.selected_item_name or not except_current:
-                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:
-            print "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:
-            print "ERROR: Failed to write to recent items file."
-            self.info('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
-        """
-        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 ..."))
-            #gerber_obj.create_geometry()
-            #gerber_obj.solid_geometry = gerber_obj.otf_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()
-            print "ERROR:", e[0]
-            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()
-            print "ERROR:", e[0]
-            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_offset_object(self, widget):
-        """
-        Offsets the object's geometry by the vector specified
-        in the form. Re-plots.
-
-        :param widget: Ignored
-        :return: None
-        """
-
-        obj = self.collection.get_active()
-        obj.read_form()
-        assert isinstance(obj, FlatCAMObj)
-        try:
-            vect = self.get_eval("entry_eval_" + obj.kind + "_offset")
-        except:
-            self.info("ERROR: Vector is not in (x, y) format.")
-            return
-        assert isinstance(obj, Geometry)
-        obj.offset(vect)
-        obj.plot()
-        return
-
-    def on_cb_plot_toggled(self, widget):
-        """
-        Callback for toggling the "Plot" checkbox. Re-plots.
-
-        :param widget: Ignored.
-        :return: None
-        """
-
-        self.collection.get_active().read_form()
-        self.collection.get_active().plot()
-
-    def on_about(self, widget):
-        """
-        Opens the 'About' dialog box.
-
-        :param widget: Ignored.
-        :return: None
-        """
-
-        about = self.builder.get_object("aboutdialog")
-        response = 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)
-        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
-
-        combo_sel = self.combo_options.get_active()
-        options_set = [self.options, self.defaults][combo_sel]
-
-        # 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(factor):
-            for dim in dimensions:
-                options_set[dim] *= factor
-
-        # The scaling factor depending on choice of units.
-        factor = 1/25.4
-        if self.builder.get_object('rb_mm').get_active():
-            factor = 25.4
-
-        # App units. Convert without warning.
-        if combo_sel == 1:
-            self.read_form()
-            scale_options(factor)
-            self.options2form()
-            return
-
-        # 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:
-            #print "Converting units..."
-            #print "Converting options..."
-            self.read_form()
-            scale_options(factor)
-            self.options2form()
-            for obj in self.collection.get_list():
-                units = self.get_radio_value({"rb_mm": "MM", "rb_inch": "IN"})
-                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.builder.get_object('rb_mm').get_active():
-                self.builder.get_object('rb_inch').set_active(True)
-            else:
-                self.builder.get_object('rb_mm').set_active(True)
-            self.toggle_units_ignore = False
-
-        self.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)
-
-        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.options.update(self.defaults)
-        self.options2form()  # Update UI
-
-    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.defaults.update(self.options)
-        self.options2form()  # Update UI
-
-    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
-        """
-
-        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.options2form()  # Update UI
-
-    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.options2form()  # Update UI
-
-    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
-        """
-
-        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:
-            self.info("ERROR: Could not load defaults file.")
-            return
-
-        try:
-            defaults = json.loads(options)
-        except:
-            e = sys.exc_info()[0]
-            print e
-            self.info("ERROR: Failed to parse defaults file.")
-            return
-
-        # Update options
-        assert isinstance(defaults, dict)
-        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.combo_options.get_active()
-        #print "Options --> ", combo_sel
-        self.options2form()
-
-    def on_options_update(self, widget):
-        """
-        Called whenever a value in the options/defaults form changes.
-        All values are updated. Can be inhibited by setting ``self.options_update_ignore = True``,
-        which may be necessary when updating the UI from code and not by the user.
-
-        :param widget: The widget from which this was called. Ignore.
-        :return: None
-        """
-
-        if self.options_update_ignore:
-            return
-        self.read_form()
-
-    def on_scale_object(self, widget):
-        """
-        Callback for request to change an objects geometry scale. The object
-        is re-scaled and replotted.
-
-        :param widget: Ignored.
-        :return: None
-        """
-
-        obj = self.collection.get_active()
-        factor = self.get_eval("entry_eval_" + obj.kind + "_scalefactor")
-        obj.scale(factor)
-        obj.to_form()
-        self.on_update_plot(None)
-
-    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)
-
-    def on_generate_gerber_bounding_box(self, widget):
-        """
-        Callback for request from the Gerber form to generate a bounding box for the
-        geometry in the object. Creates a FlatCAMGeometry with the bounding box.
-        The box can have rounded corners if specified in the form.
-
-        :param widget: Ignored.
-        :return: None
-        """
-        # TODO: Use Gerber.get_bounding_box(...)
-        gerber = self.collection.get_active()
-        gerber.read_form()
-        name = gerber.options["name"] + "_bbox"
-
-        def geo_init(geo_obj, app_obj):
-            assert isinstance(geo_obj, FlatCAMGeometry)
-            # Bounding box with rounded corners
-            bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["bboxmargin"])
-            if not gerber.options["bboxrounded"]:  # Remove rounded corners
-                bounding_box = bounding_box.envelope
-            geo_obj.solid_geometry = bounding_box
-
-        self.new_object("geometry", name, geo_init)
-
-    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_generate_excellon_cncjob(self, widget):
-        """
-        Callback for button active/click on Excellon form to
-        create a CNC Job for the Excellon file.
-
-        :param widget: Ignored
-        :return: None
-        """
-
-        excellon = self.collection.get_active()
-        excellon.read_form()
-        job_name = excellon.options["name"] + "_cnc"
-
-        # Object initialization function for app.new_object()
-        def job_init(job_obj, app_obj):
-            # excellon_ = self.get_current()
-            # assert isinstance(excellon_, FlatCAMExcellon)
-            assert isinstance(job_obj, FlatCAMCNCjob)
-
-            GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
-            job_obj.z_cut = excellon.options["drillz"]
-            job_obj.z_move = excellon.options["travelz"]
-            job_obj.feedrate = excellon.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(excellon, excellon.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.worker.add_task(job_thread, [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_gerber_generate_noncopper(self, widget):
-        """
-        Callback for button on Gerber form to create a geometry object
-        with polygons covering the area without copper or negative of the
-        Gerber.
-
-        :param widget: The widget from which this was called.
-        :return: None
-        """
-
-        gerb = self.collection.get_active()
-        gerb.read_form()
-        name = gerb.options["name"] + "_noncopper"
-
-        def geo_init(geo_obj, app_obj):
-            assert isinstance(geo_obj, FlatCAMGeometry)
-            bounding_box = gerb.solid_geometry.envelope.buffer(gerb.options["noncoppermargin"])
-            if not gerb.options["noncopperrounded"]:
-                bounding_box = bounding_box.envelope
-            non_copper = bounding_box.difference(gerb.solid_geometry)
-            geo_obj.solid_geometry = non_copper
-
-        # TODO: Check for None
-        self.new_object("geometry", name, geo_init)
-
-    def on_gerber_generate_cutout(self, widget):
-        """
-        Callback for button on Gerber form to create geometry with lines
-        for cutting off the board.
-
-        :param widget: The widget from which this was called.
-        :return: None
-        """
-
-        gerb = self.collection.get_active()
-        gerb.read_form()
-        name = gerb.options["name"] + "_cutout"
-
-        def geo_init(geo_obj, app_obj):
-            margin = gerb.options["cutoutmargin"] + gerb.options["cutouttooldia"]/2
-            gap_size = gerb.options["cutoutgapsize"] + gerb.options["cutouttooldia"]
-            minx, miny, maxx, maxy = gerb.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[app.get_radio_value({"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"})]
-            geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
-
-        # TODO: Check for None
-        self.new_object("geometry", name, geo_init)
-
-    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_generate_isolation(self, widget):
-        """
-        Callback for button on Gerber form to create isolation routing geometry.
-
-        :param widget: The widget from which this was called.
-        :return: None
-        """
-
-        gerb = self.collection.get_active()
-        gerb.read_form()
-        dia = gerb.options["isotooldia"]
-        passes = int(gerb.options["isopasses"])
-        overlap = gerb.options["isooverlap"] * dia
-
-        for i in range(passes):
-
-            offset = (2*i + 1)/2.0 * dia - i*overlap
-            iso_name = gerb.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"] = gerb.options["isotooldia"]
-
-                geo_obj.solid_geometry = gerb.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.new_object("geometry", iso_name, iso_init)
-
-    def on_generate_cncjob(self, widget):
-        """
-        Callback for button on geometry form to generate CNC job.
-
-        :param widget: The widget from which this was called.
-        :return: None
-        """
-
-        source_geo = self.collection.get_active()
-        source_geo.read_form()
-        job_name = source_geo.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"] = source_geo.options["cnctooldia"]
-
-            GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
-            job_obj.z_cut = source_geo.options["cutz"]
-            job_obj.z_move = source_geo.options["travelz"]
-            job_obj.feedrate = source_geo.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(source_geo, 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, ""))
-
-        # Send to worker
-        self.worker.add_task(job_thread, [self])
-
-    def on_generate_paintarea(self, widget):
-        """
-        Callback for button on geometry form.
-        Subscribes to the "Click on plot" event and continues
-        after the click. Finds the polygon containing
-        the clicked point and runs clear_poly() on it, resulting
-        in a new FlatCAMGeometry object.
-
-        :param widget: The  widget from which this was called.
-        :return: None
-        """
-
-        self.info("Click inside the desired polygon.")
-        geo = self.collection.get_active()
-        geo.read_form()
-        assert isinstance(geo, FlatCAMGeometry)
-        tooldia = geo.options["painttooldia"]
-        overlap = geo.options["paintoverlap"]
-
-        # Connection ID for the click event
-        subscription = None
-
-        # To be called after clicking on the plot.
-        def doit(event):
-            #self.plot_click_subscribers.pop("generate_paintarea")
-            self.plotcanvas.mpl_disconnect(subscription)
-            self.info("Painting")
-            point = [event.xdata, event.ydata]
-            poly = find_polygon(geo.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(-geo.options["paintmargin"]), tooldia, overlap)
-                geo_obj.solid_geometry = cp
-                geo_obj.options["cnctooldia"] = tooldia
-
-            #name = self.selected_item_name + "_paint"
-            name = geo.options["name"] + "_paint"
-            self.new_object("geometry", name, gen_paintarea)
-
-        #self.plot_click_subscribers["generate_paintarea"] = doit
-        subscription = self.plotcanvas.mpl_connect('button_press_event', doit)
-
-    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.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
-        """
-
-        self.collection.get_active().read_form()
-
-        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_activate_name(self, entry):
-        """
-        Hitting 'Enter' after changing the name of an item
-        updates the item dictionary and re-builds the item list.
-
-        :param entry: The widget from which this was called.
-        :return: None
-        """
-
-        old_name = copy.copy(self.collection.get_active().options["name"])
-        new_name = entry.get_text()
-        self.collection.change_name(old_name, new_name)
-        self.info("Name changed from %s to %s" % (old_name, new_name))
-
-    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
-
-        # GUI things
-        def task():
-            # Clear plot
-            self.plotcanvas.clear()
-
-            # Delete data
-            self.collection.delete_all()
-
-            # Clear object 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.window,
-                                       Gtk.FileChooserAction.OPEN,
-                                       (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
-                                        Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
-        response = dialog.run()
-        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()
-
-    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
-        """
-
-        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.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.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:
-            print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
-                event.button, event.x, event.y, event.xdata, event.ydata)
-
-            # TODO: This custom subscription mechanism is probably not necessary.
-            # for subscriber in self.plot_click_subscribers:
-            #     self.plot_click_subscribers[subscriber](event)
-
-            self.clipboard.set_text("(%.4f, %.4f)" % (event.xdata, event.ydata), -1)
-
-        except Exception, e:
-            print "Outside plot!"
-
-    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
-            print "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.copy(self.point1)
-                self.point1 = (event.xdata, event.ydata)
-            self.on_move(event)
-
-
-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()
-        self.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
-        """
-
-        print "PC.adjust_axes()"
-
-        width = xmax - xmin
-        height = ymax - ymin
-        try:
-            r = width / height
-        except:
-            print "ERROR: Height is", 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 + ymin) / 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]
-
-
-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):
-        print "OC.delete_all()"
-        # self.collection = []
-        # self.active = None
-        self.store.clear()
-
-    def delete_active(self):
-        print "OC.delete_active()"
-        try:
-            model, treeiter = self.tree_selection.get_selected()
-            self.store.remove(treeiter)
-        except:
-            pass
-
-    def on_row_activated(self, *args):
-        """
-        Does nothing right now.
-        :param args: Ignored.
-        :return: None
-        """
-        print "OC.on_row_activated()"
-        return
-
-    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
-        """
-        print "OC.on_list_selection_change()"
-        try:
-            self.get_active().build_ui()
-        except:
-            pass
-
-        # TODO: Now we don't have a reference to the previously
-        # TODO: active, so cannot read form.
-
-    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
-        """
-        print "OC.set_active()"
-        self.set_list_selection(name)
-
-    def get_active(self):
-        print "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
-        """
-        print "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
-        """
-        print "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
-        """
-        print "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
-        """
-        print "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:
-                print "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
-        """
-        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
-        """
-        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
-        """
-        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
-
+from gi.repository import Gtk
+from FlatCAMApp import *
 
 app = App()
 Gtk.main()

Plik diff jest za duży
+ 0 - 2588
FlatCAM.ui


+ 3025 - 0
FlatCAMApp.py

@@ -0,0 +1,3025 @@
+import threading
+import traceback
+import sys
+import urllib
+import copy
+import random
+import logging
+
+from gi.repository import Gtk, GdkPixbuf, GObject, Gdk
+from matplotlib.figure import Figure
+from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
+from shapely import speedups
+
+
+########################################
+##      Imports part of FlatCAM       ##
+########################################
+from FlatCAMWorker import Worker
+from ObjectCollection import *
+from FlatCAMObj 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)
+    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.
+        GObject.threads_init()
+
+        # GLib.log_set_handler()
+
+        #### GUI ####
+        # Glade init
+        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.setup_project_list()  # The "Project" tab
+        self.setup_component_editor()  # The "Selected" tab
+
+        ## Setup the toolbar. Adds buttons.
+        self.setup_toolbar()
+
+        #### Event handling ####
+        self.builder.connect_signals(self)
+
+        #### Make plot area ####
+        self.plotcanvas = PlotCanvas(self.grid)
+        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)
+        # 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 = {
+            "units": "in"
+        }
+        self.defaults_form = GlobalOptionsUI()
+
+        ## Current Project ##
+        self.options = {}  # Project options
+        self.project_filename = None
+        self.options_form = GlobalOptionsUI()
+
+        self.options_box.pack_start(self.defaults_form, False, False, 1)
+
+        # self.form_kinds = {
+        #     "units": "radio"
+        # }
+
+        # self.radios = {"units": {"rb_inch": "IN", "rb_mm": "MM"},
+        #                "gerber_gaps": {"rb_app_2tb": "tb", "rb_app_2lr": "lr", "rb_app_4": "4"}}
+        # self.radios_inv = {"units": {"IN": "rb_inch", "MM": "rb_mm"},
+        #                    "gerber_gaps": {"tb": "rb_app_2tb", "lr": "rb_app_2lr", "4": "rb_app_4"}}
+
+        # Options for each kind of FlatCAMObj.
+        # Example: 'gerber_plot': 'cb'. The widget name would be: 'cb_app_gerber_plot'
+        # for FlatCAMClass in [FlatCAMExcellon, FlatCAMGeometry, FlatCAMGerber, FlatCAMCNCjob]:
+        #     obj = FlatCAMClass("no_name")
+        #     for option in obj.form_kinds:
+        #         self.form_kinds[obj.kind + "_" + option] = obj.form_kinds[option]
+        #         # if obj.form_kinds[option] == "radio":
+        #         #     self.radios.update({obj.kind + "_" + option: obj.radios[option]})
+        #         #     self.radios_inv.update({obj.kind + "_" + option: obj.radios_inv[option]})
+
+        ## Event subscriptions ##
+
+        ## Tools ##
+        self.measure = Measurement(self.builder.get_object("box39"), 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)
+
+        #### Initialization ####
+        self.load_defaults()
+        self.options.update(self.defaults)  # Copy app defaults to project options
+        # self.options2form()  # Populate the app defaults form
+        self.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 = 4
+        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 4 UNSTABLE")
+        self.window.set_default_size(900, 600)
+        self.window.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.window, 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)
+
+        # 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)
+
+        # 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)
+
+        # 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)
+
+        # 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)
+
+        # 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)
+
+    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")
+
+        # Remove anything else in the box
+        box_children = box_selected.get_children()
+        for child in box_children:
+            box_selected.remove(child)
+
+        box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
+        label1 = Gtk.Label("Choose an item from Project")
+        box1.pack_start(label1, True, False, 1)
+        box_selected.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)
+        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.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):
+        """
+        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()")
+
+        ### Check for existing name
+        if name in self.collection.get_names():
+            ## Create a new name
+            # Ends with number?
+            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"
+
+        # 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=True)
+
+        # Show object details now.
+        GLib.idle_add(lambda: self.notebook.set_current_page(1))
+
+        # Plot
+        # TODO: (Thread-safe?)
+        obj.plot()
+
+        GLib.idle_add(lambda: self.on_zoom_fit(None))
+        #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)
+        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 read_form(self):
+        """
+        Reads the options form into self.defaults/self.options.
+
+        :return: None
+        :rtype: None
+        """
+        combo_sel = self.combo_options.get_active()
+        options_set = [self.options, self.defaults][combo_sel]
+        for option in options_set:
+            self.read_form_item(option, options_set)
+
+    def read_form_item(self, name, dest):
+        """
+        Reads the value of a form item in the defaults/options form and
+        saves it to the corresponding dictionary.
+
+        :param name: Name of the form item. A key in ``self.defaults`` or
+            ``self.options``.
+        :type name: str
+        :param dest: Dictionary to which to save the value.
+        :type dest: dict
+        :return: None
+        """
+        fkind = self.form_kinds[name]
+        fname = fkind + "_" + "app" + "_" + name
+
+        if fkind == 'entry_text':
+            dest[name] = self.builder.get_object(fname).get_text()
+            return
+        if fkind == 'entry_eval':
+            dest[name] = self.get_eval(fname)
+            return
+        if fkind == 'cb':
+            dest[name] = self.builder.get_object(fname).get_active()
+            return
+        if fkind == 'radio':
+            dest[name] = self.get_radio_value(self.radios[name])
+            return
+        print "Unknown kind of form item:", fkind
+
+    # def options2form(self):
+    #     """
+    #     Sets the 'Project Options' or 'Application Defaults' form with values from
+    #     ``self.options`` or ``self.defaults``.
+    #
+    #     :return: None
+    #     :rtype: None
+    #     """
+    #
+    #     # Set the on-change callback to do nothing while we do the changes.
+    #     self.options_update_ignore = True
+    #     self.toggle_units_ignore = True
+    #
+    #     combo_sel = self.combo_options.get_active()
+    #     options_set = [self.options, self.defaults][combo_sel]
+    #     for option in options_set:
+    #         self.set_form_item(option, options_set[option])
+    #
+    #     self.options_update_ignore = False
+    #     self.toggle_units_ignore = False
+
+    def set_form_item(self, name, value):
+        """
+        Sets a form item 'name' in the GUI with the given 'value'. The syntax of
+        form names in the GUI is <kind>_app_<name>, where kind is one of: rb (radio button),
+        cb (check button), entry_eval or entry_text (entry), combo (combo box). name is
+        whatever name it's been given. For self.defaults, name is a key in the dictionary.
+
+        :param name: Name of the form field.
+        :type name: str
+        :param value: The value to set the form field to.
+        :type value: Depends on field kind.
+        :return: None
+        """
+        if name not in self.form_kinds:
+            print "WARNING: Tried to set unknown option/form item:", name
+            return
+        fkind = self.form_kinds[name]
+        fname = fkind + "_" + "app" + "_" + name
+        if fkind == 'entry_eval' or fkind == 'entry_text':
+            try:
+                self.builder.get_object(fname).set_text(str(value))
+            except:
+                print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
+            return
+        if fkind == 'cb':
+            try:
+                self.builder.get_object(fname).set_active(value)
+            except:
+                print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
+            return
+        if fkind == 'radio':
+            try:
+                self.builder.get_object(self.radios_inv[name][value]).set_active(True)
+            except:
+                print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
+            return
+        print "Unknown kind of form item:", fkind
+
+    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:
+            print "ERROR: Failed to open file for saving:", filename
+            return
+
+        try:
+            json.dump(d, f, default=to_dict)
+        except:
+            print "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
+        """
+
+        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
+        for obj in d['objs']:
+            def obj_init(obj_inst, app_inst):
+                obj_inst.from_dict(obj)
+            self.new_object(obj['kind'], obj['options']['name'], obj_init)
+
+        self.info("Project loaded from: " + filename)
+
+    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
+        """
+        print "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 setup_tooltips(self):
+    #     tooltips = {
+    #         "cb_gerber_plot": "Plot this object on the main window.",
+    #         # "cb_gerber_mergepolys": "Show overlapping polygons as single.",
+    #         "cb_gerber_solid": "Paint inside polygons.",
+    #         "cb_gerber_multicolored": "Draw polygons with different colors."
+    #     }
+    #
+    #     for widget in tooltips:
+    #         self.builder.get_object(widget).set_tooltip_markup(tooltips[widget])
+
+    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 i != app_obj.selected_item_name or not except_current:
+                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
+        """
+        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_offset_object(self, widget):
+    #     """
+    #     Offsets the object's geometry by the vector specified
+    #     in the form. Re-plots.
+    #
+    #     :param widget: Ignored
+    #     :return: None
+    #     """
+    #
+    #     obj = self.collection.get_active()
+    #     obj.read_form()
+    #     assert isinstance(obj, FlatCAMObj)
+    #     try:
+    #         vect = self.get_eval("entry_eval_" + obj.kind + "_offset")
+    #     except:
+    #         self.info("ERROR: Vector is not in (x, y) format.")
+    #         return
+    #     assert isinstance(obj, Geometry)
+    #     obj.offset(vect)
+    #     obj.plot()
+    #     return
+
+    # def on_cb_plot_toggled(self, widget):
+    #     """
+    #     Callback for toggling the "Plot" checkbox. Re-plots.
+    #
+    #     :param widget: Ignored.
+    #     :return: None
+    #     """
+    #
+    #     self.collection.get_active().read_form()
+    #     self.collection.get_active().plot()
+
+    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)
+        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
+
+        combo_sel = self.combo_options.get_active()
+        options_set = [self.options, self.defaults][combo_sel]
+
+        # 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:
+                options_set[dim] *= sfactor
+
+        # The scaling factor depending on choice of units.
+        factor = 1/25.4
+        if self.builder.get_object('rb_mm').get_active():
+            factor = 25.4
+
+        # App units. Convert without warning.
+        if combo_sel == 1:
+            self.read_form()
+            scale_options(factor)
+            self.options2form()
+            return
+
+        # 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:
+            #print "Converting units..."
+            #print "Converting options..."
+            self.read_form()
+            scale_options(factor)
+            self.options2form()
+            for obj in self.collection.get_list():
+                units = self.get_radio_value({"rb_mm": "MM", "rb_inch": "IN"})
+                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.builder.get_object('rb_mm').get_active():
+                self.builder.get_object('rb_inch').set_active(True)
+            else:
+                self.builder.get_object('rb_mm').set_active(True)
+            self.toggle_units_ignore = False
+
+        self.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)
+
+        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.options.update(self.defaults)
+        self.options2form()  # Update UI
+
+    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.defaults.update(self.options)
+        self.options2form()  # Update UI
+
+    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
+        """
+
+        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.options2form()  # Update UI
+
+    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.options2form()  # Update UI
+
+    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
+        """
+
+        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:
+            self.info("ERROR: Could not load defaults file.")
+            return
+
+        try:
+            defaults = json.loads(options)
+        except:
+            e = sys.exc_info()[0]
+            print e
+            self.info("ERROR: Failed to parse defaults file.")
+            return
+
+        # Update options
+        assert isinstance(defaults, dict)
+        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.combo_options.get_active()
+        print "Options --> ", combo_sel
+
+        # Remove anything else in the box
+        box_children = self.options_box.get_children()
+        for child in box_children:
+            self.options_box.remove(child)
+
+        form = [self.options_form, self.defaults_form][combo_sel]
+        self.options_box.pack_start(form, False, False, 1)
+        form.show_all()
+
+        # self.options2form()
+
+    def on_options_update(self, widget):
+        """
+        Called whenever a value in the options/defaults form changes.
+        All values are updated. Can be inhibited by setting ``self.options_update_ignore = True``,
+        which may be necessary when updating the UI from code and not by the user.
+
+        :param widget: The widget from which this was called. Ignore.
+        :return: None
+        """
+
+        if self.options_update_ignore:
+            return
+        self.read_form()
+
+    # def on_scale_object(self, widget):
+    #     """
+    #     Callback for request to change an objects geometry scale. The object
+    #     is re-scaled and replotted.
+    #
+    #     :param widget: Ignored.
+    #     :return: None
+    #     """
+    #
+    #     obj = self.collection.get_active()
+    #     factor = self.get_eval("entry_eval_" + obj.kind + "_scalefactor")
+    #     obj.scale(factor)
+    #     obj.to_form()
+    #     self.on_update_plot(None)
+
+    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)
+
+    # def on_generate_gerber_bounding_box(self, widget):
+    #     """
+    #     Callback for request from the Gerber form to generate a bounding box for the
+    #     geometry in the object. Creates a FlatCAMGeometry with the bounding box.
+    #     The box can have rounded corners if specified in the form.
+    #
+    #     :param widget: Ignored.
+    #     :return: None
+    #     """
+    #     # TODO: Use Gerber.get_bounding_box(...)
+    #     gerber = self.collection.get_active()
+    #     gerber.read_form()
+    #     name = gerber.options["name"] + "_bbox"
+    #
+    #     def geo_init(geo_obj, app_obj):
+    #         assert isinstance(geo_obj, FlatCAMGeometry)
+    #         # Bounding box with rounded corners
+    #         bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["bboxmargin"])
+    #         if not gerber.options["bboxrounded"]:  # Remove rounded corners
+    #             bounding_box = bounding_box.envelope
+    #         geo_obj.solid_geometry = bounding_box
+    #
+    #     self.new_object("geometry", name, geo_init)
+
+    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_generate_excellon_cncjob(self, widget):
+        """
+        Callback for button active/click on Excellon form to
+        create a CNC Job for the Excellon file.
+
+        :param widget: Ignored
+        :return: None
+        """
+
+        excellon = self.collection.get_active()
+        excellon.read_form()
+        job_name = excellon.options["name"] + "_cnc"
+
+        # Object initialization function for app.new_object()
+        def job_init(job_obj, app_obj):
+            # excellon_ = self.get_current()
+            # assert isinstance(excellon_, FlatCAMExcellon)
+            assert isinstance(job_obj, FlatCAMCNCjob)
+
+            GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
+            job_obj.z_cut = excellon.options["drillz"]
+            job_obj.z_move = excellon.options["travelz"]
+            job_obj.feedrate = excellon.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(excellon, excellon.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.worker.add_task(job_thread, [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_gerber_generate_noncopper(self, widget):
+    #     """
+    #     Callback for button on Gerber form to create a geometry object
+    #     with polygons covering the area without copper or negative of the
+    #     Gerber.
+    #
+    #     :param widget: The widget from which this was called.
+    #     :return: None
+    #     """
+    #
+    #     gerb = self.collection.get_active()
+    #     gerb.read_form()
+    #     name = gerb.options["name"] + "_noncopper"
+    #
+    #     def geo_init(geo_obj, app_obj):
+    #         assert isinstance(geo_obj, FlatCAMGeometry)
+    #         bounding_box = gerb.solid_geometry.envelope.buffer(gerb.options["noncoppermargin"])
+    #         if not gerb.options["noncopperrounded"]:
+    #             bounding_box = bounding_box.envelope
+    #         non_copper = bounding_box.difference(gerb.solid_geometry)
+    #         geo_obj.solid_geometry = non_copper
+    #
+    #     # TODO: Check for None
+    #     self.new_object("geometry", name, geo_init)
+
+    # def on_gerber_generate_cutout(self, widget):
+    #     """
+    #     Callback for button on Gerber form to create geometry with lines
+    #     for cutting off the board.
+    #
+    #     :param widget: The widget from which this was called.
+    #     :return: None
+    #     """
+    #
+    #     gerb = self.collection.get_active()
+    #     gerb.read_form()
+    #     name = gerb.options["name"] + "_cutout"
+    #
+    #     def geo_init(geo_obj, app_obj):
+    #         margin = gerb.options["cutoutmargin"] + gerb.options["cutouttooldia"]/2
+    #         gap_size = gerb.options["cutoutgapsize"] + gerb.options["cutouttooldia"]
+    #         minx, miny, maxx, maxy = gerb.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[app.get_radio_value({"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"})]
+    #         geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
+    #
+    #     # TODO: Check for None
+    #     self.new_object("geometry", name, geo_init)
+
+    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_generate_isolation(self, widget):
+    #     """
+    #     Callback for button on Gerber form to create isolation routing geometry.
+    #
+    #     :param widget: The widget from which this was called.
+    #     :return: None
+    #     """
+    #
+    #     gerb = self.collection.get_active()
+    #     gerb.read_form()
+    #     dia = gerb.options["isotooldia"]
+    #     passes = int(gerb.options["isopasses"])
+    #     overlap = gerb.options["isooverlap"] * dia
+    #
+    #     for i in range(passes):
+    #
+    #         offset = (2*i + 1)/2.0 * dia - i*overlap
+    #         iso_name = gerb.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"] = gerb.options["isotooldia"]
+    #
+    #             geo_obj.solid_geometry = gerb.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.new_object("geometry", iso_name, iso_init)
+
+    # def on_generate_cncjob(self, widget):
+    #     """
+    #     Callback for button on geometry form to generate CNC job.
+    #
+    #     :param widget: The widget from which this was called.
+    #     :return: None
+    #     """
+    #
+    #     source_geo = self.collection.get_active()
+    #     source_geo.read_form()
+    #     job_name = source_geo.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"] = source_geo.options["cnctooldia"]
+    #
+    #         GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
+    #         job_obj.z_cut = source_geo.options["cutz"]
+    #         job_obj.z_move = source_geo.options["travelz"]
+    #         job_obj.feedrate = source_geo.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(source_geo, 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, ""))
+    #
+    #     # Send to worker
+    #     self.worker.add_task(job_thread, [self])
+
+    # def on_generate_paintarea(self, widget):
+    #     """
+    #     Callback for button on geometry form.
+    #     Subscribes to the "Click on plot" event and continues
+    #     after the click. Finds the polygon containing
+    #     the clicked point and runs clear_poly() on it, resulting
+    #     in a new FlatCAMGeometry object.
+    #
+    #     :param widget: The  widget from which this was called.
+    #     :return: None
+    #     """
+    #
+    #     self.info("Click inside the desired polygon.")
+    #     geo = self.collection.get_active()
+    #     geo.read_form()
+    #     assert isinstance(geo, FlatCAMGeometry)
+    #     tooldia = geo.options["painttooldia"]
+    #     overlap = geo.options["paintoverlap"]
+    #
+    #     # Connection ID for the click event
+    #     subscription = None
+    #
+    #     # To be called after clicking on the plot.
+    #     def doit(event):
+    #         #self.plot_click_subscribers.pop("generate_paintarea")
+    #         self.plotcanvas.mpl_disconnect(subscription)
+    #         self.info("Painting")
+    #         point = [event.xdata, event.ydata]
+    #         poly = find_polygon(geo.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(-geo.options["paintmargin"]), tooldia, overlap)
+    #             geo_obj.solid_geometry = cp
+    #             geo_obj.options["cnctooldia"] = tooldia
+    #
+    #         #name = self.selected_item_name + "_paint"
+    #         name = geo.options["name"] + "_paint"
+    #         self.new_object("geometry", name, gen_paintarea)
+    #
+    #     #self.plot_click_subscribers["generate_paintarea"] = doit
+    #     subscription = self.plotcanvas.mpl_connect('button_press_event', doit)
+
+    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.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
+        """
+
+        self.collection.get_active().read_form()
+
+        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_activate_name(self, entry):
+    #     """
+    #     Hitting 'Enter' after changing the name of an item
+    #     updates the item dictionary and re-builds the item list.
+    #
+    #     :param entry: The widget from which this was called.
+    #     :return: None
+    #     """
+    #
+    #     old_name = copy.copy(self.collection.get_active().options["name"])
+    #     new_name = entry.get_text()
+    #     self.collection.change_name(old_name, new_name)
+    #     self.info("Name changed from %s to %s" % (old_name, new_name))
+
+    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
+
+        # GUI things
+        def task():
+            # Clear plot
+            self.plotcanvas.clear()
+
+            # Delete data
+            self.collection.delete_all()
+
+            # Clear object 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.window,
+                                       Gtk.FileChooserAction.OPEN,
+                                       (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+                                        Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
+        response = dialog.run()
+        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()
+
+    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
+        """
+
+        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.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.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:
+            print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
+                event.button, event.x, event.y, event.xdata, event.ydata)
+
+            # TODO: This custom subscription mechanism is probably not necessary.
+            # for subscriber in self.plot_click_subscribers:
+            #     self.plot_click_subscribers[subscriber](event)
+
+            self.clipboard.set_text("(%.4f, %.4f)" % (event.xdata, event.ydata), -1)
+
+        except Exception, e:
+            print "Outside plot!"
+
+    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
+            print "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.copy(self.point1)
+                self.point1 = (event.xdata, event.ydata)
+            self.on_move(event)
+
+
+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()
+        self.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
+        """
+
+        print "PC.adjust_axes()"
+
+        width = xmax - xmin
+        height = ymax - ymin
+        try:
+            r = width / height
+        except:
+            print "ERROR: Height is", 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 + ymin) / 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]

+ 401 - 94
FlatCAMObj.py

@@ -11,7 +11,32 @@ from gi.repository import Gdk
 from gi.repository import GLib
 from gi.repository import GObject
 
+import inspect  # TODO: Remove
+
+from FlatCAMApp import *
 from camlib import *
+from ObjectUI import *
+
+
+class LoudDict(dict):
+    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):
+        if self.silence:
+            return
+        self.callback = callback
+
+    def __setitem__(self, key, value):
+        super(LoudDict, self).__setitem__(key, value)
+        try:
+            if self.__getitem__(key) == value:
+                return
+        except KeyError:
+            pass
+        self.callback(key)
 
 
 ########################################
@@ -28,19 +53,63 @@ class FlatCAMObj(GObject.GObject, object):
     # The app should set this value.
     app = None
 
-    def __init__(self, name):
+    # name = GObject.property(type=str)
+
+    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)
 
-        self.options = {"name": name}
-        self.form_kinds = {"name": "entry_text"}  # Kind of form element for each option
+        # 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
@@ -89,6 +158,7 @@ class FlatCAMObj(GObject.GObject, object):
         :return: None
         :rtype: None
         """
+        print inspect.stack()[1][3], "--> FlatCAMObj.read_form()"
         for option in self.options:
             self.read_form_item(option)
 
@@ -100,27 +170,25 @@ class FlatCAMObj(GObject.GObject, object):
         :rtype: None
         """
 
+        self.muted_ui = True
+        print 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("box_selected")
+        box_selected = self.app.builder.get_object("vp_selected")
 
         # Remove anything else in the box
         box_children = box_selected.get_children()
         for child in box_children:
             box_selected.remove(child)
 
-        osw = self.app.builder.get_object("offscrwindow_" + self.kind)  # offscreenwindow
-        sw = self.app.builder.get_object("sw_" + self.kind)  # scrollwindows
-        osw.remove(sw)  # TODO: Is this needed ?
-        vp = self.app.builder.get_object("vp_" + self.kind)  # Viewport
-        vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
-
         # Put in the UI
-        box_selected.pack_start(sw, True, True, 0)
-
-        # entry_name = self.app.builder.get_object("entry_text_" + self.kind + "_name")
-        # entry_name.connect("activate", self.app.on_activate_name)
+        # box_selected.pack_start(sw, True, True, 0)
+        box_selected.add(self.ui)
         self.to_form()
-        sw.show()
+        box_selected.show_all()
+        self.ui.show()
+        self.muted_ui = False
 
     def set_form_item(self, option):
         """
@@ -130,19 +198,11 @@ class FlatCAMObj(GObject.GObject, object):
         :type option: str
         :return: None
         """
-        fkind = self.form_kinds[option]
-        fname = fkind + "_" + self.kind + "_" + option
 
-        if fkind == 'entry_eval' or fkind == 'entry_text':
-            self.app.builder.get_object(fname).set_text(str(self.options[option]))
-            return
-        if fkind == 'cb':
-            self.app.builder.get_object(fname).set_active(self.options[option])
-            return
-        if fkind == 'radio':
-            self.app.builder.get_object(self.radios_inv[option][self.options[option]]).set_active(True)
-            return
-        print "Unknown kind of form item:", fkind
+        try:
+            self.form_fields[option].set_value(self.options[option])
+        except KeyError:
+            App.log.warn("Tried to set an option or field that does not exist: %s" % option)
 
     def read_form_item(self, option):
         """
@@ -152,22 +212,11 @@ class FlatCAMObj(GObject.GObject, object):
         :type option: str
         :return: None
         """
-        fkind = self.form_kinds[option]
-        fname = fkind + "_" + self.kind + "_" + option
 
-        if fkind == 'entry_text':
-            self.options[option] = self.app.builder.get_object(fname).get_text()
-            return
-        if fkind == 'entry_eval':
-            self.options[option] = self.app.get_eval(fname)
-            return
-        if fkind == 'cb':
-            self.options[option] = self.app.builder.get_object(fname).get_active()
-            return
-        if fkind == 'radio':
-            self.options[option] = self.app.get_radio_value(self.radios[option])
-            return
-        print "Unknown kind of form item:", fkind
+        try:
+            self.options[option] = self.form_fields[option].get_value()
+        except KeyError:
+            App.log.warning("Failed to read option from field: %s" % option)
 
     def plot(self):
         """
@@ -221,14 +270,31 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
     def __init__(self, name):
         Gerber.__init__(self)
-        FlatCAMObj.__init__(self, name)
+        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,
-            "mergepolys": True,
             "multicolored": False,
             "solid": False,
             "isotooldia": 0.016,
@@ -244,33 +310,137 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             "bboxrounded": False
         })
 
-        # The 'name' is already in self.form_kinds from FlatCAMObj
-        self.form_kinds.update({
-            "plot": "cb",
-            "mergepolys": "cb",
-            "multicolored": "cb",
-            "solid": "cb",
-            "isotooldia": "entry_eval",
-            "isopasses": "entry_eval",
-            "isooverlap": "entry_eval",
-            "cutouttooldia": "entry_eval",
-            "cutoutmargin": "entry_eval",
-            "cutoutgapsize": "entry_eval",
-            "gaps": "radio",
-            "noncoppermargin": "entry_eval",
-            "noncopperrounded": "cb",
-            "bboxmargin": "entry_eval",
-            "bboxrounded": "cb"
-        })
-
-        self.radios = {"gaps": {"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"}}
-        self.radios_inv = {"gaps": {"tb": "rb_2tb", "lr": "rb_2lr", "4": "rb_4"}}
-
         # 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
@@ -354,10 +524,20 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
 
     def __init__(self, name):
         Excellon.__init__(self)
-        FlatCAMObj.__init__(self, name)
+        FlatCAMObj.__init__(self, name, ExcellonObjectUI())
 
         self.kind = "excellon"
 
+        self.form_fields.update({
+            "name": self.ui.name_entry,
+            "plot": self.ui.plot_cb,
+            "solid": self.ui.solid_cb,
+            "drillz": self.ui.cutz_entry,
+            "travelz": self.ui.travelz_entry,
+            "feedrate": self.ui.feedrate_entry,
+            "toolselection": self.ui.tools_entry
+        })
+
         self.options.update({
             "plot": True,
             "solid": False,
@@ -367,14 +547,14 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             "toolselection": ""
         })
 
-        self.form_kinds.update({
-            "plot": "cb",
-            "solid": "cb",
-            "drillz": "entry_eval",
-            "travelz": "entry_eval",
-            "feedrate": "entry_eval",
-            "toolselection": "entry_text"
-        })
+        # self.form_kinds.update({
+        #     "plot": "cb",
+        #     "solid": "cb",
+        #     "drillz": "entry_eval",
+        #     "travelz": "entry_eval",
+        #     "feedrate": "entry_eval",
+        #     "toolselection": "entry_text"
+        # })
 
         # TODO: Document this.
         self.tool_cbs = {}
@@ -384,6 +564,23 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         # 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.solid_cb.connect('clicked', self.on_solid_cb_click)
+        self.ui.solid_cb.connect('activate', self.on_solid_cb_click)
+
+    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)
 
@@ -457,7 +654,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
                  feedrate=3.0, z_cut=-0.002, tooldia=0.0):
         CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
                         feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
-        FlatCAMObj.__init__(self, name)
+        FlatCAMObj.__init__(self, name, CNCObjectUI())
 
         self.kind = "cncjob"
 
@@ -466,16 +663,31 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
             "tooldia": 0.4 / 25.4  # 0.4mm in inches
         })
 
-        self.form_kinds.update({
-            "plot": "cb",
-            "tooldia": "entry_eval"
+        self.form_fields.update({
+            "name": self.ui.name_entry,
+            "plot": self.ui.plot_cb,
+            "tooldia": self.ui.tooldia_entry
         })
 
+        # self.form_kinds.update({
+        #     "plot": "cb",
+        #     "tooldia": "entry_eval"
+        # })
+
         # 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)
+
+    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
@@ -501,15 +713,29 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
     """
 
     def __init__(self, name):
-        FlatCAMObj.__init__(self, name)
+        FlatCAMObj.__init__(self, name, GeometryObjectUI())
         Geometry.__init__(self)
 
         self.kind = "geometry"
 
+        self.form_fields.update({
+            "name": self.ui.name_entry,
+            "plot": self.ui.plot_cb,
+            # "solid": self.ui.sol,
+            # "multicolored": self.ui.,
+            "cutz": self.ui.cutz_entry,
+            "travelz": self.ui.travelz_entry,
+            "feedrate": self.ui.cncfeedrate_entry,
+            "cnctooldia": self.ui.cnctooldia_entry,
+            "painttooldia": self.ui.painttooldia_entry,
+            "paintoverlap": self.ui.paintoverlap_entry,
+            "paintmargin": self.ui.paintmargin_entry
+        })
+
         self.options.update({
             "plot": True,
-            "solid": False,
-            "multicolored": False,
+            # "solid": False,
+            # "multicolored": False,
             "cutz": -0.002,
             "travelz": 0.1,
             "feedrate": 5.0,
@@ -519,24 +745,105 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             "paintmargin": 0.01
         })
 
-        self.form_kinds.update({
-            "plot": "cb",
-            "solid": "cb",
-            "multicolored": "cb",
-            "cutz": "entry_eval",
-            "travelz": "entry_eval",
-            "feedrate": "entry_eval",
-            "cnctooldia": "entry_eval",
-            "painttooldia": "entry_eval",
-            "paintoverlap": "entry_eval",
-            "paintmargin": "entry_eval"
-        })
+        # 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.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.

+ 143 - 0
GUIElements.py

@@ -0,0 +1,143 @@
+from gi.repository import Gtk
+import re
+from copy import copy
+
+
+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)
+
+    # def on_toggle(self, *args):
+    #     return
+
+    def get_value(self):
+        for choice in self.choices:
+            if choice['radio'].get_active():
+                return choice['value']
+        print "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
+        print "ERROR: Value given is not part of this RadioSet:", val
+
+
+class LengthEntry(Gtk.Entry):
+    def __init__(self, output_units='IN'):
+        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': {'MM': 1/25.4},
+            'MM': {'IN': 25.4}
+        }
+
+        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:
+            print "WARNING: Could not interpret entry:", self.get_text()
+
+    def get_value(self):
+        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(match.group(1))*self.scales[self.output_units][match.group(2).upper()]
+            else:
+                return float(match.group(1))
+        except:
+            print "ERROR: Could not parse value in entry:", 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:
+            print "WARNING: Could not interpret entry:", self.get_text()
+
+    def get_value(self):
+        raw = self.get_text().strip(' ')
+        try:
+            evaled = eval(raw)
+        except:
+            print "ERROR: Could not evaluate:", 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 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)

+ 254 - 0
ObjectCollection.py

@@ -0,0 +1,254 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://caram.cl/software/flatcam                         #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 4/20/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+from FlatCAMObj import *
+from gi.repository import Gtk, GdkPixbuf
+import inspect  # TODO: Remove
+
+
+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):
+        print "OC.delete_all()"
+        self.store.clear()
+
+    def delete_active(self):
+        print "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
+        """
+        print 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
+        """
+        print "OC.set_active()"
+        self.set_list_selection(name)
+
+    def get_active(self):
+        print 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
+        """
+        print 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
+        """
+        print 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
+        """
+        print "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
+        """
+        print "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:
+                print "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
+        """
+        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
+        """
+        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

+ 375 - 0
ObjectUI.py

@@ -0,0 +1,375 @@
+
+from gi.repository import Gtk
+import re
+from copy import copy
+
+from GUIElements import *
+
+
+class ObjectUI(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=True, 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.pack_start(self.scale_label, expand=True, fill=False, padding=2)
+
+        grid5 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid5, expand=True, fill=False, padding=2)
+
+        # Factor
+        l10 = Gtk.Label('Factor:', xalign=1)
+        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.pack_start(self.scale_button, expand=True, 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.pack_start(self.offset_label, expand=True, fill=False, padding=2)
+
+        grid6 = Gtk.Grid(column_spacing=3, row_spacing=2)
+        self.pack_start(grid6, expand=True, fill=False, padding=2)
+
+        # Vector
+        l11 = Gtk.Label('Offset Vector:', xalign=1)
+        grid6.attach(l11, 0, 0, 1, 1)
+        self.offsetvector_entry = FCEntry()
+        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.pack_start(self.offset_button, expand=True, 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):
+    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.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)
+
+        # Update plot button
+        self.updateplot_button = Gtk.Button(label='Update Plot')
+        self.pack_start(self.updateplot_button, expand=True, 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.pack_start(self.export_gcode_label, expand=True, fill=False, padding=2)
+
+        # GO Button
+        self.export_gcode_button = Gtk.Button(label='Export G-Code')
+        self.pack_start(self.export_gcode_button, expand=True, fill=False, padding=2)
+
+
+class GeometryObjectUI(ObjectUI):
+    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')
+        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.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)
+        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)
+
+        self.generate_cnc_button = Gtk.Button(label='Generate')
+        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.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)
+        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)
+
+        # GO Button
+        self.generate_paint_button = Gtk.Button(label='Generate')
+        self.custom_box.pack_start(self.generate_paint_button, expand=True, fill=False, padding=2)
+
+
+class ExcellonObjectUI(ObjectUI):
+    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')
+        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.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)
+        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)
+
+        l4 = Gtk.Label('Tools:', xalign=1)
+        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...')
+        boxt.pack_start(self.choose_tools_button, expand=True, fill=False, padding=2)
+
+        self.generate_cnc_button = Gtk.Button(label='Generate')
+        self.custom_box.pack_start(self.generate_cnc_button, expand=True, fill=False, padding=2)
+
+
+class GerberObjectUI(ObjectUI):
+    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')
+        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.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)
+        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)
+
+        self.generate_iso_button = Gtk.Button(label='Generate Geometry')
+        self.custom_box.pack_start(self.generate_iso_button, expand=True, fill=False, padding=2)
+
+        ## 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.custom_box.pack_start(self.isolation_routing_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)
+        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)
+
+        self.generate_cutout_button = Gtk.Button(label='Generate Geometry')
+        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.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)
+        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)
+
+        self.generate_noncopper_button = Gtk.Button(label='Generate Geometry')
+        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.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)
+        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)
+
+        self.generate_bb_button = Gtk.Button(label='Generate Geometry')
+        self.custom_box.pack_start(self.generate_bb_button, expand=True, fill=False, padding=2)

+ 28 - 90
camlib.py

@@ -554,40 +554,6 @@ class Gerber (Geometry):
     | others    | Depend on ``type``                |
     +-----------+-----------------------------------+
 
-    * ``paths`` (list): A path is described by a line an aperture that follows that
-      line. Each paths[i] is a dictionary:
-
-    +------------+------------------------------------------------+
-    | Key        | Value                                          |
-    +============+================================================+
-    | linestring | (Shapely.LineString) The actual path.          |
-    +------------+------------------------------------------------+
-    | aperture   | (str) The key for an aperture in apertures.    |
-    +------------+------------------------------------------------+
-
-    * ``flashes`` (list): Flashes are single-point strokes of an aperture. Each
-      is a dictionary:
-
-    +------------+------------------------------------------------+
-    | Key        | Value                                          |
-    +============+================================================+
-    | loc        | (Point) Shapely Point indicating location.     |
-    +------------+------------------------------------------------+
-    | aperture   | (str) The key for an aperture in apertures.    |
-    +------------+------------------------------------------------+
-
-    * ``regions`` (list): Are surfaces defined by a polygon (Shapely.Polygon),
-      which have an exterior and zero or more interiors. An aperture is also
-      associated with a region. Each is a dictionary:
-
-    +------------+-----------------------------------------------------+
-    | Key        | Value                                               |
-    +============+=====================================================+
-    | polygon    | (Shapely.Polygon) The polygon defining the region.  |
-    +------------+-----------------------------------------------------+
-    | aperture   | (str) The key for an aperture in apertures.         |
-    +------------+-----------------------------------------------------+
-
     * ``aperture_macros`` (dictionary): Are predefined geometrical structures
       that can be instanciated with different parameters in an aperture
       definition. See ``apertures`` above. The key is the name of the macro,
@@ -635,23 +601,6 @@ class Gerber (Geometry):
         #             ['size':float], ['width':float],
         #             ['height':float]}, ...}
         self.apertures = {}
-        
-        # Paths [{'linestring':LineString, 'aperture':str}]
-        # self.paths = []
-        
-        # Buffered Paths [Polygon]
-        # Paths transformed into Polygons by
-        # offsetting the aperture size/2
-        # self.buffered_paths = []
-        
-        # Polygon regions [{'polygon':Polygon, 'aperture':str}]
-        # self.regions = []
-        
-        # Flashes [{'loc':[float,float], 'aperture':str}]
-        # self.flashes = []
-        
-        # Geometry from flashes
-        # self.flash_geometry = []
 
         # Aperture Macros
         self.aperture_macros = {}
@@ -659,9 +608,8 @@ class Gerber (Geometry):
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # from Geometry.
-        self.ser_attrs += ['int_digits', 'frac_digits', 'apertures', 'paths',
-                           'buffered_paths', 'regions', 'flashes',
-                           'flash_geometry', 'aperture_macros']
+        self.ser_attrs += ['int_digits', 'frac_digits', 'apertures',
+                           'aperture_macros', 'solid_geometry']
 
         #### Parser patterns ####
         # FS - Format Specification
@@ -1432,8 +1380,8 @@ class Excellon(Geometry):
         
         self.drills = []
 
-        # Trailing "T" or leading "L"
-        self.zeros = ""
+        # Trailing "T" or leading "L" (default)
+        self.zeros = "L"
 
         # Attributes to be included in serialization
         # Always append to it because it carries contents
@@ -1504,6 +1452,9 @@ class Excellon(Geometry):
 
         # Various stop/pause commands
         self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))')
+
+        # Parse coordinates
+        self.leadingzeros_re = re.compile(r'^(0*)(\d*)')
         
     def parse_file(self, filename):
         """
@@ -1538,7 +1489,7 @@ class Excellon(Geometry):
         for eline in elines:
             line_num += 1
 
-            ### Cleanup
+            ### Cleanup lines
             eline = eline.strip(' \r\n')
 
             ## Header Begin/End ##
@@ -1563,13 +1514,15 @@ class Excellon(Geometry):
                 match = self.coordsnoperiod_re.search(eline)
                 if match:
                     try:
-                        x = float(match.group(1))/10000
+                        #x = float(match.group(1))/10000
+                        x = self.parse_number(match.group(1))
                         current_x = x
                     except TypeError:
                         x = current_x
 
                     try:
-                        y = float(match.group(2))/10000
+                        #y = float(match.group(2))/10000
+                        y = self.parse_number(match.group(2))
                         current_y = y
                     except TypeError:
                         y = current_y
@@ -1581,7 +1534,7 @@ class Excellon(Geometry):
                     self.drills.append({'point': Point((x, y)), 'tool': current_tool})
                     continue
 
-                ## Coordinates with period ##
+                ## Coordinates with period: Use literally. ##
                 match = self.coordsperiod_re.search(eline)
                 if match:
                     try:
@@ -1630,6 +1583,21 @@ class Excellon(Geometry):
 
             print "WARNING: Line ignored:", eline
         
+    def parse_number(self, number_str):
+        """
+        Parses coordinate numbers without period.
+
+        :param number_str: String representing the numerical value.
+        :type number_str: str
+        :return: Floating point representation of the number
+        :rtype: foat
+        """
+        if self.zeros == "L":
+            match = self.leadingzeros_re.search(number_str)
+            return float(number_str)/(10**(len(match.group(2))-2+len(match.group(1))))
+        else:  # Trailing
+            return float(number_str)/10000
+
     def create_geometry(self):
         """
         Creates circles of the tool diameter at every point
@@ -2443,33 +2411,3 @@ def parse_gerber_number(strnumber, frac_digits):
     """
     return int(strnumber)*(10**(-frac_digits))
 
-
-def parse_gerber_coords(gstr, int_digits, frac_digits):
-    """
-    Parse Gerber coordinates
-
-    :param gstr: Line of G-Code containing coordinates.
-    :type gstr: str
-    :param int_digits: Number of digits in integer part of a number.
-    :type int_digits: int
-    :param frac_digits: Number of digits in frac_digits part of a number.
-    :type frac_digits: int
-    :return: [x, y] coordinates.
-    :rtype: list
-    """
-    global gerbx, gerby
-    xindex = gstr.find("X")
-    yindex = gstr.find("Y")
-    index = gstr.find("D")
-    if xindex == -1:
-        x = gerbx
-        y = int(gstr[(yindex+1):index])*(10**(-frac_digits))
-    elif yindex == -1:
-        y = gerby
-        x = int(gstr[(xindex+1):index])*(10**(-frac_digits))
-    else:
-        x = int(gstr[(xindex+1):yindex])*(10**(-frac_digits))
-        y = int(gstr[(yindex+1):index])*(10**(-frac_digits))
-    gerbx = x
-    gerby = y
-    return [x, y]

+ 1 - 1
defaults.json

@@ -1 +1 @@
-{"gerber_noncopperrounded": false, "geometry_paintoverlap": 0.15, "geometry_plot": true, "excellon_feedrate": 5.0, "gerber_plot": true, "gerber_mergepolys": 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, "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, "geometry_painttooldia": 0.0625, "gerber_gaps": "4", "gerber_bboxmargin": 0.0, "cncjob_plot": true, "gerber_cutoutgapsize": 0.15, "gerber_isooverlap": 0.15, "gerber_bboxrounded": false, "geometry_multicolored": false, "gerber_noncoppermargin": 0.0, "geometry_solid": false}
+{"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, "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, "geometry_painttooldia": 0.0625, "gerber_gaps": "4", "gerber_bboxmargin": 0.0, "cncjob_plot": true, "gerber_cutoutgapsize": 0.15, "gerber_isooverlap": 0.15, "gerber_bboxrounded": false, "geometry_multicolored": false, "gerber_noncoppermargin": 0.0, "geometry_solid": false}

+ 2 - 2
doc/build/app.html

@@ -1434,8 +1434,8 @@ side of the main window.</p>
 </dd></dl>
 
 <dl class="method">
-<dt id="FlatCAM.App.versionCheck">
-<tt class="descname">versionCheck</tt><big>(</big><em>*args</em><big>)</big><a class="headerlink" href="#FlatCAM.App.versionCheck" title="Permalink to this definition">¶</a></dt>
+<dt id="FlatCAM.App.version_check">
+<tt class="descname">version_check</tt><big>(</big><em>*args</em><big>)</big><a class="headerlink" href="#FlatCAM.App.version_check" title="Permalink to this definition">¶</a></dt>
 <dd><p>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.</p>

+ 1 - 1
doc/build/genindex.html

@@ -1021,7 +1021,7 @@
 <table style="width: 100%" class="indextable genindextable"><tr>
   <td style="width: 33%" valign="top"><dl>
       
-  <dt><a href="app.html#FlatCAM.App.versionCheck">versionCheck() (FlatCAM.App method)</a>
+  <dt><a href="app.html#FlatCAM.App.version_check">version_check() (FlatCAM.App method)</a>
   </dt>
 
   </dl></td>

+ 1 - 1
recent.json

@@ -1 +1 @@
-[{"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\VNA\\KiCad_Bridge2\\KiCad_Bridge2-F_Cu.gtl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\WindMills - Bottom Copper 2.gbr"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\Example1_copper_bottom_Gndplane_modified.gbr"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\Example1_copper_bottom_Gndplane.gbr"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\CC_LOAD_7000164-00_REV_A_copper_top.gbr"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\bedini 7 coils capacitor discharge.gbr"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\Gerbers\\AVR_Transistor_Tester_copper_bottom.GBL"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\Gerbers\\AVR_Transistor_Tester_copper_top.GTL"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\PlacaReles-F_Cu.gtl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\maitest.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\\VNA\\KiCad_Bridge2\\KiCad_Bridge2.drl"}, {"kind": "excellon", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\LockController_v1.0_pcb-RoundHoles.TXT\\LockController_v1.0_pcb-RoundHoles.TXT"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\VNA\\KiCad_Bridge2\\KiCad_Bridge2-F_Cu.gtl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\WindMills - Bottom Copper 2.gbr"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\Example1_copper_bottom_Gndplane_modified.gbr"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\Example1_copper_bottom_Gndplane.gbr"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\CC_LOAD_7000164-00_REV_A_copper_top.gbr"}]

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików