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
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"
[1885]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"
[1885]207                        print msg
[1884]208
[1885]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
[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())
[1888]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,
316                          relief='sunken',borderwidth=3)
[1884]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)
[1885]322        self.anchval = Tk.IntVar(master=radio,value=0)
[1884]323        self.rFig = self._NewRadioButton(radio,"figure",state=Tk.NORMAL,
[1885]324                                         variable=self.anchval,value=0,
[1884]325                                         side=Tk.LEFT)
326        self.rAxis = self._NewRadioButton(radio,"panel",state=Tk.DISABLED,
[1885]327                                          variable=self.anchval,value=1,
[1884]328                                          side=Tk.LEFT)
329        self.rData = self._NewRadioButton(radio,"data",state=Tk.DISABLED,
[1885]330                                          variable=self.anchval,value=2,
[1884]331                                          side=Tk.LEFT)
332        # set initial selection "figure"
[1885]333        self.anchval.set(0)
[1884]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):
[1886]343        """Enable 'panel' and 'data' radio button"""
[1884]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):
[1886]350        """Disable 'panel' and 'data' radio button"""
[1884]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):
[1886]357        """Select a specified radio button"""
[1884]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
[1885]367    def _get_anchval(self):
[1886]368        """Returns a integer of a selected radio button"""
[1885]369        return self.anchval.get()
370
[1884]371    def _get_note(self):
[1886]372        """Returns a note string specified in the text box"""
[1884]373        return self.textbox.get("1.0",Tk.END)
374
375    def _clear_textbox(self):
[1886]376        """Clear the text box"""
[1884]377        self.textbox.delete("1.0",Tk.END)
378
379    def _set_note(self,note=None):
[1886]380        """Set a note string to the text box"""
[1884]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):
[1886]400        """
401        Cancel adding/modifying a note and close notaion window.
402        called when 'cancel' is selected.
403        """
[1885]404        self.close_textwindow()
[1884]405
406    def _print_text(self):
[1886]407        """
408        Add/Modify a note. Called when 'print' is selected on the
409        notation window.
410        """
[1884]411        self.print_text()
[1885]412        self.close_textwindow()
[1884]413
414    def load_textwindow(self,event):
[1885]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()
[1884]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)
[1885]428        offset = 5
[1884]429        self.show_textwindow(xpix+offset,ypix+offset,enableaxes=is_ax)
430       
431    def show_textwindow(self,xpix,ypix,basetext=None,enableaxes=False):
[1885]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        """
[1884]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()
[1889]452        self.textwin.geometry("%sx%s+%s+%s"%(w,h,xpix,ypix))
[1888]453        self.textwin.lift()
[1884]454
[1885]455    def close_textwindow(self):
456        """Close text window."""
457        self.seltext = {}
[1884]458        self._reset_radio()
459        self._clear_textbox()
460        self.textwin.withdraw()
461
[1885]462
463    ### Modify/Delete menu widget
[1884]464    def _create_modmenu(self,master=None):
[1886]465        """Create modify/delete menu widget"""
[1884]466        if master:
[1885]467            self.parent = master
[1884]468        if not self.parent:
469            return False
470        menu = Tk.Menu(master=self.parent,tearoff=False)
[1885]471        menu.add_command(label="Modify",command=self._modify_note)
472        menu.add_command(label="Delete",command=self._delnote_dialog)
[1884]473        return menu
474
[1885]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)
[1884]485        if len(self.seltext) == 3:
[1885]486            tkcanvas = event.canvas._tkcanvas
[1884]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
[1885]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"""
[1884]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
[1885]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):
[1886]524        """Load dialog to confirm deletion of the text"""
[1884]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
[1885]534    ### helper functions
[1884]535    def _disppix2screen(self,xpixd,ypixd):
[1886]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        """
[1884]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.