cirkuix.py 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107
  1. import threading
  2. from gi.repository import Gtk, Gdk, GLib, GObject
  3. import simplejson as json
  4. from matplotlib.figure import Figure
  5. from numpy import arange, sin, pi
  6. from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
  7. #from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
  8. #from matplotlib.backends.backend_cairo import FigureCanvasCairo as FigureCanvas
  9. from camlib import *
  10. import sys
  11. ########################################
  12. ## CirkuixObj ##
  13. ########################################
  14. class CirkuixObj:
  15. """
  16. Base type of objects handled in Cirkuix. These become interactive
  17. in the GUI, can be plotted, and their options can be modified
  18. by the user in their respective forms.
  19. """
  20. # Instance of the application to which these are related.
  21. # The app should set this value.
  22. app = None
  23. def __init__(self, name):
  24. self.options = {"name": name}
  25. self.form_kinds = {"name": "entry_text"} # Kind of form element for each option
  26. self.radios = {} # Name value pairs for radio sets
  27. self.radios_inv = {} # Inverse of self.radios
  28. self.axes = None # Matplotlib axes
  29. self.kind = None # Override with proper name
  30. def setup_axes(self, figure):
  31. """
  32. 1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
  33. them to figure if not part of the figure. 4) Sets transparent
  34. background. 5) Sets 1:1 scale aspect ratio.
  35. :param figure: A Matplotlib.Figure on which to add/configure axes.
  36. :type figure: matplotlib.figure.Figure
  37. :return: None
  38. :rtype: None
  39. """
  40. if self.axes is None:
  41. print "New axes"
  42. self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
  43. label=self.options["name"])
  44. elif self.axes not in figure.axes:
  45. print "Clearing and attaching axes"
  46. self.axes.cla()
  47. figure.add_axes(self.axes)
  48. else:
  49. print "Clearing Axes"
  50. self.axes.cla()
  51. # Remove all decoration. The app's axes will have
  52. # the ticks and grid.
  53. self.axes.set_frame_on(False) # No frame
  54. self.axes.set_xticks([]) # No tick
  55. self.axes.set_yticks([]) # No ticks
  56. self.axes.patch.set_visible(False) # No background
  57. self.axes.set_aspect(1)
  58. def to_form(self):
  59. """
  60. Copies options to the UI form.
  61. :return: None
  62. """
  63. for option in self.options:
  64. self.set_form_item(option)
  65. def read_form(self):
  66. """
  67. Reads form into ``self.options``.
  68. :return: None
  69. :rtype : None
  70. """
  71. for option in self.options:
  72. self.read_form_item(option)
  73. def build_ui(self):
  74. """
  75. Sets up the UI/form for this object.
  76. :return: None
  77. :rtype : None
  78. """
  79. # Where the UI for this object is drawn
  80. box_selected = self.app.builder.get_object("box_selected")
  81. # Remove anything else in the box
  82. box_children = box_selected.get_children()
  83. for child in box_children:
  84. box_selected.remove(child)
  85. osw = self.app.builder.get_object("offscrwindow_" + self.kind) # offscreenwindow
  86. sw = self.app.builder.get_object("sw_" + self.kind) # scrollwindows
  87. osw.remove(sw) # TODO: Is this needed ?
  88. vp = self.app.builder.get_object("vp_" + self.kind) # Viewport
  89. vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
  90. # Put in the UI
  91. box_selected.pack_start(sw, True, True, 0)
  92. entry_name = self.app.builder.get_object("entry_text_" + self.kind + "_name")
  93. entry_name.connect("activate", self.app.on_activate_name)
  94. self.to_form()
  95. sw.show()
  96. def set_form_item(self, option):
  97. """
  98. Copies the specified options to the UI form.
  99. :param option: Name of the option (Key in ``self.options``).
  100. :type option: str
  101. :return: None
  102. """
  103. fkind = self.form_kinds[option]
  104. fname = fkind + "_" + self.kind + "_" + option
  105. if fkind == 'entry_eval' or fkind == 'entry_text':
  106. self.app.builder.get_object(fname).set_text(str(self.options[option]))
  107. return
  108. if fkind == 'cb':
  109. self.app.builder.get_object(fname).set_active(self.options[option])
  110. return
  111. if fkind == 'radio':
  112. self.app.builder.get_object(self.radios_inv[option][self.options[option]]).set_active(True)
  113. return
  114. print "Unknown kind of form item:", fkind
  115. def read_form_item(self, option):
  116. fkind = self.form_kinds[option]
  117. fname = fkind + "_" + self.kind + "_" + option
  118. if fkind == 'entry_text':
  119. self.options[option] = self.app.builder.get_object(fname).get_text()
  120. return
  121. if fkind == 'entry_eval':
  122. self.options[option] = self.app.get_eval(fname)
  123. return
  124. if fkind == 'cb':
  125. self.options[option] = self.app.builder.get_object(fname).get_active()
  126. return
  127. if fkind == 'radio':
  128. self.options[option] = self.app.get_radio_value(self.radios[option])
  129. return
  130. print "Unknown kind of form item:", fkind
  131. def plot(self, figure):
  132. """
  133. Extend this method! Sets up axes if needed and
  134. clears them. Descendants must do the actual plotting.
  135. """
  136. # Creates the axes if necessary and sets them up.
  137. self.setup_axes(figure)
  138. # Clear axes.
  139. # self.axes.cla()
  140. # return
  141. def serialize(self):
  142. """
  143. Returns a representation of the object as a dictionary so
  144. it can be later exported as JSON. Override this method.
  145. @return: Dictionary representing the object
  146. @rtype: dict
  147. """
  148. return
  149. def deserialize(self, obj_dict):
  150. """
  151. Re-builds an object from its serialized version.
  152. @param obj_dict: Dictionary representing a CirkuixObj
  153. @type obj_dict: dict
  154. @return None
  155. """
  156. return
  157. class CirkuixGerber(CirkuixObj, Gerber):
  158. """
  159. Represents Gerber code.
  160. """
  161. def __init__(self, name):
  162. Gerber.__init__(self)
  163. CirkuixObj.__init__(self, name)
  164. self.kind = "gerber"
  165. # The 'name' is already in self.options from CirkuixObj
  166. self.options.update({
  167. "plot": True,
  168. "mergepolys": True,
  169. "multicolored": False,
  170. "solid": False,
  171. "isotooldia": 0.4 / 25.4,
  172. "cutoutmargin": 0.2,
  173. "cutoutgapsize": 0.15,
  174. "gaps": "tb",
  175. "noncoppermargin": 0.0,
  176. "bboxmargin": 0.0,
  177. "bboxrounded": False
  178. })
  179. # The 'name' is already in self.form_kinds from CirkuixObj
  180. self.form_kinds.update({
  181. "plot": "cb",
  182. "mergepolys": "cb",
  183. "multicolored": "cb",
  184. "solid": "cb",
  185. "isotooldia": "entry_eval",
  186. "cutoutmargin": "entry_eval",
  187. "cutoutgapsize": "entry_eval",
  188. "gaps": "radio",
  189. "noncoppermargin": "entry_eval",
  190. "bboxmargin": "entry_eval",
  191. "bboxrounded": "cb"
  192. })
  193. self.radios = {"gaps": {"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"}}
  194. self.radios_inv = {"gaps": {"tb": "rb_2tb", "lr": "rb_2lr", "4": "rb_4"}}
  195. # Attributes to be included in serialization
  196. # Always append to it because it carries contents
  197. # from predecessors.
  198. self.ser_attrs += ['options']
  199. def convert_units(self, units):
  200. factor = Gerber.convert_units(self, units)
  201. self.options['isotooldia'] *= factor
  202. self.options['cutoutmargin'] *= factor
  203. self.options['cutoutgapsize'] *= factor
  204. self.options['noncoppermargin'] *= factor
  205. self.options['bboxmargin'] *= factor
  206. def plot(self, figure):
  207. CirkuixObj.plot(self, figure)
  208. self.create_geometry()
  209. if self.options["mergepolys"]:
  210. geometry = self.solid_geometry
  211. else:
  212. geometry = self.buffered_paths + \
  213. [poly['polygon'] for poly in self.regions] + \
  214. self.flash_geometry
  215. if self.options["multicolored"]:
  216. linespec = '-'
  217. else:
  218. linespec = 'k-'
  219. for poly in geometry:
  220. x, y = poly.exterior.xy
  221. self.axes.plot(x, y, linespec)
  222. for ints in poly.interiors:
  223. x, y = ints.coords.xy
  224. self.axes.plot(x, y, linespec)
  225. self.app.canvas.queue_draw()
  226. def serialize(self):
  227. return {
  228. "options": self.options,
  229. "kind": self.kind
  230. }
  231. class CirkuixExcellon(CirkuixObj, Excellon):
  232. """
  233. Represents Excellon code.
  234. """
  235. def __init__(self, name):
  236. Excellon.__init__(self)
  237. CirkuixObj.__init__(self, name)
  238. self.kind = "excellon"
  239. self.options.update({
  240. "plot": True,
  241. "solid": False,
  242. "multicolored": False,
  243. "drillz": -0.1,
  244. "travelz": 0.1,
  245. "feedrate": 5.0,
  246. "toolselection": ""
  247. })
  248. self.form_kinds.update({
  249. "plot": "cb",
  250. "solid": "cb",
  251. "multicolored": "cb",
  252. "drillz": "entry_eval",
  253. "travelz": "entry_eval",
  254. "feedrate": "entry_eval",
  255. "toolselection": "entry_text"
  256. })
  257. # TODO: Document this.
  258. self.tool_cbs = {}
  259. # Attributes to be included in serialization
  260. # Always append to it because it carries contents
  261. # from predecessors.
  262. self.ser_attrs += ['options']
  263. def convert_units(self, units):
  264. factor = Excellon.convert_units(self, units)
  265. self.options['drillz'] *= factor
  266. self.options['travelz'] *= factor
  267. self.options['feedrate'] *= factor
  268. def plot(self, figure):
  269. CirkuixObj.plot(self, figure)
  270. #self.setup_axes(figure)
  271. self.create_geometry()
  272. # Plot excellon
  273. for geo in self.solid_geometry:
  274. x, y = geo.exterior.coords.xy
  275. self.axes.plot(x, y, 'r-')
  276. for ints in geo.interiors:
  277. x, y = ints.coords.xy
  278. self.axes.plot(x, y, 'g-')
  279. self.app.on_zoom_fit(None)
  280. self.app.canvas.queue_draw()
  281. def show_tool_chooser(self):
  282. win = Gtk.Window()
  283. box = Gtk.Box(spacing=2)
  284. box.set_orientation(Gtk.Orientation(1))
  285. win.add(box)
  286. for tool in self.tools:
  287. self.tool_cbs[tool] = Gtk.CheckButton(label=tool + ": " + self.tools[tool])
  288. box.pack_start(self.tool_cbs[tool], False, False, 1)
  289. button = Gtk.Button(label="Accept")
  290. box.pack_start(button, False, False, 1)
  291. win.show_all()
  292. def on_accept(widget):
  293. win.destroy()
  294. tool_list = []
  295. for tool in self.tool_cbs:
  296. if self.tool_cbs[tool].get_active():
  297. tool_list.append(tool)
  298. self.options["toolselection"] = ", ".join(tool_list)
  299. self.to_form()
  300. button.connect("activate", on_accept)
  301. button.connect("clicked", on_accept)
  302. class CirkuixCNCjob(CirkuixObj, CNCjob):
  303. """
  304. Represents G-Code.
  305. """
  306. def __init__(self, name, units="in", kind="generic", z_move=0.1,
  307. feedrate=3.0, z_cut=-0.002, tooldia=0.0):
  308. CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
  309. feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
  310. CirkuixObj.__init__(self, name)
  311. self.kind = "cncjob"
  312. self.options.update({
  313. "plot": True,
  314. "solid": False,
  315. "multicolored": False,
  316. "tooldia": 0.4 / 25.4 # 0.4mm in inches
  317. })
  318. self.form_kinds.update({
  319. "plot": "cb",
  320. "solid": "cb",
  321. "multicolored": "cb",
  322. "tooldia": "entry_eval"
  323. })
  324. # Attributes to be included in serialization
  325. # Always append to it because it carries contents
  326. # from predecessors.
  327. self.ser_attrs += ['options']
  328. def plot(self, figure):
  329. CirkuixObj.plot(self, figure)
  330. #self.setup_axes(figure)
  331. self.plot2(self.axes, tooldia=self.options["tooldia"])
  332. self.app.on_zoom_fit(None)
  333. self.app.canvas.queue_draw()
  334. class CirkuixGeometry(CirkuixObj, Geometry):
  335. """
  336. Geometric object not associated with a specific
  337. format.
  338. """
  339. def __init__(self, name):
  340. CirkuixObj.__init__(self, name)
  341. Geometry.__init__(self)
  342. self.kind = "geometry"
  343. self.options.update({
  344. "plot": True,
  345. "solid": False,
  346. "multicolored": False,
  347. "cutz": -0.002,
  348. "travelz": 0.1,
  349. "feedrate": 5.0,
  350. "cnctooldia": 0.4 / 25.4,
  351. "painttooldia": 0.0625,
  352. "paintoverlap": 0.15,
  353. "paintmargin": 0.01
  354. })
  355. self.form_kinds.update({
  356. "plot": "cb",
  357. "solid": "cb",
  358. "multicolored": "cb",
  359. "cutz": "entry_eval",
  360. "travelz": "entry_eval",
  361. "feedrate": "entry_eval",
  362. "cnctooldia": "entry_eval",
  363. "painttooldia": "entry_eval",
  364. "paintoverlap": "entry_eval",
  365. "paintmargin": "entry_eval"
  366. })
  367. # Attributes to be included in serialization
  368. # Always append to it because it carries contents
  369. # from predecessors.
  370. self.ser_attrs += ['options']
  371. def scale(self, factor):
  372. if type(self.solid_geometry) == list:
  373. self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0))
  374. for g in self.solid_geometry]
  375. else:
  376. self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor,
  377. origin=(0, 0))
  378. def convert_units(self, units):
  379. factor = Geometry.convert_units(self, units)
  380. self.options['cutz'] *= factor
  381. self.options['travelz'] *= factor
  382. self.options['feedrate'] *= factor
  383. self.options['cnctooldia'] *= factor
  384. self.options['painttooldia'] *= factor
  385. self.options['paintmargin'] *= factor
  386. return factor
  387. def plot(self, figure):
  388. CirkuixObj.plot(self, figure)
  389. #self.setup_axes(figure)
  390. try:
  391. _ = iter(self.solid_geometry)
  392. except TypeError:
  393. self.solid_geometry = [self.solid_geometry]
  394. for geo in self.solid_geometry:
  395. if type(geo) == Polygon:
  396. x, y = geo.exterior.coords.xy
  397. self.axes.plot(x, y, 'r-')
  398. for ints in geo.interiors:
  399. x, y = ints.coords.xy
  400. self.axes.plot(x, y, 'r-')
  401. continue
  402. if type(geo) == LineString or type(geo) == LinearRing:
  403. x, y = geo.coords.xy
  404. self.axes.plot(x, y, 'r-')
  405. continue
  406. if type(geo) == MultiPolygon:
  407. for poly in geo:
  408. x, y = poly.exterior.coords.xy
  409. self.axes.plot(x, y, 'r-')
  410. for ints in poly.interiors:
  411. x, y = ints.coords.xy
  412. self.axes.plot(x, y, 'r-')
  413. continue
  414. print "WARNING: Did not plot:", str(type(geo))
  415. self.app.on_zoom_fit(None)
  416. self.app.canvas.queue_draw()
  417. ########################################
  418. ## App ##
  419. ########################################
  420. class App:
  421. """
  422. The main application class. The constructor starts the GUI.
  423. """
  424. def __init__(self):
  425. """
  426. Starts the application and the Gtk.main().
  427. @return: app
  428. @rtype: App
  429. """
  430. # Needed to interact with the GUI from other threads.
  431. GObject.threads_init()
  432. ## GUI ##
  433. self.gladefile = "cirkuix.ui"
  434. self.builder = Gtk.Builder()
  435. self.builder.add_from_file(self.gladefile)
  436. self.window = self.builder.get_object("window1")
  437. self.window.set_title("Cirkuix")
  438. self.position_label = self.builder.get_object("label3")
  439. self.grid = self.builder.get_object("grid1")
  440. self.notebook = self.builder.get_object("notebook1")
  441. self.info_label = self.builder.get_object("label_status")
  442. self.progress_bar = self.builder.get_object("progressbar")
  443. self.progress_bar.set_show_text(True)
  444. self.units_label = self.builder.get_object("label_units")
  445. # White (transparent) background on the "Options" tab.
  446. self.builder.get_object("vp_options").override_background_color(Gtk.StateType.NORMAL,
  447. Gdk.RGBA(1, 1, 1, 1))
  448. # Combo box to choose between project and application options.
  449. self.combo_options = self.builder.get_object("combo_options")
  450. self.combo_options.set_active(1)
  451. ## Event handling ##
  452. self.builder.connect_signals(self)
  453. ## Make plot area ##
  454. self.figure = None
  455. self.axes = None
  456. self.canvas = None
  457. self.setup_plot()
  458. self.setup_project_list()
  459. self.setup_component_editor()
  460. ## DATA ##
  461. self.setup_obj_classes()
  462. self.stuff = {} # CirkuixObj's by name
  463. self.mouse = None # Mouse coordinates over plot
  464. # What is selected by the user. It is
  465. # a key if self.stuff
  466. self.selected_item_name = None
  467. # Used to inhibit the on_options_update callback when
  468. # the options are being changed by the program and not the user.
  469. self.options_update_ignore = False
  470. self.defaults = {
  471. "units": "in"
  472. } # Application defaults
  473. self.options = {} # Project options
  474. self.form_kinds = {
  475. "units": "radio"
  476. }
  477. self.radios = {"units": {"rb_inch": "IN", "rb_mm": "MM"},
  478. "gerber_gaps": {"rb_app_2tb": "tb", "rb_app_2lr": "lr", "rb_app_4": "4"}}
  479. self.radios_inv = {"units": {"IN": "rb_inch", "MM": "rb_mm"},
  480. "gerber_gaps": {"tb": "rb_app_2tb", "lr": "rb_app_2lr", "4": "rb_app_4"}}
  481. # self.combos = []
  482. # Options for each kind of CirkuixObj.
  483. # Example: 'gerber_plot': 'cb'. The widget name would be: 'cb_app_gerber_plot'
  484. for CirkuixClass in [CirkuixExcellon, CirkuixGeometry, CirkuixGerber, CirkuixCNCjob]:
  485. obj = CirkuixClass("no_name")
  486. for option in obj.form_kinds:
  487. self.form_kinds[obj.kind + "_" + option] = obj.form_kinds[option]
  488. # if obj.form_kinds[option] == "radio":
  489. # self.radios.update({obj.kind + "_" + option: obj.radios[option]})
  490. # self.radios_inv.update({obj.kind + "_" + option: obj.radios_inv[option]})
  491. self.plot_click_subscribers = {}
  492. # Initialization
  493. self.load_defaults()
  494. self.options.update(self.defaults) # Copy app defaults to project options
  495. self.options2form() # Populate the app defaults form
  496. self.units_label.set_text("[" + self.options["units"] + "]")
  497. # For debugging only
  498. def someThreadFunc(self):
  499. print "Hello World!"
  500. t = threading.Thread(target=someThreadFunc, args=(self,))
  501. t.start()
  502. ########################################
  503. ## START ##
  504. ########################################
  505. self.window.set_default_size(900, 600)
  506. self.window.show_all()
  507. def setup_plot(self):
  508. """
  509. Sets up the main plotting area by creating a Matplotlib
  510. figure in self.canvas, adding axes and configuring them.
  511. These axes should not be ploted on and are just there to
  512. display the axes ticks and grid.
  513. :return: None
  514. :rtype: None
  515. """
  516. self.figure = Figure(dpi=50)
  517. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
  518. self.axes.set_aspect(1)
  519. #t = arange(0.0,5.0,0.01)
  520. #s = sin(2*pi*t)
  521. #self.axes.plot(t,s)
  522. self.axes.grid(True)
  523. self.figure.patch.set_visible(False)
  524. self.canvas = FigureCanvas(self.figure) # a Gtk.DrawingArea
  525. self.canvas.set_hexpand(1)
  526. self.canvas.set_vexpand(1)
  527. # Events
  528. self.canvas.mpl_connect('button_press_event', self.on_click_over_plot)
  529. self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
  530. self.canvas.set_can_focus(True) # For key press
  531. self.canvas.mpl_connect('key_press_event', self.on_key_over_plot)
  532. #self.canvas.mpl_connect('scroll_event', self.on_scroll_over_plot)
  533. self.canvas.connect("configure-event", self.on_canvas_configure)
  534. self.grid.attach(self.canvas, 0, 0, 600, 400)
  535. def setup_obj_classes(self):
  536. """
  537. Sets up application specifics on the CirkuixObj class.
  538. :return: None
  539. """
  540. CirkuixObj.app = self
  541. def setup_project_list(self):
  542. """
  543. Sets up list or Tree where whatever has been loaded or created is
  544. displayed.
  545. :return: None
  546. """
  547. self.store = Gtk.ListStore(str)
  548. self.tree = Gtk.TreeView(self.store)
  549. #self.list = Gtk.ListBox()
  550. self.tree.connect("row_activated", self.on_row_activated)
  551. self.tree_select = self.tree.get_selection()
  552. self.signal_id = self.tree_select.connect("changed", self.on_tree_selection_changed)
  553. renderer = Gtk.CellRendererText()
  554. column = Gtk.TreeViewColumn("Title", renderer, text=0)
  555. self.tree.append_column(column)
  556. self.builder.get_object("box_project").pack_start(self.tree, False, False, 1)
  557. def setup_component_editor(self):
  558. """
  559. Initial configuration of the component editor. Creates
  560. a page titled "Selection" on the notebook on the left
  561. side of the main window.
  562. :return: None
  563. """
  564. box_selected = self.builder.get_object("box_selected")
  565. # Remove anything else in the box
  566. box_children = box_selected.get_children()
  567. for child in box_children:
  568. box_selected.remove(child)
  569. box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
  570. label1 = Gtk.Label("Choose an item from Project")
  571. box1.pack_start(label1, True, False, 1)
  572. box_selected.pack_start(box1, True, True, 0)
  573. #box_selected.show()
  574. box1.show()
  575. label1.show()
  576. def info(self, text):
  577. """
  578. Show text on the status bar.
  579. :param text: Text to display.
  580. :type text: str
  581. :return: None
  582. """
  583. self.info_label.set_text(text)
  584. def zoom(self, factor, center=None):
  585. """
  586. Zooms the plot by factor around a given
  587. center point. Takes care of re-drawing.
  588. :param factor: Number by which to scale the plot.
  589. :type factor: float
  590. :param center: Coordinates [x, y] of the point around which to scale the plot.
  591. :type center: list
  592. :return: None
  593. """
  594. xmin, xmax = self.axes.get_xlim()
  595. ymin, ymax = self.axes.get_ylim()
  596. width = xmax - xmin
  597. height = ymax - ymin
  598. if center is None:
  599. center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
  600. # For keeping the point at the pointer location
  601. relx = (xmax - center[0]) / width
  602. rely = (ymax - center[1]) / height
  603. new_width = width / factor
  604. new_height = height / factor
  605. xmin = center[0] - new_width * (1 - relx)
  606. xmax = center[0] + new_width * relx
  607. ymin = center[1] - new_height * (1 - rely)
  608. ymax = center[1] + new_height * rely
  609. for name in self.stuff:
  610. self.stuff[name].axes.set_xlim((xmin, xmax))
  611. self.stuff[name].axes.set_ylim((ymin, ymax))
  612. self.axes.set_xlim((xmin, xmax))
  613. self.axes.set_ylim((ymin, ymax))
  614. self.canvas.queue_draw()
  615. def build_list(self):
  616. """
  617. Clears and re-populates the list of objects in currently
  618. in the project.
  619. :return: None
  620. """
  621. print "build_list(): clearing"
  622. self.tree_select.unselect_all()
  623. self.store.clear()
  624. print "repopulating...",
  625. for key in self.stuff:
  626. print key,
  627. self.store.append([key])
  628. print
  629. def get_radio_value(self, radio_set):
  630. """
  631. Returns the radio_set[key] of the radiobutton
  632. whose name is key is active.
  633. :param radio_set: A dictionary containing widget_name: value pairs.
  634. :type radio_set: dict
  635. :return: radio_set[key]
  636. """
  637. for name in radio_set:
  638. if self.builder.get_object(name).get_active():
  639. return radio_set[name]
  640. def plot_all(self):
  641. """
  642. Re-generates all plots from all objects.
  643. :return: None
  644. """
  645. self.clear_plots()
  646. self.set_progress_bar(0.1, "Re-plotting...")
  647. def thread_func(app_obj):
  648. percentage = 0.1
  649. try:
  650. delta = 0.9 / len(self.stuff)
  651. except ZeroDivisionError:
  652. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  653. return
  654. for i in self.stuff:
  655. self.stuff[i].plot(self.figure)
  656. percentage += delta
  657. GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
  658. self.on_zoom_fit(None)
  659. self.axes.grid(True)
  660. self.canvas.queue_draw()
  661. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  662. t = threading.Thread(target=thread_func, args=(self,))
  663. t.daemon = True
  664. t.start()
  665. def clear_plots(self):
  666. """
  667. Clears self.axes and self.figure.
  668. :return: None
  669. """
  670. # TODO: Create a setup_axes method that gets called here and in setup_plot?
  671. self.axes.cla()
  672. self.figure.clf()
  673. self.figure.add_axes(self.axes)
  674. self.axes.set_aspect(1)
  675. self.axes.grid(True)
  676. self.canvas.queue_draw()
  677. def get_eval(self, widget_name):
  678. """
  679. Runs eval() on the on the text entry of name 'widget_name'
  680. and returns the results.
  681. :param widget_name: Name of Gtk.Entry
  682. :type widget_name: str
  683. :return: Depends on contents of the entry text.
  684. """
  685. value = self.builder.get_object(widget_name).get_text()
  686. return eval(value)
  687. def set_list_selection(self, name):
  688. """
  689. Marks a given object as selected in the list ob objects
  690. in the GUI. This selection will in turn trigger
  691. ``self.on_tree_selection_changed()``.
  692. :param name: Name of the object.
  693. :type name: str
  694. :return: None
  695. """
  696. iter = self.store.get_iter_first()
  697. while iter is not None and self.store[iter][0] != name:
  698. iter = self.store.iter_next(iter)
  699. self.tree_select.unselect_all()
  700. self.tree_select.select_iter(iter)
  701. # Need to return False such that GLib.idle_add
  702. # or .timeout_add do not repear.
  703. return False
  704. def new_object(self, kind, name, initialize):
  705. """
  706. Creates a new specalized CirkuixObj and attaches it to the application,
  707. this is, updates the GUI accordingly, any other records and plots it.
  708. :param kind: The kind of object to create. One of 'gerber',
  709. 'excellon', 'cncjob' and 'geometry'.
  710. :type kind: str
  711. :param name: Name for the object.
  712. :type name: str
  713. :param initialize: Function to run after creation of the object
  714. but before it is attached to the application. The function is
  715. called with 2 parameters: the new object and the App instance.
  716. :type initialize: function
  717. :return: None
  718. :rtype: None
  719. """
  720. # Check for existing name
  721. if name in self.stuff:
  722. self.info("Rename " + name + " in project first.")
  723. return None
  724. # Create object
  725. classdict = {
  726. "gerber": CirkuixGerber,
  727. "excellon": CirkuixExcellon,
  728. "cncjob": CirkuixCNCjob,
  729. "geometry": CirkuixGeometry
  730. }
  731. obj = classdict[kind](name)
  732. # Initialize as per user request
  733. # User must take care to implement initialize
  734. # in a thread-safe way as is is likely that we
  735. # have been invoked in a separate thread.
  736. initialize(obj, self)
  737. # Check units and convert if necessary
  738. if self.options["units"].upper() != obj.units.upper():
  739. GLib.idle_add(lambda: self.info("Converting units to " + self.options["units"] + "."))
  740. obj.convert_units(self.options["units"])
  741. # Set default options from self.options
  742. for option in self.options:
  743. if option.find(kind + "_") == 0:
  744. oname = option[len(kind)+1:]
  745. obj.options[oname] = self.options[option]
  746. # Add to our records
  747. self.stuff[name] = obj
  748. # Update GUI list and select it (Thread-safe?)
  749. self.store.append([name])
  750. #self.build_list()
  751. GLib.idle_add(lambda: self.set_list_selection(name))
  752. # TODO: Gtk.notebook.set_current_page is not known to
  753. # TODO: return False. Fix this??
  754. GLib.timeout_add(100, lambda: self.notebook.set_current_page(1))
  755. # Plot
  756. # TODO: (Thread-safe?)
  757. obj.plot(self.figure)
  758. obj.axes.set_alpha(0.0)
  759. self.on_zoom_fit(None)
  760. return obj
  761. def set_progress_bar(self, percentage, text=""):
  762. """
  763. Sets the application's progress bar to a given fraction and text.
  764. :param percentage: The fraction (0.0-1.0) of the progress.
  765. :type percentage: float
  766. :param text: Text to display on the progress bar.
  767. :type text: str
  768. :return:
  769. """
  770. self.progress_bar.set_text(text)
  771. self.progress_bar.set_fraction(percentage)
  772. return False
  773. def save_project(self):
  774. return
  775. def get_current(self):
  776. """
  777. Returns the currently selected CirkuixObj in the application.
  778. :return: Currently selected CirkuixObj in the application.
  779. :rtype: CirkuixObj or None
  780. """
  781. try:
  782. return self.stuff[self.selected_item_name]
  783. except:
  784. return None
  785. def adjust_axes(self, xmin, ymin, xmax, ymax):
  786. """
  787. Adjusts axes of all plots while maintaining the use of the whole canvas
  788. and an aspect ratio to 1:1 between x and y axes. The parameters are an original
  789. request that will be modified to fit these restrictions.
  790. :param xmin: Requested minimum value for the X axis.
  791. :type xmin: float
  792. :param ymin: Requested minimum value for the Y axis.
  793. :type ymin: float
  794. :param xmax: Requested maximum value for the X axis.
  795. :type xmax: float
  796. :param ymax: Requested maximum value for the Y axis.
  797. :type ymax: float
  798. :return: None
  799. """
  800. m_x = 15 # pixels
  801. m_y = 25 # pixels
  802. width = xmax - xmin
  803. height = ymax - ymin
  804. r = width / height
  805. Fw, Fh = self.canvas.get_width_height()
  806. Fr = float(Fw) / Fh
  807. x_ratio = float(m_x) / Fw
  808. y_ratio = float(m_y) / Fh
  809. if r > Fr:
  810. ycenter = (ymin + ymax) / 2.0
  811. newheight = height * r / Fr
  812. ymin = ycenter - newheight / 2.0
  813. ymax = ycenter + newheight / 2.0
  814. else:
  815. xcenter = (xmax + ymin) / 2.0
  816. newwidth = width * Fr / r
  817. xmin = xcenter - newwidth / 2.0
  818. xmax = xcenter + newwidth / 2.0
  819. for name in self.stuff:
  820. if self.stuff[name].axes is None:
  821. continue
  822. self.stuff[name].axes.set_xlim((xmin, xmax))
  823. self.stuff[name].axes.set_ylim((ymin, ymax))
  824. self.stuff[name].axes.set_position([x_ratio, y_ratio,
  825. 1 - 2 * x_ratio, 1 - 2 * y_ratio])
  826. self.axes.set_xlim((xmin, xmax))
  827. self.axes.set_ylim((ymin, ymax))
  828. self.axes.set_position([x_ratio, y_ratio,
  829. 1 - 2 * x_ratio, 1 - 2 * y_ratio])
  830. self.canvas.queue_draw()
  831. def load_defaults(self):
  832. """
  833. Loads the aplication's default settings from defaults.json into
  834. ``self.defaults``.
  835. :return: None
  836. """
  837. try:
  838. f = open("defaults.json")
  839. options = f.read()
  840. f.close()
  841. except:
  842. self.info("ERROR: Could not load defaults file.")
  843. return
  844. try:
  845. defaults = json.loads(options)
  846. except:
  847. e = sys.exc_info()[0]
  848. print e
  849. self.info("ERROR: Failed to parse defaults file.")
  850. return
  851. self.defaults.update(defaults)
  852. def read_form(self):
  853. """
  854. Reads the options form into self.defaults/self.options.
  855. :return: None
  856. :rtype: None
  857. """
  858. combo_sel = self.combo_options.get_active()
  859. options_set = [self.options, self.defaults][combo_sel]
  860. for option in options_set:
  861. self.read_form_item(option, options_set)
  862. def read_form_item(self, name, dest):
  863. """
  864. Reads the value of a form item in the defaults/options form and
  865. saves it to the corresponding dictionary.
  866. :param name: Name of the form item. A key in ``self.defaults`` or
  867. ``self.options``.
  868. :type name: str
  869. :param dest: Dictionary to which to save the value.
  870. :type dest: dict
  871. :return: None
  872. """
  873. fkind = self.form_kinds[name]
  874. fname = fkind + "_" + "app" + "_" + name
  875. if fkind == 'entry_text':
  876. dest[name] = self.builder.get_object(fname).get_text()
  877. return
  878. if fkind == 'entry_eval':
  879. dest[name] = self.get_eval(fname)
  880. return
  881. if fkind == 'cb':
  882. dest[name] = self.builder.get_object(fname).get_active()
  883. return
  884. if fkind == 'radio':
  885. dest[name] = self.get_radio_value(self.radios[name])
  886. return
  887. print "Unknown kind of form item:", fkind
  888. def options2form(self):
  889. """
  890. Sets the 'Project Options' or 'Application Defaults' form with values from
  891. ``self.options``or ``self.defaults``.
  892. :return : None
  893. :rtype : None
  894. """
  895. # Set the on-change callback to do nothing while we do the changes.
  896. self.options_update_ignore = True
  897. combo_sel = self.combo_options.get_active()
  898. options_set = [self.options, self.defaults][combo_sel]
  899. for option in options_set:
  900. self.set_form_item(option, options_set[option])
  901. self.options_update_ignore = False
  902. def set_form_item(self, name, value):
  903. """
  904. Sets a form item 'name' in the GUI with the given 'value'. The syntax of
  905. form names in the GUI is <kind>_app_<name>, where kind is one of: rb (radio button),
  906. cb (check button), entry_eval or entry_text (entry), combo (combo box). name is
  907. whatever name it's been given. For self.defaults, name is a key in the dictionary.
  908. :param name: Name of the form field.
  909. :type name: str
  910. :param value: The value to set the form field to.
  911. :type value: Depends on field kind.
  912. :return: None
  913. """
  914. if name not in self.form_kinds:
  915. print "WARNING: Tried to set unknown option/form item:", name
  916. return
  917. fkind = self.form_kinds[name]
  918. fname = fkind + "_" + "app" + "_" + name
  919. if fkind == 'entry_eval' or fkind == 'entry_text':
  920. try:
  921. self.builder.get_object(fname).set_text(str(value))
  922. except:
  923. print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
  924. return
  925. if fkind == 'cb':
  926. try:
  927. self.builder.get_object(fname).set_active(value)
  928. except:
  929. print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
  930. return
  931. if fkind == 'radio':
  932. try:
  933. self.builder.get_object(self.radios_inv[name][value]).set_active(True)
  934. except:
  935. print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
  936. return
  937. print "Unknown kind of form item:", fkind
  938. def save_project(self, filename):
  939. """
  940. Saves the current project to the specified file.
  941. :param filename: Name of the file in which to save.
  942. :type filename: str
  943. :return: None
  944. """
  945. d = {"objs": [self.stuff[o].to_dict() for o in self.stuff],
  946. "options": self.options}
  947. try:
  948. f = open(filename, 'w')
  949. except:
  950. print "ERROR: Failed to open file for saving:", filename
  951. return
  952. try:
  953. json.dump(d, f, default=to_dict)
  954. except:
  955. print "ERROR: File open but failed to write:", filename
  956. f.close()
  957. return
  958. f.close()
  959. ########################################
  960. ## EVENT HANDLERS ##
  961. ########################################
  962. def on_file_saveproject(self, param):
  963. return
  964. def on_file_saveprojectas(self, param):
  965. def on_success(app_obj, filename):
  966. assert isinstance(app_obj, App)
  967. app_obj.save_project(filename)
  968. app_obj.info("Project saved to: " + filename)
  969. self.file_chooser_save_action(on_success)
  970. return
  971. def on_file_saveprojectcopy(self, param):
  972. return
  973. def on_options_app2project(self, param):
  974. """
  975. Callback for Options->Transfer Options->App=>Project. Copies options
  976. from application defaults to project defaults.
  977. :param param: Ignored.
  978. :return: None
  979. """
  980. self.options.update(self.defaults)
  981. self.options2form() # Update UI
  982. def on_options_project2app(self, param):
  983. """
  984. Callback for Options->Transfer Options->Project=>App. Copies options
  985. from project defaults to application defaults.
  986. :param param: Ignored.
  987. :return: None
  988. """
  989. self.defaults.update(self.options)
  990. self.options2form() # Update UI
  991. def on_options_project2object(self, param):
  992. """
  993. Callback for Options->Transfer Options->Project=>Object. Copies options
  994. from project defaults to the currently selected object.
  995. :param param: Ignored.
  996. :return: None
  997. """
  998. obj = self.get_current()
  999. if obj is None:
  1000. print "WARNING: No object selected."
  1001. return
  1002. for option in self.options:
  1003. if option.find(obj.kind + "_") == 0:
  1004. oname = option[len(obj.kind)+1:]
  1005. obj.options[oname] = self.options[option]
  1006. obj.to_form() # Update UI
  1007. def on_options_object2project(self, param):
  1008. """
  1009. Callback for Options->Transfer Options->Object=>Project. Copies options
  1010. from the currently selected object to project defaults.
  1011. :param param: Ignored.
  1012. :return: None
  1013. """
  1014. obj = self.get_current()
  1015. if obj is None:
  1016. print "WARNING: No object selected."
  1017. return
  1018. obj.read_form()
  1019. for option in obj.options:
  1020. if option in ['name']: # TODO: Handle this better...
  1021. continue
  1022. self.options[obj.kind + "_" + option] = obj.options[option]
  1023. self.options2form() # Update UI
  1024. def on_options_object2app(self, param):
  1025. """
  1026. Callback for Options->Transfer Options->Object=>App. Copies options
  1027. from the currently selected object to application defaults.
  1028. :param param: Ignored.
  1029. :return: None
  1030. """
  1031. obj = self.get_current()
  1032. if obj is None:
  1033. print "WARNING: No object selected."
  1034. return
  1035. obj.read_form()
  1036. for option in obj.options:
  1037. if option in ['name']: # TODO: Handle this better...
  1038. continue
  1039. self.defaults[obj.kind + "_" + option] = obj.options[option]
  1040. self.options2form() # Update UI
  1041. def on_options_app2object(self, param):
  1042. """
  1043. Callback for Options->Transfer Options->App=>Object. Copies options
  1044. from application defaults to the currently selected object.
  1045. :param param: Ignored.
  1046. :return: None
  1047. """
  1048. obj = self.get_current()
  1049. if obj is None:
  1050. print "WARNING: No object selected."
  1051. return
  1052. for option in self.defaults:
  1053. if option.find(obj.kind + "_") == 0:
  1054. oname = option[len(obj.kind)+1:]
  1055. obj.options[oname] = self.defaults[option]
  1056. obj.to_form() # Update UI
  1057. def on_file_savedefaults(self, param):
  1058. """
  1059. Callback for menu item File->Save Defaults. Saves application default options
  1060. (``self.defaults``) to defaults.json.
  1061. :param param: Ignored.
  1062. :return: None
  1063. """
  1064. try:
  1065. f = open("defaults.json")
  1066. options = f.read()
  1067. f.close()
  1068. except:
  1069. self.info("ERROR: Could not load defaults file.")
  1070. return
  1071. try:
  1072. defaults = json.loads(options)
  1073. except:
  1074. e = sys.exc_info()[0]
  1075. print e
  1076. self.info("ERROR: Failed to parse defaults file.")
  1077. return
  1078. assert isinstance(defaults, dict)
  1079. defaults.update(self.defaults)
  1080. try:
  1081. f = open("defaults.json", "w")
  1082. json.dump(defaults, f)
  1083. f.close()
  1084. except:
  1085. self.info("ERROR: Failed to write defaults to file.")
  1086. return
  1087. self.info("Defaults saved.")
  1088. def on_options_combo_change(self, widget):
  1089. """
  1090. Called when the combo box to choose between application defaults and
  1091. project option changes value. The corresponding variables are
  1092. copied to the UI.
  1093. :param widget: The widget from which this was called. Ignore.
  1094. :return: None
  1095. """
  1096. combo_sel = self.combo_options.get_active()
  1097. print "Options --> ", combo_sel
  1098. self.options2form()
  1099. def on_options_update(self, widget):
  1100. """
  1101. Called whenever a value in the options/defaults form changes.
  1102. All values are updated. Can be inhibited by setting ``self.options_update_ignore = True``,
  1103. which may be necessary when updating the UI from code and not by the user.
  1104. :param widget: The widget from which this was called. Ignore.
  1105. :return: None
  1106. """
  1107. if self.options_update_ignore:
  1108. return
  1109. self.read_form()
  1110. def on_scale_object(self, widget):
  1111. """
  1112. Callback for request to change an objects geometry scale. The object
  1113. is re-scaled and replotted.
  1114. :param widget: Ignored.
  1115. :return: None
  1116. """
  1117. obj = self.get_current()
  1118. assert isinstance(obj, CirkuixObj)
  1119. factor = self.get_eval("entry_eval_" + obj.kind + "_scalefactor")
  1120. obj.scale(factor)
  1121. obj.to_form()
  1122. self.on_update_plot(None)
  1123. def on_canvas_configure(self, widget, event):
  1124. """
  1125. Called whenever the canvas changes size. The axes are updated such
  1126. as to use the whole canvas.
  1127. :param widget: Ignored.
  1128. :param event: Ignored.
  1129. :return: None
  1130. """
  1131. print "on_canvas_configure()"
  1132. xmin, xmax = self.axes.get_xlim()
  1133. ymin, ymax = self.axes.get_ylim()
  1134. self.adjust_axes(xmin, ymin, xmax, ymax)
  1135. def on_row_activated(self, widget, path, col):
  1136. """
  1137. Callback for selection activation (Enter or double-click) on the Project list.
  1138. Switches the notebook page to the object properties form. Calls
  1139. ``self.notebook.set_current_page(1)``.
  1140. :param widget: Ignored.
  1141. :param path: Ignored.
  1142. :param col: Ignored.
  1143. :return: None
  1144. """
  1145. self.notebook.set_current_page(1)
  1146. def on_generate_gerber_bounding_box(self, widget):
  1147. """
  1148. Callback for request from the Gerber form to generate a bounding box for the
  1149. geometry in the object. Creates a CirkuixGeometry with the bounding box.
  1150. :param widget: Ignored.
  1151. :return: None
  1152. """
  1153. gerber = self.get_current()
  1154. gerber.read_form()
  1155. name = self.selected_item_name + "_bbox"
  1156. def geo_init(geo_obj, app_obj):
  1157. assert isinstance(geo_obj, CirkuixGeometry)
  1158. bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["bboxmargin"])
  1159. if not gerber.options["bboxrounded"]:
  1160. bounding_box = bounding_box.envelope
  1161. geo_obj.solid_geometry = bounding_box
  1162. self.new_object("geometry", name, geo_init)
  1163. def on_update_plot(self, widget):
  1164. """
  1165. Callback for button on form for all kinds of objects.
  1166. Re-plots the current object only.
  1167. :param widget: The widget from which this was called.
  1168. :return: None
  1169. """
  1170. print "Re-plotting"
  1171. self.get_current().read_form()
  1172. self.set_progress_bar(0.5, "Plotting...")
  1173. #GLib.idle_add(lambda: self.set_progress_bar(0.5, "Plotting..."))
  1174. def thread_func(app_obj):
  1175. assert isinstance(app_obj, App)
  1176. #GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Plotting..."))
  1177. #GLib.idle_add(lambda: app_obj.get_current().plot(app_obj.figure))
  1178. app_obj.get_current().plot(app_obj.figure)
  1179. GLib.idle_add(lambda: app_obj.on_zoom_fit(None))
  1180. GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
  1181. t = threading.Thread(target=thread_func, args=(self,))
  1182. t.daemon = True
  1183. t.start()
  1184. def on_generate_excellon_cncjob(self, widget):
  1185. """
  1186. Callback for button active/click on Excellon form to
  1187. create a CNC Job for the Excellon file.
  1188. :param widget: The widget from which this was called.
  1189. :return: None
  1190. """
  1191. job_name = self.selected_item_name + "_cnc"
  1192. excellon = self.get_current()
  1193. assert isinstance(excellon, CirkuixExcellon)
  1194. excellon.read_form()
  1195. # Object initialization function for app.new_object()
  1196. def job_init(job_obj, app_obj):
  1197. excellon_ = self.get_current()
  1198. assert isinstance(excellon_, CirkuixExcellon)
  1199. assert isinstance(job_obj, CirkuixCNCjob)
  1200. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
  1201. job_obj.z_cut = excellon_.options["drillz"]
  1202. job_obj.z_move = excellon_.options["travelz"]
  1203. job_obj.feedrate = excellon_.options["feedrate"]
  1204. # There could be more than one drill size...
  1205. # job_obj.tooldia = # TODO: duplicate variable!
  1206. # job_obj.options["tooldia"] =
  1207. job_obj.generate_from_excellon_by_tool(excellon_, excellon_.options["toolselection"])
  1208. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
  1209. job_obj.gcode_parse()
  1210. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
  1211. job_obj.create_geometry()
  1212. GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
  1213. # To be run in separate thread
  1214. def job_thread(app_obj):
  1215. app_obj.new_object("cncjob", job_name, job_init)
  1216. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1217. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1218. # Start the thread
  1219. t = threading.Thread(target=job_thread, args=(self,))
  1220. t.daemon = True
  1221. t.start()
  1222. def on_excellon_tool_choose(self, widget):
  1223. """
  1224. Callback for button on Excellon form to open up a window for
  1225. selecting tools.
  1226. :param widget: The widget from which this was called.
  1227. :return: None
  1228. """
  1229. excellon = self.get_current()
  1230. assert isinstance(excellon, CirkuixExcellon)
  1231. excellon.show_tool_chooser()
  1232. def on_entry_eval_activate(self, widget):
  1233. """
  1234. Called when an entry is activated (eg. by hitting enter) if
  1235. set to do so. Its text is eval()'d and set to the returned value.
  1236. The current object is updated.
  1237. :param widget:
  1238. :return:
  1239. """
  1240. self.on_eval_update(widget)
  1241. obj = self.get_current()
  1242. assert isinstance(obj, CirkuixObj)
  1243. obj.read_form()
  1244. def on_gerber_generate_noncopper(self, widget):
  1245. """
  1246. Callback for button on Gerber form to create a geometry object
  1247. with polygons covering the area without copper or negative of the
  1248. Gerber.
  1249. :param widget: The widget from which this was called.
  1250. :return: None
  1251. """
  1252. name = self.selected_item_name + "_noncopper"
  1253. def geo_init(geo_obj, app_obj):
  1254. assert isinstance(geo_obj, CirkuixGeometry)
  1255. gerber = app_obj.stuff[app_obj.selected_item_name]
  1256. assert isinstance(gerber, CirkuixGerber)
  1257. gerber.read_form()
  1258. bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["noncoppermargin"])
  1259. non_copper = bounding_box.difference(gerber.solid_geometry)
  1260. geo_obj.solid_geometry = non_copper
  1261. # TODO: Check for None
  1262. self.new_object("geometry", name, geo_init)
  1263. def on_gerber_generate_cutout(self, widget):
  1264. """
  1265. Callback for button on Gerber form to create geometry with lines
  1266. for cutting off the board.
  1267. :param widget: The widget from which this was called.
  1268. :return: None
  1269. """
  1270. name = self.selected_item_name + "_cutout"
  1271. def geo_init(geo_obj, app_obj):
  1272. # TODO: get from object
  1273. margin = app_obj.get_eval("entry_eval_gerber_cutoutmargin")
  1274. gap_size = app_obj.get_eval("entry_eval_gerber_cutoutgapsize")
  1275. gerber = app_obj.stuff[app_obj.selected_item_name]
  1276. minx, miny, maxx, maxy = gerber.bounds()
  1277. minx -= margin
  1278. maxx += margin
  1279. miny -= margin
  1280. maxy += margin
  1281. midx = 0.5 * (minx + maxx)
  1282. midy = 0.5 * (miny + maxy)
  1283. hgap = 0.5 * gap_size
  1284. pts = [[midx - hgap, maxy],
  1285. [minx, maxy],
  1286. [minx, midy + hgap],
  1287. [minx, midy - hgap],
  1288. [minx, miny],
  1289. [midx - hgap, miny],
  1290. [midx + hgap, miny],
  1291. [maxx, miny],
  1292. [maxx, midy - hgap],
  1293. [maxx, midy + hgap],
  1294. [maxx, maxy],
  1295. [midx + hgap, maxy]]
  1296. cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
  1297. [pts[6], pts[7], pts[10], pts[11]]],
  1298. "lr": [[pts[9], pts[10], pts[1], pts[2]],
  1299. [pts[3], pts[4], pts[7], pts[8]]],
  1300. "4": [[pts[0], pts[1], pts[2]],
  1301. [pts[3], pts[4], pts[5]],
  1302. [pts[6], pts[7], pts[8]],
  1303. [pts[9], pts[10], pts[11]]]}
  1304. cuts = cases[app.get_radio_value({"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"})]
  1305. geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
  1306. # TODO: Check for None
  1307. self.new_object("geometry", name, geo_init)
  1308. def on_eval_update(self, widget):
  1309. """
  1310. Modifies the content of a Gtk.Entry by running
  1311. eval() on its contents and puting it back as a
  1312. string.
  1313. :param widget: The widget from which this was called.
  1314. :return: None
  1315. """
  1316. # TODO: error handling here
  1317. widget.set_text(str(eval(widget.get_text())))
  1318. def on_generate_isolation(self, widget):
  1319. """
  1320. Callback for button on Gerber form to create isolation routing geometry.
  1321. :param widget: The widget from which this was called.
  1322. :return: None
  1323. """
  1324. print "Generating Isolation Geometry:"
  1325. iso_name = self.selected_item_name + "_iso"
  1326. def iso_init(geo_obj, app_obj):
  1327. # TODO: Object must be updated on form change and the options
  1328. # TODO: read from the object.
  1329. tooldia = app_obj.get_eval("entry_eval_gerber_isotooldia")
  1330. geo_obj.solid_geometry = self.get_current().isolation_geometry(tooldia / 2.0)
  1331. # TODO: Do something if this is None. Offer changing name?
  1332. self.new_object("geometry", iso_name, iso_init)
  1333. def on_generate_cncjob(self, widget):
  1334. """
  1335. Callback for button on geometry form to generate CNC job.
  1336. :param widget: The widget from which this was called.
  1337. :return: None
  1338. """
  1339. print "Generating CNC job"
  1340. job_name = self.selected_item_name + "_cnc"
  1341. # Object initialization function for app.new_object()
  1342. def job_init(job_obj, app_obj):
  1343. assert isinstance(job_obj, CirkuixCNCjob)
  1344. geometry = app_obj.stuff[app_obj.selected_item_name]
  1345. assert isinstance(geometry, CirkuixGeometry)
  1346. geometry.read_form()
  1347. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
  1348. job_obj.z_cut = geometry.options["cutz"]
  1349. job_obj.z_move = geometry.options["travelz"]
  1350. job_obj.feedrate = geometry.options["feedrate"]
  1351. job_obj.options["tooldia"] = geometry.options["cnctooldia"]
  1352. GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
  1353. job_obj.generate_from_geometry(geometry)
  1354. GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
  1355. job_obj.gcode_parse()
  1356. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
  1357. job_obj.create_geometry()
  1358. GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
  1359. # To be run in separate thread
  1360. def job_thread(app_obj):
  1361. app_obj.new_object("cncjob", job_name, job_init)
  1362. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1363. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1364. # Start the thread
  1365. t = threading.Thread(target=job_thread, args=(self,))
  1366. t.daemon = True
  1367. t.start()
  1368. def on_generate_paintarea(self, widget):
  1369. """
  1370. Callback for button on geometry form.
  1371. Subscribes to the "Click on plot" event and continues
  1372. after the click. Finds the polygon containing
  1373. the clicked point and runs clear_poly() on it, resulting
  1374. in a new CirkuixGeometry object.
  1375. :param widget: The widget from which this was called.
  1376. :return: None
  1377. """
  1378. self.info("Click inside the desired polygon.")
  1379. geo = self.get_current()
  1380. geo.read_form()
  1381. tooldia = geo.options["painttooldia"]
  1382. overlap = geo.options["paintoverlap"]
  1383. # To be called after clicking on the plot.
  1384. def doit(event):
  1385. self.plot_click_subscribers.pop("generate_paintarea")
  1386. self.info("")
  1387. point = [event.xdata, event.ydata]
  1388. poly = find_polygon(geo.solid_geometry, point)
  1389. # Initializes the new geometry object
  1390. def gen_paintarea(geo_obj, app_obj):
  1391. assert isinstance(geo_obj, CirkuixGeometry)
  1392. assert isinstance(app_obj, App)
  1393. cp = clear_poly(poly.buffer(-geo.options["paintmargin"]), tooldia, overlap)
  1394. geo_obj.solid_geometry = cp
  1395. name = self.selected_item_name + "_paint"
  1396. self.new_object("geometry", name, gen_paintarea)
  1397. self.plot_click_subscribers["generate_paintarea"] = doit
  1398. def on_cncjob_exportgcode(self, widget):
  1399. """
  1400. Called from button on CNCjob form to save the G-Code from the object.
  1401. :param widget: The widget from which this was called.
  1402. :return: None
  1403. """
  1404. def on_success(self, filename):
  1405. cncjob = self.get_current()
  1406. f = open(filename, 'w')
  1407. f.write(cncjob.gcode)
  1408. f.close()
  1409. print "Saved to:", filename
  1410. self.file_chooser_save_action(on_success)
  1411. def on_delete(self, widget):
  1412. """
  1413. Delete the currently selected CirkuixObj.
  1414. :param widget: The widget from which this was called.
  1415. :return: None
  1416. """
  1417. print "on_delete():", self.selected_item_name
  1418. # Remove plot
  1419. self.figure.delaxes(self.get_current().axes)
  1420. self.canvas.queue_draw()
  1421. # Remove from dictionary
  1422. self.stuff.pop(self.selected_item_name)
  1423. # Update UI
  1424. self.build_list() # Update the items list
  1425. def on_replot(self, widget):
  1426. """
  1427. Callback for toolbar button. Re-plots all objects.
  1428. :param widget: The widget from which this was called.
  1429. :return: None
  1430. """
  1431. self.plot_all()
  1432. def on_clear_plots(self, widget):
  1433. """
  1434. Callback for toolbar button. Clears all plots.
  1435. :param widget: The widget from which this was called.
  1436. :return: None
  1437. """
  1438. self.clear_plots()
  1439. def on_activate_name(self, entry):
  1440. """
  1441. Hitting 'Enter' after changing the name of an item
  1442. updates the item dictionary and re-builds the item list.
  1443. :param entry: The widget from which this was called.
  1444. :return: None
  1445. """
  1446. # Disconnect event listener
  1447. self.tree.get_selection().disconnect(self.signal_id)
  1448. new_name = entry.get_text() # Get from form
  1449. self.stuff[new_name] = self.stuff.pop(self.selected_item_name) # Update dictionary
  1450. self.stuff[new_name].options["name"] = new_name # update object
  1451. self.info('Name change: ' + self.selected_item_name + " to " + new_name)
  1452. self.selected_item_name = new_name # Update selection name
  1453. self.build_list() # Update the items list
  1454. # Reconnect event listener
  1455. self.signal_id = self.tree.get_selection().connect(
  1456. "changed", self.on_tree_selection_changed)
  1457. def on_tree_selection_changed(self, selection):
  1458. """
  1459. Callback for selection change in the project list. This changes
  1460. the currently selected CirkuixObj.
  1461. :param selection: Selection associated to the project tree or list
  1462. :type selection: Gtk.TreeSelection
  1463. :return: None
  1464. """
  1465. print "on_tree_selection_change(): ",
  1466. model, treeiter = selection.get_selected()
  1467. if treeiter is not None:
  1468. # Save data for previous selection
  1469. obj = self.get_current()
  1470. if obj is not None:
  1471. obj.read_form()
  1472. print "You selected", model[treeiter][0]
  1473. self.selected_item_name = model[treeiter][0]
  1474. GLib.idle_add(lambda: self.get_current().build_ui())
  1475. else:
  1476. print "Nothing selected"
  1477. self.selected_item_name = None
  1478. self.setup_component_editor()
  1479. def on_file_new(self, param):
  1480. """
  1481. Callback for menu item File->New. Returns the application to its
  1482. startup state.
  1483. :param param: Whatever is passed by the event. Ignore.
  1484. :return: None
  1485. """
  1486. # Remove everythong from memory
  1487. # Clear plot
  1488. self.clear_plots()
  1489. # Clear object editor
  1490. #self.setup_component_editor()
  1491. # Clear data
  1492. self.stuff = {}
  1493. # Clear list
  1494. #self.tree_select.unselect_all()
  1495. self.build_list()
  1496. #print "File->New not implemented yet."
  1497. def on_filequit(self, param):
  1498. """
  1499. Callback for menu item File->Quit. Closes the application.
  1500. :param param: Whatever is passed by the event. Ignore.
  1501. :return: None
  1502. """
  1503. print "quit from menu"
  1504. self.window.destroy()
  1505. Gtk.main_quit()
  1506. def on_closewindow(self, param):
  1507. """
  1508. Callback for closing the main window.
  1509. :param param: Whatever is passed by the event. Ignore.
  1510. :return: None
  1511. """
  1512. print "quit from X"
  1513. self.window.destroy()
  1514. Gtk.main_quit()
  1515. def file_chooser_action(self, on_success):
  1516. """
  1517. Opens the file chooser and runs on_success on a separate thread
  1518. upon completion of valid file choice.
  1519. :param on_success: A function to run upon completion of a valid file
  1520. selection. Takes 2 parameters: The app instance and the filename.
  1521. Note that it is run on a separate thread, therefore it must take the
  1522. appropriate precautions when accessing shared resources.
  1523. :type on_success: func
  1524. :return: None
  1525. """
  1526. dialog = Gtk.FileChooserDialog("Please choose a file", self.window,
  1527. Gtk.FileChooserAction.OPEN,
  1528. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1529. Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
  1530. response = dialog.run()
  1531. if response == Gtk.ResponseType.OK:
  1532. filename = dialog.get_filename()
  1533. dialog.destroy()
  1534. t = threading.Thread(target=on_success, args=(self, filename))
  1535. t.daemon = True
  1536. t.start()
  1537. #on_success(self, filename)
  1538. elif response == Gtk.ResponseType.CANCEL:
  1539. print("Cancel clicked")
  1540. dialog.destroy()
  1541. def file_chooser_save_action(self, on_success):
  1542. """
  1543. Opens the file chooser and runs on_success upon completion of valid file choice.
  1544. :param on_success: A function to run upon selection of a filename. Takes 2
  1545. parameters: The instance of the application (App) and the chosen filename. This
  1546. gets run immediately in the same thread.
  1547. :return: None
  1548. """
  1549. dialog = Gtk.FileChooserDialog("Save file", self.window,
  1550. Gtk.FileChooserAction.SAVE,
  1551. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  1552. Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
  1553. dialog.set_current_name("Untitled")
  1554. response = dialog.run()
  1555. if response == Gtk.ResponseType.OK:
  1556. filename = dialog.get_filename()
  1557. dialog.destroy()
  1558. on_success(self, filename)
  1559. elif response == Gtk.ResponseType.CANCEL:
  1560. print("Cancel clicked")
  1561. dialog.destroy()
  1562. def on_fileopengerber(self, param):
  1563. """
  1564. Callback for menu item File->Open Gerber. Defines a function that is then passed
  1565. to ``self.file_chooser_action()``. It requests the creation of a CirkuixGerber object
  1566. and updates the progress bar throughout the process.
  1567. :param param: Ignore
  1568. :return: None
  1569. """
  1570. # IMPORTANT: on_success will run on a separate thread. Use
  1571. # GLib.idle_add(function, **kwargs) to launch actions that will
  1572. # updata the GUI.
  1573. def on_success(app_obj, filename):
  1574. assert isinstance(app_obj, App)
  1575. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Gerber ..."))
  1576. def obj_init(gerber_obj, app_obj):
  1577. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1578. gerber_obj.parse_file(filename)
  1579. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1580. name = filename.split('/')[-1].split('\\')[-1]
  1581. app_obj.new_object("gerber", name, obj_init)
  1582. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1583. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1584. # on_success gets run on a separate thread
  1585. self.file_chooser_action(on_success)
  1586. def on_fileopenexcellon(self, param):
  1587. """
  1588. Callback for menu item File->Open Excellon. Defines a function that is then passed
  1589. to ``self.file_chooser_action()``. It requests the creation of a CirkuixExcellon object
  1590. and updates the progress bar throughout the process.
  1591. :param param: Ignore
  1592. :return: None
  1593. """
  1594. # IMPORTANT: on_success will run on a separate thread. Use
  1595. # GLib.idle_add(function, **kwargs) to launch actions that will
  1596. # updata the GUI.
  1597. def on_success(app_obj, filename):
  1598. assert isinstance(app_obj, App)
  1599. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Excellon ..."))
  1600. def obj_init(excellon_obj, app_obj):
  1601. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1602. excellon_obj.parse_file(filename)
  1603. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1604. name = filename.split('/')[-1].split('\\')[-1]
  1605. app_obj.new_object("excellon", name, obj_init)
  1606. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1607. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1608. # on_success gets run on a separate thread
  1609. self.file_chooser_action(on_success)
  1610. def on_fileopengcode(self, param):
  1611. """
  1612. Callback for menu item File->Open G-Code. Defines a function that is then passed
  1613. to ``self.file_chooser_action()``. It requests the creation of a CirkuixCNCjob object
  1614. and updates the progress bar throughout the process.
  1615. :param param: Ignore
  1616. :return: None
  1617. """
  1618. # IMPORTANT: on_success will run on a separate thread. Use
  1619. # GLib.idle_add(function, **kwargs) to launch actions that will
  1620. # updata the GUI.
  1621. def on_success(app_obj, filename):
  1622. assert isinstance(app_obj, App)
  1623. def obj_init(job_obj, app_obj):
  1624. assert isinstance(app_obj, App)
  1625. GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening G-Code ..."))
  1626. f = open(filename)
  1627. gcode = f.read()
  1628. f.close()
  1629. job_obj.gcode = gcode
  1630. GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
  1631. job_obj.gcode_parse()
  1632. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating geometry ..."))
  1633. job_obj.create_geometry()
  1634. GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
  1635. name = filename.split('/')[-1].split('\\')[-1]
  1636. app_obj.new_object("cncjob", name, obj_init)
  1637. GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
  1638. GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
  1639. # on_success gets run on a separate thread
  1640. self.file_chooser_action(on_success)
  1641. def on_mouse_move_over_plot(self, event):
  1642. """
  1643. Callback for the mouse motion event over the plot. This event is generated
  1644. by the Matplotlib backend and has been registered in ``self.__init__()``.
  1645. For details, see: http://matplotlib.org/users/event_handling.html
  1646. :param event: Contains information about the event.
  1647. :return: None
  1648. """
  1649. try: # May fail in case mouse not within axes
  1650. self.position_label.set_label("X: %.4f Y: %.4f" % (
  1651. event.xdata, event.ydata))
  1652. self.mouse = [event.xdata, event.ydata]
  1653. except:
  1654. self.position_label.set_label("")
  1655. self.mouse = None
  1656. def on_click_over_plot(self, event):
  1657. """
  1658. Callback for the mouse click event over the plot. This event is generated
  1659. by the Matplotlib backend and has been registered in ``self.__init__()``.
  1660. For details, see: http://matplotlib.org/users/event_handling.html
  1661. :param event:
  1662. :return:
  1663. """
  1664. # For key presses
  1665. self.canvas.grab_focus()
  1666. try:
  1667. print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
  1668. event.button, event.x, event.y, event.xdata, event.ydata)
  1669. for subscriber in self.plot_click_subscribers:
  1670. self.plot_click_subscribers[subscriber](event)
  1671. except Exception, e:
  1672. print "Outside plot!"
  1673. def on_zoom_in(self, event):
  1674. """
  1675. Callback for zoom-in request. This can be either from the corresponding
  1676. toolbar button or the '3' key when the canvas is focused. Calls ``self.zoom()``.
  1677. :param event: Ignored.
  1678. :return: None
  1679. """
  1680. self.zoom(1.5)
  1681. return
  1682. def on_zoom_out(self, event):
  1683. """
  1684. Callback for zoom-out request. This can be either from the corresponding
  1685. toolbar button or the '2' key when the canvas is focused. Calls ``self.zoom()``.
  1686. :param event: Ignored.
  1687. :return: None
  1688. """
  1689. self.zoom(1 / 1.5)
  1690. def on_zoom_fit(self, event):
  1691. """
  1692. Callback for zoom-out request. This can be either from the corresponding
  1693. toolbar button or the '1' key when the canvas is focused. Calls ``self.adjust_axes()``
  1694. with axes limits from the geometry bounds of all objects.
  1695. :param event: Ignored.
  1696. :return: None
  1697. """
  1698. xmin, ymin, xmax, ymax = get_bounds(self.stuff)
  1699. width = xmax - xmin
  1700. height = ymax - ymin
  1701. xmin -= 0.05 * width
  1702. xmax += 0.05 * width
  1703. ymin -= 0.05 * height
  1704. ymax += 0.05 * height
  1705. self.adjust_axes(xmin, ymin, xmax, ymax)
  1706. # def on_scroll_over_plot(self, event):
  1707. # print "Scroll"
  1708. # center = [event.xdata, event.ydata]
  1709. # if sign(event.step):
  1710. # self.zoom(1.5, center=center)
  1711. # else:
  1712. # self.zoom(1/1.5, center=center)
  1713. #
  1714. # def on_window_scroll(self, event):
  1715. # print "Scroll"
  1716. def on_key_over_plot(self, event):
  1717. """
  1718. Callback for the key pressed event when the canvas is focused. Keyboard
  1719. shortcuts are handled here. So far, these are the shortcuts:
  1720. ========== ============================================
  1721. Key Action
  1722. ========== ============================================
  1723. '1' Zoom-fit. Fits the axes limits to the data.
  1724. '2' Zoom-out.
  1725. '3' Zoom-in.
  1726. ========== ============================================
  1727. :param event: Ignored.
  1728. :return: None
  1729. """
  1730. print 'you pressed', event.key, event.xdata, event.ydata
  1731. if event.key == '1': # 1
  1732. self.on_zoom_fit(None)
  1733. return
  1734. if event.key == '2': # 2
  1735. self.zoom(1 / 1.5, self.mouse)
  1736. return
  1737. if event.key == '3': # 3
  1738. self.zoom(1.5, self.mouse)
  1739. return
  1740. app = App()
  1741. Gtk.main()