diff --git a/cirkuix.py b/cirkuix.py index dbccd001..5b1f0151 100644 --- a/cirkuix.py +++ b/cirkuix.py @@ -1,6 +1,6 @@ import threading from gi.repository import Gtk, Gdk, GLib, GObject - +import simplejson as json from matplotlib.figure import Figure from numpy import arange, sin, pi @@ -54,10 +54,13 @@ class CirkuixObj: print "Clearing Axes" self.axes.cla() + self.axes.set_frame_on(False) + self.axes.set_xticks([]) + self.axes.set_yticks([]) self.axes.patch.set_visible(False) # No background self.axes.set_aspect(1) - return self.axes + #return self.axes def set_options(self, options): for name in options: @@ -150,6 +153,24 @@ class CirkuixObj: # self.axes.cla() # return + def serialize(self): + """ + Returns a representation of the object as a dictionary so + it can be later exported as JSON. Override this method. + @return: Dictionary representing the object + @rtype: dict + """ + return + + def deserialize(self, obj_dict): + """ + Re-builds an object from its serialized version. + @param obj_dict: Dictionary representing a CirkuixObj + @type obj_dict: dict + @return None + """ + return + class CirkuixGerber(CirkuixObj, Gerber): """ @@ -221,6 +242,12 @@ class CirkuixGerber(CirkuixObj, Gerber): x, y = ints.coords.xy self.axes.plot(x, y, linespec) + def serialize(self): + return { + "options": self.options, + "kind": self.kind + } + class CirkuixExcellon(CirkuixObj, Excellon): """ @@ -408,12 +435,11 @@ class App: """ Starts the application and the Gtk.main(). @return: app + @rtype: App """ # Needed to interact with the GUI from other threads. - #GLib.threads_init() GObject.threads_init() - #Gdk.threads_init() ## GUI ## self.gladefile = "cirkuix.ui" @@ -427,6 +453,7 @@ class App: 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") ## Event handling ## self.builder.connect_signals(self) @@ -449,8 +476,18 @@ class App: # a key if self.stuff self.selected_item_name = None + self.defaults = { + "units": "in" + } # Application defaults + self.options = {} # Project options + self.plot_click_subscribers = {} + # Initialization + self.load_defaults() + self.options.update(self.defaults) + self.units_label.set_text("[" + self.options["units"] + "]") + # For debugging only def someThreadFunc(self): print "Hello World!" @@ -460,6 +497,7 @@ class App: ######################################## ## START ## ######################################## + self.window.set_default_size(900, 600) self.window.show_all() #Gtk.main() @@ -478,7 +516,7 @@ class App: #t = arange(0.0,5.0,0.01) #s = sin(2*pi*t) #self.axes.plot(t,s) - self.axes.grid() + self.axes.grid(True) self.figure.patch.set_visible(False) self.canvas = FigureCanvas(self.figure) # a Gtk.DrawingArea @@ -491,6 +529,7 @@ class App: self.canvas.set_can_focus(True) # For key press self.canvas.mpl_connect('key_press_event', self.on_key_over_plot) #self.canvas.mpl_connect('scroll_event', self.on_scroll_over_plot) + self.canvas.connect("configure-event", self.on_canvas_configure) self.grid.attach(self.canvas, 0, 0, 600, 400) @@ -499,7 +538,7 @@ class App: def setup_component_viewer(self): """ - List or Tree where whatever has been loaded or created is + Sets up list or Tree where whatever has been loaded or created is displayed. @return: None """ @@ -507,6 +546,7 @@ class App: self.store = Gtk.ListStore(str) self.tree = Gtk.TreeView(self.store) #self.list = Gtk.ListBox() + self.tree.connect("row_activated", self.on_row_activated) self.tree_select = self.tree.get_selection() self.signal_id = self.tree_select.connect("changed", self.on_tree_selection_changed) renderer = Gtk.CellRendererText() @@ -531,8 +571,11 @@ class App: box1 = Gtk.Box(Gtk.Orientation.VERTICAL) label1 = Gtk.Label("Choose an item from Project") - box1.pack_start(label1, False, False, 1) + box1.pack_start(label1, True, False, 1) box_selected.pack_start(box1, True, True, 0) + #box_selected.show() + box1.show() + label1.show() def info(self, text): """ @@ -578,13 +621,18 @@ class App: def build_list(self): """ - Clears and re-populates the list of objects in tcurrently + Clears and re-populates the list of objects in currently in the project. @return: None """ + print "build_list(): clearing" + self.tree_select.unselect_all() self.store.clear() + print "repopulating...", for key in self.stuff: + print key, self.store.append([key]) + print def get_radio_value(self, radio_set): """ @@ -602,15 +650,29 @@ class App: Re-generates all plots from all objects. @return: None """ - self.clear_plots() - - for i in self.stuff: - self.stuff[i].plot(self.figure) - - self.on_zoom_fit(None) - self.axes.grid() - self.canvas.queue_draw() + self.set_progress_bar(0.1, "Re-plotting...") + + def thread_func(app_obj): + percentage = 0.1 + try: + delta = 0.9/len(self.stuff) + except ZeroDivisionError: + GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "")) + return + for i in self.stuff: + self.stuff[i].plot(self.figure) + percentage += delta + GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting...")) + + self.on_zoom_fit(None) + self.axes.grid(True) + self.canvas.queue_draw() + GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "")) + + t = threading.Thread(target=thread_func, args=(self,)) + t.daemon = True + t.start() def clear_plots(self): """ @@ -618,9 +680,12 @@ class App: @return: None """ + # TODO: Create a setup_axes method that gets called here and in setup_plot? self.axes.cla() self.figure.clf() self.figure.add_axes(self.axes) + self.axes.set_aspect(1) + self.axes.grid(True) self.canvas.queue_draw() def get_eval(self, widget_name): @@ -710,11 +775,88 @@ class App: self.progress_bar.set_fraction(percentage) return False + def save_project(self): + return + + def get_current(self): + """ + Returns the currently selected CirkuixObj in the application. + @return: Currently selected CirkuixObj in the application. + @rtype: CirkuixObj + """ + try: + return self.stuff[self.selected_item_name] + except: + return None + + def adjust_axes(self, xmin, ymin, xmax, ymax): + m_x = 15 # pixels + m_y = 25 # pixels + width = xmax-xmin + height = ymax-ymin + r = width/height + Fw, Fh = self.canvas.get_width_height() + Fr = float(Fw)/Fh + x_ratio = float(m_x)/Fw + y_ratio = float(m_y)/Fh + + if r > Fr: + ycenter = (ymin+ymax)/2.0 + newheight = height*r/Fr + ymin = ycenter-newheight/2.0 + ymax = ycenter+newheight/2.0 + else: + xcenter = (xmax+ymin)/2.0 + newwidth = width*Fr/r + xmin = xcenter-newwidth/2.0 + xmax = xcenter+newwidth/2.0 + + for name in self.stuff: + if self.stuff[name].axes is None: + continue + self.stuff[name].axes.set_xlim((xmin, xmax)) + self.stuff[name].axes.set_ylim((ymin, ymax)) + self.stuff[name].axes.set_position([x_ratio, y_ratio, + 1-2*x_ratio, 1-2*y_ratio]) + self.axes.set_xlim((xmin, xmax)) + self.axes.set_ylim((ymin, ymax)) + self.axes.set_position([x_ratio, y_ratio, + 1-2*x_ratio, 1-2*y_ratio]) + + self.canvas.queue_draw() + + def load_defaults(self): + 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: + self.info("ERROR: Failed to parse defaults file.") + return + self.defaults.update(defaults) + ######################################## ## EVENT HANDLERS ## ######################################## + + def on_canvas_configure(self, widget, event): + print "on_canvas_configure()" + + xmin, xmax = self.axes.get_xlim() + ymin, ymax = self.axes.get_ylim() + self.adjust_axes(xmin, ymin, xmax, ymax) + + def on_row_activated(self, widget, path, col): + self.notebook.set_current_page(1) + def on_generate_gerber_bounding_box(self, widget): - gerber = self.stuff[self.selected_item_name] + gerber = self.get_current() gerber.read_form() name = self.selected_item_name + "_bbox" @@ -734,8 +876,20 @@ class App: @param widget: The widget from which this was called. @return: None """ - self.stuff[self.selected_item_name].read_form() - self.stuff[self.selected_item_name].plot(self.figure) + print "Re-plotting" + + self.get_current().read_form() + self.set_progress_bar(0.5, "Plotting...") + #GLib.idle_add(lambda: self.set_progress_bar(0.5, "Plotting...")) + + def thread_func(app_obj): + #GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Plotting...")) + GLib.idle_add(lambda: app_obj.get_current().plot(app_obj.figure)) + GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "")) + + t = threading.Thread(target=thread_func, args=(self,)) + t.daemon = True + t.start() def on_generate_excellon_cncjob(self, widget): """ @@ -746,13 +900,13 @@ class App: """ job_name = self.selected_item_name + "_cnc" - excellon = self.stuff[self.selected_item_name] + excellon = self.get_current() assert isinstance(excellon, CirkuixExcellon) excellon.read_form() # Object initialization function for app.new_object() def job_init(job_obj, app_obj): - excellon_ = self.stuff[self.selected_item_name] + excellon_ = self.get_current() assert isinstance(excellon_, CirkuixExcellon) assert isinstance(job_obj, CirkuixCNCjob) @@ -791,13 +945,13 @@ class App: @param widget: The widget from which this was called. @return: None """ - excellon = self.stuff[self.selected_item_name] + excellon = self.get_current() assert isinstance(excellon, CirkuixExcellon) excellon.show_tool_chooser() def on_entry_eval_activate(self, widget): self.on_eval_update(widget) - obj = self.stuff[self.selected_item_name] + obj = self.get_current() assert isinstance(obj, CirkuixObj) obj.read_form() @@ -895,7 +1049,7 @@ class App: # TODO: Object must be updated on form change and the options # TODO: read from the object. tooldia = app_obj.get_eval("entry_eval_gerber_isotooldia") - geo_obj.solid_geometry = self.stuff[self.selected_item_name].isolation_geometry(tooldia/2.0) + geo_obj.solid_geometry = self.get_current().isolation_geometry(tooldia/2.0) # TODO: Do something if this is None. Offer changing name? self.new_object("geometry", iso_name, iso_init) @@ -953,7 +1107,7 @@ class App: in a new CirkuixGeometry object. """ self.info("Click inside the desired polygon.") - geo = self.stuff[self.selected_item_name] + geo = self.get_current() geo.read_form() tooldia = geo.options["painttooldia"] overlap = geo.options["paintoverlap"] @@ -979,7 +1133,7 @@ class App: def on_cncjob_exportgcode(self, widget): def on_success(self, filename): - cncjob = self.stuff[self.selected_item_name] + cncjob = self.get_current() f = open(filename, 'w') f.write(cncjob.gcode) f.close() @@ -987,15 +1141,22 @@ class App: self.file_chooser_save_action(on_success) def on_delete(self, widget): + """ + Delete the currently selected CirkuixObj. + @param widget: The widget from which this was called. + @return: + """ + print "on_delete():", self.selected_item_name + + # Remove plot + self.figure.delaxes(self.get_current().axes) + self.canvas.queue_draw() + + # Remove from dictionary self.stuff.pop(self.selected_item_name) - - #self.tree.get_selection().disconnect(self.signal_id) + + # Update UI self.build_list() # Update the items list - #self.signal_id = self.tree.get_selection().connect( - # "changed", self.on_tree_selection_changed) - - self.plot_all() - #self.notebook.set_current_page(1) def on_replot(self, widget): self.plot_all() @@ -1023,22 +1184,48 @@ class App: # Reconnect event listener self.signal_id = self.tree.get_selection().connect( "changed", self.on_tree_selection_changed) - + def on_tree_selection_changed(self, selection): + """ + Callback for selection change in the project list. This changes + the currently selected CirkuixObj. + @param selection: Selection associated to the project tree or list + @type selection: Gtk.TreeSelection + @return: None + """ + print "on_tree_selection_change(): ", model, treeiter = selection.get_selected() if treeiter is not None: + # Save data for previous selection + obj = self.get_current() + if obj is not None: + obj.read_form() + print "You selected", model[treeiter][0] self.selected_item_name = model[treeiter][0] - #self.stuff[self.selected_item_name].build_ui() - GLib.timeout_add(100, lambda: self.stuff[self.selected_item_name].build_ui()) + GLib.idle_add(lambda: self.get_current().build_ui()) else: print "Nothing selected" self.selected_item_name = None self.setup_component_editor() def on_file_new(self, param): - print "File->New not implemented yet." + # Remove everythong from memory + # Clear plot + self.clear_plots() + + # Clear object editor + #self.setup_component_editor() + + # Clear data + self.stuff = {} + + # Clear list + #self.tree_select.unselect_all() + self.build_list() + + #print "File->New not implemented yet." def on_filequit(self, param): print "quit from menu" @@ -1204,44 +1391,12 @@ class App: xmin, ymin, xmax, ymax = get_bounds(self.stuff) width = xmax-xmin height = ymax-ymin - r = width/height - - Fw, Fh = self.canvas.get_width_height() - Fr = float(Fw)/Fh - print "Window aspect ratio:", Fr - print "Data aspect ratio:", r - - #self.axes.set_xlim((xmin-0.05*width, xmax+0.05*width)) - #self.axes.set_ylim((ymin-0.05*height, ymax+0.05*height)) - - if r > Fr: - #self.axes.set_xlim((xmin-0.05*width, xmax+0.05*width)) - xmin -= 0.05*width - xmax += 0.05*width - ycenter = (ymin+ymax)/2.0 - newheight = height*r/Fr - ymin = ycenter-newheight/2.0 - ymax = ycenter+newheight/2.0 - #self.axes.set_ylim((ycenter-newheight/2.0, ycenter+newheight/2.0)) - else: - #self.axes.set_ylim((ymin-0.05*height, ymax+0.05*height)) - ymin -= 0.05*height - ymax += 0.05*height - xcenter = (xmax+ymin)/2.0 - newwidth = width*Fr/r - xmin = xcenter-newwidth/2.0 - xmax = xcenter+newwidth/2.0 - #self.axes.set_xlim((xcenter-newwidth/2.0, xcenter+newwidth/2.0)) + xmin -= 0.05*width + xmax += 0.05*width + ymin -= 0.05*height + ymax += 0.05*height + self.adjust_axes(xmin, ymin, xmax, ymax) - for name in self.stuff: - self.stuff[name].axes.set_xlim((xmin, xmax)) - self.stuff[name].axes.set_ylim((ymin, ymax)) - self.axes.set_xlim((xmin, xmax)) - self.axes.set_ylim((ymin, ymax)) - - self.canvas.queue_draw() - return - # def on_scroll_over_plot(self, event): # print "Scroll" # center = [event.xdata, event.ydata] diff --git a/cirkuix.ui b/cirkuix.ui index 08b39ab6..c77b21b1 100644 --- a/cirkuix.ui +++ b/cirkuix.ui @@ -1752,33 +1752,6 @@ True False - - - gtk-cut - True - False - True - True - - - - - gtk-copy - True - False - True - True - - - - - gtk-paste - True - False - True - True - - gtk-delete @@ -1999,7 +1972,95 @@ True vertical - + + True + False + vertical + + + True + False + 5 + 3 + APLICATION DEFAULTS + + + + + + False + True + 0 + + + + + True + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + True + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + True + 0 + @@ -2119,7 +2180,7 @@ - 120 + 140 True False 5 @@ -2132,6 +2193,20 @@ 1 + + + True + False + 6 + 6 + [in] + + + False + True + 2 + + 50 @@ -2147,7 +2222,7 @@ False True - 2 + 3 diff --git a/defaults.json b/defaults.json new file mode 100644 index 00000000..651d7ce4 --- /dev/null +++ b/defaults.json @@ -0,0 +1,3 @@ +{ +"units": "in" +} \ No newline at end of file