source: sasview/guiframe/local_perspectives/plotting/SectorSlicer.py @ 9dd261e

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 9dd261e was 4ac8556, checked in by Gervaise Alina <gervyh@…>, 16 years ago

change on data_loader

  • Property mode set to 100644
File size: 22.2 KB
RevLine 
[ef0c170]1#TODO: the line slicer should listen to all 2DREFRESH events, get the data and slice it
2#      before pushing a new 1D data update.
3
4#
5#TODO: NEED MAJOR REFACTOR
6#
7
8
9# Debug printout
10import math
[0d9dae8]11import wx
12from copy import deepcopy
13
14from BaseInteractor import _BaseInteractor
[6c0568b]15from sans.guicomm.events import NewPlotEvent, StatusEvent
16from sans.guicomm.events import SlicerParameterEvent,EVT_SLICER_PARS
[ef0c170]17
[4ac8556]18from sans.guiframe.dataFitting import Data1D
[ef0c170]19
20class SectorInteractor(_BaseInteractor):
21    """
[6c0568b]22         Draw a sector slicer.Allow to performQ averaging on data 2D
[ef0c170]23    """
24    def __init__(self,base,axes,color='black', zorder=3):
25       
26        _BaseInteractor.__init__(self, base, axes, color=color)
[6c0568b]27        ## Class initialization
[ef0c170]28        self.markers = []
[6c0568b]29        self.axes = axes   
30        ## connect the plot to event
[ef0c170]31        self.connect = self.base.connect
32       
[6c0568b]33        ## compute qmax limit to reset the graph     
34        x = math.pow(max(self.base.data2D.xmax,math.fabs(self.base.data2D.xmin)),2)
35        y = math.pow(max(self.base.data2D.ymax,math.fabs(self.base.data2D.ymin)),2)
36        self.qmax= math.sqrt(x + y)
[ef0c170]37        ## Number of points on the plot
[b783024]38        self.nbins = 20
[6c0568b]39        ## Angle of the middle line
[ef0c170]40        self.theta2= math.pi/3
[6c0568b]41        ## Absolute value of the Angle between the middle line and any side line
[ef0c170]42        self.phi=math.pi/12
[6c0568b]43       
44        ## Middle line
45        self.main_line = LineInteractor(self, self.base.subplot,color='blue', zorder=zorder, r=self.qmax,
[ef0c170]46                                           theta= self.theta2)
[2dda74ac]47        self.main_line.qmax = self.qmax
[6c0568b]48        ## Right Side line
[ef0c170]49        self.right_line= SideInteractor(self, self.base.subplot,color='black', zorder=zorder,
50                                     r=self.qmax,
51                                           phi= -1*self.phi,
52                                           theta2=self.theta2)
[2dda74ac]53        self.right_line.qmax = self.qmax
[6c0568b]54        ## Left Side line
55        self.left_line= SideInteractor(self, self.base.subplot,color='black', zorder=zorder,
[ef0c170]56                                     r=self.qmax,
57                                           phi= self.phi,
58                                           theta2=self.theta2)
[2dda74ac]59        self.left_line.qmax = self.qmax
[6c0568b]60        ## draw the sector               
[ef0c170]61        self.update()
62        self._post_data()
[6c0568b]63
64        ## Bind to slice parameter events
[1ce365f8]65        self.base.Bind(EVT_SLICER_PARS, self._onEVT_SLICER_PARS)
[ef0c170]66
[6c0568b]67
[ef0c170]68    def _onEVT_SLICER_PARS(self, event):
[6c0568b]69        """
70            receive an event containing parameters values to reset the slicer
71            @param event: event of type SlicerParameterEvent with params as
72            attribute
73        """
[18eba35]74        wx.PostEvent(self.base.parent, StatusEvent(status="SectorSlicer._onEVT_SLICER_PARS"))
[ef0c170]75        event.Skip()
76        if event.type == self.__class__.__name__:
77            self.set_params(event.params)
78            self.base.update()
79
80
81    def set_layer(self, n):
[6c0568b]82        """
83             Allow adding plot to the same panel
84             @param n: the number of layer
85        """
[ef0c170]86        self.layernum = n
87        self.update()
88       
[6c0568b]89       
[ef0c170]90    def clear(self):
[6c0568b]91        """
92            Clear the slicer and all connected events related to this slicer
93        """
[ef0c170]94        self.clear_markers()
95        self.main_line.clear()
96        self.left_line.clear()
97        self.right_line.clear()
[18eba35]98        self.base.connect.clearall()
[d468daa]99        self.base.Unbind(EVT_SLICER_PARS)
[ef0c170]100       
[6c0568b]101       
[ef0c170]102    def update(self):
103        """
[6c0568b]104            Respond to changes in the model by recalculating the profiles and
105            resetting the widgets.
[ef0c170]106        """
[6c0568b]107        # Update locations 
108        ## Check if the middle line was dragged and update the picture accordingly     
[ef0c170]109        if self.main_line.has_move:
110            self.main_line.update()
[7b73518]111            self.right_line.update( delta= -self.left_line.phi/2,
[6c0568b]112                                    mline= self.main_line.theta )
[7b73518]113            self.left_line.update( delta = self.left_line.phi/2,
[6c0568b]114                                   mline= self.main_line.theta )
115        ## Check if the left side has moved and update the slicer accordingly 
[ef0c170]116        if self.left_line.has_move:
117            self.main_line.update()
[6c0568b]118            self.left_line.update( phi=None, delta=None, mline=self.main_line ,
119                                  side=True, left=True )
120            self.right_line.update( phi= self.left_line.phi, delta= None,
121                                     mline= self.main_line, side= True,
122                                     left=False, right= True )
123        ## Check if the right side line has moved and update the slicer accordingly
[ef0c170]124        if self.right_line.has_move:
125            self.main_line.update()
[6c0568b]126            self.right_line.update( phi=None, delta=None, mline=self.main_line,
127                                   side=True, left=False, right=True )
128            self.left_line.update( phi=self.right_line.phi, delta=None,
129                                    mline=self.main_line, side=True, left=False )
130
[ef0c170]131
132    def save(self, ev):
133        """
134        Remember the roughness for this layer and the next so that we
135        can restore on Esc.
136        """
137        self.base.freeze_axes()
[6c0568b]138        self.main_line.save(ev)
139        self.right_line.save(ev)
140        self.left_line.save(ev)
[ef0c170]141
142    def _post_data(self, nbins=None):
[6c0568b]143        """
144            compute sector averaging of data2D into data1D
145            @param nbins: the number of point to plot for the average 1D data
146        """
147        ## get the data2D to average
[ef0c170]148        data = self.base.data2D
149        # If we have no data, just return
150        if data == None:
151            return
[6c0568b]152        ## Averaging
[ef0c170]153        from DataLoader.manipulations import SectorQ
[6c0568b]154        radius = self.qmax
155        phimin =  -self.left_line.phi + self.main_line.theta
156        phimax = self.left_line.phi + self.main_line.theta
157       
[1ce365f8]158        if nbins==None:
159            nbins = 20
[6c0568b]160        sect = SectorQ(r_min= 0.0, r_max= radius ,
161                        phi_min= phimin + math.pi,
162                        phi_max= phimax + math.pi, nbins=nbins)
163     
[ef0c170]164        sector = sect(self.base.data2D)
[6c0568b]165        ##Create 1D data resulting from average
[4ac8556]166       
[ef0c170]167        if hasattr(sector,"dxl"):
168            dxl= sector.dxl
169        else:
170            dxl= None
171        if hasattr(sector,"dxw"):
172            dxw= sector.dxw
173        else:
174            dxw= None
175       
[4ac8556]176        new_plot = Data1D(x=sector.x,y=sector.y,dy=sector.dy)
177        new_plot.dxl = dxl
178        new_plot.dxw = dxw
[ef0c170]179        new_plot.name = "SectorQ" +"("+ self.base.data2D.name+")"
180       
181        new_plot.source=self.base.data2D.source
[8b30e62]182        #new_plot.info=self.base.data2D.info
[ef0c170]183        new_plot.interactive = True
184        new_plot.detector =self.base.data2D.detector
185        # If the data file does not tell us what the axes are, just assume...
[e23a20c]186        new_plot.xaxis("\\rm{Q}", 'A^{-1}')
[ef0c170]187        new_plot.yaxis("\\rm{Intensity} ","cm^{-1}")
188        new_plot.group_id = "SectorQ"+self.base.data2D.name
[8b30e62]189        new_plot.id = "SectorQ"+self.base.data2D.name
[70cf5d3]190        new_plot.is_data= True
[ef0c170]191        wx.PostEvent(self.base.parent, NewPlotEvent(plot=new_plot,
[fbd0bece]192                                                 title="SectorQ"+self.base.data2D.name ))
[ef0c170]193       
194         
195       
196    def moveend(self, ev):
[6c0568b]197        """
198            Called a dragging motion ends.Get slicer event
199        """
[ef0c170]200        self.base.thaw_axes()
[6c0568b]201        ## Post parameters
[0d9dae8]202        event = SlicerParameterEvent()
[ef0c170]203        event.type = self.__class__.__name__
204        event.params = self.get_params()
[6c0568b]205        ## Send slicer paramers to plotter2D
[d468daa]206        wx.PostEvent(self.base, event)
[ef0c170]207        self._post_data()
208           
[6c0568b]209           
[ef0c170]210    def restore(self):
211        """
212        Restore the roughness for this layer.
213        """
214        self.main_line.restore()
215        self.left_line.restore()
216        self.right_line.restore()
217
218    def move(self, x, y, ev):
219        """
220        Process move to a new position, making sure that the move is allowed.
221        """
222        pass
223       
[6c0568b]224       
[ef0c170]225    def set_cursor(self, x, y):
226        pass
227       
[6c0568b]228       
[ef0c170]229    def get_params(self):
[6c0568b]230        """
231            Store a copy of values of parameters of the slicer into a dictionary.
232            @return params: the dictionary created
233        """
[ef0c170]234        params = {}
[6c0568b]235        ## Always make sure that the left and the right line are at phi
236        ## angle of the middle line
[ef0c170]237        if math.fabs(self.left_line.phi) != math.fabs(self.right_line.phi):
238            raise ValueError,"Phi left and phi right are different %f, %f"%(self.left_line.phi, self.right_line.phi)
[6c0568b]239       
240        params["Phi"] = self.main_line.theta
[1a6ec25]241        params["Delta_Phi"] = math.fabs(self.left_line.phi)
[ef0c170]242        params["nbins"] = self.nbins
243        return params
244   
[6c0568b]245   
[ef0c170]246    def set_params(self, params):
[6c0568b]247        """
248            Receive a dictionary and reset the slicer with values contained
249            in the values of the dictionary.
250            @param params: a dictionary containing name of slicer parameters and
251            values the user assigned to the slicer.
252        """
[1a6ec25]253        main = params["Phi"] 
254        phi = math.fabs(params["Delta_Phi"] )
[ef0c170]255        self.nbins = int(params["nbins"])
256        self.main_line.theta= main
[6c0568b]257        ## Reset the slicer parameters
[ef0c170]258        self.main_line.update()
[6c0568b]259        self.right_line.update( phi=phi,delta=None, mline=self.main_line,
260                               side=True, right=True )
261        self.left_line.update( phi=phi, delta=None, mline=self.main_line, side=True )
262        ## post the new corresponding data
[ef0c170]263        self._post_data(nbins=self.nbins)
264       
[6c0568b]265       
[ef0c170]266    def freeze_axes(self):
267        self.base.freeze_axes()
268       
[6c0568b]269       
[ef0c170]270    def thaw_axes(self):
271        self.base.thaw_axes()
272
[6c0568b]273
[ef0c170]274    def draw(self):
275        self.base.draw()
276
277       
278class SideInteractor(_BaseInteractor):
279    """
[6c0568b]280        Draw an oblique line
281        @param phi: the phase between the middle line and one side line
282        @param theta2: the angle between the middle line and x- axis
[ef0c170]283    """
284    def __init__(self,base,axes,color='black', zorder=5, r=1.0,phi=math.pi/4, theta2= math.pi/3):
285       
286        _BaseInteractor.__init__(self, base, axes, color=color)
[6c0568b]287        ## Initialize the class
[ef0c170]288        self.markers = []
289        self.axes = axes
[6c0568b]290        ## compute the value of the angle between the current line and
291        ## the x-axis 
[ef0c170]292        self.save_theta = theta2 + phi
[31e3298]293        self.theta= theta2 + phi
[6c0568b]294        ## the value of the middle line angle with respect to the x-axis
[ef0c170]295        self.theta2 = theta2
[6c0568b]296        ## Radius to find polar coordinates this line's endpoints
[ef0c170]297        self.radius = r
[6c0568b]298        ## phi is the phase between the current line and the middle line
[ef0c170]299        self.phi = phi
[6c0568b]300       
301        ## End points polar coordinates
[ef0c170]302        x1= self.radius*math.cos(self.theta)
303        y1= self.radius*math.sin(self.theta)
304        x2= -1*self.radius*math.cos(self.theta)
305        y2= -1*self.radius*math.sin(self.theta)
[6c0568b]306        ## defining a new marker
[7a28ba7]307        try:
[0c218d9]308            self.inner_marker = self.axes.plot([x1/2.5],[y1/2.5], linestyle='',
[7a28ba7]309                                          marker='s', markersize=10,
310                                          color=self.color, alpha=0.6,
311                                          pickradius=5, label="pick", 
312                                          zorder=zorder, # Prefer this to other lines
313                                          visible=True)[0]
314        except:
[0c218d9]315            self.inner_marker = self.axes.plot([x1/2.5],[y1/2.5], linestyle='',
[7a28ba7]316                                          marker='s', markersize=10,
317                                          color=self.color, alpha=0.6,
318                                          label="pick", 
319                                          visible=True)[0]
320            message  = "\nTHIS PROTOTYPE NEEDS THE LATEST VERSION OF MATPLOTLIB\n"
321            message += "Get the SVN version that is at least as recent as June 1, 2007"
[6c0568b]322            owner=self.base.base.parent
323            wx.PostEvent(owner, StatusEvent(status="sectorSlicer: %s"%message))
324       
325        ## Defining the current line
[ef0c170]326        self.line = self.axes.plot([x1,x2],[y1,y2],
327                                      linestyle='-', marker='',
328                                      color=self.color,
329                                      visible=True)[0]
[6c0568b]330        ## Flag to differentiate the left line from the right line motion
[7a28ba7]331        self.left_moving=False
[6c0568b]332        ## Flag to define a motion
[ef0c170]333        self.has_move=False
[6c0568b]334        ## connecting markers and draw the picture
[7a28ba7]335        self.connect_markers([self.inner_marker, self.line])
[6c0568b]336       
[ef0c170]337
338    def set_layer(self, n):
[6c0568b]339        """
340             Allow adding plot to the same panel
341             @param n: the number of layer
342        """
[ef0c170]343        self.layernum = n
344        self.update()
345       
346    def clear(self):
[6c0568b]347        """
348            Clear the slicer and all connected events related to this slicer
349        """
[ef0c170]350        self.clear_markers()
351        try:
352            self.line.remove()
[7a28ba7]353            self.inner_marker.remove()
[ef0c170]354        except:
355            # Old version of matplotlib
356            for item in range(len(self.axes.lines)):
357                del self.axes.lines[0]
358       
359       
[6c0568b]360    def update(self, phi=None, delta=None, mline=None,
361               side=False, left= False, right=False):
[ef0c170]362        """
[6c0568b]363            Draw oblique line
364            @param phi: the phase between the middle line and the current line
[7b73518]365            @param delta: phi/2 applied only when the mline was moved
[ef0c170]366        """
367        #print "update left or right ", self.has_move
[7a28ba7]368        self.left_moving=left
[31e3298]369        theta3=0
[ef0c170]370        if phi !=None:
[7a28ba7]371            self.phi= phi
[31e3298]372        if delta==None:
373            delta = 0
374
[7a28ba7]375        if  right:
376            self.phi = -1*math.fabs(self.phi)
[31e3298]377            #delta=-delta
[7a28ba7]378        else:
379            self.phi =math.fabs(self.phi)
[e23a20c]380        if side:
[ef0c170]381            self.theta=  mline.theta + self.phi
[31e3298]382                   
383        if mline!=None :
384            if delta!=0:
385                self.theta2 = mline+delta
386            else:
387                self.theta2 = mline.theta
388        if delta==0:
389            theta3=self.theta+delta
390        else:
391            theta3=self.theta2+delta
[7a28ba7]392       
[6c0568b]393     
[31e3298]394        x1= self.radius*math.cos(theta3)
395        y1= self.radius*math.sin(theta3)
396        x2= -1*self.radius*math.cos(theta3)
397        y2= -1*self.radius*math.sin(theta3)
[ef0c170]398       
[0c218d9]399        self.inner_marker.set(xdata=[x1/2.5],ydata=[y1/2.5])
[ef0c170]400        self.line.set(xdata=[x1,x2], ydata=[y1,y2]) 
401       
402       
403       
404       
405    def save(self, ev):
406        """
407        Remember the roughness for this layer and the next so that we
408        can restore on Esc.
409        """
410        self.save_theta= self.theta
411        self.base.freeze_axes()
412
413    def moveend(self, ev):
414       
415        self.has_move=False
416        self.base.moveend(ev)
417           
418    def restore(self):
419        """
420        Restore the roughness for this layer.
421        """
422        self.theta = self.save_theta
423
424    def move(self, x, y, ev):
425        """
426        Process move to a new position, making sure that the move is allowed.
427        """
428       
429        self.theta= math.atan2(y,x)
[7a28ba7]430        self.has_move=True
[0c218d9]431        #ToDo: Simplify below
[7a28ba7]432        if not self.left_moving:
[0c218d9]433            if  self.theta2-self.theta <= 0 and self.theta2>0:#>= self.theta2:
434                #print "my theta", self.theta
435                self.restore()
436                return 
437            elif self.theta2 < 0 and self.theta < 0 and self.theta-self.theta2 >= 0:
438                self.restore()
439                return                             
440            elif  self.theta2 < 0 and self.theta > 0 and self.theta2+2*math.pi-self.theta >=math.pi/2:
441                #print "my theta", self.theta
442                self.restore()
443                return 
444            elif  self.theta2 < 0 and self.theta < 0 and self.theta2-self.theta >=math.pi/2:
445                #print "my theta", self.theta
[7a28ba7]446                self.restore()
447                return 
[0c218d9]448            elif self.theta2>0 and (self.theta2-self.theta >= math.pi/2 or (self.theta2-self.theta >= math.pi/2)):#<= self.theta2 -math.pi/2:
449                #print "self theta encore"
[7a28ba7]450                self.restore()
451                return 
452        else:
[0c218d9]453            #print "left move"
454            if  self.theta < 0 and self.theta+math.pi*2-self.theta2 <= 0:
[7a28ba7]455                self.restore()
456                return 
[0c218d9]457            elif self.theta2 < 0 and self.theta-self.theta2 <= 0:
458                self.restore()
459                return                             
460            elif  self.theta > 0 and self.theta-self.theta2 <=0:
461                #print "my theta", self.theta
462                self.restore()
463                return 
464            elif self.theta-self.theta2 >= math.pi/2 or  (self.theta+math.pi*2-self.theta2 >= math.pi/2 and self.theta<0 and self.theta2>0):
465                #print "self theta encore"
[7a28ba7]466                self.restore()
467                return 
[0c218d9]468           
[e23a20c]469        self.phi= math.fabs(self.theta2 - self.theta)
[0c218d9]470        if self.phi>math.pi:
471            self.phi= 2*math.pi-math.fabs(self.theta2 - self.theta)
[ef0c170]472       
[0c218d9]473        #print "move , self.phi, self.theta,", self.theta*180/math.pi,self.theta2*180/math.pi,self.phi*180/math.pi
[7a28ba7]474       
475           
476       
[ef0c170]477        self.base.base.update()
478       
479    def set_cursor(self, x, y):
480        self.move(x, y, None)
481        self.update()
482       
483       
484    def get_params(self):
485        params = {}
486        params["radius"] = self.radius
487        params["theta"] = self.theta
488        return params
489   
490    def set_params(self, params):
491
492        x = params["radius"] 
493        self.set_cursor(x, self._inner_mouse_y)
494       
[7a28ba7]495 
[ef0c170]496       
497class LineInteractor(_BaseInteractor):
498    """
499         Select an annulus through a 2D plot
500    """
501    def __init__(self,base,axes,color='black', zorder=5, r=1.0,theta=math.pi/4):
502       
503        _BaseInteractor.__init__(self, base, axes, color=color)
504        self.markers = []
505        self.axes = axes
506       
507        self.save_theta = theta
508        self.theta= theta
509       
510        self.radius = r
511     
512        self.scale = 10.0
513     
514        #raise "Version error", message
515           
516        # Inner circle
517        x1= self.radius*math.cos(self.theta)
518        y1= self.radius*math.sin(self.theta)
519        x2= -1*self.radius*math.cos(self.theta)
520        y2= -1*self.radius*math.sin(self.theta)
[7a28ba7]521        try:
522            # Inner circle marker
[0c218d9]523            self.inner_marker = self.axes.plot([x1/2.5],[y1/2.5], linestyle='',
[7a28ba7]524                                          marker='s', markersize=10,
525                                          color=self.color, alpha=0.6,
526                                          pickradius=5, label="pick", 
527                                          zorder=zorder, # Prefer this to other lines
528                                          visible=True)[0]
529        except:
[0c218d9]530            self.inner_marker = self.axes.plot([x1/2.5],[y1/2.5], linestyle='',
[7a28ba7]531                                          marker='s', markersize=10,
532                                          color=self.color, alpha=0.6,
533                                          label="pick", 
534                                          visible=True)[0]
535            message  = "\nTHIS PROTOTYPE NEEDS THE LATEST VERSION OF MATPLOTLIB\n"
536            message += "Get the SVN version that is at least as recent as June 1, 2007"
537           
[ef0c170]538       
539        self.line = self.axes.plot([x1,x2],[y1,y2],
540                                      linestyle='-', marker='',
541                                      color=self.color,
542                                      visible=True)[0]
543     
544        self.npts = 20
545        self.has_move=False
[7a28ba7]546        self.connect_markers([self.inner_marker, self.line])
[ef0c170]547        self.update()
548
549    def set_layer(self, n):
550        self.layernum = n
551        self.update()
552       
553    def clear(self):
554        self.clear_markers()
555        try:
[7a28ba7]556            self.inner_marker.remove()
[ef0c170]557            self.line.remove()
558        except:
559            # Old version of matplotlib
560            for item in range(len(self.axes.lines)):
561                del self.axes.lines[0]
[6c0568b]562 
563 
[ef0c170]564    def update(self, theta=None):
565        """
566        Draw the new roughness on the graph.
567        """
[6c0568b]568       
[ef0c170]569        if theta !=None:
570            self.theta= theta
571        x1= self.radius*math.cos(self.theta)
572        y1= self.radius*math.sin(self.theta)
573        x2= -1*self.radius*math.cos(self.theta)
574        y2= -1*self.radius*math.sin(self.theta)
[7a28ba7]575       
[0c218d9]576        self.inner_marker.set(xdata=[x1/2.5],ydata=[y1/2.5])
[ef0c170]577        self.line.set(xdata=[x1,x2], ydata=[y1,y2]) 
578     
579       
580       
581    def save(self, ev):
582        """
583        Remember the roughness for this layer and the next so that we
584        can restore on Esc.
585        """
586        self.save_theta= self.theta
587        self.base.freeze_axes()
588
589    def moveend(self, ev):
590       
591        self.has_move=False
592        self.base.moveend(ev)
593           
594    def restore(self):
595        """
596        Restore the roughness for this layer.
597        """
598        self.theta = self.save_theta
599
600    def move(self, x, y, ev):
601        """
602        Process move to a new position, making sure that the move is allowed.
603        """
604       
605        self.theta= math.atan2(y,x)
[0c218d9]606        #print "main_line previous theta --- next theta ",math.degrees(self.save_theta),math.degrees(self.theta)
[ef0c170]607       
608        self.has_move=True
[0c218d9]609       
[ef0c170]610        self.base.base.update()
611       
612    def set_cursor(self, x, y):
613        self.move(x, y, None)
614        self.update()
615       
616       
617    def get_params(self):
618        params = {}
619        params["radius"] = self.radius
620        params["theta"] = self.theta
621        return params
622   
623    def set_params(self, params):
624
625        x = params["radius"] 
626        self.set_cursor(x, self._inner_mouse_y)
627
628
[356aea78]629       
Note: See TracBrowser for help on using the repository browser.