FlatCAMApp.py 86 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413
  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. import traceback
  10. import sys
  11. import urllib
  12. from copy import copy
  13. import random
  14. import logging
  15. from gi.repository import Gtk, GdkPixbuf, GObject, Gdk, GLib
  16. from shapely import speedups
  17. ########################################
  18. ## Imports part of FlatCAM ##
  19. ########################################
  20. from FlatCAMWorker import Worker
  21. from ObjectCollection import *
  22. from FlatCAMObj import *
  23. from PlotCanvas import *
  24. class GerberOptionsGroupUI(Gtk.VBox):
  25. def __init__(self):
  26. Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
  27. ## Plot options
  28. self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
  29. self.plot_options_label.set_markup("<b>Plot Options:</b>")
  30. self.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
  31. grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
  32. self.pack_start(grid0, expand=True, fill=False, padding=2)
  33. # Plot CB
  34. self.plot_cb = FCCheckBox(label='Plot')
  35. grid0.attach(self.plot_cb, 0, 0, 1, 1)
  36. # Solid CB
  37. self.solid_cb = FCCheckBox(label='Solid')
  38. grid0.attach(self.solid_cb, 1, 0, 1, 1)
  39. # Multicolored CB
  40. self.multicolored_cb = FCCheckBox(label='Multicolored')
  41. grid0.attach(self.multicolored_cb, 2, 0, 1, 1)
  42. ## Isolation Routing
  43. self.isolation_routing_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
  44. self.isolation_routing_label.set_markup("<b>Isolation Routing:</b>")
  45. self.pack_start(self.isolation_routing_label, expand=True, fill=False, padding=2)
  46. grid = Gtk.Grid(column_spacing=3, row_spacing=2)
  47. self.pack_start(grid, expand=True, fill=False, padding=2)
  48. l1 = Gtk.Label('Tool diam:', xalign=1)
  49. grid.attach(l1, 0, 0, 1, 1)
  50. self.iso_tool_dia_entry = LengthEntry()
  51. grid.attach(self.iso_tool_dia_entry, 1, 0, 1, 1)
  52. l2 = Gtk.Label('Width (# passes):', xalign=1)
  53. grid.attach(l2, 0, 1, 1, 1)
  54. self.iso_width_entry = IntEntry()
  55. grid.attach(self.iso_width_entry, 1, 1, 1, 1)
  56. l3 = Gtk.Label('Pass overlap:', xalign=1)
  57. grid.attach(l3, 0, 2, 1, 1)
  58. self.iso_overlap_entry = FloatEntry()
  59. grid.attach(self.iso_overlap_entry, 1, 2, 1, 1)
  60. ## Board cuttout
  61. self.isolation_routing_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
  62. self.isolation_routing_label.set_markup("<b>Board cutout:</b>")
  63. self.pack_start(self.isolation_routing_label, expand=True, fill=False, padding=2)
  64. grid2 = Gtk.Grid(column_spacing=3, row_spacing=2)
  65. self.pack_start(grid2, expand=True, fill=False, padding=2)
  66. l4 = Gtk.Label('Tool dia:', xalign=1)
  67. grid2.attach(l4, 0, 0, 1, 1)
  68. self.cutout_tooldia_entry = LengthEntry()
  69. grid2.attach(self.cutout_tooldia_entry, 1, 0, 1, 1)
  70. l5 = Gtk.Label('Margin:', xalign=1)
  71. grid2.attach(l5, 0, 1, 1, 1)
  72. self.cutout_margin_entry = LengthEntry()
  73. grid2.attach(self.cutout_margin_entry, 1, 1, 1, 1)
  74. l6 = Gtk.Label('Gap size:', xalign=1)
  75. grid2.attach(l6, 0, 2, 1, 1)
  76. self.cutout_gap_entry = LengthEntry()
  77. grid2.attach(self.cutout_gap_entry, 1, 2, 1, 1)
  78. l7 = Gtk.Label('Gaps:', xalign=1)
  79. grid2.attach(l7, 0, 3, 1, 1)
  80. self.gaps_radio = RadioSet([{'label': '2 (T/B)', 'value': 'tb'},
  81. {'label': '2 (L/R)', 'value': 'lr'},
  82. {'label': '4', 'value': '4'}])
  83. grid2.attach(self.gaps_radio, 1, 3, 1, 1)
  84. ## Non-copper regions
  85. self.noncopper_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
  86. self.noncopper_label.set_markup("<b>Non-copper regions:</b>")
  87. self.pack_start(self.noncopper_label, expand=True, fill=False, padding=2)
  88. grid3 = Gtk.Grid(column_spacing=3, row_spacing=2)
  89. self.pack_start(grid3, expand=True, fill=False, padding=2)
  90. l8 = Gtk.Label('Boundary margin:', xalign=1)
  91. grid3.attach(l8, 0, 0, 1, 1)
  92. self.noncopper_margin_entry = LengthEntry()
  93. grid3.attach(self.noncopper_margin_entry, 1, 0, 1, 1)
  94. self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners")
  95. grid3.attach(self.noncopper_rounded_cb, 0, 1, 2, 1)
  96. ## Bounding box
  97. self.boundingbox_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
  98. self.boundingbox_label.set_markup('<b>Bounding Box:</b>')
  99. self.pack_start(self.boundingbox_label, expand=True, fill=False, padding=2)
  100. grid4 = Gtk.Grid(column_spacing=3, row_spacing=2)
  101. self.pack_start(grid4, expand=True, fill=False, padding=2)
  102. l9 = Gtk.Label('Boundary Margin:', xalign=1)
  103. grid4.attach(l9, 0, 0, 1, 1)
  104. self.bbmargin_entry = LengthEntry()
  105. grid4.attach(self.bbmargin_entry, 1, 0, 1, 1)
  106. self.bbrounded_cb = FCCheckBox(label="Rounded corners")
  107. grid4.attach(self.bbrounded_cb, 0, 1, 2, 1)
  108. class ExcellonOptionsGroupUI(Gtk.VBox):
  109. def __init__(self):
  110. Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
  111. ## Plot options
  112. self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
  113. self.plot_options_label.set_markup("<b>Plot Options:</b>")
  114. self.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
  115. grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
  116. self.pack_start(grid0, expand=True, fill=False, padding=2)
  117. self.plot_cb = FCCheckBox(label='Plot')
  118. grid0.attach(self.plot_cb, 0, 0, 1, 1)
  119. self.solid_cb = FCCheckBox(label='Solid')
  120. grid0.attach(self.solid_cb, 1, 0, 1, 1)
  121. ## Create CNC Job
  122. self.cncjob_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
  123. self.cncjob_label.set_markup('<b>Create CNC Job</b>')
  124. self.pack_start(self.cncjob_label, expand=True, fill=False, padding=2)
  125. grid1 = Gtk.Grid(column_spacing=3, row_spacing=2)
  126. self.pack_start(grid1, expand=True, fill=False, padding=2)
  127. l1 = Gtk.Label('Cut Z:', xalign=1)
  128. grid1.attach(l1, 0, 0, 1, 1)
  129. self.cutz_entry = LengthEntry()
  130. grid1.attach(self.cutz_entry, 1, 0, 1, 1)
  131. l2 = Gtk.Label('Travel Z:', xalign=1)
  132. grid1.attach(l2, 0, 1, 1, 1)
  133. self.travelz_entry = LengthEntry()
  134. grid1.attach(self.travelz_entry, 1, 1, 1, 1)
  135. l3 = Gtk.Label('Feed rate:', xalign=1)
  136. grid1.attach(l3, 0, 2, 1, 1)
  137. self.feedrate_entry = LengthEntry()
  138. grid1.attach(self.feedrate_entry, 1, 2, 1, 1)
  139. class GeometryOptionsGroupUI(Gtk.VBox):
  140. def __init__(self):
  141. Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
  142. ## Plot options
  143. self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
  144. self.plot_options_label.set_markup("<b>Plot Options:</b>")
  145. self.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
  146. grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
  147. self.pack_start(grid0, expand=True, fill=False, padding=2)
  148. # Plot CB
  149. self.plot_cb = FCCheckBox(label='Plot')
  150. grid0.attach(self.plot_cb, 0, 0, 1, 1)
  151. ## Create CNC Job
  152. self.cncjob_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
  153. self.cncjob_label.set_markup('<b>Create CNC Job:</b>')
  154. self.pack_start(self.cncjob_label, expand=True, fill=False, padding=2)
  155. grid1 = Gtk.Grid(column_spacing=3, row_spacing=2)
  156. self.pack_start(grid1, expand=True, fill=False, padding=2)
  157. # Cut Z
  158. l1 = Gtk.Label('Cut Z:', xalign=1)
  159. grid1.attach(l1, 0, 0, 1, 1)
  160. self.cutz_entry = LengthEntry()
  161. grid1.attach(self.cutz_entry, 1, 0, 1, 1)
  162. # Travel Z
  163. l2 = Gtk.Label('Travel Z:', xalign=1)
  164. grid1.attach(l2, 0, 1, 1, 1)
  165. self.travelz_entry = LengthEntry()
  166. grid1.attach(self.travelz_entry, 1, 1, 1, 1)
  167. l3 = Gtk.Label('Feed rate:', xalign=1)
  168. grid1.attach(l3, 0, 2, 1, 1)
  169. self.cncfeedrate_entry = LengthEntry()
  170. grid1.attach(self.cncfeedrate_entry, 1, 2, 1, 1)
  171. l4 = Gtk.Label('Tool dia:', xalign=1)
  172. grid1.attach(l4, 0, 3, 1, 1)
  173. self.cnctooldia_entry = LengthEntry()
  174. grid1.attach(self.cnctooldia_entry, 1, 3, 1, 1)
  175. ## Paint Area
  176. self.paint_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
  177. self.paint_label.set_markup('<b>Paint Area:</b>')
  178. self.pack_start(self.paint_label, expand=True, fill=False, padding=2)
  179. grid2 = Gtk.Grid(column_spacing=3, row_spacing=2)
  180. self.pack_start(grid2, expand=True, fill=False, padding=2)
  181. # Tool dia
  182. l5 = Gtk.Label('Tool dia:', xalign=1)
  183. grid2.attach(l5, 0, 0, 1, 1)
  184. self.painttooldia_entry = LengthEntry()
  185. grid2.attach(self.painttooldia_entry, 1, 0, 1, 1)
  186. # Overlap
  187. l6 = Gtk.Label('Overlap:', xalign=1)
  188. grid2.attach(l6, 0, 1, 1, 1)
  189. self.paintoverlap_entry = LengthEntry()
  190. grid2.attach(self.paintoverlap_entry, 1, 1, 1, 1)
  191. # Margin
  192. l7 = Gtk.Label('Margin:', xalign=1)
  193. grid2.attach(l7, 0, 2, 1, 1)
  194. self.paintmargin_entry = LengthEntry()
  195. grid2.attach(self.paintmargin_entry, 1, 2, 1, 1)
  196. class CNCJobOptionsGroupUI(Gtk.VBox):
  197. def __init__(self):
  198. Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
  199. ## Plot options
  200. self.plot_options_label = Gtk.Label(justify=Gtk.Justification.LEFT, xalign=0, margin_top=5)
  201. self.plot_options_label.set_markup("<b>Plot Options:</b>")
  202. self.pack_start(self.plot_options_label, expand=False, fill=True, padding=2)
  203. grid0 = Gtk.Grid(column_spacing=3, row_spacing=2)
  204. self.pack_start(grid0, expand=True, fill=False, padding=2)
  205. # Plot CB
  206. self.plot_cb = FCCheckBox(label='Plot')
  207. grid0.attach(self.plot_cb, 0, 0, 2, 1)
  208. # Tool dia for plot
  209. l1 = Gtk.Label('Tool dia:', xalign=1)
  210. grid0.attach(l1, 0, 1, 1, 1)
  211. self.tooldia_entry = LengthEntry()
  212. grid0.attach(self.tooldia_entry, 1, 1, 1, 1)
  213. class GlobalOptionsUI(Gtk.VBox):
  214. def __init__(self):
  215. Gtk.VBox.__init__(self, spacing=3, margin=5, vexpand=False)
  216. box1 = Gtk.Box()
  217. self.pack_start(box1, expand=False, fill=False, padding=2)
  218. l1 = Gtk.Label('Units:')
  219. box1.pack_start(l1, expand=False, fill=False, padding=2)
  220. self.units_radio = RadioSet([{'label': 'inch', 'value': 'IN'},
  221. {'label': 'mm', 'value': 'MM'}])
  222. box1.pack_start(self.units_radio, expand=False, fill=False, padding=2)
  223. ####### Gerber #######
  224. l2 = Gtk.Label(margin=5)
  225. l2.set_markup('<b>Gerber Options</b>')
  226. frame1 = Gtk.Frame(label_widget=l2)
  227. self.pack_start(frame1, expand=False, fill=False, padding=2)
  228. self.gerber_group = GerberOptionsGroupUI()
  229. frame1.add(self.gerber_group)
  230. ######## Excellon #########
  231. l3 = Gtk.Label(margin=5)
  232. l3.set_markup('<b>Excellon Options</b>')
  233. frame2 = Gtk.Frame(label_widget=l3)
  234. self.pack_start(frame2, expand=False, fill=False, padding=2)
  235. self.excellon_group = ExcellonOptionsGroupUI()
  236. frame2.add(self.excellon_group)
  237. ########## Geometry ##########
  238. l4 = Gtk.Label(margin=5)
  239. l4.set_markup('<b>Geometry Options</b>')
  240. frame3 = Gtk.Frame(label_widget=l4)
  241. self.pack_start(frame3, expand=False, fill=False, padding=2)
  242. self.geometry_group = GeometryOptionsGroupUI()
  243. frame3.add(self.geometry_group)
  244. ########## CNC ############
  245. l5 = Gtk.Label(margin=5)
  246. l5.set_markup('<b>CNC Job Options</b>')
  247. frame4 = Gtk.Frame(label_widget=l5)
  248. self.pack_start(frame4, expand=False, fill=False, padding=2)
  249. self.cncjob_group = CNCJobOptionsGroupUI()
  250. frame4.add(self.cncjob_group)
  251. ########################################
  252. ## App ##
  253. ########################################
  254. class App:
  255. """
  256. The main application class. The constructor starts the GUI.
  257. """
  258. log = logging.getLogger('base')
  259. log.setLevel(logging.DEBUG)
  260. formatter = logging.Formatter('[%(levelname)s] %(message)s')
  261. handler = logging.StreamHandler()
  262. handler.setFormatter(formatter)
  263. log.addHandler(handler)
  264. version_url = "http://caram.cl/flatcam/VERSION"
  265. def __init__(self):
  266. """
  267. Starts the application. Takes no parameters.
  268. :return: app
  269. :rtype: App
  270. """
  271. App.log.info("FlatCAM Starting...")
  272. if speedups.available:
  273. App.log.info("Enabling geometry speedups...")
  274. speedups.enable()
  275. # Needed to interact with the GUI from other threads.
  276. GObject.threads_init()
  277. #### GUI ####
  278. # Glade init
  279. self.gladefile = "FlatCAM.ui"
  280. self.builder = Gtk.Builder()
  281. self.builder.add_from_file(self.gladefile)
  282. # References to UI widgets
  283. self.window = self.builder.get_object("window1")
  284. self.position_label = self.builder.get_object("label3")
  285. self.grid = self.builder.get_object("grid1")
  286. self.notebook = self.builder.get_object("notebook1")
  287. self.info_label = self.builder.get_object("label_status")
  288. self.progress_bar = self.builder.get_object("progressbar")
  289. self.progress_bar.set_show_text(True)
  290. self.units_label = self.builder.get_object("label_units")
  291. self.toolbar = self.builder.get_object("toolbar_main")
  292. # White (transparent) background on the "Options" tab.
  293. self.builder.get_object("vp_options").override_background_color(Gtk.StateType.NORMAL,
  294. Gdk.RGBA(1, 1, 1, 1))
  295. # Combo box to choose between project and application options.
  296. self.combo_options = self.builder.get_object("combo_options")
  297. self.combo_options.set_active(1)
  298. #self.setup_project_list() # The "Project" tab
  299. self.setup_component_editor() # The "Selected" tab
  300. ## Setup the toolbar. Adds buttons.
  301. self.setup_toolbar()
  302. #### Event handling ####
  303. self.builder.connect_signals(self)
  304. #### Make plot area ####
  305. self.plotcanvas = PlotCanvas(self.grid)
  306. self.plotcanvas.mpl_connect('button_press_event', self.on_click_over_plot)
  307. self.plotcanvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
  308. self.plotcanvas.mpl_connect('key_press_event', self.on_key_over_plot)
  309. #### DATA ####
  310. self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
  311. self.setup_obj_classes()
  312. self.mouse = None # Mouse coordinates over plot
  313. self.recent = []
  314. self.collection = ObjectCollection()
  315. self.builder.get_object("box_project").pack_start(self.collection.view, False, False, 1)
  316. # TODO: Do this different
  317. self.collection.view.connect("row_activated", self.on_row_activated)
  318. # Used to inhibit the on_options_update callback when
  319. # the options are being changed by the program and not the user.
  320. self.options_update_ignore = False
  321. self.toggle_units_ignore = False
  322. self.options_box = self.builder.get_object('options_box')
  323. ## Application defaults ##
  324. self.defaults_form = GlobalOptionsUI()
  325. self.defaults_form_fields = {
  326. "units": self.defaults_form.units_radio,
  327. "gerber_plot": self.defaults_form.gerber_group.plot_cb,
  328. "gerber_solid": self.defaults_form.gerber_group.solid_cb,
  329. "gerber_multicolored": self.defaults_form.gerber_group.multicolored_cb,
  330. "gerber_isotooldia": self.defaults_form.gerber_group.iso_tool_dia_entry,
  331. "gerber_isopasses": self.defaults_form.gerber_group.iso_width_entry,
  332. "gerber_isooverlap": self.defaults_form.gerber_group.iso_overlap_entry,
  333. "gerber_cutouttooldia": self.defaults_form.gerber_group.cutout_tooldia_entry,
  334. "gerber_cutoutmargin": self.defaults_form.gerber_group.cutout_margin_entry,
  335. "gerber_cutoutgapsize": self.defaults_form.gerber_group.cutout_gap_entry,
  336. "gerber_gaps": self.defaults_form.gerber_group.gaps_radio,
  337. "gerber_noncoppermargin": self.defaults_form.gerber_group.noncopper_margin_entry,
  338. "gerber_noncopperrounded": self.defaults_form.gerber_group.noncopper_rounded_cb,
  339. "gerber_bboxmargin": self.defaults_form.gerber_group.bbmargin_entry,
  340. "gerber_bboxrounded": self.defaults_form.gerber_group.bbrounded_cb,
  341. "excellon_plot": self.defaults_form.excellon_group.plot_cb,
  342. "excellon_solid": self.defaults_form.excellon_group.solid_cb,
  343. "excellon_drillz": self.defaults_form.excellon_group.cutz_entry,
  344. "excellon_travelz": self.defaults_form.excellon_group.travelz_entry,
  345. "excellon_feedrate": self.defaults_form.excellon_group.feedrate_entry,
  346. "geometry_plot": self.defaults_form.geometry_group.plot_cb,
  347. "geometry_cutz": self.defaults_form.geometry_group.cutz_entry,
  348. "geometry_travelz": self.defaults_form.geometry_group.travelz_entry,
  349. "geometry_feedrate": self.defaults_form.geometry_group.cncfeedrate_entry,
  350. "geometry_cnctooldia": self.defaults_form.geometry_group.cnctooldia_entry,
  351. "geometry_painttooldia": self.defaults_form.geometry_group.painttooldia_entry,
  352. "geometry_paintoverlap": self.defaults_form.geometry_group.paintoverlap_entry,
  353. "geometry_paintmargin": self.defaults_form.geometry_group.paintmargin_entry,
  354. "cncjob_plot": self.defaults_form.cncjob_group.plot_cb,
  355. "cncjob_tooldia": self.defaults_form.cncjob_group.tooldia_entry
  356. }
  357. self.defaults = {
  358. "units": "IN",
  359. "gerber_plot": True,
  360. "gerber_solid": True,
  361. "gerber_multicolored": False,
  362. "gerber_isotooldia": 0.016,
  363. "gerber_isopasses": 1,
  364. "gerber_isooverlap": 0.15,
  365. "gerber_cutouttooldia": 0.07,
  366. "gerber_cutoutmargin": 0.1,
  367. "gerber_cutoutgapsize": 0.15,
  368. "gerber_gaps": "4",
  369. "gerber_noncoppermargin": 0.0,
  370. "gerber_noncopperrounded": False,
  371. "gerber_bboxmargin": 0.0,
  372. "gerber_bboxrounded": False,
  373. "excellon_plot": True,
  374. "excellon_solid": False,
  375. "excellon_drillz": -0.1,
  376. "excellon_travelz": 0.1,
  377. "excellon_feedrate": 3.0,
  378. "geometry_plot": True,
  379. "geometry_cutz": -0.002,
  380. "geometry_travelz": 0.1,
  381. "geometry_feedrate": 3.0,
  382. "geometry_cnctooldia": 0.016,
  383. "geometry_painttooldia": 0.07,
  384. "geometry_paintoverlap": 0.15,
  385. "geometry_paintmargin": 0.0,
  386. "cncjob_plot": True,
  387. "cncjob_tooldia": 0.016
  388. }
  389. self.load_defaults()
  390. self.defaults_write_form()
  391. ## Current Project ##
  392. self.options_form = GlobalOptionsUI()
  393. self.options_form_fields = {
  394. "units": self.options_form.units_radio,
  395. "gerber_plot": self.options_form.gerber_group.plot_cb,
  396. "gerber_solid": self.options_form.gerber_group.solid_cb,
  397. "gerber_multicolored": self.options_form.gerber_group.multicolored_cb,
  398. "gerber_isotooldia": self.options_form.gerber_group.iso_tool_dia_entry,
  399. "gerber_isopasses": self.options_form.gerber_group.iso_width_entry,
  400. "gerber_isooverlap": self.options_form.gerber_group.iso_overlap_entry,
  401. "gerber_cutouttooldia": self.options_form.gerber_group.cutout_tooldia_entry,
  402. "gerber_cutoutmargin": self.options_form.gerber_group.cutout_margin_entry,
  403. "gerber_cutoutgapsize": self.options_form.gerber_group.cutout_gap_entry,
  404. "gerber_gaps": self.options_form.gerber_group.gaps_radio,
  405. "gerber_noncoppermargin": self.options_form.gerber_group.noncopper_margin_entry,
  406. "gerber_noncopperrounded": self.options_form.gerber_group.noncopper_rounded_cb,
  407. "gerber_bboxmargin": self.options_form.gerber_group.bbmargin_entry,
  408. "gerber_bboxrounded": self.options_form.gerber_group.bbrounded_cb,
  409. "excellon_plot": self.options_form.excellon_group.plot_cb,
  410. "excellon_solid": self.options_form.excellon_group.solid_cb,
  411. "excellon_drillz": self.options_form.excellon_group.cutz_entry,
  412. "excellon_travelz": self.options_form.excellon_group.travelz_entry,
  413. "excellon_feedrate": self.options_form.excellon_group.feedrate_entry,
  414. "geometry_plot": self.options_form.geometry_group.plot_cb,
  415. "geometry_cutz": self.options_form.geometry_group.cutz_entry,
  416. "geometry_travelz": self.options_form.geometry_group.travelz_entry,
  417. "geometry_feedrate": self.options_form.geometry_group.cncfeedrate_entry,
  418. "geometry_cnctooldia": self.options_form.geometry_group.cnctooldia_entry,
  419. "geometry_painttooldia": self.options_form.geometry_group.painttooldia_entry,
  420. "geometry_paintoverlap": self.options_form.geometry_group.paintoverlap_entry,
  421. "geometry_paintmargin": self.options_form.geometry_group.paintmargin_entry,
  422. "cncjob_plot": self.options_form.cncjob_group.plot_cb,
  423. "cncjob_tooldia": self.options_form.cncjob_group.tooldia_entry
  424. }
  425. # Project options
  426. self.options = {
  427. "units": "IN",
  428. "gerber_plot": True,
  429. "gerber_solid": True,
  430. "gerber_multicolored": False,
  431. "gerber_isotooldia": 0.016,
  432. "gerber_isopasses": 1,
  433. "gerber_isooverlap": 0.15,
  434. "gerber_cutouttooldia": 0.07,
  435. "gerber_cutoutmargin": 0.1,
  436. "gerber_cutoutgapsize": 0.15,
  437. "gerber_gaps": "4",
  438. "gerber_noncoppermargin": 0.0,
  439. "gerber_noncopperrounded": False,
  440. "gerber_bboxmargin": 0.0,
  441. "gerber_bboxrounded": False,
  442. "excellon_plot": True,
  443. "excellon_solid": False,
  444. "excellon_drillz": -0.1,
  445. "excellon_travelz": 0.1,
  446. "excellon_feedrate": 3.0,
  447. "geometry_plot": True,
  448. "geometry_cutz": -0.002,
  449. "geometry_travelz": 0.1,
  450. "geometry_feedrate": 3.0,
  451. "geometry_cnctooldia": 0.016,
  452. "geometry_painttooldia": 0.07,
  453. "geometry_paintoverlap": 0.15,
  454. "geometry_paintmargin": 0.0,
  455. "cncjob_plot": True,
  456. "cncjob_tooldia": 0.016
  457. }
  458. self.options.update(self.defaults) # Copy app defaults to project options
  459. self.options_write_form()
  460. self.project_filename = None
  461. # Where we draw the options/defaults forms.
  462. self.on_options_combo_change(None)
  463. #self.options_box.pack_start(self.defaults_form, False, False, 1)
  464. self.options_form.units_radio.group_toggle_fn = lambda x, y: self.on_toggle_units(x)
  465. ## Event subscriptions ##
  466. ## Tools ##
  467. self.measure = Measurement(self.builder.get_object("box39"), self.plotcanvas)
  468. # Toolbar icon
  469. # TODO: Where should I put this? Tool should have a method to add to toolbar?
  470. meas_ico = Gtk.Image.new_from_file('share/measure32.png')
  471. measure = Gtk.ToolButton.new(meas_ico, "")
  472. measure.connect("clicked", self.measure.toggle_active)
  473. measure.set_tooltip_markup("<b>Measure Tool:</b> Enable/disable tool.\n" +
  474. "Click on point to set reference.\n" +
  475. "(Click on plot and hit <b>m</b>)")
  476. self.toolbar.insert(measure, -1)
  477. #### Initialization ####
  478. self.units_label.set_text("[" + self.options["units"] + "]")
  479. self.setup_recent_items()
  480. App.log.info("Starting Worker...")
  481. self.worker = Worker()
  482. self.worker.daemon = True
  483. self.worker.start()
  484. #### Check for updates ####
  485. # Separate thread (Not worker)
  486. self.version = 5
  487. App.log.info("Checking for updates in backgroud (this is version %s)." % str(self.version))
  488. t1 = threading.Thread(target=self.version_check)
  489. t1.daemon = True
  490. t1.start()
  491. #### For debugging only ###
  492. def somethreadfunc(app_obj):
  493. App.log.info("Hello World!")
  494. t = threading.Thread(target=somethreadfunc, args=(self,))
  495. t.daemon = True
  496. t.start()
  497. ########################################
  498. ## START ##
  499. ########################################
  500. self.icon256 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon256.png')
  501. self.icon48 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon48.png')
  502. self.icon16 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon16.png')
  503. Gtk.Window.set_default_icon_list([self.icon16, self.icon48, self.icon256])
  504. self.window.set_title("FlatCAM - Alpha 5")
  505. self.window.set_default_size(900, 600)
  506. self.window.show_all()
  507. App.log.info("END of constructor. Releasing control.")
  508. def message_dialog(self, title, message, kind="info"):
  509. types = {"info": Gtk.MessageType.INFO,
  510. "warn": Gtk.MessageType.WARNING,
  511. "error": Gtk.MessageType.ERROR}
  512. dlg = Gtk.MessageDialog(self.window, 0, types[kind], Gtk.ButtonsType.OK, title)
  513. dlg.format_secondary_text(message)
  514. def lifecycle():
  515. dlg.run()
  516. dlg.destroy()
  517. GLib.idle_add(lifecycle)
  518. def question_dialog(self, title, message):
  519. label = Gtk.Label(message)
  520. dialog = Gtk.Dialog(title, self.window, 0,
  521. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  522. Gtk.STOCK_OK, Gtk.ResponseType.OK))
  523. dialog.set_default_size(150, 100)
  524. dialog.set_modal(True)
  525. box = dialog.get_content_area()
  526. box.set_border_width(10)
  527. box.add(label)
  528. dialog.show_all()
  529. response = dialog.run()
  530. dialog.destroy()
  531. return response
  532. def setup_toolbar(self):
  533. # Zoom fit
  534. zf_ico = Gtk.Image.new_from_file('share/zoom_fit32.png')
  535. zoom_fit = Gtk.ToolButton.new(zf_ico, "")
  536. zoom_fit.connect("clicked", self.on_zoom_fit)
  537. zoom_fit.set_tooltip_markup("Zoom Fit.\n(Click on plot and hit <b>1</b>)")
  538. self.toolbar.insert(zoom_fit, -1)
  539. # Zoom out
  540. zo_ico = Gtk.Image.new_from_file('share/zoom_out32.png')
  541. zoom_out = Gtk.ToolButton.new(zo_ico, "")
  542. zoom_out.connect("clicked", self.on_zoom_out)
  543. zoom_out.set_tooltip_markup("Zoom Out.\n(Click on plot and hit <b>2</b>)")
  544. self.toolbar.insert(zoom_out, -1)
  545. # Zoom in
  546. zi_ico = Gtk.Image.new_from_file('share/zoom_in32.png')
  547. zoom_in = Gtk.ToolButton.new(zi_ico, "")
  548. zoom_in.connect("clicked", self.on_zoom_in)
  549. zoom_in.set_tooltip_markup("Zoom In.\n(Click on plot and hit <b>3</b>)")
  550. self.toolbar.insert(zoom_in, -1)
  551. # Clear plot
  552. cp_ico = Gtk.Image.new_from_file('share/clear_plot32.png')
  553. clear_plot = Gtk.ToolButton.new(cp_ico, "")
  554. clear_plot.connect("clicked", self.on_clear_plots)
  555. clear_plot.set_tooltip_markup("Clear Plot")
  556. self.toolbar.insert(clear_plot, -1)
  557. # Replot
  558. rp_ico = Gtk.Image.new_from_file('share/replot32.png')
  559. replot = Gtk.ToolButton.new(rp_ico, "")
  560. replot.connect("clicked", self.on_toolbar_replot)
  561. replot.set_tooltip_markup("Re-plot all")
  562. self.toolbar.insert(replot, -1)
  563. # Delete item
  564. del_ico = Gtk.Image.new_from_file('share/delete32.png')
  565. delete = Gtk.ToolButton.new(del_ico, "")
  566. delete.connect("clicked", self.on_delete)
  567. delete.set_tooltip_markup("Delete selected\nobject.")
  568. self.toolbar.insert(delete, -1)
  569. def setup_obj_classes(self):
  570. """
  571. Sets up application specifics on the FlatCAMObj class.
  572. :return: None
  573. """
  574. FlatCAMObj.app = self
  575. def setup_component_editor(self):
  576. """
  577. Initial configuration of the component editor. Creates
  578. a page titled "Selection" on the notebook on the left
  579. side of the main window.
  580. :return: None
  581. """
  582. box_selected = self.builder.get_object("vp_selected")
  583. # White background
  584. box_selected.override_background_color(Gtk.StateType.NORMAL,
  585. Gdk.RGBA(1, 1, 1, 1))
  586. # Remove anything else in the box
  587. box_children = box_selected.get_children()
  588. for child in box_children:
  589. box_selected.remove(child)
  590. box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
  591. label1 = Gtk.Label("Choose an item from Project")
  592. box1.pack_start(label1, True, False, 1)
  593. box_selected.add(box1)
  594. box1.show()
  595. label1.show()
  596. def setup_recent_items(self):
  597. # TODO: Move this to constructor
  598. icons = {
  599. "gerber": "share/flatcam_icon16.png",
  600. "excellon": "share/drill16.png",
  601. "cncjob": "share/cnc16.png",
  602. "project": "share/project16.png"
  603. }
  604. openers = {
  605. 'gerber': self.open_gerber,
  606. 'excellon': self.open_excellon,
  607. 'cncjob': self.open_gcode,
  608. 'project': self.open_project
  609. }
  610. # Closure needed to create callbacks in a loop.
  611. # Otherwise late binding occurs.
  612. def make_callback(func, fname):
  613. def opener(*args):
  614. self.worker.add_task(func, [fname])
  615. return opener
  616. try:
  617. f = open('recent.json')
  618. except IOError:
  619. App.log.error("Failed to load recent item list.")
  620. self.info("ERROR: Failed to load recent item list.")
  621. return
  622. try:
  623. self.recent = json.load(f)
  624. except:
  625. App.log.error("Failed to parse recent item list.")
  626. self.info("ERROR: Failed to parse recent item list.")
  627. f.close()
  628. return
  629. f.close()
  630. recent_menu = Gtk.Menu()
  631. for recent in self.recent:
  632. filename = recent['filename'].split('/')[-1].split('\\')[-1]
  633. item = Gtk.ImageMenuItem.new_with_label(filename)
  634. im = Gtk.Image.new_from_file(icons[recent["kind"]])
  635. item.set_image(im)
  636. o = make_callback(openers[recent["kind"]], recent['filename'])
  637. item.connect('activate', o)
  638. recent_menu.append(item)
  639. self.builder.get_object('open_recent').set_submenu(recent_menu)
  640. recent_menu.show_all()
  641. def info(self, text):
  642. """
  643. Show text on the status bar. This method is thread safe.
  644. :param text: Text to display.
  645. :type text: str
  646. :return: None
  647. """
  648. GLib.idle_add(lambda: self.info_label.set_text(text))
  649. def get_radio_value(self, radio_set):
  650. """
  651. Returns the radio_set[key] of the radiobutton
  652. whose name is key is active.
  653. :param radio_set: A dictionary containing widget_name: value pairs.
  654. :type radio_set: dict
  655. :return: radio_set[key]
  656. """
  657. for name in radio_set:
  658. if self.builder.get_object(name).get_active():
  659. return radio_set[name]
  660. def plot_all(self):
  661. """
  662. Re-generates all plots from all objects.
  663. :return: None
  664. """
  665. self.plotcanvas.clear()
  666. self.set_progress_bar(0.1, "Re-plotting...")
  667. def worker_task(app_obj):
  668. percentage = 0.1
  669. try:
  670. delta = 0.9 / len(self.collection.get_list())
  671. except ZeroDivisionError:
  672. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  673. return
  674. for obj in self.collection.get_list():
  675. obj.plot()
  676. percentage += delta
  677. GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
  678. GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
  679. GLib.idle_add(lambda: self.on_zoom_fit(None))
  680. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
  681. # Send to worker
  682. self.worker.add_task(worker_task, [self])
  683. def get_eval(self, widget_name):
  684. """
  685. Runs eval() on the on the text entry of name 'widget_name'
  686. and returns the results.
  687. :param widget_name: Name of Gtk.Entry
  688. :type widget_name: str
  689. :return: Depends on contents of the entry text.
  690. """
  691. value = self.builder.get_object(widget_name).get_text()
  692. if value == "":
  693. value = "None"
  694. try:
  695. evald = eval(value)
  696. return evald
  697. except:
  698. self.info("Could not evaluate: " + value)
  699. return None
  700. def new_object(self, kind, name, initialize, active=True, fit=True, plot=True):
  701. """
  702. Creates a new specalized FlatCAMObj and attaches it to the application,
  703. this is, updates the GUI accordingly, any other records and plots it.
  704. This method is thread-safe.
  705. :param kind: The kind of object to create. One of 'gerber',
  706. 'excellon', 'cncjob' and 'geometry'.
  707. :type kind: str
  708. :param name: Name for the object.
  709. :type name: str
  710. :param initialize: Function to run after creation of the object
  711. but before it is attached to the application. The function is
  712. called with 2 parameters: the new object and the App instance.
  713. :type initialize: function
  714. :return: None
  715. :rtype: None
  716. """
  717. App.log.debug("new_object()")
  718. ### Check for existing name
  719. if name in self.collection.get_names():
  720. ## Create a new name
  721. # Ends with number?
  722. match = re.search(r'(.*[^\d])?(\d+)$', name)
  723. if match: # Yes: Increment the number!
  724. base = match.group(1) or ''
  725. num = int(match.group(2))
  726. name = base + str(num + 1)
  727. else: # No: add a number!
  728. name += "_1"
  729. # Create object
  730. classdict = {
  731. "gerber": FlatCAMGerber,
  732. "excellon": FlatCAMExcellon,
  733. "cncjob": FlatCAMCNCjob,
  734. "geometry": FlatCAMGeometry
  735. }
  736. obj = classdict[kind](name)
  737. obj.units = self.options["units"] # TODO: The constructor should look at defaults.
  738. # Set default options from self.options
  739. for option in self.options:
  740. if option.find(kind + "_") == 0:
  741. oname = option[len(kind)+1:]
  742. obj.options[oname] = self.options[option]
  743. # Initialize as per user request
  744. # User must take care to implement initialize
  745. # in a thread-safe way as is is likely that we
  746. # have been invoked in a separate thread.
  747. initialize(obj, self)
  748. # Check units and convert if necessary
  749. if self.options["units"].upper() != obj.units.upper():
  750. GLib.idle_add(lambda: self.info("Converting units to " + self.options["units"] + "."))
  751. obj.convert_units(self.options["units"])
  752. # Add to our records
  753. self.collection.append(obj, active=active)
  754. # Show object details now.
  755. GLib.idle_add(lambda: self.notebook.set_current_page(1))
  756. # Plot
  757. # TODO: (Thread-safe?)
  758. if plot:
  759. obj.plot()
  760. if fit:
  761. GLib.idle_add(lambda: self.on_zoom_fit(None))
  762. return obj
  763. def set_progress_bar(self, percentage, text=""):
  764. """
  765. Sets the application's progress bar to a given frac_digits and text.
  766. :param percentage: The frac_digits (0.0-1.0) of the progress.
  767. :type percentage: float
  768. :param text: Text to display on the progress bar.
  769. :type text: str
  770. :return: None
  771. """
  772. self.progress_bar.set_text(text)
  773. self.progress_bar.set_fraction(percentage)
  774. return False
  775. def load_defaults(self):
  776. """
  777. Loads the aplication's default settings from defaults.json into
  778. ``self.defaults``.
  779. :return: None
  780. """
  781. try:
  782. f = open("defaults.json")
  783. options = f.read()
  784. f.close()
  785. except IOError:
  786. App.log.error("Could not load defaults file.")
  787. self.info("ERROR: Could not load defaults file.")
  788. return
  789. try:
  790. defaults = json.loads(options)
  791. except:
  792. e = sys.exc_info()[0]
  793. App.log.error(str(e))
  794. self.info("ERROR: Failed to parse defaults file.")
  795. return
  796. self.defaults.update(defaults)
  797. def defaults_read_form(self):
  798. for option in self.defaults_form_fields:
  799. self.defaults[option] = self.defaults_form_fields[option].get_value()
  800. def options_read_form(self):
  801. for option in self.options_form_fields:
  802. self.options[option] = self.options_form_fields[option].get_value()
  803. def defaults_write_form(self):
  804. for option in self.defaults_form_fields:
  805. self.defaults_form_fields[option].set_value(self.defaults[option])
  806. def options_write_form(self):
  807. for option in self.options_form_fields:
  808. self.options_form_fields[option].set_value(self.options[option])
  809. def save_project(self, filename):
  810. """
  811. Saves the current project to the specified file.
  812. :param filename: Name of the file in which to save.
  813. :type filename: str
  814. :return: None
  815. """
  816. # Capture the latest changes
  817. try:
  818. self.collection.get_active().read_form()
  819. except:
  820. pass
  821. # Serialize the whole project
  822. d = {"objs": [obj.to_dict() for obj in self.collection.get_list()],
  823. "options": self.options}
  824. try:
  825. f = open(filename, 'w')
  826. except IOError:
  827. App.log.error("ERROR: Failed to open file for saving:", filename)
  828. return
  829. try:
  830. json.dump(d, f, default=to_dict)
  831. except:
  832. App.log.error("ERROR: File open but failed to write:", filename)
  833. f.close()
  834. return
  835. f.close()
  836. def open_project(self, filename):
  837. """
  838. Loads a project from the specified file.
  839. :param filename: Name of the file from which to load.
  840. :type filename: str
  841. :return: None
  842. """
  843. App.log.debug("Opening project: " + filename)
  844. try:
  845. f = open(filename, 'r')
  846. except IOError:
  847. App.log.error("Failed to open project file: %s" % filename)
  848. self.info("ERROR: Failed to open project file: %s" % filename)
  849. return
  850. try:
  851. d = json.load(f, object_hook=dict2obj)
  852. except:
  853. App.log.error("Failed to parse project file: %s" % filename)
  854. self.info("ERROR: Failed to parse project file: %s" % filename)
  855. f.close()
  856. return
  857. self.register_recent("project", filename)
  858. # Clear the current project
  859. self.on_file_new(None)
  860. # Project options
  861. self.options.update(d['options'])
  862. self.project_filename = filename
  863. GLib.idle_add(lambda: self.units_label.set_text(self.options["units"]))
  864. # Re create objects
  865. App.log.debug("Re-creating objects...")
  866. for obj in d['objs']:
  867. def obj_init(obj_inst, app_inst):
  868. obj_inst.from_dict(obj)
  869. App.log.debug(obj['kind'] + ": " + obj['options']['name'])
  870. self.new_object(obj['kind'], obj['options']['name'], obj_init, active=False, fit=False, plot=False)
  871. self.plot_all()
  872. self.info("Project loaded from: " + filename)
  873. App.log.debug("Project loaded")
  874. def populate_objects_combo(self, combo):
  875. """
  876. Populates a Gtk.Comboboxtext with the list of the object in the project.
  877. :param combo: Name or instance of the comboboxtext.
  878. :type combo: str or Gtk.ComboBoxText
  879. :return: None
  880. """
  881. App.log.debug("Populating combo!")
  882. if type(combo) == str:
  883. combo = self.builder.get_object(combo)
  884. combo.remove_all()
  885. for name in self.collection.get_names():
  886. combo.append_text(name)
  887. def version_check(self, *args):
  888. """
  889. Checks for the latest version of the program. Alerts the
  890. user if theirs is outdated. This method is meant to be run
  891. in a saeparate thread.
  892. :return: None
  893. """
  894. try:
  895. f = urllib.urlopen(App.version_url)
  896. except:
  897. App.log.warning("Failed checking for latest version. Could not connect.")
  898. GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
  899. return
  900. try:
  901. data = json.load(f)
  902. except:
  903. App.log.error("Could nor parse information about latest version.")
  904. GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
  905. f.close()
  906. return
  907. f.close()
  908. if self.version >= data["version"]:
  909. GLib.idle_add(lambda: self.info("FlatCAM is up to date!"))
  910. return
  911. label = Gtk.Label("There is a newer version of FlatCAM\n" +
  912. "available for download:\n\n" +
  913. data["name"] + "\n\n" + data["message"])
  914. dialog = Gtk.Dialog("Newer Version Available", self.window, 0,
  915. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  916. Gtk.STOCK_OK, Gtk.ResponseType.OK))
  917. dialog.set_default_size(150, 100)
  918. dialog.set_modal(True)
  919. box = dialog.get_content_area()
  920. box.set_border_width(10)
  921. box.add(label)
  922. def do_dialog():
  923. dialog.show_all()
  924. response = dialog.run()
  925. dialog.destroy()
  926. GLib.idle_add(lambda: do_dialog())
  927. return
  928. def do_nothing(self, param):
  929. return
  930. def disable_plots(self, except_current=False):
  931. """
  932. Disables all plots with exception of the current object if specified.
  933. :param except_current: Wether to skip the current object.
  934. :rtype except_current: boolean
  935. :return: None
  936. """
  937. # TODO: This method is very similar to replot_all. Try to merge.
  938. self.set_progress_bar(0.1, "Re-plotting...")
  939. def worker_task(app_obj):
  940. percentage = 0.1
  941. try:
  942. delta = 0.9 / len(self.collection.get_list())
  943. except ZeroDivisionError:
  944. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  945. return
  946. for obj in self.collection.get_list():
  947. if obj != self.collection.get_active() or not except_current:
  948. obj.options['plot'] = False
  949. obj.plot()
  950. percentage += delta
  951. GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
  952. GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
  953. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  954. # Send to worker
  955. self.worker.add_task(worker_task, [self])
  956. def enable_all_plots(self, *args):
  957. self.plotcanvas.clear()
  958. self.set_progress_bar(0.1, "Re-plotting...")
  959. def worker_task(app_obj):
  960. percentage = 0.1
  961. try:
  962. delta = 0.9 / len(self.collection.get_list())
  963. except ZeroDivisionError:
  964. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  965. return
  966. for obj in self.collection.get_list():
  967. obj.options['plot'] = True
  968. obj.plot()
  969. percentage += delta
  970. GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
  971. GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
  972. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  973. # Send to worker
  974. self.worker.add_task(worker_task, [self])
  975. def register_recent(self, kind, filename):
  976. record = {'kind': kind, 'filename': filename}
  977. if record in self.recent:
  978. return
  979. self.recent.insert(0, record)
  980. if len(self.recent) > 10: # Limit reached
  981. self.recent.pop()
  982. try:
  983. f = open('recent.json', 'w')
  984. except IOError:
  985. App.log.error("Failed to open recent items file for writing.")
  986. self.info('Failed to open recent files file for writing.')
  987. return
  988. try:
  989. json.dump(self.recent, f)
  990. except:
  991. App.log.error("Failed to write to recent items file.")
  992. self.info('ERROR: Failed to write to recent items file.')
  993. f.close()
  994. f.close()
  995. def open_gerber(self, filename):
  996. """
  997. Opens a Gerber file, parses it and creates a new object for
  998. it in the program. Thread-safe.
  999. :param filename: Gerber file filename
  1000. :type filename: str
  1001. :return: None
  1002. """
  1003. GLib.idle_add(lambda: self.set_progress_bar(0.1, "Opening Gerber ..."))
  1004. # How the object should be initialized
  1005. def obj_init(gerber_obj, app_obj):
  1006. assert isinstance(gerber_obj, FlatCAMGerber)
  1007. # Opening the file happens here
  1008. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1009. gerber_obj.parse_file(filename)
  1010. # Further parsing
  1011. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Creating Geometry ..."))
  1012. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1013. # Object name
  1014. name = filename.split('/')[-1].split('\\')[-1]
  1015. self.new_object("gerber", name, obj_init)
  1016. # New object creation and file processing
  1017. # try:
  1018. # self.new_object("gerber", name, obj_init)
  1019. # except:
  1020. # e = sys.exc_info()
  1021. # print "ERROR:", e[0]
  1022. # traceback.print_exc()
  1023. # self.message_dialog("Failed to create Gerber Object",
  1024. # "Attempting to create a FlatCAM Gerber Object from " +
  1025. # "Gerber file failed during processing:\n" +
  1026. # str(e[0]) + " " + str(e[1]), kind="error")
  1027. # GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
  1028. # self.collection.delete_active()
  1029. # return
  1030. # Register recent file
  1031. self.register_recent("gerber", filename)
  1032. # GUI feedback
  1033. self.info("Opened: " + filename)
  1034. GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
  1035. GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
  1036. def open_excellon(self, filename):
  1037. """
  1038. Opens an Excellon file, parses it and creates a new object for
  1039. it in the program. Thread-safe.
  1040. :param filename: Excellon file filename
  1041. :type filename: str
  1042. :return: None
  1043. """
  1044. GLib.idle_add(lambda: self.set_progress_bar(0.1, "Opening Excellon ..."))
  1045. # How the object should be initialized
  1046. def obj_init(excellon_obj, app_obj):
  1047. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1048. excellon_obj.parse_file(filename)
  1049. excellon_obj.create_geometry()
  1050. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1051. # Object name
  1052. name = filename.split('/')[-1].split('\\')[-1]
  1053. # New object creation and file processing
  1054. try:
  1055. self.new_object("excellon", name, obj_init)
  1056. except:
  1057. e = sys.exc_info()
  1058. App.log.error(str(e))
  1059. self.message_dialog("Failed to create Excellon Object",
  1060. "Attempting to create a FlatCAM Excellon Object from " +
  1061. "Excellon file failed during processing:\n" +
  1062. str(e[0]) + " " + str(e[1]), kind="error")
  1063. GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
  1064. self.collection.delete_active()
  1065. return
  1066. # Register recent file
  1067. self.register_recent("excellon", filename)
  1068. # GUI feedback
  1069. self.info("Opened: " + filename)
  1070. GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
  1071. GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, ""))
  1072. def open_gcode(self, filename):
  1073. """
  1074. Opens a G-gcode file, parses it and creates a new object for
  1075. it in the program. Thread-safe.
  1076. :param filename: G-code file filename
  1077. :type filename: str
  1078. :return: None
  1079. """
  1080. # How the object should be initialized
  1081. def obj_init(job_obj, app_obj_):
  1082. """
  1083. :type app_obj_: App
  1084. """
  1085. assert isinstance(app_obj_, App)
  1086. GLib.idle_add(lambda: app_obj_.set_progress_bar(0.1, "Opening G-Code ..."))
  1087. f = open(filename)
  1088. gcode = f.read()
  1089. f.close()
  1090. job_obj.gcode = gcode
  1091. GLib.idle_add(lambda: app_obj_.set_progress_bar(0.2, "Parsing ..."))
  1092. job_obj.gcode_parse()
  1093. GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Creating geometry ..."))
  1094. job_obj.create_geometry()
  1095. GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Plotting ..."))
  1096. # Object name
  1097. name = filename.split('/')[-1].split('\\')[-1]
  1098. # New object creation and file processing
  1099. try:
  1100. self.new_object("cncjob", name, obj_init)
  1101. except:
  1102. e = sys.exc_info()
  1103. App.log.error(str(e))
  1104. self.message_dialog("Failed to create CNCJob Object",
  1105. "Attempting to create a FlatCAM CNCJob Object from " +
  1106. "G-Code file failed during processing:\n" +
  1107. str(e[0]) + " " + str(e[1]), kind="error")
  1108. GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, "Idle"))
  1109. self.collection.delete_active()
  1110. return
  1111. # Register recent file
  1112. self.register_recent("cncjob", filename)
  1113. # GUI feedback
  1114. self.info("Opened: " + filename)
  1115. GLib.idle_add(lambda: self.set_progress_bar(1.0, "Done!"))
  1116. GLib.timeout_add_seconds(1, lambda: self.set_progress_bar(0.0, ""))
  1117. ########################################
  1118. ## EVENT HANDLERS ##
  1119. ########################################
  1120. def on_debug_printlist(self, *args):
  1121. self.collection.print_list()
  1122. def on_disable_all_plots(self, widget):
  1123. self.disable_plots()
  1124. def on_disable_all_plots_not_current(self, widget):
  1125. self.disable_plots(except_current=True)
  1126. def on_about(self, widget):
  1127. """
  1128. Opens the 'About' dialog box.
  1129. :param widget: Ignored.
  1130. :return: None
  1131. """
  1132. about = self.builder.get_object("aboutdialog")
  1133. about.run()
  1134. about.hide()
  1135. def on_create_mirror(self, widget):
  1136. """
  1137. Creates a mirror image of an object to be used as a bottom layer.
  1138. :param widget: Ignored.
  1139. :return: None
  1140. """
  1141. # TODO: Move (some of) this to camlib!
  1142. # Object to mirror
  1143. obj_name = self.builder.get_object("comboboxtext_bottomlayer").get_active_text()
  1144. fcobj = self.collection.get_by_name(obj_name)
  1145. # For now, lets limit to Gerbers and Excellons.
  1146. # assert isinstance(gerb, FlatCAMGerber)
  1147. if not isinstance(fcobj, FlatCAMGerber) and not isinstance(fcobj, FlatCAMExcellon):
  1148. self.info("ERROR: Only Gerber and Excellon objects can be mirrored.")
  1149. return
  1150. # Mirror axis "X" or "Y
  1151. axis = self.get_radio_value({"rb_mirror_x": "X",
  1152. "rb_mirror_y": "Y"})
  1153. mode = self.get_radio_value({"rb_mirror_box": "box",
  1154. "rb_mirror_point": "point"})
  1155. if mode == "point": # A single point defines the mirror axis
  1156. # TODO: Error handling
  1157. px, py = eval(self.point_entry.get_text())
  1158. else: # The axis is the line dividing the box in the middle
  1159. name = self.box_combo.get_active_text()
  1160. bb_obj = self.collection.get_by_name(name)
  1161. xmin, ymin, xmax, ymax = bb_obj.bounds()
  1162. px = 0.5*(xmin+xmax)
  1163. py = 0.5*(ymin+ymax)
  1164. fcobj.mirror(axis, [px, py])
  1165. fcobj.plot()
  1166. def on_create_aligndrill(self, widget):
  1167. """
  1168. Creates alignment holes Excellon object. Creates mirror duplicates
  1169. of the specified holes around the specified axis.
  1170. :param widget: Ignored.
  1171. :return: None
  1172. """
  1173. # Mirror axis. Same as in on_create_mirror.
  1174. axis = self.get_radio_value({"rb_mirror_x": "X",
  1175. "rb_mirror_y": "Y"})
  1176. # TODO: Error handling
  1177. mode = self.get_radio_value({"rb_mirror_box": "box",
  1178. "rb_mirror_point": "point"})
  1179. if mode == "point":
  1180. px, py = eval(self.point_entry.get_text())
  1181. else:
  1182. name = self.box_combo.get_active_text()
  1183. bb_obj = self.collection.get_by_name(name)
  1184. xmin, ymin, xmax, ymax = bb_obj.bounds()
  1185. px = 0.5*(xmin+xmax)
  1186. py = 0.5*(ymin+ymax)
  1187. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  1188. # Tools
  1189. dia = self.get_eval("entry_dblsided_alignholediam")
  1190. tools = {"1": {"C": dia}}
  1191. # Parse hole list
  1192. # TODO: Better parsing
  1193. holes = self.builder.get_object("entry_dblsided_alignholes").get_text()
  1194. holes = eval("[" + holes + "]")
  1195. drills = []
  1196. for hole in holes:
  1197. point = Point(hole)
  1198. point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
  1199. drills.append({"point": point, "tool": "1"})
  1200. drills.append({"point": point_mirror, "tool": "1"})
  1201. def obj_init(obj_inst, app_inst):
  1202. obj_inst.tools = tools
  1203. obj_inst.drills = drills
  1204. obj_inst.create_geometry()
  1205. self.new_object("excellon", "Alignment Drills", obj_init)
  1206. def on_toggle_pointbox(self, widget):
  1207. """
  1208. Callback for radio selection change between point and box in the
  1209. Double-sided PCB tool. Updates the UI accordingly.
  1210. :param widget: Ignored.
  1211. :return: None
  1212. """
  1213. # Where the entry or combo go
  1214. box = self.builder.get_object("box_pointbox")
  1215. # Clear contents
  1216. children = box.get_children()
  1217. for child in children:
  1218. box.remove(child)
  1219. choice = self.get_radio_value({"rb_mirror_point": "point",
  1220. "rb_mirror_box": "box"})
  1221. if choice == "point":
  1222. self.point_entry = Gtk.Entry()
  1223. self.builder.get_object("box_pointbox").pack_start(self.point_entry,
  1224. False, False, 1)
  1225. self.point_entry.show()
  1226. else:
  1227. self.box_combo = Gtk.ComboBoxText()
  1228. self.builder.get_object("box_pointbox").pack_start(self.box_combo,
  1229. False, False, 1)
  1230. self.populate_objects_combo(self.box_combo)
  1231. self.box_combo.show()
  1232. def on_tools_doublesided(self, param):
  1233. """
  1234. Callback for menu item Tools->Double Sided PCB Tool. Launches the
  1235. tool placing its UI in the "Tool" tab in the notebook.
  1236. :param param: Ignored.
  1237. :return: None
  1238. """
  1239. # Were are we drawing the UI
  1240. box_tool = self.builder.get_object("box_tool")
  1241. # Remove anything else in the box
  1242. box_children = box_tool.get_children()
  1243. for child in box_children:
  1244. box_tool.remove(child)
  1245. # Get the UI
  1246. osw = self.builder.get_object("offscreenwindow_dblsided")
  1247. sw = self.builder.get_object("sw_dblsided")
  1248. osw.remove(sw)
  1249. vp = self.builder.get_object("vp_dblsided")
  1250. vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
  1251. # Put in the UI
  1252. box_tool.pack_start(sw, True, True, 0)
  1253. # INITIALIZATION
  1254. # Populate combo box
  1255. self.populate_objects_combo("comboboxtext_bottomlayer")
  1256. # Point entry
  1257. self.point_entry = Gtk.Entry()
  1258. box = self.builder.get_object("box_pointbox")
  1259. for child in box.get_children():
  1260. box.remove(child)
  1261. box.pack_start(self.point_entry, False, False, 1)
  1262. # Show the "Tool" tab
  1263. self.notebook.set_current_page(3)
  1264. sw.show_all()
  1265. def on_toggle_units(self, widget):
  1266. """
  1267. Callback for the Units radio-button change in the Options tab.
  1268. Changes the application's default units or the current project's units.
  1269. If changing the project's units, the change propagates to all of
  1270. the objects in the project.
  1271. :param widget: Ignored.
  1272. :return: None
  1273. """
  1274. if self.toggle_units_ignore:
  1275. return
  1276. # Options to scale
  1277. dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize',
  1278. 'gerber_noncoppermargin', 'gerber_bboxmargin', 'excellon_drillz',
  1279. 'excellon_travelz', 'excellon_feedrate', 'cncjob_tooldia',
  1280. 'geometry_cutz', 'geometry_travelz', 'geometry_feedrate',
  1281. 'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap',
  1282. 'geometry_paintmargin']
  1283. def scale_options(sfactor):
  1284. for dim in dimensions:
  1285. self.options[dim] *= sfactor
  1286. # The scaling factor depending on choice of units.
  1287. factor = 1/25.4
  1288. if self.options_form.units_radio.get_value().upper() == 'MM':
  1289. factor = 25.4
  1290. # Changing project units. Warn user.
  1291. label = Gtk.Label("Changing the units of the project causes all geometrical \n" +
  1292. "properties of all objects to be scaled accordingly. Continue?")
  1293. dialog = Gtk.Dialog("Changing Project Units", self.window, 0,
  1294. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1295. Gtk.STOCK_OK, Gtk.ResponseType.OK))
  1296. dialog.set_default_size(150, 100)
  1297. dialog.set_modal(True)
  1298. box = dialog.get_content_area()
  1299. box.set_border_width(10)
  1300. box.add(label)
  1301. dialog.show_all()
  1302. response = dialog.run()
  1303. dialog.destroy()
  1304. if response == Gtk.ResponseType.OK:
  1305. self.options_read_form()
  1306. scale_options(factor)
  1307. self.options_write_form()
  1308. for obj in self.collection.get_list():
  1309. units = self.options_form.units_radio.get_value().upper()
  1310. obj.convert_units(units)
  1311. current = self.collection.get_active()
  1312. if current is not None:
  1313. current.to_form()
  1314. self.plot_all()
  1315. else:
  1316. # Undo toggling
  1317. self.toggle_units_ignore = True
  1318. if self.options_form.units_radio.get_value().upper() == 'MM':
  1319. self.options_form.units_radio.set_value('IN')
  1320. else:
  1321. self.options_form.units_radio.set_value('MM')
  1322. self.toggle_units_ignore = False
  1323. self.options_read_form()
  1324. self.info("Converted units to %s" % self.options["units"])
  1325. self.units_label.set_text("[" + self.options["units"] + "]")
  1326. def on_file_openproject(self, param):
  1327. """
  1328. Callback for menu item File->Open Project. Opens a file chooser and calls
  1329. ``self.open_project()`` after successful selection of a filename.
  1330. :param param: Ignored.
  1331. :return: None
  1332. """
  1333. def on_success(app_obj, filename):
  1334. app_obj.open_project(filename)
  1335. # Runs on_success on worker
  1336. self.file_chooser_action(on_success)
  1337. def on_file_saveproject(self, param):
  1338. """
  1339. Callback for menu item File->Save Project. Saves the project to
  1340. ``self.project_filename`` or calls ``self.on_file_saveprojectas()``
  1341. if set to None. The project is saved by calling ``self.save_project()``.
  1342. :param param: Ignored.
  1343. :return: None
  1344. """
  1345. if self.project_filename is None:
  1346. self.on_file_saveprojectas(None)
  1347. else:
  1348. self.save_project(self.project_filename)
  1349. self.register_recent("project", self.project_filename)
  1350. self.info("Project saved to: " + self.project_filename)
  1351. def on_file_saveprojectas(self, param):
  1352. """
  1353. Callback for menu item File->Save Project As... Opens a file
  1354. chooser and saves the project to the given file via
  1355. ``self.save_project()``.
  1356. :param param: Ignored.
  1357. :return: None
  1358. """
  1359. def on_success(app_obj, filename):
  1360. assert isinstance(app_obj, App)
  1361. try:
  1362. f = open(filename, 'r')
  1363. f.close()
  1364. exists = True
  1365. except IOError:
  1366. exists = False
  1367. msg = "File exists. Overwrite?"
  1368. if exists and self.question_dialog("File exists", msg) == Gtk.ResponseType.CANCEL:
  1369. return
  1370. app_obj.save_project(filename)
  1371. self.project_filename = filename
  1372. self.register_recent("project", filename)
  1373. app_obj.info("Project saved to: " + filename)
  1374. self.file_chooser_save_action(on_success)
  1375. def on_file_saveprojectcopy(self, param):
  1376. """
  1377. Callback for menu item File->Save Project Copy... Opens a file
  1378. chooser and saves the project to the given file via
  1379. ``self.save_project``. It does not update ``self.project_filename`` so
  1380. subsequent save requests are done on the previous known filename.
  1381. :param param: Ignore.
  1382. :return: None
  1383. """
  1384. def on_success(app_obj, filename):
  1385. assert isinstance(app_obj, App)
  1386. try:
  1387. f = open(filename, 'r')
  1388. f.close()
  1389. exists = True
  1390. except IOError:
  1391. exists = False
  1392. msg = "File exists. Overwrite?"
  1393. if exists and self.question_dialog("File exists", msg) == Gtk.ResponseType.CANCEL:
  1394. return
  1395. app_obj.save_project(filename)
  1396. self.register_recent("project", filename)
  1397. app_obj.info("Project copy saved to: " + filename)
  1398. self.file_chooser_save_action(on_success)
  1399. def on_options_app2project(self, param):
  1400. """
  1401. Callback for Options->Transfer Options->App=>Project. Copies options
  1402. from application defaults to project defaults.
  1403. :param param: Ignored.
  1404. :return: None
  1405. """
  1406. self.defaults_read_form()
  1407. self.options.update(self.defaults)
  1408. self.options_write_form()
  1409. def on_options_project2app(self, param):
  1410. """
  1411. Callback for Options->Transfer Options->Project=>App. Copies options
  1412. from project defaults to application defaults.
  1413. :param param: Ignored.
  1414. :return: None
  1415. """
  1416. self.options_read_form()
  1417. self.defaults.update(self.options)
  1418. self.defaults_write_form()
  1419. def on_options_project2object(self, param):
  1420. """
  1421. Callback for Options->Transfer Options->Project=>Object. Copies options
  1422. from project defaults to the currently selected object.
  1423. :param param: Ignored.
  1424. :return: None
  1425. """
  1426. self.options_read_form()
  1427. obj = self.collection.get_active()
  1428. if obj is None:
  1429. self.info("WARNING: No object selected.")
  1430. return
  1431. for option in self.options:
  1432. if option.find(obj.kind + "_") == 0:
  1433. oname = option[len(obj.kind)+1:]
  1434. obj.options[oname] = self.options[option]
  1435. obj.to_form() # Update UI
  1436. def on_options_object2project(self, param):
  1437. """
  1438. Callback for Options->Transfer Options->Object=>Project. Copies options
  1439. from the currently selected object to project defaults.
  1440. :param param: Ignored.
  1441. :return: None
  1442. """
  1443. obj = self.collection.get_active()
  1444. if obj is None:
  1445. self.info("WARNING: No object selected.")
  1446. return
  1447. obj.read_form()
  1448. for option in obj.options:
  1449. if option in ['name']: # TODO: Handle this better...
  1450. continue
  1451. self.options[obj.kind + "_" + option] = obj.options[option]
  1452. self.options_write_form()
  1453. def on_options_object2app(self, param):
  1454. """
  1455. Callback for Options->Transfer Options->Object=>App. Copies options
  1456. from the currently selected object to application defaults.
  1457. :param param: Ignored.
  1458. :return: None
  1459. """
  1460. obj = self.collection.get_active()
  1461. if obj is None:
  1462. self.info("WARNING: No object selected.")
  1463. return
  1464. obj.read_form()
  1465. for option in obj.options:
  1466. if option in ['name']: # TODO: Handle this better...
  1467. continue
  1468. self.defaults[obj.kind + "_" + option] = obj.options[option]
  1469. self.defaults_write_form()
  1470. def on_options_app2object(self, param):
  1471. """
  1472. Callback for Options->Transfer Options->App=>Object. Copies options
  1473. from application defaults to the currently selected object.
  1474. :param param: Ignored.
  1475. :return: None
  1476. """
  1477. self.defaults_read_form()
  1478. obj = self.collection.get_active()
  1479. if obj is None:
  1480. self.info("WARNING: No object selected.")
  1481. return
  1482. for option in self.defaults:
  1483. if option.find(obj.kind + "_") == 0:
  1484. oname = option[len(obj.kind)+1:]
  1485. obj.options[oname] = self.defaults[option]
  1486. obj.to_form() # Update UI
  1487. def on_file_savedefaults(self, param):
  1488. """
  1489. Callback for menu item File->Save Defaults. Saves application default options
  1490. ``self.defaults`` to defaults.json.
  1491. :param param: Ignored.
  1492. :return: None
  1493. """
  1494. # Read options from file
  1495. try:
  1496. f = open("defaults.json")
  1497. options = f.read()
  1498. f.close()
  1499. except:
  1500. App.log.error("Could not load defaults file.")
  1501. self.info("ERROR: Could not load defaults file.")
  1502. return
  1503. try:
  1504. defaults = json.loads(options)
  1505. except:
  1506. e = sys.exc_info()[0]
  1507. App.log.error("Failed to parse defaults file.")
  1508. App.log.error(str(e))
  1509. self.info("ERROR: Failed to parse defaults file.")
  1510. return
  1511. # Update options
  1512. self.defaults_read_form()
  1513. defaults.update(self.defaults)
  1514. # Save update options
  1515. try:
  1516. f = open("defaults.json", "w")
  1517. json.dump(defaults, f)
  1518. f.close()
  1519. except:
  1520. self.info("ERROR: Failed to write defaults to file.")
  1521. return
  1522. self.info("Defaults saved.")
  1523. def on_options_combo_change(self, widget):
  1524. """
  1525. Called when the combo box to choose between application defaults and
  1526. project option changes value. The corresponding variables are
  1527. copied to the UI.
  1528. :param widget: The widget from which this was called. Ignore.
  1529. :return: None
  1530. """
  1531. combo_sel = self.combo_options.get_active()
  1532. App.log.debug("Options --> %s" % combo_sel)
  1533. # Remove anything else in the box
  1534. box_children = self.options_box.get_children()
  1535. for child in box_children:
  1536. self.options_box.remove(child)
  1537. form = [self.options_form, self.defaults_form][combo_sel]
  1538. self.options_box.pack_start(form, False, False, 1)
  1539. form.show_all()
  1540. # self.options2form()
  1541. def on_canvas_configure(self, widget, event):
  1542. """
  1543. Called whenever the canvas changes size. The axes are updated such
  1544. as to use the whole canvas.
  1545. :param widget: Ignored.
  1546. :param event: Ignored.
  1547. :return: None
  1548. """
  1549. self.plotcanvas.auto_adjust_axes()
  1550. def on_row_activated(self, widget, path, col):
  1551. """
  1552. Callback for selection activation (Enter or double-click) on the Project list.
  1553. Switches the notebook page to the object properties form. Calls
  1554. ``self.notebook.set_current_page(1)``.
  1555. :param widget: Ignored.
  1556. :param path: Ignored.
  1557. :param col: Ignored.
  1558. :return: None
  1559. """
  1560. self.notebook.set_current_page(1)
  1561. def on_update_plot(self, widget):
  1562. """
  1563. Callback for button on form for all kinds of objects.
  1564. Re-plots the current object only.
  1565. :param widget: The widget from which this was called. Ignored.
  1566. :return: None
  1567. """
  1568. obj = self.collection.get_active()
  1569. obj.read_form()
  1570. self.set_progress_bar(0.5, "Plotting...")
  1571. def thread_func(app_obj):
  1572. assert isinstance(app_obj, App)
  1573. obj.plot()
  1574. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
  1575. # Send to worker
  1576. self.worker.add_task(thread_func, [self])
  1577. def on_excellon_tool_choose(self, widget):
  1578. """
  1579. Callback for button on Excellon form to open up a window for
  1580. selecting tools.
  1581. :param widget: The widget from which this was called.
  1582. :return: None
  1583. """
  1584. excellon = self.collection.get_active()
  1585. assert isinstance(excellon, FlatCAMExcellon)
  1586. excellon.show_tool_chooser()
  1587. def on_entry_eval_activate(self, widget):
  1588. """
  1589. Called when an entry is activated (eg. by hitting enter) if
  1590. set to do so. Its text is eval()'d and set to the returned value.
  1591. The current object is updated.
  1592. :param widget:
  1593. :return:
  1594. """
  1595. self.on_eval_update(widget)
  1596. obj = self.collection.get_active()
  1597. assert isinstance(obj, FlatCAMObj)
  1598. obj.read_form()
  1599. def on_eval_update(self, widget):
  1600. """
  1601. Modifies the content of a Gtk.Entry by running
  1602. eval() on its contents and puting it back as a
  1603. string.
  1604. :param widget: The widget from which this was called.
  1605. :return: None
  1606. """
  1607. # TODO: error handling here
  1608. widget.set_text(str(eval(widget.get_text())))
  1609. # def on_cncjob_exportgcode(self, widget):
  1610. # """
  1611. # Called from button on CNCjob form to save the G-Code from the object.
  1612. #
  1613. # :param widget: The widget from which this was called.
  1614. # :return: None
  1615. # """
  1616. # def on_success(app_obj, filename):
  1617. # cncjob = app_obj.collection.get_active()
  1618. # f = open(filename, 'w')
  1619. # f.write(cncjob.gcode)
  1620. # f.close()
  1621. # app_obj.info("Saved to: " + filename)
  1622. #
  1623. # self.file_chooser_save_action(on_success)
  1624. def on_delete(self, widget):
  1625. """
  1626. Delete the currently selected FlatCAMObj.
  1627. :param widget: The widget from which this was called. Ignored.
  1628. :return: None
  1629. """
  1630. # Keep this for later
  1631. name = copy(self.collection.get_active().options["name"])
  1632. # Remove plot
  1633. self.plotcanvas.figure.delaxes(self.collection.get_active().axes)
  1634. self.plotcanvas.auto_adjust_axes()
  1635. # Clear form
  1636. self.setup_component_editor()
  1637. # Remove from dictionary
  1638. self.collection.delete_active()
  1639. self.info("Object deleted: %s" % name)
  1640. def on_toolbar_replot(self, widget):
  1641. """
  1642. Callback for toolbar button. Re-plots all objects.
  1643. :param widget: The widget from which this was called.
  1644. :return: None
  1645. """
  1646. try:
  1647. self.collection.get_active().read_form()
  1648. except AttributeError:
  1649. pass
  1650. self.plot_all()
  1651. def on_clear_plots(self, widget):
  1652. """
  1653. Callback for toolbar button. Clears all plots.
  1654. :param widget: The widget from which this was called.
  1655. :return: None
  1656. """
  1657. self.plotcanvas.clear()
  1658. def on_file_new(self, param):
  1659. """
  1660. Callback for menu item File->New. Returns the application to its
  1661. startup state. This method is thread-safe.
  1662. :param param: Whatever is passed by the event. Ignore.
  1663. :return: None
  1664. """
  1665. # Remove everything from memory
  1666. App.log.debug("on_file_bew()")
  1667. # GUI things
  1668. def task():
  1669. # Clear plot
  1670. App.log.debug(" self.plotcanvas.clear()")
  1671. self.plotcanvas.clear()
  1672. # Delete data
  1673. App.log.debug(" self.collection.delete_all()")
  1674. self.collection.delete_all()
  1675. # Clear object editor
  1676. App.log.debug(" self.setup_component_editor()")
  1677. self.setup_component_editor()
  1678. GLib.idle_add(task)
  1679. # Clear project filename
  1680. self.project_filename = None
  1681. # Re-fresh project options
  1682. self.on_options_app2project(None)
  1683. def on_filequit(self, param):
  1684. """
  1685. Callback for menu item File->Quit. Closes the application.
  1686. :param param: Whatever is passed by the event. Ignore.
  1687. :return: None
  1688. """
  1689. self.window.destroy()
  1690. Gtk.main_quit()
  1691. def on_closewindow(self, param):
  1692. """
  1693. Callback for closing the main window.
  1694. :param param: Whatever is passed by the event. Ignore.
  1695. :return: None
  1696. """
  1697. self.window.destroy()
  1698. Gtk.main_quit()
  1699. def file_chooser_action(self, on_success):
  1700. """
  1701. Opens the file chooser and runs on_success on a separate thread
  1702. upon completion of valid file choice.
  1703. :param on_success: A function to run upon completion of a valid file
  1704. selection. Takes 2 parameters: The app instance and the filename.
  1705. Note that it is run on a separate thread, therefore it must take the
  1706. appropriate precautions when accessing shared resources.
  1707. :type on_success: func
  1708. :return: None
  1709. """
  1710. dialog = Gtk.FileChooserDialog("Please choose a file", self.window,
  1711. Gtk.FileChooserAction.OPEN,
  1712. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1713. Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
  1714. response = dialog.run()
  1715. if response == Gtk.ResponseType.OK:
  1716. filename = dialog.get_filename()
  1717. dialog.destroy()
  1718. # Send to worker.
  1719. self.worker.add_task(on_success, [self, filename])
  1720. elif response == Gtk.ResponseType.CANCEL:
  1721. self.info("Open cancelled.")
  1722. dialog.destroy()
  1723. def file_chooser_save_action(self, on_success):
  1724. """
  1725. Opens the file chooser and runs on_success upon completion of valid file choice.
  1726. :param on_success: A function to run upon selection of a filename. Takes 2
  1727. parameters: The instance of the application (App) and the chosen filename. This
  1728. gets run immediately in the same thread.
  1729. :return: None
  1730. """
  1731. dialog = Gtk.FileChooserDialog("Save file", self.window,
  1732. Gtk.FileChooserAction.SAVE,
  1733. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1734. Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
  1735. dialog.set_current_name("Untitled")
  1736. response = dialog.run()
  1737. if response == Gtk.ResponseType.OK:
  1738. filename = dialog.get_filename()
  1739. dialog.destroy()
  1740. on_success(self, filename)
  1741. elif response == Gtk.ResponseType.CANCEL:
  1742. self.info("Save cancelled.") # print("Cancel clicked")
  1743. dialog.destroy()
  1744. def on_fileopengerber(self, param):
  1745. """
  1746. Callback for menu item File->Open Gerber. Defines a function that is then passed
  1747. to ``self.file_chooser_action()``. It requests the creation of a FlatCAMGerber object
  1748. and updates the progress bar throughout the process.
  1749. :param param: Ignore
  1750. :return: None
  1751. """
  1752. self.file_chooser_action(lambda ao, filename: self.open_gerber(filename))
  1753. def on_fileopenexcellon(self, param):
  1754. """
  1755. Callback for menu item File->Open Excellon. Defines a function that is then passed
  1756. to ``self.file_chooser_action()``. It requests the creation of a FlatCAMExcellon object
  1757. and updates the progress bar throughout the process.
  1758. :param param: Ignore
  1759. :return: None
  1760. """
  1761. self.file_chooser_action(lambda ao, filename: self.open_excellon(filename))
  1762. def on_fileopengcode(self, param):
  1763. """
  1764. Callback for menu item File->Open G-Code. Defines a function that is then passed
  1765. to ``self.file_chooser_action()``. It requests the creation of a FlatCAMCNCjob object
  1766. and updates the progress bar throughout the process.
  1767. :param param: Ignore
  1768. :return: None
  1769. """
  1770. self.file_chooser_action(lambda ao, filename: self.open_gcode(filename))
  1771. def on_mouse_move_over_plot(self, event):
  1772. """
  1773. Callback for the mouse motion event over the plot. This event is generated
  1774. by the Matplotlib backend and has been registered in ``self.__init__()``.
  1775. For details, see: http://matplotlib.org/users/event_handling.html
  1776. :param event: Contains information about the event.
  1777. :return: None
  1778. """
  1779. try: # May fail in case mouse not within axes
  1780. self.position_label.set_label("X: %.4f Y: %.4f" % (
  1781. event.xdata, event.ydata))
  1782. self.mouse = [event.xdata, event.ydata]
  1783. # for subscriber in self.plot_mousemove_subscribers:
  1784. # self.plot_mousemove_subscribers[subscriber](event)
  1785. except:
  1786. self.position_label.set_label("")
  1787. self.mouse = None
  1788. def on_click_over_plot(self, event):
  1789. """
  1790. Callback for the mouse click event over the plot. This event is generated
  1791. by the Matplotlib backend and has been registered in ``self.__init__()``.
  1792. For details, see: http://matplotlib.org/users/event_handling.html
  1793. Default actions are:
  1794. * Copy coordinates to clipboard. Ex.: (65.5473, -13.2679)
  1795. :param event: Contains information about the event, like which button
  1796. was clicked, the pixel coordinates and the axes coordinates.
  1797. :return: None
  1798. """
  1799. # So it can receive key presses
  1800. self.plotcanvas.canvas.grab_focus()
  1801. try:
  1802. App.log.debug('button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
  1803. event.button, event.x, event.y, event.xdata, event.ydata))
  1804. self.clipboard.set_text("(%.4f, %.4f)" % (event.xdata, event.ydata), -1)
  1805. except Exception, e:
  1806. App.log.debug("Outside plot?")
  1807. App.log.debug(str(e))
  1808. def on_zoom_in(self, event):
  1809. """
  1810. Callback for zoom-in request. This can be either from the corresponding
  1811. toolbar button or the '3' key when the canvas is focused. Calls ``self.zoom()``.
  1812. :param event: Ignored.
  1813. :return: None
  1814. """
  1815. self.plotcanvas.zoom(1.5)
  1816. return
  1817. def on_zoom_out(self, event):
  1818. """
  1819. Callback for zoom-out request. This can be either from the corresponding
  1820. toolbar button or the '2' key when the canvas is focused. Calls ``self.zoom()``.
  1821. :param event: Ignored.
  1822. :return: None
  1823. """
  1824. self.plotcanvas.zoom(1 / 1.5)
  1825. def on_zoom_fit(self, event):
  1826. """
  1827. Callback for zoom-out request. This can be either from the corresponding
  1828. toolbar button or the '1' key when the canvas is focused. Calls ``self.adjust_axes()``
  1829. with axes limits from the geometry bounds of all objects.
  1830. :param event: Ignored.
  1831. :return: None
  1832. """
  1833. xmin, ymin, xmax, ymax = self.collection.get_bounds()
  1834. width = xmax - xmin
  1835. height = ymax - ymin
  1836. xmin -= 0.05 * width
  1837. xmax += 0.05 * width
  1838. ymin -= 0.05 * height
  1839. ymax += 0.05 * height
  1840. self.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax)
  1841. def on_key_over_plot(self, event):
  1842. """
  1843. Callback for the key pressed event when the canvas is focused. Keyboard
  1844. shortcuts are handled here. So far, these are the shortcuts:
  1845. ========== ============================================
  1846. Key Action
  1847. ========== ============================================
  1848. '1' Zoom-fit. Fits the axes limits to the data.
  1849. '2' Zoom-out.
  1850. '3' Zoom-in.
  1851. 'm' Toggle on-off the measuring tool.
  1852. ========== ============================================
  1853. :param event: Ignored.
  1854. :return: None
  1855. """
  1856. if event.key == '1': # 1
  1857. self.on_zoom_fit(None)
  1858. return
  1859. if event.key == '2': # 2
  1860. self.plotcanvas.zoom(1 / 1.5, self.mouse)
  1861. return
  1862. if event.key == '3': # 3
  1863. self.plotcanvas.zoom(1.5, self.mouse)
  1864. return
  1865. if event.key == 'm':
  1866. if self.measure.toggle_active():
  1867. self.info("Measuring tool ON")
  1868. else:
  1869. self.info("Measuring tool OFF")
  1870. return
  1871. class BaseDraw:
  1872. def __init__(self, plotcanvas, name=None):
  1873. """
  1874. :param plotcanvas: The PlotCanvas where the drawing tool will operate.
  1875. :type plotcanvas: PlotCanvas
  1876. """
  1877. self.plotcanvas = plotcanvas
  1878. # Must have unique axes
  1879. charset = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"
  1880. self.name = name or [random.choice(charset) for i in range(20)]
  1881. self.axes = self.plotcanvas.new_axes(self.name)
  1882. class DrawingObject(BaseDraw):
  1883. def __init__(self, plotcanvas, name=None):
  1884. """
  1885. Possible objects are:
  1886. * Point
  1887. * Line
  1888. * Rectangle
  1889. * Circle
  1890. * Polygon
  1891. """
  1892. BaseDraw.__init__(self, plotcanvas)
  1893. self.properties = {}
  1894. def plot(self):
  1895. return
  1896. def update_plot(self):
  1897. self.axes.cla()
  1898. self.plot()
  1899. self.plotcanvas.auto_adjust_axes()
  1900. class DrawingPoint(DrawingObject):
  1901. def __init__(self, plotcanvas, name=None, coord=None):
  1902. DrawingObject.__init__(self, plotcanvas)
  1903. self.properties.update({
  1904. "coordinate": coord
  1905. })
  1906. def plot(self):
  1907. x, y = self.properties["coordinate"]
  1908. self.axes.plot(x, y, 'o')
  1909. class Measurement:
  1910. def __init__(self, container, plotcanvas, update=None):
  1911. self.update = update
  1912. self.container = container
  1913. self.frame = None
  1914. self.label = None
  1915. self.point1 = None
  1916. self.point2 = None
  1917. self.active = False
  1918. self.plotcanvas = plotcanvas
  1919. self.click_subscription = None
  1920. self.move_subscription = None
  1921. def toggle_active(self, *args):
  1922. if self.active: # Deactivate
  1923. self.active = False
  1924. self.container.remove(self.frame)
  1925. if self.update is not None:
  1926. self.update()
  1927. self.plotcanvas.mpl_disconnect(self.click_subscription)
  1928. self.plotcanvas.mpl_disconnect(self.move_subscription)
  1929. return False
  1930. else: # Activate
  1931. App.log.debug("DEBUG: Activating Measurement Tool...")
  1932. self.active = True
  1933. self.click_subscription = self.plotcanvas.mpl_connect("button_press_event", self.on_click)
  1934. self.move_subscription = self.plotcanvas.mpl_connect('motion_notify_event', self.on_move)
  1935. self.frame = Gtk.Frame()
  1936. self.frame.set_margin_right(5)
  1937. self.frame.set_margin_top(3)
  1938. align = Gtk.Alignment()
  1939. align.set(0, 0.5, 0, 0)
  1940. align.set_padding(4, 4, 4, 4)
  1941. self.label = Gtk.Label()
  1942. self.label.set_label("Click on a reference point...")
  1943. abox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 10)
  1944. abox.pack_start(Gtk.Image.new_from_file('share/measure16.png'), False, False, 0)
  1945. abox.pack_start(self.label, False, False, 0)
  1946. align.add(abox)
  1947. self.frame.add(align)
  1948. self.container.pack_end(self.frame, False, True, 1)
  1949. self.frame.show_all()
  1950. return True
  1951. def on_move(self, event):
  1952. if self.point1 is None:
  1953. self.label.set_label("Click on a reference point...")
  1954. else:
  1955. try:
  1956. dx = event.xdata - self.point1[0]
  1957. dy = event.ydata - self.point1[1]
  1958. d = sqrt(dx**2 + dy**2)
  1959. self.label.set_label("D = %.4f D(x) = %.4f D(y) = %.4f" % (d, dx, dy))
  1960. except TypeError:
  1961. pass
  1962. if self.update is not None:
  1963. self.update()
  1964. def on_click(self, event):
  1965. if self.point1 is None:
  1966. self.point1 = (event.xdata, event.ydata)
  1967. else:
  1968. self.point2 = copy(self.point1)
  1969. self.point1 = (event.xdata, event.ydata)
  1970. self.on_move(event)