source: trunk/python/notationwindow.py @ 1885

Last change on this file since 1885 was 1885, 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:

Put in Release Notes: No

Module(s): plotter

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


File size: 18.3 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        """
31        anchor = self.anchors[self._get_anchval()]
32        notestr = self._get_note().rstrip("\n")
33        if len(notestr.strip()) == 0:
34            #self._clear_textbox()
35            #print "Empty string!"
36            return
37
38        myaxes = None
39        calcpos = True
40        xpos = None
41        ypos = None
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
52                parent = self.seltext['parent']
53                transform = self.seltext['textobj'].get_transform()
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'
91        #va = 'center'
92        va = 'top'
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
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)
127        asaplog.push( msg )
128
129    def _get_axes_from_pos(self,pos,canvas):
130        """helper function to get axes of a position in a plot (fig-coord)"""
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):
144        """
145        helper function to convert a position in figure-coord (0-1) to
146        data-coordinate of the axes       
147        """
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
170    def _get_selected_text(self,event):
171        """helper function to return a dictionary of the nearest note to the event."""
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:
179                    dist2 = d2
180                    selected = {'anchor': 'figure', \
181                                'parent': event.canvas.figure, 'textobj': textobj}
182                    msg = "Fig loop: a text, '"+textobj.get_text()+"', at "
183                    msg += str(textobj.get_position())+" detected"
184                    print msg
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():
192                            anchor = 'data'                   
193                        selected = {'anchor': anchor, \
194                                    'parent': ax, 'textobj': textobj}
195                        msg = "Ax loop: a text, '"+textobj.get_text()+"', at "
196                        msg += str(textobj.get_position())+" detected"
197                        print msg
198
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
205        return selected
206
207    def _get_text_dist2(self,event,textobj):
208        """
209        helper function to calculate square of distance between
210        a event position and a text object.
211        """
212        (x,y) = textobj._get_xy_display()
213        return (x-event.x)**2+(y-event.y)**2
214
215    def delete_note(self):
216        """
217        Remove selected note.
218        """
219        #print "You selected 'OK'"
220        self._remove_seltext()
221        self.canvas.draw()
222
223    @asaplog_post_dec
224    def _remove_seltext(self):
225        """helper function to remove the selected note"""
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']
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)
240        asaplog.push( msg )
241
242        self.seltext = {}
243
244    @asaplog_post_dec
245    def cancel_delete(self):
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()+"'" )
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):
263    """
264    Backend based
265    """
266    def __init__(self,master=None):
267        self.parent = master._tkcanvas
268        NotationWindowCommon.__init__(self,master=master)
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
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)
299        self.anchval = Tk.IntVar(master=radio,value=0)
300        self.rFig = self._NewRadioButton(radio,"figure",state=Tk.NORMAL,
301                                         variable=self.anchval,value=0,
302                                         side=Tk.LEFT)
303        self.rAxis = self._NewRadioButton(radio,"panel",state=Tk.DISABLED,
304                                          variable=self.anchval,value=1,
305                                          side=Tk.LEFT)
306        self.rData = self._NewRadioButton(radio,"data",state=Tk.DISABLED,
307                                          variable=self.anchval,value=2,
308                                          side=Tk.LEFT)
309        # set initial selection "figure"
310        self.anchval.set(0)
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
341    def _get_anchval(self):
342        return self.anchval.get()
343
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):
370        self.close_textwindow()
371
372    def _print_text(self):
373        self.print_text()
374        self.close_textwindow()
375
376    def load_textwindow(self,event):
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()
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)
390        offset = 5
391        self.show_textwindow(xpix+offset,ypix+offset,enableaxes=is_ax)
392       
393    def show_textwindow(self,xpix,ypix,basetext=None,enableaxes=False):
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        """
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
416    def close_textwindow(self):
417        """Close text window."""
418        self.seltext = {}
419        self._reset_radio()
420        self._clear_textbox()
421        self.textwin.withdraw()
422
423
424    ### Modify/Delete menu widget
425    def _create_modmenu(self,master=None):
426        if master:
427            self.parent = master
428        if not self.parent:
429            return False
430        menu = Tk.Menu(master=self.parent,tearoff=False)
431        menu.add_command(label="Modify",command=self._modify_note)
432        menu.add_command(label="Delete",command=self._delnote_dialog)
433        return menu
434
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)
445        if len(self.seltext) == 3:
446            tkcanvas = event.canvas._tkcanvas
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
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"""
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
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):
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
493    ### helper functions
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.