FlatCAMApp.py 104 KB

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