source: trunk/python/notationwindow.py@ 1885

Last change on this file since 1885 was 1885, checked in by Kana Sugimoto, 15 years ago

New Development: No

JIRA Issue: Yes (CAS-1801)

Ready for Test: Yes

Interface Changes: No

What Interface Changed:

Test Programs:

Put in Release Notes: No

Module(s): plotter

Description: bug fixes and a bit reorganization of the code.


File size: 18.3 KB
RevLine 
[1884]1import os
2import matplotlib
3from asap.logging import asaplog, asaplog_post_dec
4
5######################################
6## Notation box window ##
7######################################
8class NotationWindowCommon:
[1885]9 """
10 A base class to define the functions that the backend-based
11 GUI notation class must implement to print/modify/delete notes on a canvas.
12
13 The following methods *must* be implemented in the backend-based
14 parent class:
15 _get_note : get text in text box
16 _get_anchval : get anchor value selected
17 """
[1884]18 def __init__(self,master=None):
19 #self.parent = master
20 self.canvas = master
21 self.event = None
22 self.note = None
23 self.anchors = ["figure","axes","data"]
24 self.seltext = {}
25 self.numnote = 0
26
27 @asaplog_post_dec
28 def print_text(self):
[1885]29 """
30 """
31 anchor = self.anchors[self._get_anchval()]
[1884]32 notestr = self._get_note().rstrip("\n")
33 if len(notestr.strip()) == 0:
[1885]34 #self._clear_textbox()
[1884]35 #print "Empty string!"
36 return
37
38 myaxes = None
39 calcpos = True
[1885]40 xpos = None
41 ypos = None
[1884]42 if self.seltext:
43 # You are modifying a text
44 mycanvas = self.canvas
45 oldanch = self.seltext['anchor']
46 if oldanch != 'figure':
47 myaxes = self.seltext['parent']
48 calcpos = (anchor != oldanch)
49 if not calcpos:
50 # printing text in the same coord.
51 # you don't have to recalc position
[1885]52 parent = self.seltext['parent']
53 transform = self.seltext['textobj'].get_transform()
[1884]54 (xpos, ypos) = self.seltext['textobj'].get_position()
55 elif anchor == "figure":
56 # converting from "axes"/"data" -> "figure"
57 (x, y) = self.seltext['textobj']._get_xy_display()
58 elif oldanch == "data":
59 # converting from "data" -> "axes".
60 # need xdata & ydata in the axes
61 (x, y) = self.seltext['textobj'].get_position()
62 else:
63 # converting "figure"/"axes" -> "data"
64 # need to calculate xdata & ydata in the axes
65 pixpos = self.seltext['textobj']._get_xy_display()
66 (w,h) = mycanvas.get_width_height()
67 relpos = (pixpos[0]/float(w), pixpos[1]/float(h))
68 if not myaxes:
69 myaxes = self._get_axes_from_pos(relpos,mycanvas)
70 if not myaxes:
71 raise RuntimeError, "Axes resolution failed!"
72 (x, y) = self._convert_pix2dat(relpos,myaxes)
73 self._remove_seltext()
74 elif self.event:
75 mycanvas = self.event.canvas
76 myaxes = self.event.inaxes
77 if myaxes and (anchor != "figure"):
78 x = self.event.xdata
79 y = self.event.ydata
80 else:
81 x = self.event.x
82 y = self.event.y
83 else:
84 raise RuntimeError, "No valid position to print data"
85 return
86
87 # now you know
88 picker = True
89 # alignment of the text: ha (horizontal), va (vertical)
90 ha = 'left'
[1885]91 #va = 'center'
92 va = 'top'
[1884]93 if not calcpos:
94 # you aready know parent, tansform, xpos and ypos
95 pass
96 elif anchor == "figure":
97 # text instance will be appended to mycanvas.figure.texts
98 parent = mycanvas.figure
99 transform = parent.transFigure
100 (w,h) = mycanvas.get_width_height()
101 xpos = x/float(w)
102 ypos = y/float(h)
103 elif myaxes:
104 ## text instance will be appended to myaxes.texts
105 parent = myaxes
106 if anchor == "axes":
107 transform = myaxes.transAxes
108 lims = myaxes.get_xlim()
109 xpos = (x-lims[0])/(lims[1]-lims[0])
110 lims = myaxes.get_ylim()
111 ypos = (y-lims[0])/(lims[1]-lims[0])
112 else:
113 # anchored on "data"
114 transform = myaxes.transData
115 xpos = x
116 ypos = y
117 parent.text(xpos,ypos,notestr,transform=transform,
118 ha=ha,va=va,picker=picker)
119 mycanvas.draw()
120
121 self.numnote += 1
122
[1885]123 #self._clear_textbox()
124 msg = "Added note: '"+notestr+"'"
125 msg += " @["+str(xpos)+", "+str(ypos)+"] ("+anchor+"-coord)"
126 msg += "\ntotal number of notes are "+str(self.numnote)
[1884]127 asaplog.push( msg )
128
129 def _get_axes_from_pos(self,pos,canvas):
[1885]130 """helper function to get axes of a position in a plot (fig-coord)"""
[1884]131 if len(pos) != 2:
132 raise ValueError, "pixel position should have 2 elements"
133 for axes in canvas.figure.axes:
134 ##check if pos is in the axes
135 #if axes.contains_point(pos): ### seems not working
136 # return axes
137 axbox = axes.get_position().get_points()
138 if (axbox[0][0] <= pos[0] <= axbox[1][0]) and \
139 (axbox[0][1] <= pos[1] <= axbox[1][1]):
140 return axes
141 return None
142
143 def _convert_pix2dat(self,pos,axes):
[1885]144 """
145 helper function to convert a position in figure-coord (0-1) to
146 data-coordinate of the axes
147 """
[1884]148 # convert a relative position from lower-left of the canvas
149 # to a data in axes
150 if len(pos) != 2:
151 raise ValueError, "pixel position should have 2 elements"
152 # left-/bottom-pixel, and pixel width & height of the axes
153 bbox = axes.get_position()
154 lbpos = bbox.get_points()[0]
155 wax = bbox.width
156 hax = bbox.height
157 # check pos value
158 if (pos[0] < lbpos[0]) or (pos[1] < lbpos[1]) \
159 or (pos[0] > (lbpos[0]+wax)) or (pos[1] > (lbpos[1]+hax)):
160 raise ValueError, "The position is out of the axes"
161 xlims = axes.get_xlim()
162 ylims = axes.get_ylim()
163 wdat = xlims[1] - xlims[0]
164 hdat = ylims[1] - ylims[0]
165 xdat = xlims[0] + wdat*(pos[0] - lbpos[0])/wax
166 ydat = ylims[0] + hdat*(pos[1] - lbpos[1])/hax
167 return (xdat, ydat)
168
169 @asaplog_post_dec
[1885]170 def _get_selected_text(self,event):
171 """helper function to return a dictionary of the nearest note to the event."""
[1884]172 (w,h) = event.canvas.get_width_height()
173 dist2 = w*w+h*h
174 selected = {}
175 for textobj in self.canvas.figure.texts:
176 if textobj.contains(event)[0]:
177 d2 = self._get_text_dist2(event,textobj)
178 if dist2 >= d2:
[1885]179 dist2 = d2
180 selected = {'anchor': 'figure', \
181 'parent': event.canvas.figure, 'textobj': textobj}
[1884]182 msg = "Fig loop: a text, '"+textobj.get_text()+"', at "
183 msg += str(textobj.get_position())+" detected"
[1885]184 print msg
[1884]185 for ax in self.canvas.figure.axes:
186 for textobj in ax.texts:
187 if textobj.contains(event)[0]:
188 d2 = self._get_text_dist2(event,textobj)
189 if dist2 >= d2:
190 anchor='axes'
191 if ax.transData == textobj.get_transform():
[1885]192 anchor = 'data'
193 selected = {'anchor': anchor, \
194 'parent': ax, 'textobj': textobj}
[1884]195 msg = "Ax loop: a text, '"+textobj.get_text()+"', at "
196 msg += str(textobj.get_position())+" detected"
[1885]197 print msg
[1884]198
[1885]199 if selected:
200 msg = "Selected (modify/delete): '"+textobj.get_text()
201 msg += "' @"+str(textobj.get_position())
202 msg += " ("+selected['anchor']+"-coord)"
203 asaplog.push(msg)
204
[1884]205 return selected
206
207 def _get_text_dist2(self,event,textobj):
[1885]208 """
209 helper function to calculate square of distance between
210 a event position and a text object.
211 """
[1884]212 (x,y) = textobj._get_xy_display()
213 return (x-event.x)**2+(y-event.y)**2
214
215 def delete_note(self):
[1885]216 """
217 Remove selected note.
218 """
[1884]219 #print "You selected 'OK'"
220 self._remove_seltext()
221 self.canvas.draw()
222
223 @asaplog_post_dec
224 def _remove_seltext(self):
[1885]225 """helper function to remove the selected note"""
[1884]226 if len(self.seltext) < 3:
227 raise ValueError, "Don't under stand selected text obj."
228 return
229 try:
230 self.seltext['textobj'].remove()
231 except NotImplementedError:
232 self.seltext['parent'].texts.pop(self.seltext['parent'].texts.index(self.seltext['textobj']))
233 self.numnote -= 1
234
235 textobj = self.seltext['textobj']
[1885]236 msg = "Deleted note: '"+textobj.get_text()+"'"
237 msg += "@"+str(textobj.get_position())\
238 +" ("+self.seltext['anchor']+"-coord)"
239 msg += "\ntotal number of notes are "+str(self.numnote)
[1884]240 asaplog.push( msg )
241
242 self.seltext = {}
243
[1885]244 @asaplog_post_dec
[1884]245 def cancel_delete(self):
[1885]246 """
247 Cancel deleting the selected note.
248 Fired when 'Cancel' button selected on confirmation dialog
249 """
250 asaplog.push( "Cancel deleting: '"+self.seltext['textobj'].get_text()+"'" )
[1884]251 self.seltext = {}
252
253
254#####################################
255## Backend dependent Classes ##
256#####################################
257### TkAgg
258if matplotlib.get_backend() == 'TkAgg':
259 import Tkinter as Tk
260 import tkMessageBox
261
262class NotationWindowTkAgg(NotationWindowCommon):
[1885]263 """
264 Backend based
265 """
[1884]266 def __init__(self,master=None):
267 self.parent = master._tkcanvas
268 NotationWindowCommon.__init__(self,master=master)
[1885]269 self.anchval = None
270 self.textwin = self._create_textwindow(master=None)
271 self.menu = self._create_modmenu(master=self.parent)
272
273 ### Notation window widget
[1884]274 def _create_textwindow(self,master=None):
275 twin = Tk.Toplevel(padx=3,pady=3)
276 twin.title("Annotation")
277 twin.resizable(width=True,height=True)
278 self.textbox = self._NotationBox(parent=twin)
279 self.radio = self._AnchorRadio(parent=twin)
280 self.actionbs = self._ActionButtons(parent=twin)
281
282 self.textbox.pack(side=Tk.TOP,fill=Tk.BOTH,expand=True)
283 self.actionbs.pack(side=Tk.BOTTOM)
284 self.radio.pack(side=Tk.BOTTOM)
285 #twin.deiconify()
286 #twin.minsize(width=twin.winfo_width(),height=twin.winfo_height())
287 twin.withdraw()
288 return twin
289
290 def _NotationBox(self,parent=None):
291 textbox = Tk.Text(master=parent,background='white',
292 height=2,width=20,cursor="xterm",
293 padx=2,pady=2,undo=True,maxundo=10)
294 return textbox
295
296 def _AnchorRadio(self,parent=None):
297 radio = Tk.LabelFrame(master=parent,text="anchor",
298 labelanchor="nw",padx=5,pady=3)
[1885]299 self.anchval = Tk.IntVar(master=radio,value=0)
[1884]300 self.rFig = self._NewRadioButton(radio,"figure",state=Tk.NORMAL,
[1885]301 variable=self.anchval,value=0,
[1884]302 side=Tk.LEFT)
303 self.rAxis = self._NewRadioButton(radio,"panel",state=Tk.DISABLED,
[1885]304 variable=self.anchval,value=1,
[1884]305 side=Tk.LEFT)
306 self.rData = self._NewRadioButton(radio,"data",state=Tk.DISABLED,
[1885]307 variable=self.anchval,value=2,
[1884]308 side=Tk.LEFT)
309 # set initial selection "figure"
[1885]310 self.anchval.set(0)
[1884]311 return radio
312
313 def _NewRadioButton(self,parent,text,state=Tk.NORMAL,variable=None,value=None,side=Tk.LEFT):
314 rb = Tk.Radiobutton(master=parent,text=text,state=state,
315 variable=variable,value=value)
316 rb.pack(side=side)
317 return rb
318
319 def _enable_radio(self):
320 self.rAxis.config(state=Tk.NORMAL)
321 self.rData.config(state=Tk.NORMAL)
322 #self.rFig.config(state=Tk.NORMAL)
323 self.rFig.select()
324
325 def _reset_radio(self):
326 self.rAxis.config(state=Tk.DISABLED)
327 self.rData.config(state=Tk.DISABLED)
328 self.rFig.config(state=Tk.NORMAL)
329 self.rFig.select()
330
331 def _select_radio(self,selection):
332 if not selection in self.anchors:
333 return
334 if selection == "data":
335 self.rData.select()
336 elif selection == "axes":
337 self.rAxis.select()
338 else:
339 self.rFig.select()
340
[1885]341 def _get_anchval(self):
342 return self.anchval.get()
343
[1884]344 def _get_note(self):
345 return self.textbox.get("1.0",Tk.END)
346
347 def _clear_textbox(self):
348 self.textbox.delete("1.0",Tk.END)
349
350 def _set_note(self,note=None):
351 self._clear_textbox()
352 if len(note) >0:
353 self.textbox.insert("1.0",note)
354
355 def _ActionButtons(self,parent=None):
356 actbuts = Tk.Frame(master=parent)
357 bCancel = self._NewButton(actbuts,"cancel",self._cancel_text,side=Tk.LEFT)
358 bPrint = self._NewButton(actbuts,"print", self._print_text,side=Tk.LEFT)
359 return actbuts
360
361 def _NewButton(self, parent, text, command, side=Tk.LEFT):
362 if(os.uname()[0] == 'Darwin'):
363 b = Tk.Button(master=parent, text=text, command=command)
364 else:
365 b = Tk.Button(master=parent, text=text, padx=2, pady=2, command=command)
366 b.pack(side=side)
367 return b
368
369 def _cancel_text(self):
[1885]370 self.close_textwindow()
[1884]371
372 def _print_text(self):
373 self.print_text()
[1885]374 self.close_textwindow()
[1884]375
376 def load_textwindow(self,event):
[1885]377 """
378 Load text window at a event position to add a note on a plot.
379 Parameter:
380 event: an even object to specify the position to load
381 text window.
382 """
383 self.close_modmenu()
[1884]384 if event.canvas._tkcanvas != self.parent:
385 raise RuntimeError, "Got invalid event!"
386
387 self.event = event
388 is_ax = (event.inaxes != None)
389 (xpix, ypix) = self._disppix2screen(event.x, event.y)
[1885]390 offset = 5
[1884]391 self.show_textwindow(xpix+offset,ypix+offset,enableaxes=is_ax)
392
393 def show_textwindow(self,xpix,ypix,basetext=None,enableaxes=False):
[1885]394 """
395 Load text window at a position of screen to add a note on a plot.
396 Parameters:
397 xpix, ypix: a pixel position from Upper-left corner
398 of the screen.
399 basetext: None (default) or any string.
400 A string to be printed on text box when loaded.
401 enable axes: False (default) or True.
402 If True, 'panel' & 'data' radio button is enabled.
403 """
[1884]404 if not self.textwin: return
405 self._reset_radio()
406 if enableaxes:
407 self._enable_radio()
408 self.textwin.deiconify()
409 (w,h) = self.textwin.minsize()
410 if w*h <= 1:
411 self.textwin.minsize(width=self.textwin.winfo_width(),
412 height=self.textwin.winfo_height())
413 (w,h) = self.textwin.minsize()
414 self.textwin.geometry("%sx%s+%s+%s"%(w,h,xpix,ypix))
415
[1885]416 def close_textwindow(self):
417 """Close text window."""
418 self.seltext = {}
[1884]419 self._reset_radio()
420 self._clear_textbox()
421 self.textwin.withdraw()
422
[1885]423
424 ### Modify/Delete menu widget
[1884]425 def _create_modmenu(self,master=None):
426 if master:
[1885]427 self.parent = master
[1884]428 if not self.parent:
429 return False
430 menu = Tk.Menu(master=self.parent,tearoff=False)
[1885]431 menu.add_command(label="Modify",command=self._modify_note)
432 menu.add_command(label="Delete",command=self._delnote_dialog)
[1884]433 return menu
434
[1885]435 def load_modmenu(self,event):
436 """
437 Load cascade menu at a event position to modify or delete
438 selected text.
439 Parameter:
440 event: an even object to specify the position to load
441 text window.
442 """
443 self.close_textwindow()
444 self.seltext = self._get_selected_text(event)
[1884]445 if len(self.seltext) == 3:
[1885]446 tkcanvas = event.canvas._tkcanvas
[1884]447 xpos = tkcanvas.winfo_rootx() + int(event.x)
448 ypos = tkcanvas.winfo_rooty() \
449 + tkcanvas.winfo_height() - int(event.y)
450 self.menu.post(xpos,ypos)
451
[1885]452 def close_modmenu(self):
453 """Close cascade menu."""
454 self.seltext = {}
455 self.menu.unpost()
456
457 ### load text window for modification
458 def _modify_note(self):
459 """helper function to load text window to modify selected note"""
[1884]460 #print "Modify selected!!"
461 textobj = self.seltext['textobj']
462 (xtx, ytx) = textobj._get_xy_display()
463 is_ax = (self.seltext['anchor'] != 'figure')
464 if not is_ax:
465 # previous anchor is figure
466 pos = textobj.get_position()
467 is_ax = (self._get_axes_from_pos(pos,self.canvas) != None)
468
469 (xpix, ypix) = self._disppix2screen(xtx,ytx)
470 offset = int(textobj.get_size())*2
471 self.show_textwindow(xpix,ypix+offset,basetext=textobj.get_text(),\
472 enableaxes=is_ax)
473 self._select_radio(self.seltext['anchor'])
474 self._set_note(textobj.get_text())
475
[1885]476 ### close all widgets
477 def close_widgets(self):
478 """Close note window and menu"""
479 self.close_textwindow()
480 self.close_modmenu()
481
482 ### dialog to confirm deleting note
483 def _delnote_dialog(self):
[1884]484 remind = "Delete text?\n '"+self.seltext['textobj'].get_text()+"'"
485 answer = tkMessageBox.askokcancel(parent=self.parent,title="Delete?",
486 message=remind,
487 default=tkMessageBox.CANCEL)
488 if answer:
489 self.delete_note()
490 else:
491 self.cancel_delete()
492
[1885]493 ### helper functions
[1884]494 def _disppix2screen(self,xpixd,ypixd):
495 # calculate a pixel position form Upper-left of the SCREEN
496 # from a pixel from Lower-left of the CANVAS (e.g., event.x/y)
497 xpixs = self.parent.winfo_rootx() + xpixd
498 ypixs = self.parent.winfo_rooty() + self.parent.winfo_height() \
499 - ypixd
500 return (int(xpixs), int(ypixs))
501
Note: See TracBrowser for help on using the repository browser.