FlatCAMApp.py 86 KB

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