source: trunk/python/notationwindow.py @ 1889

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

New Development: No

JIRA Issue: Yes (CAS-1801)

Ready for Test: Yes

Interface Changes: No

Test Programs:

Put in Release Notes: No

Module(s): asap plotter

Description: an osx gui handling


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                          relief='sunken',borderwidth=3)
317        return textbox
318
319    def _AnchorRadio(self,parent=None):
320        radio = Tk.LabelFrame(master=parent,text="anchor",
321                            labelanchor="nw",padx=5,pady=3)
322        self.anchval = Tk.IntVar(master=radio,value=0)
323        self.rFig = self._NewRadioButton(radio,"figure",state=Tk.NORMAL,
324                                         variable=self.anchval,value=0,
325                                         side=Tk.LEFT)
326        self.rAxis = self._NewRadioButton(radio,"panel",state=Tk.DISABLED,
327                                          variable=self.anchval,value=1,
328                                          side=Tk.LEFT)
329        self.rData = self._NewRadioButton(radio,"data",state=Tk.DISABLED,
330                                          variable=self.anchval,value=2,
331                                          side=Tk.LEFT)
332        # set initial selection "figure"
333        self.anchval.set(0)
334        return radio
335
336    def _NewRadioButton(self,parent,text,state=Tk.NORMAL,variable=None,value=None,side=Tk.LEFT):
337        rb = Tk.Radiobutton(master=parent,text=text,state=state,
338                          variable=variable,value=value)
339        rb.pack(side=side)
340        return rb
341
342    def _enable_radio(self):
343        """Enable 'panel' and 'data' radio button"""
344        self.rAxis.config(state=Tk.NORMAL)
345        self.rData.config(state=Tk.NORMAL)
346        #self.rFig.config(state=Tk.NORMAL)
347        self.rFig.select()
348
349    def _reset_radio(self):
350        """Disable 'panel' and 'data' radio button"""
351        self.rAxis.config(state=Tk.DISABLED)
352        self.rData.config(state=Tk.DISABLED)
353        self.rFig.config(state=Tk.NORMAL)
354        self.rFig.select()
355
356    def _select_radio(self,selection):
357        """Select a specified radio button"""
358        if not selection in self.anchors:
359            return
360        if selection == "data":
361            self.rData.select()
362        elif selection == "axes":
363            self.rAxis.select()
364        else:
365            self.rFig.select()
366
367    def _get_anchval(self):
368        """Returns a integer of a selected radio button"""
369        return self.anchval.get()
370
371    def _get_note(self):
372        """Returns a note string specified in the text box"""
373        return self.textbox.get("1.0",Tk.END)
374
375    def _clear_textbox(self):
376        """Clear the text box"""
377        self.textbox.delete("1.0",Tk.END)
378
379    def _set_note(self,note=None):
380        """Set a note string to the text box"""
381        self._clear_textbox()
382        if len(note) >0:
383            self.textbox.insert("1.0",note)
384
385    def _ActionButtons(self,parent=None):
386        actbuts = Tk.Frame(master=parent)
387        bCancel = self._NewButton(actbuts,"cancel",self._cancel_text,side=Tk.LEFT)
388        bPrint = self._NewButton(actbuts,"print", self._print_text,side=Tk.LEFT)
389        return actbuts
390
391    def _NewButton(self, parent, text, command, side=Tk.LEFT):
392        if(os.uname()[0] == 'Darwin'):
393            b = Tk.Button(master=parent, text=text, command=command)
394        else:
395            b = Tk.Button(master=parent, text=text, padx=2, pady=2, command=command)
396        b.pack(side=side)
397        return b
398
399    def _cancel_text(self):
400        """
401        Cancel adding/modifying a note and close notaion window.
402        called when 'cancel' is selected.
403        """
404        self.close_textwindow()
405
406    def _print_text(self):
407        """
408        Add/Modify a note. Called when 'print' is selected on the
409        notation window.
410        """
411        self.print_text()
412        self.close_textwindow()
413
414    def load_textwindow(self,event):
415        """
416        Load text window at a event position to add a note on a plot.
417        Parameter:
418            event:   an even object to specify the position to load
419                     text window.
420        """
421        self.close_modmenu()
422        if event.canvas._tkcanvas != self.parent:
423            raise RuntimeError, "Got invalid event!"
424       
425        self.event = event
426        is_ax = (event.inaxes != None)
427        (xpix, ypix) = self._disppix2screen(event.x, event.y)
428        offset = 5
429        self.show_textwindow(xpix+offset,ypix+offset,enableaxes=is_ax)
430       
431    def show_textwindow(self,xpix,ypix,basetext=None,enableaxes=False):
432        """
433        Load text window at a position of screen to add a note on a plot.
434        Parameters:
435            xpix, ypix:   a pixel position from Upper-left corner
436                          of the screen.
437            basetext:     None (default) or any string.
438                          A string to be printed on text box when loaded.
439            enable axes:  False (default) or True.
440                          If True, 'panel' & 'data' radio button is enabled.
441        """
442        if not self.textwin: return
443        self._reset_radio()
444        if enableaxes:
445            self._enable_radio()
446        self.textwin.deiconify()
447        (w,h) = self.textwin.minsize()
448        if w*h <= 1:
449            self.textwin.minsize(width=self.textwin.winfo_width(),
450                                 height=self.textwin.winfo_height())
451            (w,h) = self.textwin.minsize()
452        self.textwin.geometry("%sx%s+%s+%s"%(w,h,xpix,ypix))
453        self.textwin.lift()
454
455    def close_textwindow(self):
456        """Close text window."""
457        self.seltext = {}
458        self._reset_radio()
459        self._clear_textbox()
460        self.textwin.withdraw()
461
462
463    ### Modify/Delete menu widget
464    def _create_modmenu(self,master=None):
465        """Create modify/delete menu widget"""
466        if master:
467            self.parent = master
468        if not self.parent:
469            return False
470        menu = Tk.Menu(master=self.parent,tearoff=False)
471        menu.add_command(label="Modify",command=self._modify_note)
472        menu.add_command(label="Delete",command=self._delnote_dialog)
473        return menu
474
475    def load_modmenu(self,event):
476        """
477        Load cascade menu at a event position to modify or delete
478        selected text.
479        Parameter:
480            event:  an even object to specify the position to load
481                    text window.
482        """
483        self.close_textwindow()
484        self.seltext = self._get_selected_text(event)
485        if len(self.seltext) == 3:
486            tkcanvas = event.canvas._tkcanvas
487            xpos = tkcanvas.winfo_rootx() + int(event.x)
488            ypos = tkcanvas.winfo_rooty() \
489                   + tkcanvas.winfo_height() - int(event.y)
490            self.menu.post(xpos,ypos)
491
492    def close_modmenu(self):
493        """Close cascade menu."""
494        self.seltext = {}
495        self.menu.unpost()
496
497    ### load text window for modification
498    def _modify_note(self):
499        """helper function to load text window to modify selected note"""
500        #print "Modify selected!!"
501        textobj = self.seltext['textobj']
502        (xtx, ytx) = textobj._get_xy_display()
503        is_ax = (self.seltext['anchor'] != 'figure')
504        if not is_ax:
505            # previous anchor is figure
506            pos = textobj.get_position()
507            is_ax = (self._get_axes_from_pos(pos,self.canvas) != None)
508
509        (xpix, ypix) = self._disppix2screen(xtx,ytx)
510        offset = int(textobj.get_size())*2
511        self.show_textwindow(xpix,ypix+offset,basetext=textobj.get_text(),\
512                             enableaxes=is_ax)
513        self._select_radio(self.seltext['anchor'])
514        self._set_note(textobj.get_text())
515
516    ### close all widgets
517    def close_widgets(self):
518        """Close note window and menu"""
519        self.close_textwindow()
520        self.close_modmenu()
521
522    ### dialog to confirm deleting note
523    def _delnote_dialog(self):
524        """Load dialog to confirm deletion of the text"""
525        remind = "Delete text?\n '"+self.seltext['textobj'].get_text()+"'"
526        answer = tkMessageBox.askokcancel(parent=self.parent,title="Delete?",
527                                          message=remind,
528                                          default=tkMessageBox.CANCEL)
529        if answer:
530            self.delete_note()
531        else:
532            self.cancel_delete()
533
534    ### helper functions
535    def _disppix2screen(self,xpixd,ypixd):
536        """
537        helper function to calculate a pixel position form Upper-left
538        corner of the SCREEN from a pixel position (xpixd, ypixd)
539        from Lower-left of the CANVAS (which, e.g., event.x/y returns)
540
541        Returns:
542            (x, y):  pixel position from Upper-left corner of the SCREEN.
543        """
544        xpixs = self.parent.winfo_rootx() + xpixd
545        ypixs = self.parent.winfo_rooty() + self.parent.winfo_height() \
546               - ypixd
547        return (int(xpixs), int(ypixs))
548       
Note: See TracBrowser for help on using the repository browser.