source: trunk/python/notationwindow.py @ 1888

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

New Development: No

JIRA Issue: Yes (1801)

Ready for Test: Yes

Interface Changes: Yes

What Interface Changed: A name of a button on toolbar changed to "notation"

Test Programs:

Put in Release Notes: No

Module(s): asap plotter

Description:

+ A name of a button on toolbar changed from "note" to "notation"
+ osx GUI handlings
+ workaround for old matplotlib


File size: 20.4 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            try:
140                axbox = axes.get_position().get_points()
141            except AttributeError: ### WORKAROUND for old matplotlib
142                axbox = self._oldpos2new(axes.get_position())
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
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]]]
151       
152    def _convert_pix2dat(self,pos,axes):
153        """
154        helper function to convert a position in figure-coord (0-1) to
155        data-coordinate of the axes       
156        """
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()
163        try:
164            axpos = bbox.get_points()
165        except AttributeError: ### WORKAROUND for old matplotlib
166            axpos = self._oldpos2new(bbox)
167        # check pos value
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]):
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]
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])
177        return (xdat, ydat)
178
179    @asaplog_post_dec
180    def _get_selected_text(self,event):
181        """helper function to return a dictionary of the nearest note to the event."""
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:
189                    dist2 = d2
190                    selected = {'anchor': 'figure', \
191                                'parent': event.canvas.figure, 'textobj': textobj}
192                    msg = "Fig loop: a text, '"+textobj.get_text()+"', at "
193                    msg += str(textobj.get_position())+" detected"
194                    print msg
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():
202                            anchor = 'data'                   
203                        selected = {'anchor': anchor, \
204                                    'parent': ax, 'textobj': textobj}
205                        msg = "Ax loop: a text, '"+textobj.get_text()+"', at "
206                        msg += str(textobj.get_position())+" detected"
207                        print msg
208
209        if selected:
210            msg = "Selected (modify/delete): '"+textobj.get_text()
211            msg += "' @"+str(textobj.get_position())
212            msg += " ("+selected['anchor']+"-coord)"
213            asaplog.push(msg)
214
215        return selected
216
217    def _get_text_dist2(self,event,textobj):
218        """
219        helper function to calculate square of distance between
220        a event position and a text object.
221        """
222        (x,y) = textobj._get_xy_display()
223        return (x-event.x)**2+(y-event.y)**2
224
225    def delete_note(self):
226        """
227        Remove selected note.
228        """
229        #print "You selected 'OK'"
230        self._remove_seltext()
231        self.canvas.draw()
232
233    @asaplog_post_dec
234    def _remove_seltext(self):
235        """helper function to remove the selected note"""
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']
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)
250        asaplog.push( msg )
251
252        self.seltext = {}
253
254    @asaplog_post_dec
255    def cancel_delete(self):
256        """
257        Cancel deleting the selected note.
258        Called when 'cancel' button selected on confirmation dialog.
259        """
260        asaplog.push( "Cancel deleting: '"+self.seltext['textobj'].get_text()+"'" )
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):
273    """
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.
285    """
286    def __init__(self,master=None):
287        self.parent = master._tkcanvas
288        NotationWindowCommon.__init__(self,master=master)
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
294    def _create_textwindow(self,master=None):
295        """Create notation window widget and iconfy it"""
296        twin = Tk.Toplevel(padx=3,pady=3)
297        twin.title("Notation")
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())
308        twin.lift()
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",
315                          padx=2,pady=2,undo=True,maxundo=10)
316        return textbox
317
318    def _AnchorRadio(self,parent=None):
319        radio = Tk.LabelFrame(master=parent,text="anchor",
320                            labelanchor="nw",padx=5,pady=3)
321        self.anchval = Tk.IntVar(master=radio,value=0)
322        self.rFig = self._NewRadioButton(radio,"figure",state=Tk.NORMAL,
323                                         variable=self.anchval,value=0,
324                                         side=Tk.LEFT)
325        self.rAxis = self._NewRadioButton(radio,"panel",state=Tk.DISABLED,
326                                          variable=self.anchval,value=1,
327                                          side=Tk.LEFT)
328        self.rData = self._NewRadioButton(radio,"data",state=Tk.DISABLED,
329                                          variable=self.anchval,value=2,
330                                          side=Tk.LEFT)
331        # set initial selection "figure"
332        self.anchval.set(0)
333        return radio
334
335    def _NewRadioButton(self,parent,text,state=Tk.NORMAL,variable=None,value=None,side=Tk.LEFT):
336        rb = Tk.Radiobutton(master=parent,text=text,state=state,
337                          variable=variable,value=value)
338        rb.pack(side=side)
339        return rb
340
341    def _enable_radio(self):
342        """Enable 'panel' and 'data' radio button"""
343        self.rAxis.config(state=Tk.NORMAL)
344        self.rData.config(state=Tk.NORMAL)
345        #self.rFig.config(state=Tk.NORMAL)
346        self.rFig.select()
347
348    def _reset_radio(self):
349        """Disable 'panel' and 'data' radio button"""
350        self.rAxis.config(state=Tk.DISABLED)
351        self.rData.config(state=Tk.DISABLED)
352        self.rFig.config(state=Tk.NORMAL)
353        self.rFig.select()
354
355    def _select_radio(self,selection):
356        """Select a specified radio button"""
357        if not selection in self.anchors:
358            return
359        if selection == "data":
360            self.rData.select()
361        elif selection == "axes":
362            self.rAxis.select()
363        else:
364            self.rFig.select()
365
366    def _get_anchval(self):
367        """Returns a integer of a selected radio button"""
368        return self.anchval.get()
369
370    def _get_note(self):
371        """Returns a note string specified in the text box"""
372        return self.textbox.get("1.0",Tk.END)
373
374    def _clear_textbox(self):
375        """Clear the text box"""
376        self.textbox.delete("1.0",Tk.END)
377
378    def _set_note(self,note=None):
379        """Set a note string to the text box"""
380        self._clear_textbox()
381        if len(note) >0:
382            self.textbox.insert("1.0",note)
383
384    def _ActionButtons(self,parent=None):
385        actbuts = Tk.Frame(master=parent)
386        bCancel = self._NewButton(actbuts,"cancel",self._cancel_text,side=Tk.LEFT)
387        bPrint = self._NewButton(actbuts,"print", self._print_text,side=Tk.LEFT)
388        return actbuts
389
390    def _NewButton(self, parent, text, command, side=Tk.LEFT):
391        if(os.uname()[0] == 'Darwin'):
392            b = Tk.Button(master=parent, text=text, command=command)
393        else:
394            b = Tk.Button(master=parent, text=text, padx=2, pady=2, command=command)
395        b.pack(side=side)
396        return b
397
398    def _cancel_text(self):
399        """
400        Cancel adding/modifying a note and close notaion window.
401        called when 'cancel' is selected.
402        """
403        self.close_textwindow()
404
405    def _print_text(self):
406        """
407        Add/Modify a note. Called when 'print' is selected on the
408        notation window.
409        """
410        self.print_text()
411        self.close_textwindow()
412
413    def load_textwindow(self,event):
414        """
415        Load text window at a event position to add a note on a plot.
416        Parameter:
417            event:   an even object to specify the position to load
418                     text window.
419        """
420        self.close_modmenu()
421        if event.canvas._tkcanvas != self.parent:
422            raise RuntimeError, "Got invalid event!"
423       
424        self.event = event
425        is_ax = (event.inaxes != None)
426        (xpix, ypix) = self._disppix2screen(event.x, event.y)
427        offset = 5
428        self.show_textwindow(xpix+offset,ypix+offset,enableaxes=is_ax)
429       
430    def show_textwindow(self,xpix,ypix,basetext=None,enableaxes=False):
431        """
432        Load text window at a position of screen to add a note on a plot.
433        Parameters:
434            xpix, ypix:   a pixel position from Upper-left corner
435                          of the screen.
436            basetext:     None (default) or any string.
437                          A string to be printed on text box when loaded.
438            enable axes:  False (default) or True.
439                          If True, 'panel' & 'data' radio button is enabled.
440        """
441        if not self.textwin: return
442        self._reset_radio()
443        if enableaxes:
444            self._enable_radio()
445        self.textwin.deiconify()
446        (w,h) = self.textwin.minsize()
447        if w*h <= 1:
448            self.textwin.minsize(width=self.textwin.winfo_width(),
449                                 height=self.textwin.winfo_height())
450            (w,h) = self.textwin.minsize()
451        self.textwin.lift()
452        self.textwin.geometry("%sx%s+%s+%s"%(w,h,xpix,ypix))
453
454    def close_textwindow(self):
455        """Close text window."""
456        self.seltext = {}
457        self._reset_radio()
458        self._clear_textbox()
459        self.textwin.withdraw()
460
461
462    ### Modify/Delete menu widget
463    def _create_modmenu(self,master=None):
464        """Create modify/delete menu widget"""
465        if master:
466            self.parent = master
467        if not self.parent:
468            return False
469        menu = Tk.Menu(master=self.parent,tearoff=False)
470        menu.add_command(label="Modify",command=self._modify_note)
471        menu.add_command(label="Delete",command=self._delnote_dialog)
472        return menu
473
474    def load_modmenu(self,event):
475        """
476        Load cascade menu at a event position to modify or delete
477        selected text.
478        Parameter:
479            event:  an even object to specify the position to load
480                    text window.
481        """
482        self.close_textwindow()
483        self.seltext = self._get_selected_text(event)
484        if len(self.seltext) == 3:
485            tkcanvas = event.canvas._tkcanvas
486            xpos = tkcanvas.winfo_rootx() + int(event.x)
487            ypos = tkcanvas.winfo_rooty() \
488                   + tkcanvas.winfo_height() - int(event.y)
489            self.menu.post(xpos,ypos)
490
491    def close_modmenu(self):
492        """Close cascade menu."""
493        self.seltext = {}
494        self.menu.unpost()
495
496    ### load text window for modification
497    def _modify_note(self):
498        """helper function to load text window to modify selected note"""
499        #print "Modify selected!!"
500        textobj = self.seltext['textobj']
501        (xtx, ytx) = textobj._get_xy_display()
502        is_ax = (self.seltext['anchor'] != 'figure')
503        if not is_ax:
504            # previous anchor is figure
505            pos = textobj.get_position()
506            is_ax = (self._get_axes_from_pos(pos,self.canvas) != None)
507
508        (xpix, ypix) = self._disppix2screen(xtx,ytx)
509        offset = int(textobj.get_size())*2
510        self.show_textwindow(xpix,ypix+offset,basetext=textobj.get_text(),\
511                             enableaxes=is_ax)
512        self._select_radio(self.seltext['anchor'])
513        self._set_note(textobj.get_text())
514
515    ### close all widgets
516    def close_widgets(self):
517        """Close note window and menu"""
518        self.close_textwindow()
519        self.close_modmenu()
520
521    ### dialog to confirm deleting note
522    def _delnote_dialog(self):
523        """Load dialog to confirm deletion of the text"""
524        remind = "Delete text?\n '"+self.seltext['textobj'].get_text()+"'"
525        answer = tkMessageBox.askokcancel(parent=self.parent,title="Delete?",
526                                          message=remind,
527                                          default=tkMessageBox.CANCEL)
528        if answer:
529            self.delete_note()
530        else:
531            self.cancel_delete()
532
533    ### helper functions
534    def _disppix2screen(self,xpixd,ypixd):
535        """
536        helper function to calculate a pixel position form Upper-left
537        corner of the SCREEN from a pixel position (xpixd, ypixd)
538        from Lower-left of the CANVAS (which, e.g., event.x/y returns)
539
540        Returns:
541            (x, y):  pixel position from Upper-left corner of the SCREEN.
542        """
543        xpixs = self.parent.winfo_rootx() + xpixd
544        ypixs = self.parent.winfo_rooty() + self.parent.winfo_height() \
545               - ypixd
546        return (int(xpixs), int(ypixs))
547       
Note: See TracBrowser for help on using the repository browser.