FlatCAM.py 100 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944
  1. ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://caram.cl/software/flatcam #
  4. # Author: Juan Pablo Caram (c) #
  5. # Date: 2/5/2014 #
  6. # MIT Licence #
  7. ############################################################
  8. import threading
  9. # TODO: Bundle together. This is just for debugging.
  10. from docutils.nodes import image
  11. from gi.repository import Gtk
  12. from gi.repository import Gdk
  13. from gi.repository import GdkPixbuf
  14. from gi.repository import GLib
  15. from gi.repository import GObject
  16. import simplejson as json
  17. import matplotlib
  18. from matplotlib.figure import Figure
  19. from numpy import arange, sin, pi
  20. from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
  21. #from mpl_toolkits.axes_grid.anchored_artists import AnchoredText
  22. import sys
  23. import urllib
  24. import copy
  25. import random
  26. ########################################
  27. ## Imports part of FlatCAM ##
  28. ########################################
  29. from camlib import *
  30. from FlatCAMObj import *
  31. from FlatCAMWorker import Worker
  32. ########################################
  33. ## App ##
  34. ########################################
  35. class App:
  36. """
  37. The main application class. The constructor starts the GUI.
  38. """
  39. version_url = "http://caram.cl/flatcam/VERSION"
  40. def __init__(self):
  41. """
  42. Starts the application. Takes no parameters.
  43. :return: app
  44. :rtype: App
  45. """
  46. # Needed to interact with the GUI from other threads.
  47. GObject.threads_init()
  48. # GLib.log_set_handler()
  49. #### GUI ####
  50. # Glade init
  51. self.gladefile = "FlatCAM.ui"
  52. self.builder = Gtk.Builder()
  53. self.builder.add_from_file(self.gladefile)
  54. # References to UI widgets
  55. self.window = self.builder.get_object("window1")
  56. self.position_label = self.builder.get_object("label3")
  57. self.grid = self.builder.get_object("grid1")
  58. self.notebook = self.builder.get_object("notebook1")
  59. self.info_label = self.builder.get_object("label_status")
  60. self.progress_bar = self.builder.get_object("progressbar")
  61. self.progress_bar.set_show_text(True)
  62. self.units_label = self.builder.get_object("label_units")
  63. self.toolbar = self.builder.get_object("toolbar_main")
  64. # White (transparent) background on the "Options" tab.
  65. self.builder.get_object("vp_options").override_background_color(Gtk.StateType.NORMAL,
  66. Gdk.RGBA(1, 1, 1, 1))
  67. # Combo box to choose between project and application options.
  68. self.combo_options = self.builder.get_object("combo_options")
  69. self.combo_options.set_active(1)
  70. #self.setup_project_list() # The "Project" tab
  71. self.setup_component_editor() # The "Selected" tab
  72. ## Setup the toolbar. Adds buttons.
  73. self.setup_toolbar()
  74. #### Event handling ####
  75. self.builder.connect_signals(self)
  76. #### Make plot area ####
  77. self.plotcanvas = PlotCanvas(self.grid)
  78. self.plotcanvas.mpl_connect('button_press_event', self.on_click_over_plot)
  79. self.plotcanvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
  80. self.plotcanvas.mpl_connect('key_press_event', self.on_key_over_plot)
  81. self.setup_tooltips()
  82. # TODO: Hardcoded list
  83. for kind in ['gerber', 'excellon', 'geometry', 'cncjob']:
  84. entry_name = self.builder.get_object("entry_text_" + kind + "_name")
  85. entry_name.connect("activate", self.on_activate_name)
  86. #### DATA ####
  87. self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
  88. self.setup_obj_classes()
  89. self.mouse = None # Mouse coordinates over plot
  90. self.recent = []
  91. self.collection = ObjectCollection()
  92. self.builder.get_object("box_project").pack_start(self.collection.view, False, False, 1)
  93. # TODO: Do this different
  94. self.collection.view.connect("row_activated", self.on_row_activated)
  95. # Used to inhibit the on_options_update callback when
  96. # the options are being changed by the program and not the user.
  97. self.options_update_ignore = False
  98. self.toggle_units_ignore = False
  99. self.defaults = {
  100. "units": "in"
  101. } # Application defaults
  102. ## Current Project ##
  103. self.options = {} # Project options
  104. self.project_filename = None
  105. self.form_kinds = {
  106. "units": "radio"
  107. }
  108. self.radios = {"units": {"rb_inch": "IN", "rb_mm": "MM"},
  109. "gerber_gaps": {"rb_app_2tb": "tb", "rb_app_2lr": "lr", "rb_app_4": "4"}}
  110. self.radios_inv = {"units": {"IN": "rb_inch", "MM": "rb_mm"},
  111. "gerber_gaps": {"tb": "rb_app_2tb", "lr": "rb_app_2lr", "4": "rb_app_4"}}
  112. # Options for each kind of FlatCAMObj.
  113. # Example: 'gerber_plot': 'cb'. The widget name would be: 'cb_app_gerber_plot'
  114. for FlatCAMClass in [FlatCAMExcellon, FlatCAMGeometry, FlatCAMGerber, FlatCAMCNCjob]:
  115. obj = FlatCAMClass("no_name")
  116. for option in obj.form_kinds:
  117. self.form_kinds[obj.kind + "_" + option] = obj.form_kinds[option]
  118. # if obj.form_kinds[option] == "radio":
  119. # self.radios.update({obj.kind + "_" + option: obj.radios[option]})
  120. # self.radios_inv.update({obj.kind + "_" + option: obj.radios_inv[option]})
  121. ## Event subscriptions ##
  122. # TODO: Move to plotcanvas
  123. self.plot_click_subscribers = {}
  124. self.plot_mousemove_subscribers = {}
  125. ## Tools ##
  126. self.measure = Measurement(self.builder.get_object("box39"), self.plotcanvas.axes,
  127. self.plot_click_subscribers, self.plot_mousemove_subscribers)
  128. # Toolbar icon
  129. # TODO: Where should I put this? Tool should have a method to add to toolbar?
  130. meas_ico = Gtk.Image.new_from_file('share/measure32.png')
  131. measure = Gtk.ToolButton.new(meas_ico, "")
  132. measure.connect("clicked", self.measure.toggle_active)
  133. measure.set_tooltip_markup("<b>Measure Tool:</b> Enable/disable tool.\n" +
  134. "Click on point to set reference.\n" +
  135. "(Click on plot and hit <b>m</b>)")
  136. self.toolbar.insert(measure, -1)
  137. #### Initialization ####
  138. self.load_defaults()
  139. self.options.update(self.defaults) # Copy app defaults to project options
  140. self.options2form() # Populate the app defaults form
  141. self.units_label.set_text("[" + self.options["units"] + "]")
  142. self.setup_recent_items()
  143. self.worker = Worker()
  144. self.worker.daemon = True
  145. self.worker.start()
  146. #### Check for updates ####
  147. # Separate thread (Not worker)
  148. self.version = 4
  149. t1 = threading.Thread(target=self.versionCheck)
  150. t1.daemon = True
  151. t1.start()
  152. #### For debugging only ###
  153. def somethreadfunc(app_obj):
  154. print "Hello World!"
  155. t = threading.Thread(target=somethreadfunc, args=(self,))
  156. t.daemon = True
  157. t.start()
  158. ########################################
  159. ## START ##
  160. ########################################
  161. self.icon256 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon256.png')
  162. self.icon48 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon48.png')
  163. self.icon16 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon16.png')
  164. Gtk.Window.set_default_icon_list([self.icon16, self.icon48, self.icon256])
  165. self.window.set_title("FlatCAM - Alpha 4 UNSTABLE")
  166. self.window.set_default_size(900, 600)
  167. self.window.show_all()
  168. def setup_toolbar(self):
  169. # Zoom fit
  170. zf_ico = Gtk.Image.new_from_file('share/zoom_fit32.png')
  171. zoom_fit = Gtk.ToolButton.new(zf_ico, "")
  172. zoom_fit.connect("clicked", self.on_zoom_fit)
  173. zoom_fit.set_tooltip_markup("Zoom Fit.\n(Click on plot and hit <b>1</b>)")
  174. self.toolbar.insert(zoom_fit, -1)
  175. # Zoom out
  176. zo_ico = Gtk.Image.new_from_file('share/zoom_out32.png')
  177. zoom_out = Gtk.ToolButton.new(zo_ico, "")
  178. zoom_out.connect("clicked", self.on_zoom_out)
  179. zoom_out.set_tooltip_markup("Zoom Out.\n(Click on plot and hit <b>2</b>)")
  180. self.toolbar.insert(zoom_out, -1)
  181. # Zoom in
  182. zi_ico = Gtk.Image.new_from_file('share/zoom_in32.png')
  183. zoom_in = Gtk.ToolButton.new(zi_ico, "")
  184. zoom_in.connect("clicked", self.on_zoom_in)
  185. zoom_in.set_tooltip_markup("Zoom In.\n(Click on plot and hit <b>3</b>)")
  186. self.toolbar.insert(zoom_in, -1)
  187. # Clear plot
  188. cp_ico = Gtk.Image.new_from_file('share/clear_plot32.png')
  189. clear_plot = Gtk.ToolButton.new(cp_ico, "")
  190. clear_plot.connect("clicked", self.on_clear_plots)
  191. clear_plot.set_tooltip_markup("Clear Plot")
  192. self.toolbar.insert(clear_plot, -1)
  193. # Replot
  194. rp_ico = Gtk.Image.new_from_file('share/replot32.png')
  195. replot = Gtk.ToolButton.new(rp_ico, "")
  196. replot.connect("clicked", self.on_toolbar_replot)
  197. replot.set_tooltip_markup("Re-plot all")
  198. self.toolbar.insert(replot, -1)
  199. # Delete item
  200. del_ico = Gtk.Image.new_from_file('share/delete32.png')
  201. delete = Gtk.ToolButton.new(del_ico, "")
  202. delete.connect("clicked", self.on_delete)
  203. delete.set_tooltip_markup("Delete selected\nobject.")
  204. self.toolbar.insert(delete, -1)
  205. def setup_obj_classes(self):
  206. """
  207. Sets up application specifics on the FlatCAMObj class.
  208. :return: None
  209. """
  210. FlatCAMObj.app = self
  211. # def setup_project_list(self):
  212. # """
  213. # Sets up list or Tree where whatever has been loaded or created is
  214. # displayed.
  215. #
  216. # :return: None
  217. # """
  218. #
  219. # # Model
  220. # self.store = Gtk.ListStore(GdkPixbuf.Pixbuf, str)
  221. # #self.store = Gtk.ListStore(str, str)
  222. #
  223. # # View
  224. # self.tree = Gtk.TreeView(model=self.store)
  225. #
  226. # # Renderers
  227. # renderer_pixbuf = Gtk.CellRendererPixbuf()
  228. # column_pixbuf = Gtk.TreeViewColumn("Type", renderer_pixbuf, pixbuf=0)
  229. # # column_pixbuf = Gtk.TreeViewColumn("Type")
  230. # # column_pixbuf.pack_start(renderer_pixbuf, False)
  231. # # column_pixbuf.add_attribute(renderer_pixbuf, "pixbuf", 1)
  232. # self.tree.append_column(column_pixbuf)
  233. #
  234. # # renderer1 = Gtk.CellRendererText()
  235. # # column1 = Gtk.TreeViewColumn("Type", renderer1, text=0)
  236. # # self.tree.append_column(column1)
  237. #
  238. # renderer = Gtk.CellRendererText()
  239. # column = Gtk.TreeViewColumn("Object name", renderer, text=1)
  240. # self.tree.append_column(column)
  241. #
  242. # self.tree_select = self.tree.get_selection()
  243. #
  244. #
  245. # self.builder.get_object("box_project").pack_start(self.tree, False, False, 1)
  246. #
  247. # # Double-click or Enter takes you to the object's options
  248. # self.tree.connect("row_activated", self.on_row_activated)
  249. #
  250. # # Changes the selected item and populates the object options form
  251. # self.signal_id = self.tree_select.connect("changed", self.on_tree_selection_changed)
  252. def setup_component_editor(self):
  253. """
  254. Initial configuration of the component editor. Creates
  255. a page titled "Selection" on the notebook on the left
  256. side of the main window.
  257. :return: None
  258. """
  259. box_selected = self.builder.get_object("box_selected")
  260. # Remove anything else in the box
  261. box_children = box_selected.get_children()
  262. for child in box_children:
  263. box_selected.remove(child)
  264. box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
  265. label1 = Gtk.Label("Choose an item from Project")
  266. box1.pack_start(label1, True, False, 1)
  267. box_selected.pack_start(box1, True, True, 0)
  268. #box_selected.show()
  269. box1.show()
  270. label1.show()
  271. def setup_recent_items(self):
  272. # TODO: Move this to constructor
  273. icons = {
  274. "gerber": "share/flatcam_icon16.png",
  275. "excellon": "share/drill16.png",
  276. "cncjob": "share/cnc16.png",
  277. "project": "share/project16.png"
  278. }
  279. openers = {
  280. 'gerber': self.open_gerber,
  281. 'excellon': self.open_excellon,
  282. 'cncjob': self.open_gcode,
  283. 'project': lambda x: ""
  284. }
  285. # Closure needed to create callbacks in a loop.
  286. # Otherwise late binding occurs.
  287. def make_callback(func, fname):
  288. def opener(*args):
  289. self.worker.add_task(func, [fname])
  290. return opener
  291. try:
  292. f = open('recent.json')
  293. except:
  294. print "ERROR: Failed to load recent item list."
  295. self.info("Failed to load recent item list.")
  296. return
  297. try:
  298. self.recent = json.load(f)
  299. except:
  300. print "ERROR: Failed to parse recent item list."
  301. self.info("Failed to parse recent item list.")
  302. f.close()
  303. return
  304. f.close()
  305. recent_menu = Gtk.Menu()
  306. for recent in self.recent:
  307. filename = recent['filename'].split('/')[-1].split('\\')[-1]
  308. item = Gtk.ImageMenuItem.new_with_label(filename)
  309. im = Gtk.Image.new_from_file(icons[recent["kind"]])
  310. item.set_image(im)
  311. o = make_callback(openers[recent["kind"]], recent['filename'])
  312. item.connect('activate', o)
  313. recent_menu.append(item)
  314. self.builder.get_object('open_recent').set_submenu(recent_menu)
  315. recent_menu.show_all()
  316. def info(self, text):
  317. """
  318. Show text on the status bar.
  319. :param text: Text to display.
  320. :type text: str
  321. :return: None
  322. """
  323. self.info_label.set_text(text)
  324. # def build_list(self):
  325. # """
  326. # Clears and re-populates the list of objects in currently
  327. # in the project.
  328. #
  329. # :return: None
  330. # """
  331. # icons = {
  332. # "gerber": "share/flatcam_icon16.png",
  333. # "excellon": "share/drill16.png",
  334. # "cncjob": "share/cnc16.png",
  335. # "geometry": "share/geometry16.png"
  336. # }
  337. #
  338. #
  339. # print "build_list(): clearing"
  340. # self.tree_select.unselect_all()
  341. # self.store.clear()
  342. # print "repopulating...",
  343. # for key in self.stuff:
  344. # print key,
  345. # obj = self.stuff[key]
  346. # icon = GdkPixbuf.Pixbuf.new_from_file(icons[obj.kind])
  347. # self.store.append([icon, key])
  348. # print
  349. def get_radio_value(self, radio_set):
  350. """
  351. Returns the radio_set[key] of the radiobutton
  352. whose name is key is active.
  353. :param radio_set: A dictionary containing widget_name: value pairs.
  354. :type radio_set: dict
  355. :return: radio_set[key]
  356. """
  357. for name in radio_set:
  358. if self.builder.get_object(name).get_active():
  359. return radio_set[name]
  360. def plot_all(self):
  361. """
  362. Re-generates all plots from all objects.
  363. :return: None
  364. """
  365. self.plotcanvas.clear()
  366. self.set_progress_bar(0.1, "Re-plotting...")
  367. def worker_task(app_obj):
  368. percentage = 0.1
  369. try:
  370. delta = 0.9 / len(self.collection.get_list())
  371. except ZeroDivisionError:
  372. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  373. return
  374. for obj in self.collection.get_list():
  375. obj.plot()
  376. percentage += delta
  377. GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
  378. GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
  379. GLib.idle_add(lambda: self.on_zoom_fit(None))
  380. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
  381. # Send to worker
  382. self.worker.add_task(worker_task, [self])
  383. def get_eval(self, widget_name):
  384. """
  385. Runs eval() on the on the text entry of name 'widget_name'
  386. and returns the results.
  387. :param widget_name: Name of Gtk.Entry
  388. :type widget_name: str
  389. :return: Depends on contents of the entry text.
  390. """
  391. value = self.builder.get_object(widget_name).get_text()
  392. if value == "":
  393. value = "None"
  394. try:
  395. evald = eval(value)
  396. return evald
  397. except:
  398. self.info("Could not evaluate: " + value)
  399. return None
  400. # def set_list_selection(self, name):
  401. # """
  402. # Marks a given object as selected in the list ob objects
  403. # in the GUI. This selection will in turn trigger
  404. # ``self.on_tree_selection_changed()``.
  405. #
  406. # :param name: Name of the object.
  407. # :type name: str
  408. # :return: None
  409. # """
  410. #
  411. # iter = self.store.get_iter_first()
  412. # while iter is not None and self.store[iter][1] != name:
  413. # iter = self.store.iter_next(iter)
  414. # self.tree_select.unselect_all()
  415. # self.tree_select.select_iter(iter)
  416. #
  417. # # Need to return False such that GLib.idle_add
  418. # # or .timeout_add do not repeat.
  419. # return False
  420. def new_object(self, kind, name, initialize):
  421. """
  422. Creates a new specalized FlatCAMObj and attaches it to the application,
  423. this is, updates the GUI accordingly, any other records and plots it.
  424. :param kind: The kind of object to create. One of 'gerber',
  425. 'excellon', 'cncjob' and 'geometry'.
  426. :type kind: str
  427. :param name: Name for the object.
  428. :type name: str
  429. :param initialize: Function to run after creation of the object
  430. but before it is attached to the application. The function is
  431. called with 2 parameters: the new object and the App instance.
  432. :type initialize: function
  433. :return: None
  434. :rtype: None
  435. """
  436. ### Check for existing name
  437. if name in self.collection.get_names():
  438. ## Create a new name
  439. # Ends with number?
  440. match = re.search(r'(.*[^\d])?(\d+)$', name)
  441. if match: # Yes: Increment the number!
  442. base = match.group(1) or ''
  443. num = int(match.group(2))
  444. name = base + str(num + 1)
  445. else: # No: add a number!
  446. name += "_1"
  447. # Create object
  448. classdict = {
  449. "gerber": FlatCAMGerber,
  450. "excellon": FlatCAMExcellon,
  451. "cncjob": FlatCAMCNCjob,
  452. "geometry": FlatCAMGeometry
  453. }
  454. obj = classdict[kind](name)
  455. obj.units = self.options["units"] # TODO: The constructor should look at defaults.
  456. # Set default options from self.options
  457. for option in self.options:
  458. if option.find(kind + "_") == 0:
  459. oname = option[len(kind)+1:]
  460. obj.options[oname] = self.options[option]
  461. # Initialize as per user request
  462. # User must take care to implement initialize
  463. # in a thread-safe way as is is likely that we
  464. # have been invoked in a separate thread.
  465. initialize(obj, self)
  466. # Check units and convert if necessary
  467. if self.options["units"].upper() != obj.units.upper():
  468. GLib.idle_add(lambda: self.info("Converting units to " + self.options["units"] + "."))
  469. obj.convert_units(self.options["units"])
  470. # Add to our records
  471. # TODO: Perhaps make collection thread safe instead?
  472. GLib.idle_add(lambda: self.collection.append(obj, active=True))
  473. # Show object details now.
  474. GLib.timeout_add(100, lambda: self.notebook.set_current_page(1))
  475. # Plot
  476. # TODO: (Thread-safe?)
  477. obj.plot()
  478. # TODO: Threading dissaster!
  479. GLib.idle_add(lambda: self.on_zoom_fit(None))
  480. return obj
  481. def set_progress_bar(self, percentage, text=""):
  482. """
  483. Sets the application's progress bar to a given frac_digits and text.
  484. :param percentage: The frac_digits (0.0-1.0) of the progress.
  485. :type percentage: float
  486. :param text: Text to display on the progress bar.
  487. :type text: str
  488. :return: None
  489. """
  490. self.progress_bar.set_text(text)
  491. self.progress_bar.set_fraction(percentage)
  492. return False
  493. # def get_current(self):
  494. # """
  495. # Returns the currently selected FlatCAMObj in the application.
  496. #
  497. # :return: Currently selected FlatCAMObj in the application.
  498. # :rtype: FlatCAMObj or None
  499. # """
  500. #
  501. # # TODO: Could possibly read the form into the object here.
  502. # # But there are some cases when the form for the object
  503. # # is not up yet. See on_tree_selection_changed.
  504. #
  505. # try:
  506. # return self.stuff[self.selected_item_name]
  507. # except:
  508. # return None
  509. def load_defaults(self):
  510. """
  511. Loads the aplication's default settings from defaults.json into
  512. ``self.defaults``.
  513. :return: None
  514. """
  515. try:
  516. f = open("defaults.json")
  517. options = f.read()
  518. f.close()
  519. except:
  520. self.info("ERROR: Could not load defaults file.")
  521. return
  522. try:
  523. defaults = json.loads(options)
  524. except:
  525. e = sys.exc_info()[0]
  526. print e
  527. self.info("ERROR: Failed to parse defaults file.")
  528. return
  529. self.defaults.update(defaults)
  530. def read_form(self):
  531. """
  532. Reads the options form into self.defaults/self.options.
  533. :return: None
  534. :rtype: None
  535. """
  536. combo_sel = self.combo_options.get_active()
  537. options_set = [self.options, self.defaults][combo_sel]
  538. for option in options_set:
  539. self.read_form_item(option, options_set)
  540. def read_form_item(self, name, dest):
  541. """
  542. Reads the value of a form item in the defaults/options form and
  543. saves it to the corresponding dictionary.
  544. :param name: Name of the form item. A key in ``self.defaults`` or
  545. ``self.options``.
  546. :type name: str
  547. :param dest: Dictionary to which to save the value.
  548. :type dest: dict
  549. :return: None
  550. """
  551. fkind = self.form_kinds[name]
  552. fname = fkind + "_" + "app" + "_" + name
  553. if fkind == 'entry_text':
  554. dest[name] = self.builder.get_object(fname).get_text()
  555. return
  556. if fkind == 'entry_eval':
  557. dest[name] = self.get_eval(fname)
  558. return
  559. if fkind == 'cb':
  560. dest[name] = self.builder.get_object(fname).get_active()
  561. return
  562. if fkind == 'radio':
  563. dest[name] = self.get_radio_value(self.radios[name])
  564. return
  565. print "Unknown kind of form item:", fkind
  566. def options2form(self):
  567. """
  568. Sets the 'Project Options' or 'Application Defaults' form with values from
  569. ``self.options`` or ``self.defaults``.
  570. :return: None
  571. :rtype: None
  572. """
  573. # Set the on-change callback to do nothing while we do the changes.
  574. self.options_update_ignore = True
  575. self.toggle_units_ignore = True
  576. combo_sel = self.combo_options.get_active()
  577. options_set = [self.options, self.defaults][combo_sel]
  578. for option in options_set:
  579. self.set_form_item(option, options_set[option])
  580. self.options_update_ignore = False
  581. self.toggle_units_ignore = False
  582. def set_form_item(self, name, value):
  583. """
  584. Sets a form item 'name' in the GUI with the given 'value'. The syntax of
  585. form names in the GUI is <kind>_app_<name>, where kind is one of: rb (radio button),
  586. cb (check button), entry_eval or entry_text (entry), combo (combo box). name is
  587. whatever name it's been given. For self.defaults, name is a key in the dictionary.
  588. :param name: Name of the form field.
  589. :type name: str
  590. :param value: The value to set the form field to.
  591. :type value: Depends on field kind.
  592. :return: None
  593. """
  594. if name not in self.form_kinds:
  595. print "WARNING: Tried to set unknown option/form item:", name
  596. return
  597. fkind = self.form_kinds[name]
  598. fname = fkind + "_" + "app" + "_" + name
  599. if fkind == 'entry_eval' or fkind == 'entry_text':
  600. try:
  601. self.builder.get_object(fname).set_text(str(value))
  602. except:
  603. print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
  604. return
  605. if fkind == 'cb':
  606. try:
  607. self.builder.get_object(fname).set_active(value)
  608. except:
  609. print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
  610. return
  611. if fkind == 'radio':
  612. try:
  613. self.builder.get_object(self.radios_inv[name][value]).set_active(True)
  614. except:
  615. print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
  616. return
  617. print "Unknown kind of form item:", fkind
  618. def save_project(self, filename):
  619. """
  620. Saves the current project to the specified file.
  621. :param filename: Name of the file in which to save.
  622. :type filename: str
  623. :return: None
  624. """
  625. # Capture the latest changes
  626. try:
  627. self.collection.get_active().read_form()
  628. except:
  629. pass
  630. d = {"objs": [obj.to_dict() for obj in self.collection.get_list()],
  631. "options": self.options}
  632. try:
  633. f = open(filename, 'w')
  634. except:
  635. print "ERROR: Failed to open file for saving:", filename
  636. return
  637. try:
  638. json.dump(d, f, default=to_dict)
  639. except:
  640. print "ERROR: File open but failed to write:", filename
  641. f.close()
  642. return
  643. f.close()
  644. def open_project(self, filename):
  645. """
  646. Loads a project from the specified file.
  647. :param filename: Name of the file from which to load.
  648. :type filename: str
  649. :return: None
  650. """
  651. try:
  652. f = open(filename, 'r')
  653. except:
  654. self.info("ERROR: Failed to open project file: %s" % filename)
  655. return
  656. try:
  657. d = json.load(f, object_hook=dict2obj)
  658. except:
  659. self.info("ERROR: Failed to parse project file: %s" % filename)
  660. f.close()
  661. return
  662. self.register_recent("project", filename)
  663. # Clear the current project
  664. self.on_file_new(None)
  665. # Project options
  666. self.options.update(d['options'])
  667. self.project_filename = filename
  668. self.units_label.set_text(self.options["units"])
  669. # Re create objects
  670. for obj in d['objs']:
  671. def obj_init(obj_inst, app_inst):
  672. obj_inst.from_dict(obj)
  673. self.new_object(obj['kind'], obj['options']['name'], obj_init)
  674. self.info("Project loaded from: " + filename)
  675. def populate_objects_combo(self, combo):
  676. """
  677. Populates a Gtk.Comboboxtext with the list of the object in the project.
  678. :param combo: Name or instance of the comboboxtext.
  679. :type combo: str or Gtk.ComboBoxText
  680. :return: None
  681. """
  682. print "Populating combo!"
  683. if type(combo) == str:
  684. combo = self.builder.get_object(combo)
  685. combo.remove_all()
  686. for name in self.collection.get_names():
  687. combo.append_text(name)
  688. def versionCheck(self, *args):
  689. """
  690. Checks for the latest version of the program. Alerts the
  691. user if theirs is outdated. This method is meant to be run
  692. in a saeparate thread.
  693. :return: None
  694. """
  695. try:
  696. f = urllib.urlopen(App.version_url)
  697. except:
  698. GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
  699. return
  700. try:
  701. data = json.load(f)
  702. except:
  703. GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
  704. f.close()
  705. return
  706. f.close()
  707. if self.version >= data["version"]:
  708. GLib.idle_add(lambda: self.info("FlatCAM is up to date!"))
  709. return
  710. label = Gtk.Label("There is a newer version of FlatCAM\n" +
  711. "available for download:\n\n" +
  712. data["name"] + "\n\n" + data["message"])
  713. dialog = Gtk.Dialog("Newer Version Available", self.window, 0,
  714. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  715. Gtk.STOCK_OK, Gtk.ResponseType.OK))
  716. dialog.set_default_size(150, 100)
  717. dialog.set_modal(True)
  718. box = dialog.get_content_area()
  719. box.set_border_width(10)
  720. box.add(label)
  721. def do_dialog():
  722. dialog.show_all()
  723. response = dialog.run()
  724. dialog.destroy()
  725. GLib.idle_add(lambda: do_dialog())
  726. return
  727. def setup_tooltips(self):
  728. tooltips = {
  729. "cb_gerber_plot": "Plot this object on the main window.",
  730. "cb_gerber_mergepolys": "Show overlapping polygons as single.",
  731. "cb_gerber_solid": "Paint inside polygons.",
  732. "cb_gerber_multicolored": "Draw polygons with different colors."
  733. }
  734. for widget in tooltips:
  735. self.builder.get_object(widget).set_tooltip_markup(tooltips[widget])
  736. def do_nothing(self, param):
  737. return
  738. def disable_plots(self, except_current=False):
  739. """
  740. Disables all plots with exception of the current object if specified.
  741. :param except_current: Wether to skip the current object.
  742. :rtype except_current: boolean
  743. :return: None
  744. """
  745. # TODO: This method is very similar to replot_all. Try to merge.
  746. self.set_progress_bar(0.1, "Re-plotting...")
  747. def worker_task(app_obj):
  748. percentage = 0.1
  749. try:
  750. delta = 0.9 / len(self.collection.get_list())
  751. except ZeroDivisionError:
  752. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  753. return
  754. for obj in self.collection.get_list():
  755. #if i != app_obj.selected_item_name or not except_current:
  756. if obj != self.collection.get_active() or not except_current:
  757. obj.options['plot'] = False
  758. obj.plot()
  759. percentage += delta
  760. GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
  761. GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
  762. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  763. # Send to worker
  764. self.worker.add_task(worker_task, [self])
  765. def enable_all_plots(self, *args):
  766. self.plotcanvas.clear()
  767. self.set_progress_bar(0.1, "Re-plotting...")
  768. def worker_task(app_obj):
  769. percentage = 0.1
  770. try:
  771. delta = 0.9 / len(self.collection.get_list())
  772. except ZeroDivisionError:
  773. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  774. return
  775. for obj in self.collection.get_list():
  776. obj.options['plot'] = True
  777. obj.plot()
  778. percentage += delta
  779. GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
  780. GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
  781. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  782. # Send to worker
  783. self.worker.add_task(worker_task, [self])
  784. def register_recent(self, kind, filename):
  785. record = {'kind': kind, 'filename': filename}
  786. if record in self.recent:
  787. return
  788. self.recent.insert(0, record)
  789. if len(self.recent) > 10: # Limit reached
  790. self.recent.pop()
  791. try:
  792. f = open('recent.json', 'w')
  793. except:
  794. print "ERROR: Failed to open recent items file for writing."
  795. self.info('Failed to open recent files file for writing.')
  796. return
  797. try:
  798. json.dump(self.recent, f)
  799. except:
  800. print "ERROR: Failed to write to recent items file."
  801. self.info('Failed to write to recent items file.')
  802. f.close()
  803. f.close()
  804. ########################################
  805. ## EVENT HANDLERS ##
  806. ########################################
  807. def on_disable_all_plots(self, widget):
  808. self.disable_plots()
  809. def on_disable_all_plots_not_current(self, widget):
  810. self.disable_plots(except_current=True)
  811. def on_offset_object(self, widget):
  812. """
  813. Offsets the object's geometry by the vector specified
  814. in the form. Re-plots.
  815. :param widget: Ignored
  816. :return: None
  817. """
  818. obj = self.collection.get_active()
  819. obj.read_form()
  820. assert isinstance(obj, FlatCAMObj)
  821. try:
  822. vect = self.get_eval("entry_eval_" + obj.kind + "_offset")
  823. except:
  824. self.info("ERROR: Vector is not in (x, y) format.")
  825. return
  826. assert isinstance(obj, Geometry)
  827. obj.offset(vect)
  828. obj.plot()
  829. return
  830. def on_cb_plot_toggled(self, widget):
  831. """
  832. Callback for toggling the "Plot" checkbox. Re-plots.
  833. :param widget: Ignored.
  834. :return: None
  835. """
  836. # self.get_current().read_form()
  837. # self.get_current().plot()
  838. self.collection.get_active().read_form()
  839. self.collection.get_active().plot()
  840. def on_about(self, widget):
  841. """
  842. Opens the 'About' dialog box.
  843. :param widget: Ignored.
  844. :return: None
  845. """
  846. about = self.builder.get_object("aboutdialog")
  847. response = about.run()
  848. #about.destroy()
  849. about.hide()
  850. def on_create_mirror(self, widget):
  851. """
  852. Creates a mirror image of an object to be used as a bottom layer.
  853. :param widget: Ignored.
  854. :return: None
  855. """
  856. # TODO: Move (some of) this to camlib!
  857. # Object to mirror
  858. obj_name = self.builder.get_object("comboboxtext_bottomlayer").get_active_text()
  859. fcobj = self.collection.get_by_name(obj_name)
  860. # For now, lets limit to Gerbers and Excellons.
  861. # assert isinstance(gerb, FlatCAMGerber)
  862. if not isinstance(fcobj, FlatCAMGerber) and not isinstance(fcobj, FlatCAMExcellon):
  863. self.info("ERROR: Only Gerber and Excellon objects can be mirrored.")
  864. return
  865. # Mirror axis "X" or "Y
  866. axis = self.get_radio_value({"rb_mirror_x": "X",
  867. "rb_mirror_y": "Y"})
  868. mode = self.get_radio_value({"rb_mirror_box": "box",
  869. "rb_mirror_point": "point"})
  870. if mode == "point": # A single point defines the mirror axis
  871. # TODO: Error handling
  872. px, py = eval(self.point_entry.get_text())
  873. else: # The axis is the line dividing the box in the middle
  874. name = self.box_combo.get_active_text()
  875. bb_obj = self.collection.get_by_name(name)
  876. xmin, ymin, xmax, ymax = bb_obj.bounds()
  877. px = 0.5*(xmin+xmax)
  878. py = 0.5*(ymin+ymax)
  879. fcobj.mirror(axis, [px, py])
  880. fcobj.plot()
  881. def on_create_aligndrill(self, widget):
  882. """
  883. Creates alignment holes Excellon object. Creates mirror duplicates
  884. of the specified holes around the specified axis.
  885. :param widget: Ignored.
  886. :return: None
  887. """
  888. # Mirror axis. Same as in on_create_mirror.
  889. axis = self.get_radio_value({"rb_mirror_x": "X",
  890. "rb_mirror_y": "Y"})
  891. # TODO: Error handling
  892. mode = self.get_radio_value({"rb_mirror_box": "box",
  893. "rb_mirror_point": "point"})
  894. if mode == "point":
  895. px, py = eval(self.point_entry.get_text())
  896. else:
  897. name = self.box_combo.get_active_text()
  898. bb_obj = self.collection.get_by_name(name)
  899. xmin, ymin, xmax, ymax = bb_obj.bounds()
  900. px = 0.5*(xmin+xmax)
  901. py = 0.5*(ymin+ymax)
  902. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  903. # Tools
  904. dia = self.get_eval("entry_dblsided_alignholediam")
  905. tools = {"1": {"C": dia}}
  906. # Parse hole list
  907. # TODO: Better parsing
  908. holes = self.builder.get_object("entry_dblsided_alignholes").get_text()
  909. holes = eval("[" + holes + "]")
  910. drills = []
  911. for hole in holes:
  912. point = Point(hole)
  913. point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
  914. drills.append({"point": point, "tool": "1"})
  915. drills.append({"point": point_mirror, "tool": "1"})
  916. def obj_init(obj_inst, app_inst):
  917. obj_inst.tools = tools
  918. obj_inst.drills = drills
  919. obj_inst.create_geometry()
  920. self.new_object("excellon", "Alignment Drills", obj_init)
  921. def on_toggle_pointbox(self, widget):
  922. """
  923. Callback for radio selection change between point and box in the
  924. Double-sided PCB tool. Updates the UI accordingly.
  925. :param widget: Ignored.
  926. :return: None
  927. """
  928. # Where the entry or combo go
  929. box = self.builder.get_object("box_pointbox")
  930. # Clear contents
  931. children = box.get_children()
  932. for child in children:
  933. box.remove(child)
  934. choice = self.get_radio_value({"rb_mirror_point": "point",
  935. "rb_mirror_box": "box"})
  936. if choice == "point":
  937. self.point_entry = Gtk.Entry()
  938. self.builder.get_object("box_pointbox").pack_start(self.point_entry,
  939. False, False, 1)
  940. self.point_entry.show()
  941. else:
  942. self.box_combo = Gtk.ComboBoxText()
  943. self.builder.get_object("box_pointbox").pack_start(self.box_combo,
  944. False, False, 1)
  945. self.populate_objects_combo(self.box_combo)
  946. self.box_combo.show()
  947. def on_tools_doublesided(self, param):
  948. """
  949. Callback for menu item Tools->Double Sided PCB Tool. Launches the
  950. tool placing its UI in the "Tool" tab in the notebook.
  951. :param param: Ignored.
  952. :return: None
  953. """
  954. # Were are we drawing the UI
  955. box_tool = self.builder.get_object("box_tool")
  956. # Remove anything else in the box
  957. box_children = box_tool.get_children()
  958. for child in box_children:
  959. box_tool.remove(child)
  960. # Get the UI
  961. osw = self.builder.get_object("offscreenwindow_dblsided")
  962. sw = self.builder.get_object("sw_dblsided")
  963. osw.remove(sw)
  964. vp = self.builder.get_object("vp_dblsided")
  965. vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
  966. # Put in the UI
  967. box_tool.pack_start(sw, True, True, 0)
  968. # INITIALIZATION
  969. # Populate combo box
  970. self.populate_objects_combo("comboboxtext_bottomlayer")
  971. # Point entry
  972. self.point_entry = Gtk.Entry()
  973. box = self.builder.get_object("box_pointbox")
  974. for child in box.get_children():
  975. box.remove(child)
  976. box.pack_start(self.point_entry, False, False, 1)
  977. # Show the "Tool" tab
  978. self.notebook.set_current_page(3)
  979. sw.show_all()
  980. def on_toggle_units(self, widget):
  981. """
  982. Callback for the Units radio-button change in the Options tab.
  983. Changes the application's default units or the current project's units.
  984. If changing the project's units, the change propagates to all of
  985. the objects in the project.
  986. :param widget: Ignored.
  987. :return: None
  988. """
  989. if self.toggle_units_ignore:
  990. return
  991. combo_sel = self.combo_options.get_active()
  992. options_set = [self.options, self.defaults][combo_sel]
  993. # Options to scale
  994. dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize',
  995. 'gerber_noncoppermargin', 'gerber_bboxmargin', 'excellon_drillz',
  996. 'excellon_travelz', 'excellon_feedrate', 'cncjob_tooldia',
  997. 'geometry_cutz', 'geometry_travelz', 'geometry_feedrate',
  998. 'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap',
  999. 'geometry_paintmargin']
  1000. def scale_options(factor):
  1001. for dim in dimensions:
  1002. options_set[dim] *= factor
  1003. # The scaling factor depending on choice of units.
  1004. factor = 1/25.4
  1005. if self.builder.get_object('rb_mm').get_active():
  1006. factor = 25.4
  1007. # App units. Convert without warning.
  1008. if combo_sel == 1:
  1009. self.read_form()
  1010. scale_options(factor)
  1011. self.options2form()
  1012. return
  1013. # Changing project units. Warn user.
  1014. label = Gtk.Label("Changing the units of the project causes all geometrical \n" + \
  1015. "properties of all objects to be scaled accordingly. Continue?")
  1016. dialog = Gtk.Dialog("Changing Project Units", self.window, 0,
  1017. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1018. Gtk.STOCK_OK, Gtk.ResponseType.OK))
  1019. dialog.set_default_size(150, 100)
  1020. dialog.set_modal(True)
  1021. box = dialog.get_content_area()
  1022. box.set_border_width(10)
  1023. box.add(label)
  1024. dialog.show_all()
  1025. response = dialog.run()
  1026. dialog.destroy()
  1027. if response == Gtk.ResponseType.OK:
  1028. #print "Converting units..."
  1029. #print "Converting options..."
  1030. self.read_form()
  1031. scale_options(factor)
  1032. self.options2form()
  1033. for obj in self.collection.get_list():
  1034. units = self.get_radio_value({"rb_mm": "MM", "rb_inch": "IN"})
  1035. obj.convert_units(units)
  1036. current = self.collection.get_active()
  1037. if current is not None:
  1038. current.to_form()
  1039. self.plot_all()
  1040. else:
  1041. # Undo toggling
  1042. self.toggle_units_ignore = True
  1043. if self.builder.get_object('rb_mm').get_active():
  1044. self.builder.get_object('rb_inch').set_active(True)
  1045. else:
  1046. self.builder.get_object('rb_mm').set_active(True)
  1047. self.toggle_units_ignore = False
  1048. self.read_form()
  1049. self.info("Converted units to %s" % self.options["units"])
  1050. self.units_label.set_text("[" + self.options["units"] + "]")
  1051. def on_file_openproject(self, param):
  1052. """
  1053. Callback for menu item File->Open Project. Opens a file chooser and calls
  1054. ``self.open_project()`` after successful selection of a filename.
  1055. :param param: Ignored.
  1056. :return: None
  1057. """
  1058. def on_success(app_obj, filename):
  1059. app_obj.open_project(filename)
  1060. self.file_chooser_action(on_success)
  1061. def on_file_saveproject(self, param):
  1062. """
  1063. Callback for menu item File->Save Project. Saves the project to
  1064. ``self.project_filename`` or calls ``self.on_file_saveprojectas()``
  1065. if set to None. The project is saved by calling ``self.save_project()``.
  1066. :param param: Ignored.
  1067. :return: None
  1068. """
  1069. if self.project_filename is None:
  1070. self.on_file_saveprojectas(None)
  1071. else:
  1072. self.save_project(self.project_filename)
  1073. self.register_recent("project", self.project_filename)
  1074. self.info("Project saved to: " + self.project_filename)
  1075. def on_file_saveprojectas(self, param):
  1076. """
  1077. Callback for menu item File->Save Project As... Opens a file
  1078. chooser and saves the project to the given file via
  1079. ``self.save_project()``.
  1080. :param param: Ignored.
  1081. :return: None
  1082. """
  1083. def on_success(app_obj, filename):
  1084. assert isinstance(app_obj, App)
  1085. app_obj.save_project(filename)
  1086. self.project_filename = filename
  1087. self.register_recent("project", filename)
  1088. app_obj.info("Project saved to: " + filename)
  1089. self.file_chooser_save_action(on_success)
  1090. def on_file_saveprojectcopy(self, param):
  1091. """
  1092. Callback for menu item File->Save Project Copy... Opens a file
  1093. chooser and saves the project to the given file via
  1094. ``self.save_project``. It does not update ``self.project_filename`` so
  1095. subsequent save requests are done on the previous known filename.
  1096. :param param: Ignore.
  1097. :return: None
  1098. """
  1099. def on_success(app_obj, filename):
  1100. assert isinstance(app_obj, App)
  1101. app_obj.save_project(filename)
  1102. self.register_recent("project", filename)
  1103. app_obj.info("Project copy saved to: " + filename)
  1104. self.file_chooser_save_action(on_success)
  1105. def on_options_app2project(self, param):
  1106. """
  1107. Callback for Options->Transfer Options->App=>Project. Copies options
  1108. from application defaults to project defaults.
  1109. :param param: Ignored.
  1110. :return: None
  1111. """
  1112. self.options.update(self.defaults)
  1113. self.options2form() # Update UI
  1114. def on_options_project2app(self, param):
  1115. """
  1116. Callback for Options->Transfer Options->Project=>App. Copies options
  1117. from project defaults to application defaults.
  1118. :param param: Ignored.
  1119. :return: None
  1120. """
  1121. self.defaults.update(self.options)
  1122. self.options2form() # Update UI
  1123. def on_options_project2object(self, param):
  1124. """
  1125. Callback for Options->Transfer Options->Project=>Object. Copies options
  1126. from project defaults to the currently selected object.
  1127. :param param: Ignored.
  1128. :return: None
  1129. """
  1130. obj = self.collection.get_active()
  1131. if obj is None:
  1132. self.info("WARNING: No object selected.")
  1133. return
  1134. for option in self.options:
  1135. if option.find(obj.kind + "_") == 0:
  1136. oname = option[len(obj.kind)+1:]
  1137. obj.options[oname] = self.options[option]
  1138. obj.to_form() # Update UI
  1139. def on_options_object2project(self, param):
  1140. """
  1141. Callback for Options->Transfer Options->Object=>Project. Copies options
  1142. from the currently selected object to project defaults.
  1143. :param param: Ignored.
  1144. :return: None
  1145. """
  1146. obj = self.collection.get_active()
  1147. if obj is None:
  1148. self.info("WARNING: No object selected.")
  1149. return
  1150. obj.read_form()
  1151. for option in obj.options:
  1152. if option in ['name']: # TODO: Handle this better...
  1153. continue
  1154. self.options[obj.kind + "_" + option] = obj.options[option]
  1155. self.options2form() # Update UI
  1156. def on_options_object2app(self, param):
  1157. """
  1158. Callback for Options->Transfer Options->Object=>App. Copies options
  1159. from the currently selected object to application defaults.
  1160. :param param: Ignored.
  1161. :return: None
  1162. """
  1163. obj = self.collection.get_active()
  1164. if obj is None:
  1165. self.info("WARNING: No object selected.")
  1166. return
  1167. obj.read_form()
  1168. for option in obj.options:
  1169. if option in ['name']: # TODO: Handle this better...
  1170. continue
  1171. self.defaults[obj.kind + "_" + option] = obj.options[option]
  1172. self.options2form() # Update UI
  1173. def on_options_app2object(self, param):
  1174. """
  1175. Callback for Options->Transfer Options->App=>Object. Copies options
  1176. from application defaults to the currently selected object.
  1177. :param param: Ignored.
  1178. :return: None
  1179. """
  1180. obj = self.collection.get_active()
  1181. if obj is None:
  1182. self.info("WARNING: No object selected.")
  1183. return
  1184. for option in self.defaults:
  1185. if option.find(obj.kind + "_") == 0:
  1186. oname = option[len(obj.kind)+1:]
  1187. obj.options[oname] = self.defaults[option]
  1188. obj.to_form() # Update UI
  1189. def on_file_savedefaults(self, param):
  1190. """
  1191. Callback for menu item File->Save Defaults. Saves application default options
  1192. ``self.defaults`` to defaults.json.
  1193. :param param: Ignored.
  1194. :return: None
  1195. """
  1196. # Read options from file
  1197. try:
  1198. f = open("defaults.json")
  1199. options = f.read()
  1200. f.close()
  1201. except:
  1202. self.info("ERROR: Could not load defaults file.")
  1203. return
  1204. try:
  1205. defaults = json.loads(options)
  1206. except:
  1207. e = sys.exc_info()[0]
  1208. print e
  1209. self.info("ERROR: Failed to parse defaults file.")
  1210. return
  1211. # Update options
  1212. assert isinstance(defaults, dict)
  1213. defaults.update(self.defaults)
  1214. # Save update options
  1215. try:
  1216. f = open("defaults.json", "w")
  1217. json.dump(defaults, f)
  1218. f.close()
  1219. except:
  1220. self.info("ERROR: Failed to write defaults to file.")
  1221. return
  1222. self.info("Defaults saved.")
  1223. def on_options_combo_change(self, widget):
  1224. """
  1225. Called when the combo box to choose between application defaults and
  1226. project option changes value. The corresponding variables are
  1227. copied to the UI.
  1228. :param widget: The widget from which this was called. Ignore.
  1229. :return: None
  1230. """
  1231. #combo_sel = self.combo_options.get_active()
  1232. #print "Options --> ", combo_sel
  1233. self.options2form()
  1234. def on_options_update(self, widget):
  1235. """
  1236. Called whenever a value in the options/defaults form changes.
  1237. All values are updated. Can be inhibited by setting ``self.options_update_ignore = True``,
  1238. which may be necessary when updating the UI from code and not by the user.
  1239. :param widget: The widget from which this was called. Ignore.
  1240. :return: None
  1241. """
  1242. if self.options_update_ignore:
  1243. return
  1244. self.read_form()
  1245. def on_scale_object(self, widget):
  1246. """
  1247. Callback for request to change an objects geometry scale. The object
  1248. is re-scaled and replotted.
  1249. :param widget: Ignored.
  1250. :return: None
  1251. """
  1252. obj = self.collection.get_active()
  1253. factor = self.get_eval("entry_eval_" + obj.kind + "_scalefactor")
  1254. obj.scale(factor)
  1255. obj.to_form()
  1256. self.on_update_plot(None)
  1257. def on_canvas_configure(self, widget, event):
  1258. """
  1259. Called whenever the canvas changes size. The axes are updated such
  1260. as to use the whole canvas.
  1261. :param widget: Ignored.
  1262. :param event: Ignored.
  1263. :return: None
  1264. """
  1265. self.plotcanvas.auto_adjust_axes()
  1266. def on_row_activated(self, widget, path, col):
  1267. """
  1268. Callback for selection activation (Enter or double-click) on the Project list.
  1269. Switches the notebook page to the object properties form. Calls
  1270. ``self.notebook.set_current_page(1)``.
  1271. :param widget: Ignored.
  1272. :param path: Ignored.
  1273. :param col: Ignored.
  1274. :return: None
  1275. """
  1276. self.notebook.set_current_page(1)
  1277. def on_generate_gerber_bounding_box(self, widget):
  1278. """
  1279. Callback for request from the Gerber form to generate a bounding box for the
  1280. geometry in the object. Creates a FlatCAMGeometry with the bounding box.
  1281. The box can have rounded corners if specified in the form.
  1282. :param widget: Ignored.
  1283. :return: None
  1284. """
  1285. # TODO: Use Gerber.get_bounding_box(...)
  1286. gerber = self.collection.get_active()
  1287. gerber.read_form()
  1288. name = gerber.options["name"] + "_bbox"
  1289. def geo_init(geo_obj, app_obj):
  1290. assert isinstance(geo_obj, FlatCAMGeometry)
  1291. # Bounding box with rounded corners
  1292. bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["bboxmargin"])
  1293. if not gerber.options["bboxrounded"]: # Remove rounded corners
  1294. bounding_box = bounding_box.envelope
  1295. geo_obj.solid_geometry = bounding_box
  1296. self.new_object("geometry", name, geo_init)
  1297. def on_update_plot(self, widget):
  1298. """
  1299. Callback for button on form for all kinds of objects.
  1300. Re-plots the current object only.
  1301. :param widget: The widget from which this was called. Ignored.
  1302. :return: None
  1303. """
  1304. obj = self.collection.get_active()
  1305. obj.read_form()
  1306. self.set_progress_bar(0.5, "Plotting...")
  1307. def thread_func(app_obj):
  1308. assert isinstance(app_obj, App)
  1309. obj.plot()
  1310. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
  1311. # t = threading.Thread(target=thread_func, args=(self,))
  1312. # t.daemon = True
  1313. # t.start()
  1314. self.worker.add_task(thread_func, [self])
  1315. def on_generate_excellon_cncjob(self, widget):
  1316. """
  1317. Callback for button active/click on Excellon form to
  1318. create a CNC Job for the Excellon file.
  1319. :param widget: Ignored
  1320. :return: None
  1321. """
  1322. excellon = self.collection.get_active()
  1323. excellon.read_form()
  1324. job_name = excellon.options["name"] + "_cnc"
  1325. # Object initialization function for app.new_object()
  1326. def job_init(job_obj, app_obj):
  1327. # excellon_ = self.get_current()
  1328. # assert isinstance(excellon_, FlatCAMExcellon)
  1329. assert isinstance(job_obj, FlatCAMCNCjob)
  1330. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
  1331. job_obj.z_cut = excellon.options["drillz"]
  1332. job_obj.z_move = excellon.options["travelz"]
  1333. job_obj.feedrate = excellon.options["feedrate"]
  1334. # There could be more than one drill size...
  1335. # job_obj.tooldia = # TODO: duplicate variable!
  1336. # job_obj.options["tooldia"] =
  1337. job_obj.generate_from_excellon_by_tool(excellon, excellon.options["toolselection"])
  1338. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
  1339. job_obj.gcode_parse()
  1340. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
  1341. job_obj.create_geometry()
  1342. GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
  1343. # To be run in separate thread
  1344. def job_thread(app_obj):
  1345. app_obj.new_object("cncjob", job_name, job_init)
  1346. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1347. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1348. # Start the thread
  1349. # t = threading.Thread(target=job_thread, args=(self,))
  1350. # t.daemon = True
  1351. # t.start()
  1352. self.worker.add_task(job_thread, [self])
  1353. def on_excellon_tool_choose(self, widget):
  1354. """
  1355. Callback for button on Excellon form to open up a window for
  1356. selecting tools.
  1357. :param widget: The widget from which this was called.
  1358. :return: None
  1359. """
  1360. excellon = self.collection.get_active()
  1361. assert isinstance(excellon, FlatCAMExcellon)
  1362. excellon.show_tool_chooser()
  1363. def on_entry_eval_activate(self, widget):
  1364. """
  1365. Called when an entry is activated (eg. by hitting enter) if
  1366. set to do so. Its text is eval()'d and set to the returned value.
  1367. The current object is updated.
  1368. :param widget:
  1369. :return:
  1370. """
  1371. self.on_eval_update(widget)
  1372. obj = self.collection.get_active()
  1373. assert isinstance(obj, FlatCAMObj)
  1374. obj.read_form()
  1375. def on_gerber_generate_noncopper(self, widget):
  1376. """
  1377. Callback for button on Gerber form to create a geometry object
  1378. with polygons covering the area without copper or negative of the
  1379. Gerber.
  1380. :param widget: The widget from which this was called.
  1381. :return: None
  1382. """
  1383. gerb = self.collection.get_active()
  1384. gerb.read_form()
  1385. name = gerb.options["name"] + "_noncopper"
  1386. def geo_init(geo_obj, app_obj):
  1387. assert isinstance(geo_obj, FlatCAMGeometry)
  1388. bounding_box = gerb.solid_geometry.envelope.buffer(gerb.options["noncoppermargin"])
  1389. if not gerb.options["noncopperrounded"]:
  1390. bounding_box = bounding_box.envelope
  1391. non_copper = bounding_box.difference(gerb.solid_geometry)
  1392. geo_obj.solid_geometry = non_copper
  1393. # TODO: Check for None
  1394. self.new_object("geometry", name, geo_init)
  1395. def on_gerber_generate_cutout(self, widget):
  1396. """
  1397. Callback for button on Gerber form to create geometry with lines
  1398. for cutting off the board.
  1399. :param widget: The widget from which this was called.
  1400. :return: None
  1401. """
  1402. gerb = self.collection.get_active()
  1403. gerb.read_form()
  1404. name = gerb.options["name"] + "_cutout"
  1405. def geo_init(geo_obj, app_obj):
  1406. margin = gerb.options["cutoutmargin"] + gerb.options["cutouttooldia"]/2
  1407. gap_size = gerb.options["cutoutgapsize"] + gerb.options["cutouttooldia"]
  1408. minx, miny, maxx, maxy = gerb.bounds()
  1409. minx -= margin
  1410. maxx += margin
  1411. miny -= margin
  1412. maxy += margin
  1413. midx = 0.5 * (minx + maxx)
  1414. midy = 0.5 * (miny + maxy)
  1415. hgap = 0.5 * gap_size
  1416. pts = [[midx - hgap, maxy],
  1417. [minx, maxy],
  1418. [minx, midy + hgap],
  1419. [minx, midy - hgap],
  1420. [minx, miny],
  1421. [midx - hgap, miny],
  1422. [midx + hgap, miny],
  1423. [maxx, miny],
  1424. [maxx, midy - hgap],
  1425. [maxx, midy + hgap],
  1426. [maxx, maxy],
  1427. [midx + hgap, maxy]]
  1428. cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
  1429. [pts[6], pts[7], pts[10], pts[11]]],
  1430. "lr": [[pts[9], pts[10], pts[1], pts[2]],
  1431. [pts[3], pts[4], pts[7], pts[8]]],
  1432. "4": [[pts[0], pts[1], pts[2]],
  1433. [pts[3], pts[4], pts[5]],
  1434. [pts[6], pts[7], pts[8]],
  1435. [pts[9], pts[10], pts[11]]]}
  1436. cuts = cases[app.get_radio_value({"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"})]
  1437. geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
  1438. # TODO: Check for None
  1439. self.new_object("geometry", name, geo_init)
  1440. def on_eval_update(self, widget):
  1441. """
  1442. Modifies the content of a Gtk.Entry by running
  1443. eval() on its contents and puting it back as a
  1444. string.
  1445. :param widget: The widget from which this was called.
  1446. :return: None
  1447. """
  1448. # TODO: error handling here
  1449. widget.set_text(str(eval(widget.get_text())))
  1450. def on_generate_isolation(self, widget):
  1451. """
  1452. Callback for button on Gerber form to create isolation routing geometry.
  1453. :param widget: The widget from which this was called.
  1454. :return: None
  1455. """
  1456. gerb = self.collection.get_active()
  1457. gerb.read_form()
  1458. dia = gerb.options["isotooldia"]
  1459. passes = int(gerb.options["isopasses"])
  1460. overlap = gerb.options["isooverlap"] * dia
  1461. for i in range(passes):
  1462. offset = (2*i + 1)/2.0 * dia - i*overlap
  1463. iso_name = gerb.options["name"] + "_iso%d" % (i+1)
  1464. # TODO: This is ugly. Create way to pass data into init function.
  1465. def iso_init(geo_obj, app_obj):
  1466. # Propagate options
  1467. geo_obj.options["cnctooldia"] = gerb.options["isotooldia"]
  1468. geo_obj.solid_geometry = gerb.isolation_geometry(offset)
  1469. app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
  1470. # TODO: Do something if this is None. Offer changing name?
  1471. self.new_object("geometry", iso_name, iso_init)
  1472. def on_generate_cncjob(self, widget):
  1473. """
  1474. Callback for button on geometry form to generate CNC job.
  1475. :param widget: The widget from which this was called.
  1476. :return: None
  1477. """
  1478. source_geo = self.collection.get_active()
  1479. source_geo.read_form()
  1480. job_name = source_geo.options["name"] + "_cnc"
  1481. # Object initialization function for app.new_object()
  1482. # RUNNING ON SEPARATE THREAD!
  1483. def job_init(job_obj, app_obj):
  1484. assert isinstance(job_obj, FlatCAMCNCjob)
  1485. # Propagate options
  1486. job_obj.options["tooldia"] = source_geo.options["cnctooldia"]
  1487. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
  1488. job_obj.z_cut = source_geo.options["cutz"]
  1489. job_obj.z_move = source_geo.options["travelz"]
  1490. job_obj.feedrate = source_geo.options["feedrate"]
  1491. GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
  1492. # TODO: The tolerance should not be hard coded. Just for testing.
  1493. job_obj.generate_from_geometry(source_geo, tolerance=0.0005)
  1494. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
  1495. job_obj.gcode_parse()
  1496. # TODO: job_obj.create_geometry creates stuff that is not used.
  1497. #GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
  1498. #job_obj.create_geometry()
  1499. GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
  1500. # To be run in separate thread
  1501. def job_thread(app_obj):
  1502. app_obj.new_object("cncjob", job_name, job_init)
  1503. GLib.idle_add(lambda: app_obj.info("CNCjob created: %s" % job_name))
  1504. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1505. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1506. # Start the thread
  1507. # t = threading.Thread(target=job_thread, args=(self,))
  1508. # t.daemon = True
  1509. # t.start()
  1510. self.worker.add_task(job_thread, [self])
  1511. def on_generate_paintarea(self, widget):
  1512. """
  1513. Callback for button on geometry form.
  1514. Subscribes to the "Click on plot" event and continues
  1515. after the click. Finds the polygon containing
  1516. the clicked point and runs clear_poly() on it, resulting
  1517. in a new FlatCAMGeometry object.
  1518. :param widget: The widget from which this was called.
  1519. :return: None
  1520. """
  1521. self.info("Click inside the desired polygon.")
  1522. geo = self.collection.get_active()
  1523. geo.read_form()
  1524. assert isinstance(geo, FlatCAMGeometry)
  1525. tooldia = geo.options["painttooldia"]
  1526. overlap = geo.options["paintoverlap"]
  1527. # To be called after clicking on the plot.
  1528. def doit(event):
  1529. self.plot_click_subscribers.pop("generate_paintarea")
  1530. self.info("")
  1531. point = [event.xdata, event.ydata]
  1532. poly = find_polygon(geo.solid_geometry, point)
  1533. # Initializes the new geometry object
  1534. def gen_paintarea(geo_obj, app_obj):
  1535. assert isinstance(geo_obj, FlatCAMGeometry)
  1536. assert isinstance(app_obj, App)
  1537. cp = clear_poly(poly.buffer(-geo.options["paintmargin"]), tooldia, overlap)
  1538. geo_obj.solid_geometry = cp
  1539. geo_obj.options["cnctooldia"] = tooldia
  1540. name = self.selected_item_name + "_paint"
  1541. self.new_object("geometry", name, gen_paintarea)
  1542. self.plot_click_subscribers["generate_paintarea"] = doit
  1543. def on_cncjob_exportgcode(self, widget):
  1544. """
  1545. Called from button on CNCjob form to save the G-Code from the object.
  1546. :param widget: The widget from which this was called.
  1547. :return: None
  1548. """
  1549. def on_success(app_obj, filename):
  1550. cncjob = app_obj.collection.get_active()
  1551. f = open(filename, 'w')
  1552. f.write(cncjob.gcode)
  1553. f.close()
  1554. app_obj.info("Saved to: " + filename)
  1555. self.file_chooser_save_action(on_success)
  1556. def on_delete(self, widget):
  1557. """
  1558. Delete the currently selected FlatCAMObj.
  1559. :param widget: The widget from which this was called. Ignored.
  1560. :return: None
  1561. """
  1562. # Keep this for later
  1563. name = copy.copy(self.collection.get_active().options["name"])
  1564. # Remove plot
  1565. self.plotcanvas.figure.delaxes(self.collection.get_active().axes)
  1566. self.plotcanvas.auto_adjust_axes()
  1567. # Clear form
  1568. self.setup_component_editor()
  1569. # Remove from dictionary
  1570. self.collection.delete_active()
  1571. self.info("Object deleted: %s" % name)
  1572. def on_toolbar_replot(self, widget):
  1573. """
  1574. Callback for toolbar button. Re-plots all objects.
  1575. :param widget: The widget from which this was called.
  1576. :return: None
  1577. """
  1578. self.collection.get_active().read_form()
  1579. self.plot_all()
  1580. def on_clear_plots(self, widget):
  1581. """
  1582. Callback for toolbar button. Clears all plots.
  1583. :param widget: The widget from which this was called.
  1584. :return: None
  1585. """
  1586. self.plotcanvas.clear()
  1587. def on_activate_name(self, entry):
  1588. """
  1589. Hitting 'Enter' after changing the name of an item
  1590. updates the item dictionary and re-builds the item list.
  1591. :param entry: The widget from which this was called.
  1592. :return: None
  1593. """
  1594. old_name = copy.copy(self.collection.get_active().options["name"])
  1595. new_name = entry.get_text()
  1596. self.collection.change_name(old_name, new_name)
  1597. self.info("Name changed from %s to %s" % (old_name, new_name))
  1598. def on_file_new(self, param):
  1599. """
  1600. Callback for menu item File->New. Returns the application to its
  1601. startup state.
  1602. :param param: Whatever is passed by the event. Ignore.
  1603. :return: None
  1604. """
  1605. # Remove everything from memory
  1606. # Clear plot
  1607. self.plotcanvas.clear()
  1608. # Delete data
  1609. self.collection.delete_all()
  1610. # Clear object editor
  1611. self.setup_component_editor()
  1612. # Clear list
  1613. #self.collection.build_list()
  1614. # Clear project filename
  1615. self.project_filename = None
  1616. # Re-fresh project options
  1617. self.on_options_app2project(None)
  1618. def on_filequit(self, param):
  1619. """
  1620. Callback for menu item File->Quit. Closes the application.
  1621. :param param: Whatever is passed by the event. Ignore.
  1622. :return: None
  1623. """
  1624. self.window.destroy()
  1625. Gtk.main_quit()
  1626. def on_closewindow(self, param):
  1627. """
  1628. Callback for closing the main window.
  1629. :param param: Whatever is passed by the event. Ignore.
  1630. :return: None
  1631. """
  1632. self.window.destroy()
  1633. Gtk.main_quit()
  1634. def file_chooser_action(self, on_success):
  1635. """
  1636. Opens the file chooser and runs on_success on a separate thread
  1637. upon completion of valid file choice.
  1638. :param on_success: A function to run upon completion of a valid file
  1639. selection. Takes 2 parameters: The app instance and the filename.
  1640. Note that it is run on a separate thread, therefore it must take the
  1641. appropriate precautions when accessing shared resources.
  1642. :type on_success: func
  1643. :return: None
  1644. """
  1645. dialog = Gtk.FileChooserDialog("Please choose a file", self.window,
  1646. Gtk.FileChooserAction.OPEN,
  1647. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1648. Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
  1649. response = dialog.run()
  1650. if response == Gtk.ResponseType.OK:
  1651. filename = dialog.get_filename()
  1652. dialog.destroy()
  1653. # t = threading.Thread(target=on_success, args=(self, filename))
  1654. # t.daemon = True
  1655. # t.start()
  1656. self.worker.add_task(on_success, [self, filename])
  1657. #on_success(self, filename)
  1658. elif response == Gtk.ResponseType.CANCEL:
  1659. self.info("Open cancelled.") # print("Cancel clicked")
  1660. dialog.destroy()
  1661. def file_chooser_save_action(self, on_success):
  1662. """
  1663. Opens the file chooser and runs on_success upon completion of valid file choice.
  1664. :param on_success: A function to run upon selection of a filename. Takes 2
  1665. parameters: The instance of the application (App) and the chosen filename. This
  1666. gets run immediately in the same thread.
  1667. :return: None
  1668. """
  1669. dialog = Gtk.FileChooserDialog("Save file", self.window,
  1670. Gtk.FileChooserAction.SAVE,
  1671. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1672. Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
  1673. dialog.set_current_name("Untitled")
  1674. response = dialog.run()
  1675. if response == Gtk.ResponseType.OK:
  1676. filename = dialog.get_filename()
  1677. dialog.destroy()
  1678. on_success(self, filename)
  1679. elif response == Gtk.ResponseType.CANCEL:
  1680. self.info("Save cancelled.") # print("Cancel clicked")
  1681. dialog.destroy()
  1682. def open_gerber(self, filename):
  1683. GLib.idle_add(lambda: self.set_progress_bar(0.1, "Opening Gerber ..."))
  1684. def obj_init(gerber_obj, app_obj):
  1685. assert isinstance(gerber_obj, FlatCAMGerber)
  1686. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1687. gerber_obj.parse_file(filename)
  1688. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Creating Geometry ..."))
  1689. gerber_obj.create_geometry()
  1690. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1691. name = filename.split('/')[-1].split('\\')[-1]
  1692. self.new_object("gerber", name, obj_init)
  1693. self.register_recent("gerber", filename)
  1694. GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
  1695. GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, ""))
  1696. def on_fileopengerber(self, param):
  1697. """
  1698. Callback for menu item File->Open Gerber. Defines a function that is then passed
  1699. to ``self.file_chooser_action()``. It requests the creation of a FlatCAMGerber object
  1700. and updates the progress bar throughout the process.
  1701. :param param: Ignore
  1702. :return: None
  1703. """
  1704. # IMPORTANT: on_success will run on a separate thread. Use
  1705. # GLib.idle_add(function, **kwargs) to launch actions that will
  1706. # updata the GUI.
  1707. # def on_success(app_obj, filename):
  1708. # assert isinstance(app_obj, App)
  1709. # GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Gerber ..."))
  1710. #
  1711. # def obj_init(gerber_obj, app_obj):
  1712. # assert isinstance(gerber_obj, FlatCAMGerber)
  1713. # GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1714. # gerber_obj.parse_file(filename)
  1715. # GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Creating Geometry ..."))
  1716. # gerber_obj.create_geometry()
  1717. # GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1718. #
  1719. # name = filename.split('/')[-1].split('\\')[-1]
  1720. # app_obj.new_object("gerber", name, obj_init)
  1721. # app_obj.register_recent("gerber", filename)
  1722. #
  1723. # GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1724. # GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1725. # on_success gets run on a separate thread
  1726. # self.file_chooser_action(on_success)
  1727. self.file_chooser_action(lambda ao, filename: self.open_gerber(filename))
  1728. def open_excellon(self, filename):
  1729. GLib.idle_add(lambda: self.set_progress_bar(0.1, "Opening Excellon ..."))
  1730. def obj_init(excellon_obj, app_obj):
  1731. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1732. excellon_obj.parse_file(filename)
  1733. excellon_obj.create_geometry()
  1734. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1735. name = filename.split('/')[-1].split('\\')[-1]
  1736. self.new_object("excellon", name, obj_init)
  1737. self.register_recent("excellon", filename)
  1738. GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
  1739. GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, ""))
  1740. def on_fileopenexcellon(self, param):
  1741. """
  1742. Callback for menu item File->Open Excellon. Defines a function that is then passed
  1743. to ``self.file_chooser_action()``. It requests the creation of a FlatCAMExcellon object
  1744. and updates the progress bar throughout the process.
  1745. :param param: Ignore
  1746. :return: None
  1747. """
  1748. # IMPORTANT: on_success will run on a separate thread. Use
  1749. # GLib.idle_add(function, **kwargs) to launch actions that will
  1750. # updata the GUI.
  1751. # def on_success(app_obj, filename):
  1752. # assert isinstance(app_obj, App)
  1753. # GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Excellon ..."))
  1754. #
  1755. # def obj_init(excellon_obj, app_obj):
  1756. # GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1757. # excellon_obj.parse_file(filename)
  1758. # excellon_obj.create_geometry()
  1759. # GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1760. #
  1761. # name = filename.split('/')[-1].split('\\')[-1]
  1762. # app_obj.new_object("excellon", name, obj_init)
  1763. # self.register_recent("excellon", filename)
  1764. #
  1765. # GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1766. # GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1767. # on_success gets run on a separate thread
  1768. # self.file_chooser_action(on_success)
  1769. self.file_chooser_action(lambda ao, filename: self.open_excellon(filename))
  1770. def open_gcode(self, filename):
  1771. def obj_init(job_obj, app_obj_):
  1772. """
  1773. :type app_obj_: App
  1774. """
  1775. assert isinstance(app_obj_, App)
  1776. GLib.idle_add(lambda: app_obj_.set_progress_bar(0.1, "Opening G-Code ..."))
  1777. f = open(filename)
  1778. gcode = f.read()
  1779. f.close()
  1780. job_obj.gcode = gcode
  1781. GLib.idle_add(lambda: app_obj_.set_progress_bar(0.2, "Parsing ..."))
  1782. job_obj.gcode_parse()
  1783. GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Creating geometry ..."))
  1784. job_obj.create_geometry()
  1785. GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Plotting ..."))
  1786. name = filename.split('/')[-1].split('\\')[-1]
  1787. self.new_object("cncjob", name, obj_init)
  1788. self.register_recent("cncjob", filename)
  1789. GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
  1790. GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, ""))
  1791. def on_fileopengcode(self, param):
  1792. """
  1793. Callback for menu item File->Open G-Code. Defines a function that is then passed
  1794. to ``self.file_chooser_action()``. It requests the creation of a FlatCAMCNCjob object
  1795. and updates the progress bar throughout the process.
  1796. :param param: Ignore
  1797. :return: None
  1798. """
  1799. # IMPORTANT: on_success will run on a separate thread. Use
  1800. # GLib.idle_add(function, **kwargs) to launch actions that will
  1801. # updata the GUI.
  1802. # def on_success(app_obj, filename):
  1803. # assert isinstance(app_obj, App)
  1804. #
  1805. # def obj_init(job_obj, app_obj_):
  1806. # """
  1807. #
  1808. # :type app_obj_: App
  1809. # """
  1810. # assert isinstance(app_obj_, App)
  1811. # GLib.idle_add(lambda: app_obj_.set_progress_bar(0.1, "Opening G-Code ..."))
  1812. #
  1813. # f = open(filename)
  1814. # gcode = f.read()
  1815. # f.close()
  1816. #
  1817. # job_obj.gcode = gcode
  1818. #
  1819. # GLib.idle_add(lambda: app_obj_.set_progress_bar(0.2, "Parsing ..."))
  1820. # job_obj.gcode_parse()
  1821. #
  1822. # GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Creating geometry ..."))
  1823. # job_obj.create_geometry()
  1824. #
  1825. # GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Plotting ..."))
  1826. #
  1827. # name = filename.split('/')[-1].split('\\')[-1]
  1828. # app_obj.new_object("cncjob", name, obj_init)
  1829. # self.register_recent("cncjob", filename)
  1830. #
  1831. # GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1832. # GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1833. # on_success gets run on a separate thread
  1834. # self.file_chooser_action(on_success)
  1835. self.file_chooser_action(lambda ao, filename: self.open_gcode(filename))
  1836. def on_mouse_move_over_plot(self, event):
  1837. """
  1838. Callback for the mouse motion event over the plot. This event is generated
  1839. by the Matplotlib backend and has been registered in ``self.__init__()``.
  1840. For details, see: http://matplotlib.org/users/event_handling.html
  1841. :param event: Contains information about the event.
  1842. :return: None
  1843. """
  1844. try: # May fail in case mouse not within axes
  1845. self.position_label.set_label("X: %.4f Y: %.4f" % (
  1846. event.xdata, event.ydata))
  1847. self.mouse = [event.xdata, event.ydata]
  1848. for subscriber in self.plot_mousemove_subscribers:
  1849. self.plot_mousemove_subscribers[subscriber](event)
  1850. except:
  1851. self.position_label.set_label("")
  1852. self.mouse = None
  1853. def on_click_over_plot(self, event):
  1854. """
  1855. Callback for the mouse click event over the plot. This event is generated
  1856. by the Matplotlib backend and has been registered in ``self.__init__()``.
  1857. For details, see: http://matplotlib.org/users/event_handling.html
  1858. Default actions are:
  1859. * Copy coordinates to clipboard. Ex.: (65.5473, -13.2679)
  1860. :param event: Contains information about the event, like which button
  1861. was clicked, the pixel coordinates and the axes coordinates.
  1862. :return: None
  1863. """
  1864. # For key presses
  1865. self.plotcanvas.canvas.grab_focus()
  1866. try:
  1867. print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
  1868. event.button, event.x, event.y, event.xdata, event.ydata)
  1869. # TODO: This custom subscription mechanism is probably not necessary.
  1870. for subscriber in self.plot_click_subscribers:
  1871. self.plot_click_subscribers[subscriber](event)
  1872. self.clipboard.set_text("(%.4f, %.4f)" % (event.xdata, event.ydata), -1)
  1873. except Exception, e:
  1874. print "Outside plot!"
  1875. def on_zoom_in(self, event):
  1876. """
  1877. Callback for zoom-in request. This can be either from the corresponding
  1878. toolbar button or the '3' key when the canvas is focused. Calls ``self.zoom()``.
  1879. :param event: Ignored.
  1880. :return: None
  1881. """
  1882. self.plotcanvas.zoom(1.5)
  1883. return
  1884. def on_zoom_out(self, event):
  1885. """
  1886. Callback for zoom-out request. This can be either from the corresponding
  1887. toolbar button or the '2' key when the canvas is focused. Calls ``self.zoom()``.
  1888. :param event: Ignored.
  1889. :return: None
  1890. """
  1891. self.plotcanvas.zoom(1 / 1.5)
  1892. def on_zoom_fit(self, event):
  1893. """
  1894. Callback for zoom-out request. This can be either from the corresponding
  1895. toolbar button or the '1' key when the canvas is focused. Calls ``self.adjust_axes()``
  1896. with axes limits from the geometry bounds of all objects.
  1897. :param event: Ignored.
  1898. :return: None
  1899. """
  1900. xmin, ymin, xmax, ymax = self.collection.get_bounds()
  1901. width = xmax - xmin
  1902. height = ymax - ymin
  1903. xmin -= 0.05 * width
  1904. xmax += 0.05 * width
  1905. ymin -= 0.05 * height
  1906. ymax += 0.05 * height
  1907. self.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax)
  1908. def on_key_over_plot(self, event):
  1909. """
  1910. Callback for the key pressed event when the canvas is focused. Keyboard
  1911. shortcuts are handled here. So far, these are the shortcuts:
  1912. ========== ============================================
  1913. Key Action
  1914. ========== ============================================
  1915. '1' Zoom-fit. Fits the axes limits to the data.
  1916. '2' Zoom-out.
  1917. '3' Zoom-in.
  1918. 'm' Toggle on-off the measuring tool.
  1919. ========== ============================================
  1920. :param event: Ignored.
  1921. :return: None
  1922. """
  1923. if event.key == '1': # 1
  1924. self.on_zoom_fit(None)
  1925. return
  1926. if event.key == '2': # 2
  1927. self.plotcanvas.zoom(1 / 1.5, self.mouse)
  1928. return
  1929. if event.key == '3': # 3
  1930. self.plotcanvas.zoom(1.5, self.mouse)
  1931. return
  1932. if event.key == 'm':
  1933. if self.measure.toggle_active():
  1934. self.info("Measuring tool ON")
  1935. else:
  1936. self.info("Measuring tool OFF")
  1937. return
  1938. class BaseDraw:
  1939. def __init__(self, plotcanvas, name=None):
  1940. """
  1941. :param plotcanvas: The PlotCanvas where the drawing tool will operate.
  1942. :type plotcanvas: PlotCanvas
  1943. """
  1944. self.plotcanvas = plotcanvas
  1945. # Must have unique axes
  1946. charset = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"
  1947. self.name = name or [random.choice(charset) for i in range(20)]
  1948. self.axes = self.plotcanvas.new_axes(self.name)
  1949. class DrawingObject(BaseDraw):
  1950. def __init__(self, plotcanvas, name=None):
  1951. """
  1952. Possible objects are:
  1953. * Point
  1954. * Line
  1955. * Rectangle
  1956. * Circle
  1957. * Polygon
  1958. """
  1959. BaseDraw.__init__(self, plotcanvas)
  1960. self.properties = {}
  1961. def plot(self):
  1962. return
  1963. def update_plot(self):
  1964. self.axes.cla()
  1965. self.plot()
  1966. self.plotcanvas.auto_adjust_axes()
  1967. class DrawingPoint(DrawingObject):
  1968. def __init__(self, plotcanvas, name=None, coord=None):
  1969. DrawingObject.__init__(self, plotcanvas)
  1970. self.properties.update({
  1971. "coordinate": coord
  1972. })
  1973. def plot(self):
  1974. x, y = self.properties["coordinate"]
  1975. self.axes.plot(x, y, 'o')
  1976. class Measurement:
  1977. def __init__(self, container, axes, click_subscibers, move_subscribers, update=None):
  1978. self.update = update
  1979. self.container = container
  1980. self.frame = None
  1981. self.label = None
  1982. self.click_subscribers = click_subscibers
  1983. self.move_subscribers = move_subscribers
  1984. self.point1 = None
  1985. self.point2 = None
  1986. self.active = False
  1987. def toggle_active(self, *args):
  1988. if self.active: # Deactivate
  1989. self.active = False
  1990. self.move_subscribers.pop("meas")
  1991. self.click_subscribers.pop("meas")
  1992. self.container.remove(self.frame)
  1993. if self.update is not None:
  1994. self.update()
  1995. return False
  1996. else: # Activate
  1997. print "DEBUG: Activating Measurement Tool..."
  1998. self.active = True
  1999. self.click_subscribers["meas"] = self.on_click
  2000. self.move_subscribers["meas"] = self.on_move
  2001. self.frame = Gtk.Frame()
  2002. self.frame.set_margin_right(5)
  2003. self.frame.set_margin_top(3)
  2004. align = Gtk.Alignment()
  2005. align.set(0, 0.5, 0, 0)
  2006. align.set_padding(4, 4, 4, 4)
  2007. self.label = Gtk.Label()
  2008. self.label.set_label("Click on a reference point...")
  2009. abox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 10)
  2010. abox.pack_start(Gtk.Image.new_from_file('share/measure16.png'), False, False, 0)
  2011. abox.pack_start(self.label, False, False, 0)
  2012. align.add(abox)
  2013. self.frame.add(align)
  2014. self.container.pack_end(self.frame, False, True, 1)
  2015. self.frame.show_all()
  2016. return True
  2017. def on_move(self, event):
  2018. if self.point1 is None:
  2019. self.label.set_label("Click on a reference point...")
  2020. else:
  2021. dx = event.xdata - self.point1[0]
  2022. dy = event.ydata - self.point1[1]
  2023. d = sqrt(dx**2 + dy**2)
  2024. self.label.set_label("D = %.4f D(x) = %.4f D(y) = %.4f" % (d, dx, dy))
  2025. if self.update is not None:
  2026. self.update()
  2027. def on_click(self, event):
  2028. if self.point1 is None:
  2029. self.point1 = (event.xdata, event.ydata)
  2030. else:
  2031. self.point2 = copy.copy(self.point1)
  2032. self.point1 = (event.xdata, event.ydata)
  2033. self.on_move(event)
  2034. class PlotCanvas:
  2035. """
  2036. Class handling the plotting area in the application.
  2037. """
  2038. def __init__(self, container):
  2039. """
  2040. The constructor configures the Matplotlib figure that
  2041. will contain all plots, creates the base axes and connects
  2042. events to the plotting area.
  2043. :param container: The parent container in which to draw plots.
  2044. :rtype: PlotCanvas
  2045. """
  2046. # Options
  2047. self.x_margin = 15 # pixels
  2048. self.y_margin = 25 # Pixels
  2049. # Parent container
  2050. self.container = container
  2051. # Plots go onto a single matplotlib.figure
  2052. self.figure = Figure(dpi=50) # TODO: dpi needed?
  2053. self.figure.patch.set_visible(False)
  2054. # These axes show the ticks and grid. No plotting done here.
  2055. # New axes must have a label, otherwise mpl returns an existing one.
  2056. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
  2057. self.axes.set_aspect(1)
  2058. self.axes.grid(True)
  2059. # The canvas is the top level container (Gtk.DrawingArea)
  2060. self.canvas = FigureCanvas(self.figure)
  2061. self.canvas.set_hexpand(1)
  2062. self.canvas.set_vexpand(1)
  2063. self.canvas.set_can_focus(True) # For key press
  2064. # Attach to parent
  2065. self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns??
  2066. # Events
  2067. self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
  2068. self.canvas.connect('configure-event', self.auto_adjust_axes)
  2069. self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
  2070. self.canvas.connect("scroll-event", self.on_scroll)
  2071. self.canvas.mpl_connect('key_press_event', self.on_key_down)
  2072. self.canvas.mpl_connect('key_release_event', self.on_key_up)
  2073. self.mouse = [0, 0]
  2074. self.key = None
  2075. def on_key_down(self, event):
  2076. """
  2077. :param event:
  2078. :return:
  2079. """
  2080. self.key = event.key
  2081. def on_key_up(self, event):
  2082. """
  2083. :param event:
  2084. :return:
  2085. """
  2086. self.key = None
  2087. def mpl_connect(self, event_name, callback):
  2088. """
  2089. Attach an event handler to the canvas through the Matplotlib interface.
  2090. :param event_name: Name of the event
  2091. :type event_name: str
  2092. :param callback: Function to call
  2093. :type callback: func
  2094. :return: Nothing
  2095. """
  2096. self.canvas.mpl_connect(event_name, callback)
  2097. def connect(self, event_name, callback):
  2098. """
  2099. Attach an event handler to the canvas through the native GTK interface.
  2100. :param event_name: Name of the event
  2101. :type event_name: str
  2102. :param callback: Function to call
  2103. :type callback: function
  2104. :return: Nothing
  2105. """
  2106. self.canvas.connect(event_name, callback)
  2107. def clear(self):
  2108. """
  2109. Clears axes and figure.
  2110. :return: None
  2111. """
  2112. # Clear
  2113. self.axes.cla()
  2114. self.figure.clf()
  2115. # Re-build
  2116. self.figure.add_axes(self.axes)
  2117. self.axes.set_aspect(1)
  2118. self.axes.grid(True)
  2119. # Re-draw
  2120. self.canvas.queue_draw()
  2121. def adjust_axes(self, xmin, ymin, xmax, ymax):
  2122. """
  2123. Adjusts all axes while maintaining the use of the whole canvas
  2124. and an aspect ratio to 1:1 between x and y axes. The parameters are an original
  2125. request that will be modified to fit these restrictions.
  2126. :param xmin: Requested minimum value for the X axis.
  2127. :type xmin: float
  2128. :param ymin: Requested minimum value for the Y axis.
  2129. :type ymin: float
  2130. :param xmax: Requested maximum value for the X axis.
  2131. :type xmax: float
  2132. :param ymax: Requested maximum value for the Y axis.
  2133. :type ymax: float
  2134. :return: None
  2135. """
  2136. width = xmax - xmin
  2137. height = ymax - ymin
  2138. try:
  2139. r = width / height
  2140. except:
  2141. print "ERROR: Height is", height
  2142. return
  2143. canvas_w, canvas_h = self.canvas.get_width_height()
  2144. canvas_r = float(canvas_w) / canvas_h
  2145. x_ratio = float(self.x_margin) / canvas_w
  2146. y_ratio = float(self.y_margin) / canvas_h
  2147. if r > canvas_r:
  2148. ycenter = (ymin + ymax) / 2.0
  2149. newheight = height * r / canvas_r
  2150. ymin = ycenter - newheight / 2.0
  2151. ymax = ycenter + newheight / 2.0
  2152. else:
  2153. xcenter = (xmax + ymin) / 2.0
  2154. newwidth = width * canvas_r / r
  2155. xmin = xcenter - newwidth / 2.0
  2156. xmax = xcenter + newwidth / 2.0
  2157. # Adjust axes
  2158. for ax in self.figure.get_axes():
  2159. if ax._label != 'base':
  2160. ax.set_frame_on(False) # No frame
  2161. ax.set_xticks([]) # No tick
  2162. ax.set_yticks([]) # No ticks
  2163. ax.patch.set_visible(False) # No background
  2164. ax.set_aspect(1)
  2165. ax.set_xlim((xmin, xmax))
  2166. ax.set_ylim((ymin, ymax))
  2167. ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
  2168. # Re-draw
  2169. self.canvas.queue_draw()
  2170. def auto_adjust_axes(self, *args):
  2171. """
  2172. Calls ``adjust_axes()`` using the extents of the base axes.
  2173. :rtype : None
  2174. :return: None
  2175. """
  2176. xmin, xmax = self.axes.get_xlim()
  2177. ymin, ymax = self.axes.get_ylim()
  2178. self.adjust_axes(xmin, ymin, xmax, ymax)
  2179. def zoom(self, factor, center=None):
  2180. """
  2181. Zooms the plot by factor around a given
  2182. center point. Takes care of re-drawing.
  2183. :param factor: Number by which to scale the plot.
  2184. :type factor: float
  2185. :param center: Coordinates [x, y] of the point around which to scale the plot.
  2186. :type center: list
  2187. :return: None
  2188. """
  2189. xmin, xmax = self.axes.get_xlim()
  2190. ymin, ymax = self.axes.get_ylim()
  2191. width = xmax - xmin
  2192. height = ymax - ymin
  2193. if center is None or center == [None, None]:
  2194. center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
  2195. # For keeping the point at the pointer location
  2196. relx = (xmax - center[0]) / width
  2197. rely = (ymax - center[1]) / height
  2198. new_width = width / factor
  2199. new_height = height / factor
  2200. xmin = center[0] - new_width * (1 - relx)
  2201. xmax = center[0] + new_width * relx
  2202. ymin = center[1] - new_height * (1 - rely)
  2203. ymax = center[1] + new_height * rely
  2204. # Adjust axes
  2205. for ax in self.figure.get_axes():
  2206. ax.set_xlim((xmin, xmax))
  2207. ax.set_ylim((ymin, ymax))
  2208. # Re-draw
  2209. self.canvas.queue_draw()
  2210. def pan(self, x, y):
  2211. xmin, xmax = self.axes.get_xlim()
  2212. ymin, ymax = self.axes.get_ylim()
  2213. width = xmax - xmin
  2214. height = ymax - ymin
  2215. # Adjust axes
  2216. for ax in self.figure.get_axes():
  2217. ax.set_xlim((xmin + x*width, xmax + x*width))
  2218. ax.set_ylim((ymin + y*height, ymax + y*height))
  2219. # Re-draw
  2220. self.canvas.queue_draw()
  2221. def new_axes(self, name):
  2222. """
  2223. Creates and returns an Axes object attached to this object's Figure.
  2224. :param name: Unique label for the axes.
  2225. :return: Axes attached to the figure.
  2226. :rtype: Axes
  2227. """
  2228. return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
  2229. # def plot_axes(self, axes):
  2230. #
  2231. # if axes not in self.figure.axes:
  2232. # self.figure.add_axes(axes)
  2233. #
  2234. # # Basic configuration
  2235. # axes.set_frame_on(False) # No frame
  2236. # axes.set_xticks([]) # No tick
  2237. # axes.set_yticks([]) # No ticks
  2238. # axes.patch.set_visible(False) # No background
  2239. # axes.set_aspect(1)
  2240. #
  2241. # # Adjust limits
  2242. # self.auto_adjust_axes()
  2243. def on_scroll(self, canvas, event):
  2244. """
  2245. Scroll event handler.
  2246. :param canvas: The widget generating the event. Ignored.
  2247. :param event: Event object containing the event information.
  2248. :return: None
  2249. """
  2250. z, direction = event.get_scroll_direction()
  2251. if self.key is None:
  2252. if direction is Gdk.ScrollDirection.UP:
  2253. self.zoom(1.5, self.mouse)
  2254. else:
  2255. self.zoom(1/1.5, self.mouse)
  2256. return
  2257. if self.key == 'shift':
  2258. if direction is Gdk.ScrollDirection.UP:
  2259. self.pan(0.3, 0)
  2260. else:
  2261. self.pan(-0.3, 0)
  2262. return
  2263. if self.key == 'ctrl+control':
  2264. if direction is Gdk.ScrollDirection.UP:
  2265. self.pan(0, 0.3)
  2266. else:
  2267. self.pan(0, -0.3)
  2268. return
  2269. def on_mouse_move(self, event):
  2270. """
  2271. Mouse movement event hadler.
  2272. :param event: Contains information about the event.
  2273. :return: None
  2274. """
  2275. self.mouse = [event.xdata, event.ydata]
  2276. class ObjectCollection:
  2277. classdict = {
  2278. "gerber": FlatCAMGerber,
  2279. "excellon": FlatCAMExcellon,
  2280. "cncjob": FlatCAMCNCjob,
  2281. "geometry": FlatCAMGeometry
  2282. }
  2283. icon_files = {
  2284. "gerber": "share/flatcam_icon16.png",
  2285. "excellon": "share/drill16.png",
  2286. "cncjob": "share/cnc16.png",
  2287. "geometry": "share/geometry16.png"
  2288. }
  2289. def __init__(self):
  2290. ### Icons for the list view
  2291. self.icons = {}
  2292. for kind in ObjectCollection.icon_files:
  2293. self.icons[kind] = GdkPixbuf.Pixbuf.new_from_file(ObjectCollection.icon_files[kind])
  2294. ### GUI List components
  2295. ## Model
  2296. self.store = Gtk.ListStore(FlatCAMObj)
  2297. ## View
  2298. self.view = Gtk.TreeView(model=self.store)
  2299. #self.view.connect("row_activated", self.on_row_activated)
  2300. self.tree_selection = self.view.get_selection()
  2301. self.change_subscription = self.tree_selection.connect("changed", self.on_list_selection_change)
  2302. ## Renderers
  2303. # Icon
  2304. renderer_pixbuf = Gtk.CellRendererPixbuf()
  2305. column_pixbuf = Gtk.TreeViewColumn("Type", renderer_pixbuf)
  2306. def _set_cell_icon(column, cell, model, it, data):
  2307. obj = model.get_value(it, 0)
  2308. cell.set_property('pixbuf', self.icons[obj.kind])
  2309. column_pixbuf.set_cell_data_func(renderer_pixbuf, _set_cell_icon)
  2310. self.view.append_column(column_pixbuf)
  2311. # Name
  2312. renderer_text = Gtk.CellRendererText()
  2313. column_text = Gtk.TreeViewColumn("Name", renderer_text)
  2314. def _set_cell_text(column, cell, model, it, data):
  2315. obj = model.get_value(it, 0)
  2316. cell.set_property('text', obj.options["name"])
  2317. column_text.set_cell_data_func(renderer_text, _set_cell_text)
  2318. self.view.append_column(column_text)
  2319. def delete_all(self):
  2320. print "OC.delete_all()"
  2321. # self.collection = []
  2322. # self.active = None
  2323. self.store.clear()
  2324. def delete_active(self):
  2325. print "OC.delete_active()"
  2326. try:
  2327. model, treeiter = self.tree_selection.get_selected()
  2328. self.store.remove(treeiter)
  2329. except:
  2330. pass
  2331. def on_row_activated(self, *args):
  2332. print "OC.on_row_activated()"
  2333. return
  2334. def on_list_selection_change(self, selection):
  2335. print "OC.on_list_selection_change()"
  2336. try:
  2337. self.get_active().build_ui()
  2338. except:
  2339. pass
  2340. # TODO: Now we don't have a reference to the previously
  2341. # TODO: active, so cannot read form.
  2342. def set_active(self, name):
  2343. print "OC.set_active()"
  2344. self.set_list_selection(name)
  2345. def get_active(self):
  2346. print "OC.get_active()"
  2347. try:
  2348. model, treeiter = self.tree_selection.get_selected()
  2349. return model[treeiter][0]
  2350. except ValueError:
  2351. return None
  2352. def set_list_selection(self, name):
  2353. print "OC.set_list_selection()"
  2354. iterat = self.store.get_iter_first()
  2355. while iterat is not None and self.store[iterat][0].options["name"] != name:
  2356. iterat = self.store.iter_next(iterat)
  2357. self.tree_selection.select_iter(iterat)
  2358. def append(self, obj, active=False):
  2359. print "OC.append()"
  2360. self.store.append([obj])
  2361. if active:
  2362. self.set_list_selection(obj.options["name"])
  2363. def get_names(self):
  2364. print "OC.get_names()"
  2365. names = []
  2366. iterat = self.store.get_iter_first()
  2367. while iterat is not None:
  2368. obj = self.store[iterat][0]
  2369. names.append(obj.options["name"])
  2370. iterat = self.store.iter_next(iterat)
  2371. return names
  2372. def get_bounds(self):
  2373. print "OC.get_bounds()"
  2374. # TODO: Move the operation out of here.
  2375. xmin = Inf
  2376. ymin = Inf
  2377. xmax = -Inf
  2378. ymax = -Inf
  2379. iterat = self.store.get_iter_first()
  2380. while iterat is not None:
  2381. obj = self.store[iterat][0]
  2382. try:
  2383. gxmin, gymin, gxmax, gymax = obj.bounds()
  2384. xmin = min([xmin, gxmin])
  2385. ymin = min([ymin, gymin])
  2386. xmax = max([xmax, gxmax])
  2387. ymax = max([ymax, gymax])
  2388. except:
  2389. print "DEV WARNING: Tried to get bounds of empty geometry."
  2390. iterat = self.store.iter_next(iterat)
  2391. return [xmin, ymin, xmax, ymax]
  2392. def get_list(self):
  2393. collection_list = []
  2394. iterat = self.store.get_iter_first()
  2395. while iterat is not None:
  2396. obj = self.store[iterat][0]
  2397. collection_list.append(obj)
  2398. iterat = self.store.iter_next(iterat)
  2399. return collection_list
  2400. def get_by_name(self, name):
  2401. iterat = self.store.get_iter_first()
  2402. while iterat is not None:
  2403. obj = self.store[iterat][0]
  2404. if obj.options["name"] == name:
  2405. return obj
  2406. iterat = self.store.iter_next(iterat)
  2407. return None
  2408. def change_name(self, old_name, new_name):
  2409. iterat = self.store.get_iter_first()
  2410. while iterat is not None:
  2411. obj = self.store[iterat][0]
  2412. if obj.options["name"] == old_name:
  2413. obj.options["name"] = new_name
  2414. self.store.row_changed(0, iterat)
  2415. return True
  2416. iterat = self.store.iter_next(iterat)
  2417. return False
  2418. app = App()
  2419. Gtk.main()