source: trunk/python/notationwindow.py @ 1886

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

New Development: No

JIRA Issue: Yes (CAS-1801)

Ready for Test: Yes

Interface Changes: No

What Interface Changed:

Test Programs: help FUNCNAME

Put in Release Notes: No

Module(s):

Description: added help documents of the class methods.


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