FlatCAMApp.py 88 KB

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