source: trunk/python/notationwindow.py@ 2075

Last change on this file since 2075 was 1942, checked in by Kana Sugimoto, 14 years ago

New Development: No

JIRA Issue: No (a minor bug fix)

Ready for Test: Yes

Interface Changes: No

What Interface Changed: Please list interface changes

Test Programs:

Put in Release Notes: No

Module(s): sdplot, asapplotter

Description:

Fixed a minor bug for log out put.


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