1# -*- coding: utf-8 -*-
2# Copyright (C) 2012, Almar Klein
3#
4# Visvis is distributed under the terms of the (new) BSD License.
5# The full license can be found in 'license.txt'.
6
7""" Module base
8
9Defines the Wibject and Wobject classes, and the Position class.
10
11"""
12
13import OpenGL.GL as gl
14import numpy as np
15
16import weakref
17
18from visvis.core import misc
19from visvis.core.misc import basestring
20from visvis.core.misc import (Transform_Base, Transform_Translate,
21                                    Transform_Scale, Transform_Rotate)
22from visvis.core import events
23from visvis.utils.pypoints import Pointset, Quaternion, is_Point
24
25
26# Define draw modes
27DRAW_NORMAL = 1     # draw normally.
28DRAW_FAST = 2       # draw like normal, but faster (while interacting)
29DRAW_SHAPE = 3      # draw the spape of the object in the given color
30DRAW_SCREEN = 4     # for wobjects to draw in screen coordinates
31
32
33class BaseObject(object):
34    """ BaseObject(parent)
35
36    The base class for wibjects and wobjects.
37    Instances of classes inherited from this class represent
38    something that can be drawn.
39
40    Wibjects and wobjects can have children and have a parent
41    (which can be None in which case they are in orphan and never
42    drawn). To change the structure, use the ".parent" property.
43    They also can be set visible/invisible using the property ".visible".
44
45    """
46
47    def __init__(self, parent):
48        # wheter or not the object has been destroyed
49        self._destroyed = False
50        # whether or not to draw the object
51        self._visible = True
52        # whether the object is currently being drawn
53        self._isbeingdrawn = False
54        # whether the object should draw its shape
55        self._hitTest = False
56        # the parent object
57        self._parent = None
58        # the children of this object
59        self._children = []
60        # the id of this object (can change on every draw)
61        # used to determine over which object the mouse is.
62        self._id = 0
63        # a variable to indicate whether the mouse was pressed down here
64        self._mousePressedDown = False
65
66        # set parent
67        self.parent = parent
68
69        # create events
70        self._eventMouseDown = events.EventMouseDown(self)
71        self._eventMouseUp = events.EventMouseUp(self)
72        self._eventDoubleClick = events.EventDoubleClick(self)
73        self._eventEnter = events.EventEnter(self)
74        self._eventLeave = events.EventLeave(self)
75        #
76        self._eventMotion = events.EventMotion(self)
77        self._eventScroll = events.EventScroll(self)
78        self._eventKeyDown = events.EventKeyDown(self)
79        self._eventKeyUp = events.EventKeyUp(self)
80
81
82    @property
83    def eventMouseDown(self):
84        """ Fired when the mouse is pressed down on this object. (Also
85        fired the first click of a double click.)
86        """
87        return self._eventMouseDown
88    @property
89    def eventMouseUp(self):
90        """ Fired when the mouse is released after having been clicked down
91        on this object (even if the mouse is now not over the object). (Also
92        fired on the first click of a double click.)
93        """
94        return self._eventMouseUp
95    @property
96    def eventDoubleClick(self):
97        """ Fired when the mouse is double-clicked on this object.
98        """
99        return self._eventDoubleClick
100    @property
101    def eventEnter(self):
102        """ Fired when the mouse enters this object or one of its children.
103        """
104        return self._eventEnter
105    @property
106    def eventLeave(self):
107        """ Fired when the mouse leaves this object (and is also not over any
108        of it's children).
109        """
110        return self._eventLeave
111
112    @property
113    def eventMotion(self):
114        """ Fires when the mouse is moved over the object. Not fired when
115        the mouse is over one of its children.
116        """
117        return self._eventMotion
118
119    @property
120    def eventScroll(self):
121        """ Fires when the scroll wheel is used while over the object.
122        Not fired when the mouse is over one of its children.
123        """
124        return self._eventScroll
125
126    @property
127    def eventKeyDown(self):
128        """ Fires when the mouse is moved over the object. Not fired when
129        the mouse is over one of its children.
130        """
131        return self._eventKeyDown
132    @property
133    def eventKeyUp(self):
134        """ Fires when the mouse is moved over the object. Not fired when
135        the mouse is over one of its children.
136        """
137        return self._eventKeyUp
138
139
140    def _testWhetherShouldDrawShape(self):
141        """ Tests whether any of the events has handlers registered
142        to it. If so, this object should draw its shape.
143        This method is called by the event objects when handlers are
144        added or removed.
145        """
146        self._hitTest = False
147        for name in dir(self.__class__):
148            if name.startswith('event'):
149                event = getattr(self, name, None)
150                if event and event.hasHandlers:
151                    self._hitTest = True
152                    break
153
154
155    def _DrawTree(self, mode=DRAW_NORMAL, pickerHelper=None):
156        """ Draw the wibject/wobject and all of its children.
157        """
158
159        # are we alive
160        if self._destroyed:
161            print("Warning, cannot draw destroyed object: %s" % str(self))
162            return
163
164        # only draw if visible
165        if not self.visible:
166            return
167
168        # transform
169        gl.glPushMatrix()
170        self._Transform()
171
172        # draw self
173        self._isbeingdrawn = True
174        try:
175            if mode==DRAW_SHAPE:
176                if self._hitTest:
177                    clr = pickerHelper.GetColorFromId(self._id)
178                    self.OnDrawShape(clr)
179            elif mode==DRAW_SCREEN:
180                self.OnDrawScreen()
181            elif mode==DRAW_FAST:
182                self.OnDrawFast()
183            elif mode==DRAW_NORMAL:
184                self.OnDraw()
185            else:
186                raise Exception("Invalid mode for _DrawTree.")
187
188            # draw children
189            for item in self._children:
190                if hasattr(item, '_DrawTree'):
191                    item._DrawTree(mode,pickerHelper)
192
193        finally:
194            self._isbeingdrawn = False
195
196        # transform back
197        gl.glPopMatrix()
198
199
200    def Destroy(self, setContext=True):
201        """ Destroy()
202
203        Destroy the object.
204          * Removes itself from the parent's children
205          * Calls Destroy() on all its children
206          * Calls OnDestroyGl and OnDestroy on itself
207
208        Note1: do not overload, overload OnDestroy().
209        Note2: it's best not to reuse destroyed objects. To temporary disable
210        an object, better use "ob.parent=None", or "ob.visible=False".
211
212        """
213
214        # Post draw event
215        self.Draw()
216
217        # We must make this the current OpenGL context because OnDestroy
218        # methods of objects may want to remove textures etc.
219        # When you do not do this, you can get really weird bugs.
220        # This works nice, the children will not need to do this,
221        # as when they try, THIS object is already detached and fig is None.
222        if setContext:
223            fig = self.GetFigure()
224            if fig:
225                fig._SetCurrent()
226
227        # Destroy children. This will unwind to the leafs of the tree, and
228        # thus call OnDestroy() on childless objects only. This means the
229        # parent of all objects remain intact, which can be necessary for
230        # some objects to clean up nicely.
231        for child in self.children:
232            child.Destroy(False)
233
234        # Actual destroy
235        self.OnDestroyGl()
236        self.OnDestroy()
237        self._destroyed = True
238
239        # Leave home (using the property causes recursion)
240        if hasattr(self._parent, '_children'):
241            while self in self._parent._children:
242                self._parent._children.remove(self)
243        if hasattr(self._parent, '_wobjects'):
244            while self in self._parent._wobjects:
245                self._parent._wobjects.remove(self)
246        self._parent = None
247
248
249    def DestroyGl(self, setContext=True):
250        """ DestroyGl()
251
252        Destroy the OpenGl objects managed by this object.
253          * Calls DestroyGl() on all its children.
254          * Calls OnDestroyGl() on itself.
255
256        Note: do not overload, overload OnDestroyGl().
257
258        """
259
260        # make the right openGlcontext current
261        if setContext:
262            fig = self.GetFigure()
263            if fig:
264                fig._SetCurrent()
265
266        # let children clean up their openGl stuff
267        for child in self._children:
268            child.DestroyGl(False)
269
270        # Clean up our own bits
271        self.OnDestroyGl()
272
273
274    def __del__(self):
275        self.Destroy()
276
277
278    def _Transform(self):
279        """ Add transformations to modelview matrix such that the object
280        is displayed properly.
281        """
282        pass # implemented diffently by the wibject and wobject class
283
284
285    def OnDraw(self):
286        """ OnDraw()
287        Perform the opengl commands to draw this wibject/wobject.
288        Objects should overload this method to draw themselves.
289        """
290        pass
291
292    def OnDrawFast(self):
293        """ OnDrawFast()
294
295        Overload this to provide a faster version to draw (but
296        less pretty), which is called when the scene is zoomed/translated.
297        By default, this calls OnDraw()
298
299        """
300        self.OnDraw()
301
302    def OnDrawShape(self, color):
303        """ OnDrawShape(color)
304
305        Perform  the opengl commands to draw the shape of the object
306        in the given color.
307        If not implemented, the object cannot be picked.
308
309        """
310        pass
311
312    def OnDrawScreen(self):
313        """ OnDrawScreen()
314
315        Draw in screen coordinates. To be used for wobjects that
316        need drawing in screen coordinates (like text). Wibjects are
317        always drawn in screen coordinates (using OnDraw).
318
319        """
320        pass
321
322    def OnDestroy(self):
323        """ OnDestroy()
324
325        Overload this to clean up any resources other than the GL objects.
326
327        """
328        for att in list(self.__dict__.values()):
329            if isinstance(att, events.BaseEvent):
330                att.Unbind()
331
332
333    def OnDestroyGl(self):
334        """ OnDestroyGl()
335
336        Overload this to clean up any OpenGl resources.
337
338        """
339        pass
340
341    @misc.PropWithDraw
342    def visible():
343        """ Get/Set whether the object should be drawn or not.
344        If set to False, the hittest is also not performed.
345        """
346        def fget(self):
347            return self._visible
348        def fset(self, value):
349            self._visible = bool(value)
350        return locals()
351
352
353    @misc.Property
354    def hitTest():
355        """ Get/Set whether mouse events are generated for this object.
356        From v1.7 this property is set automatically, and need not be set
357        to receive mouse events.
358        """
359        def fget(self):
360            return self._hitTest
361        def fset(self, value):
362            self._hitTest = bool(value)
363        return locals()
364
365
366    @misc.PropWithDraw
367    def parent():
368        """ Get/Set the parent of this object. Use this to change the
369        tree structure of your visualization objects (for example move a line
370        from one axes to another).
371        """
372        def fget(self):
373            return self._parent
374        def fset(self,value):
375
376            # Post draw event at parent
377            self.Draw()
378
379            # init lists to update
380            parentChildren = None
381            if hasattr(value, '_children'):
382                parentChildren = value._children
383
384            # check if this is a valid parent
385            tmp = "Cannot change to that parent, "
386            if value is None:
387                # an object can be an orphan
388                pass
389            elif value is self:
390                # an object cannot be its own parent
391                raise TypeError(tmp+"because that is the object itself!")
392            elif isinstance(value, Wibject):
393                # some wibject parents can hold wobjects
394                if isinstance(self, Wobject):
395                    if hasattr(value, '_wobjects'):
396                        parentChildren = value._wobjects
397                    else:
398                        tmp2 = "a wobject can only have a wibject-parent "
399                        raise TypeError(tmp+tmp2+"if it can hold wobjects!")
400            elif isinstance(value, Wobject):
401                # a wobject can only hold wobjects
402                if isinstance(self, Wibject):
403                    raise TypeError(tmp+"a wibject cant have a wobject-parent!")
404            else:
405                raise TypeError(tmp+"it is not a wibject or wobject or None!")
406
407            # remove from parents childrens list
408            if hasattr(self._parent, '_children'):
409                while self in self._parent._children:
410                    self._parent._children.remove(self)
411            if hasattr(self._parent, '_wobjects'):
412                while self in self._parent._wobjects:
413                    self._parent._wobjects.remove(self)
414
415            # Should we destroy GL objects (because we are removed
416            # from an OpenGL context)?
417            figure1 = self.GetFigure()
418            figure2 = None
419            if hasattr(value, 'GetFigure'):
420                figure2 = value.GetFigure()
421            if figure1 and (figure1 is not figure2):
422                self.DestroyGl()
423
424            # set and add to new parent
425            self._parent = value
426            if parentChildren is not None:
427                parentChildren.append(self)
428
429        return locals()
430
431
432    @property
433    def children(self):
434        """ Get a shallow copy of the list of children.
435        """
436        return [child for child in self._children]
437
438
439    def GetFigure(self):
440        """ GetFigure()
441
442        Get the figure that this object is part of.
443        The figure represents the OpenGL context.
444        Returns None if it has no figure.
445
446        """
447        # init
448        iter = 0
449        object = self
450        # search
451        while hasattr(object,'parent'):
452            iter +=1
453            if object.parent is None:
454                break
455            if iter > 100:
456                break
457            object = object.parent
458        # check
459        if object.parent is None and hasattr(object, '_SwapBuffers'):
460            return object
461        else:
462            return None
463
464
465    def Draw(self, fast=False):
466        """ Draw(fast=False)
467
468        For wibjects: calls Draw() on the figure that contains this object.
469        For wobjects: calls Draw() on the axes that contains this object.
470
471        """
472        if self._isbeingdrawn:
473            return False
474        else:
475            fig = self.GetFigure()
476            if fig:
477                fig.Draw()
478            return True
479
480
481    def FindObjects(self, spec):
482        """ FindObjects(pattern)
483
484        Find the objects in this objects' children, and its childrens
485        children, etc, that correspond to the given pattern.
486
487        The pattern can be a class or tuple of classes, an attribute name
488        (as a string) that the objects should have, or a callable that
489        returns True or False given an object. For example
490        'lambda x: ininstance(x, cls)' will do the same as giving a class.
491
492        If 'self' is a wibject and has a _wobject property (like the Axes
493        wibject) this method also performs the search in the list of wobjects.
494
495        """
496
497
498        # Parse input
499        if hasattr(spec, 'func_name'):
500            callback = spec
501        elif isinstance(spec, (type, tuple)):
502            callback = lambda x: isinstance(x, spec)
503        elif isinstance(spec, basestring):
504            callback = lambda x: hasattr(x, spec)
505        elif hasattr(spec, '__call__'):
506            callback = spec # other callable
507        else:
508            raise ValueError('Invalid argument for FindObjects')
509
510        # Init list with result
511        result = []
512
513        # Try all children recursively
514        for child in self._children:
515            if callback(child):
516                result.append(child)
517            result.extend( child.FindObjects(callback) )
518        if hasattr(self, '_wobjects'):
519            for child in self._wobjects:
520                if callback(child):
521                    result.append(child)
522                result.extend( child.FindObjects(callback) )
523
524        # Done
525        return result
526
527
528    def GetWeakref(self):
529        """ GetWeakref()
530
531        Get a weak reference to this object.
532        Call the weakref to obtain the real reference (or None if it's dead).
533
534        """
535        return weakref.ref( self )
536
537
538class Wibject(BaseObject):
539    """ Wibject(parent)
540
541    A Wibject (widget object) is a 2D object drawn in
542    screen coordinates. A Figure is a widget and so are an Axes and a
543    PushButton. Wibjects have a position property to set their location
544    and size. They also have a background color and multiple event properties.
545
546    This class may also be used as a container object for other wibjects.
547    An instance of this class has no visual appearance. The Box class
548    implements drawing a rectangle with an edge.
549
550    """
551
552    def __init__(self, parent):
553        BaseObject.__init__(self, parent)
554
555        # the position of the widget within its parent
556        self._position = Position( 10,10,50,50, self)
557
558        # colors and edge
559        self._bgcolor = (0.8,0.8,0.8)
560
561        # event for position
562        self._eventPosition = events.EventPosition(self)
563
564
565    @property
566    def eventPosition(self):
567        """ Fired when the position (or size) of this wibject changes.
568        """
569        return self._eventPosition
570
571
572    @misc.PropWithDraw
573    def position():
574        """ Get/Set the position of this wibject. Setting can be done
575        by supplying either a 2-element tuple or list to only change
576        the location, or a 4-element tuple or list to change location
577        and size.
578
579        See the docs of the vv.base.Position class for more information.
580        """
581        def fget(self):
582            return self._position
583        def fset(self, value):
584            self._position.Set(value)
585        return locals()
586
587
588    @misc.PropWithDraw
589    def bgcolor():
590        """ Get/Set the background color of the wibject.
591        """
592        def fget(self):
593            return self._bgcolor
594        def fset(self, value):
595            self._bgcolor = misc.getColor(value, 'setting bgcolor')
596        return locals()
597
598
599    def _Transform(self):
600        """ _Transform()
601        Apply a translation such that the wibject is
602        drawn in the correct place.
603        """
604        # skip if we are on top
605        if not self.parent:
606            return
607        # get posision in screen coordinates
608        pos = self.position
609        # apply
610        gl.glTranslatef(pos.left, pos.top, 0.0)
611
612
613    def OnDrawShape(self, clr):
614        # Implementation of the OnDrawShape method.
615        gl.glColor(clr[0], clr[1], clr[2], 1.0)
616        w,h = self.position.size
617        gl.glBegin(gl.GL_POLYGON)
618        gl.glVertex2f(0,0)
619        gl.glVertex2f(0,h)
620        gl.glVertex2f(w,h)
621        gl.glVertex2f(w,0)
622        gl.glEnd()
623
624
625
626class Wobject(BaseObject):
627    """ Wobject(parent)
628
629    A Wobject (world object) is a visual element that
630    is drawn in 3D world coordinates (in the scene). Wobjects can be
631    children of other wobjects or of an Axes object (which is the
632    wibject that represents the scene).
633
634    To each wobject, several transformations can be applied,
635    which are also applied to its children. This way complex models can
636    be build. For example, in a robot arm the fingers would be children
637    of the hand, so that when the hand moves or rotates, the fingers move
638    along automatically. The fingers can then also be moved without affecting
639    the hand or other fingers.
640
641    The transformations are represented by Transform_* objects in
642    the list named "transformations". The transformations are applied
643    in the order as they appear in the list.
644
645    """
646
647    def __init__(self, parent):
648        BaseObject.__init__(self, parent)
649
650        # the transformations applied to the object
651        self._transformations = []
652
653
654    @property
655    def transformations(self):
656        """ Get the list of transformations of this wobject. These
657        can be Transform_Translate, Transform_Scale, or Transform_Rotate
658        instances.
659        """
660        return self._transformations
661
662
663    def GetAxes(self):
664        """ GetAxes()
665
666        Get the axes in which this wobject resides.
667
668        Note that this is not necesarily an Axes instance (like the line
669        objects in the Legend wibject).
670
671        """
672        par = self.parent
673        if par is None:
674            return None
675        while not isinstance(par, Wibject):
676            par = par.parent
677            if par is None:
678                return None
679        return par
680
681
682    def Draw(self, fast=False):
683        """ Draw(fast=False)
684
685        Calls Draw on the axes that contains this object.
686
687        """
688        if self._isbeingdrawn:
689            return False
690        else:
691            axes = self.GetAxes()
692            if axes:
693                axes.Draw()
694            return True
695
696
697    def _GetLimits(self, *args):
698        """ _GetLimits(self, x1, x2, y1, y2, z1, z2)
699
700        Get the limits in world coordinates between which the object
701        exists. This is used by the Axes class to set the camera correctly.
702        If None is returned, the limits are undefined.
703
704        Inheriting Wobject classes should overload this method. However, they
705        can use this method to take all transformations into account by giving
706        the cornerpoints of the untransformed object.
707
708        Returns a 3 element tuple of vv.Range instances: xlim, ylim, zlim.
709
710        """
711
712        # Examine args
713        if not args:
714            minx, maxx, miny, maxy, minz, maxz = [], [], [], [], [], []
715        elif len(args) == 6:
716            minx, maxx, miny, maxy, minz, maxz = tuple([[arg] for arg in args])
717        else:
718            raise ValueError("_Getlimits expects 0 or 6 arguments.")
719
720        # Get limits of children
721        for ob in self.children:
722            tmp = ob._GetLimits()
723            if tmp is not None:
724                limx, limy, limz = tmp
725                minx.append(limx.min); maxx.append(limx.max)
726                miny.append(limy.min); maxy.append(limy.max)
727                minz.append(limz.min); maxz.append(limz.max)
728
729        # Do we have limits?
730        if not (minx and maxx and miny and maxy and minz and maxz):
731            return None
732
733        # Take min and max
734        x1, y1, z1 = tuple([min(val) for val in [minx, miny, minz]])
735        x2, y2, z2 = tuple([max(val) for val in [maxx, maxy, maxz]])
736
737        # Make pointset of eight cornerpoints
738        pp = Pointset(3)
739        for x in [x1, x2]:
740            for y in [y1, y2]:
741                for z in [z1, z2]:
742                    pp.append(x,y,z)
743
744        # Transform these points
745        for i in range(len(pp)):
746            pp[i] = self.TransformPoint(pp[i], self)
747
748        # Return limits
749        xlim = misc.Range( pp[:,0].min(), pp[:,0].max() )
750        ylim = misc.Range( pp[:,1].min(), pp[:,1].max() )
751        zlim = misc.Range( pp[:,2].min(), pp[:,2].max() )
752        return xlim, ylim, zlim
753
754
755    def TransformPoint(self, p, baseWobject=None):
756        """ TransformPoint(p, baseWobject=None)
757
758        Transform a point in the local coordinate system of this wobject
759        to the coordinate system of the given baseWobject (which should be
760        a parent of this wobject), or to the global (Axes) coordinate
761        system if not given.
762
763        This is done by taking into account the transformations applied
764        to this wobject and its parent wobjects.
765
766        If baseWobject is the current wobject itself, only the tranformations
767        of this wobject are applied.
768
769        """
770        if not (is_Point(p) and p.ndim==3):
771            raise ValueError('TransformPoint only accepts a 3D point')
772
773        # Init wobject as itself. Next round it will be its parent, etc.
774        wobject = self
775
776        # Iterate over wobjects until we reach the Axes or None
777        while isinstance(wobject, Wobject):
778            # Iterate over all transformations
779            for t in reversed(wobject._transformations):
780                if isinstance(t, Transform_Translate):
781                    p.x += t.dx
782                    p.y += t.dy
783                    p.z += t.dz
784                elif isinstance(t, Transform_Scale):
785                    p.x *= t.sx
786                    p.y *= t.sy
787                    p.z *= t.sz
788                elif isinstance(t, Transform_Rotate):
789                    angle = float(t.angle * np.pi / 180.0)
790                    q = Quaternion.create_from_axis_angle(angle, t.ax, t.ay, t.az)
791                    p = q.rotate_point(p)
792            # Done or move to next parent?
793            if wobject is baseWobject:
794                break
795            else:
796                wobject = wobject.parent
797
798        # Done
799        return p
800
801
802    def _Transform(self):
803        """ _Transform()
804        Apply all listed transformations of this wobject.
805        """
806        for t in self.transformations:
807            if not isinstance(t, Transform_Base):
808                continue
809            elif isinstance(t, Transform_Translate):
810                gl.glTranslate(t.dx, t.dy, t.dz)
811            elif isinstance(t, Transform_Scale):
812                gl.glScale(t.sx, t.sy, t.sz)
813            elif isinstance(t, Transform_Rotate):
814                gl.glRotate(t.angle, t.ax, t.ay, t.az)
815
816
817class Position(object):
818    """ Position(x,y,w,h, wibject_instance)
819
820    The position class stores and manages the position of wibjects. Each
821    wibject has one Position instance associated with it, which can be
822    obtained (and updated) using its position property.
823
824    The position is represented using four values: x, y, w, h. The Position
825    object can also be indexed to get or set these four values.
826
827    Each element (x,y,w,h) can be either:
828      * The integer amount of pixels relative to the wibjects parent's position.
829      * The fractional amount (float value between 0.0 and 1.0) of the parent's width or height.
830
831    Each value can be negative. For x and y this simply means a negative
832    offset from the parent's left and top. For the width and height the
833    difference from the parent's full width/height is taken.
834
835    An example: a position (-10, 0.5, 150,-100), with a parent's size of
836    (500,500) is equal to (-10, 250, 150, 400) in pixels.
837
838    Remarks:
839      * fractional, integer and negative values may be mixed.
840      * x and y are considered fractional on <-1, 1>
841      * w and h are considered fractional on [-1, 1]
842      * the value 0 can always be considered to be in pixels
843
844    The position class also implements several "long-named" properties that
845    express the position in pixel coordinates. Internally a version in pixel
846    coordinates is buffered, which is kept up to date. These long-named
847    (read-only) properties are:
848    left, top, right, bottom, width, height,
849
850    Further, there are a set of properties which express the position in
851    absolute coordinates (not relative to the wibject's parent):
852    absLeft, absTop, absRight, absBottom
853
854    Finally, there are properties that return a two-element tuple:
855    topLeft, bottomRight, absTopLeft, absBottomRight, size
856
857    The method InPixels() returns a (copy) Position object which represents
858    the position in pixels.
859
860    """
861
862    def __init__(self, x, y, w, h, owner):
863
864        # test owner
865        if not isinstance(owner , Wibject):
866            raise ValueError('A positions owner can only be a wibject.')
867
868        # set
869        self._x, self._y, self._w, self._h = x, y, w, h
870
871        # store owner using a weak reference
872        self._owner = weakref.ref(owner)
873
874        # init position in pixels and absolute (as a tuples)
875        self._inpixels = None
876        self._absolute = None
877
878        # do not _update() here, beacause the owner will not have assigned
879        # this object to its _position attribute yet.
880
881        # but we can calculate our own pixels
882        self._CalculateInPixels()
883
884
885    def Copy(self):
886        """ Copy()
887
888        Make a copy of this position instance.
889
890        """
891        p = Position(self._x, self._y, self._w, self._h, self._owner())
892        p._inpixels = self._inpixels
893        p._absolute = self._absolute
894        return p
895
896
897    def InPixels(self):
898        """ InPixels()
899
900        Return a copy, but in pixel coordinates.
901
902        """
903        p = Position(self.left,self.top,self.width,self.height, self._owner())
904        p._inpixels = self._inpixels
905        p._absolute = self._absolute
906        return p
907
908
909    def __repr__(self):
910        return "<Position %1.2f, %1.2f,  %1.2f, %1.2f>" % (
911            self.x, self.y, self.w, self.h)
912
913
914    ## For keeping _inpixels up-to-date
915
916
917    def _Update(self):
918        """ _Update()
919
920        Re-obtain the position in pixels. If the obtained position
921        differs from the current position-in-pixels, _Changed()
922        is called.
923
924        """
925
926        # get old version, obtain and store new version
927        ip1 = self._inpixels + self._absolute
928        self._CalculateInPixels()
929        ip2 = self._inpixels + self._absolute
930
931        if ip2:
932            if ip1 != ip2: # also if ip1 is None
933                self._Changed()
934
935
936    def _Changed(self):
937        """ _Changed()
938
939        To be called when the position was changed.
940        Will fire the owners eventPosition and will call
941        _Update() on the position objects of all the owners
942        children.
943
944        """
945        # only notify if this is THE position of the owner (not a copy)
946        owner = self._owner()
947        if owner and owner._position is self:
948            if hasattr(owner, 'eventPosition'):
949                owner.eventPosition.Fire()
950                #print('firing position event for', owner)
951            for child in owner._children:
952                if hasattr(child, '_position'):
953                    child._position._Update()
954
955
956    def _GetFractionals(self):
957        """ Get a list which items are considered relative.
958        Also int()'s the items which are not.
959        """
960        # init
961        fractionals = [0,0,0,0]
962        # test
963        for i in range(2):
964            if self[i] > -1 and self[i] < 1 and self[i]!=0:
965                fractionals[i] = 1
966        for i in range(2,4):
967            if self[i] >= -1 and self[i] <= 1 and self[i]!=0:
968                fractionals[i] = 1
969        # return
970        return fractionals
971
972
973    def _CalculateInPixels(self):
974        """ Return the position in screen coordinates as a tuple.
975        """
976
977        # to test if this is easy
978        fractionals = self._GetFractionals()
979        negatives = [int(self[i]<0) for i in range(4)]
980
981        # get owner
982        owner = self._owner()
983
984        # if owner is a figure, it cannot have relative values
985        if hasattr(owner, '_SwapBuffers'):
986            self._inpixels = (self._x, self._y, self._w, self._h)
987            self._absolute = self._inpixels
988            return
989
990        # test if we can calculate
991        if not isinstance(owner, Wibject):
992            raise Exception("Can only calculate the position in pixels"+
993                            " if the position instance is owned by a wibject!")
994        # else, the owner must have a parent...
995        if owner.parent is None:
996            print(owner)
997            raise Exception("Can only calculate the position in pixels"+
998                            " if the owner has a parent!")
999
1000        # get width/height of parent
1001        ppos = owner.parent.position
1002        whwh = ppos.width, ppos.height
1003        whwh = (whwh[0], whwh[1], whwh[0], whwh[1])
1004
1005        # calculate!
1006        pos = [self._x, self._y, self._w, self._h]
1007        if max(fractionals)==0 and max(negatives)==0:
1008            pass # no need to calculate
1009        else:
1010            for i in range(4):
1011                if fractionals[i]:
1012                    pos[i] = pos[i]*whwh[i]
1013                if i>1 and negatives[i]:
1014                    pos[i] = whwh[i] + pos[i]
1015                # make sure it's int (even if user supplied floats > 1)
1016                pos[i] = int(pos[i])
1017
1018        # abs pos is based on the inpixels version, but x,y corrected.
1019        apos = [p for p in pos]
1020        if ppos._owner().parent:
1021            apos[0] += ppos.absLeft
1022            apos[1] += ppos.absTop
1023
1024        # store
1025        self._inpixels = tuple(pos)
1026        self._absolute = tuple(apos)
1027
1028
1029    ## For getting and setting
1030
1031    @misc.DrawAfter
1032    def Set(self, *args):
1033        """ Set(*args)
1034
1035        Set(x, y, w, h) or Set(x, y).
1036
1037        """
1038
1039        # if tuple or list was given
1040        if len(args)==1 and hasattr(args[0],'__len__'):
1041            args = args[0]
1042
1043        # apply
1044        if len(args)==2:
1045            self._x = args[0]
1046            self._y = args[1]
1047        elif len(args)==4:
1048            self._x = args[0]
1049            self._y = args[1]
1050            self._w = args[2]
1051            self._h = args[3]
1052        else:
1053            raise ValueError("Invalid number of arguments to position.Set().")
1054
1055        # we need an update now
1056        self._Update()
1057
1058
1059    @misc.DrawAfter
1060    def Correct(self, dx=0, dy=0, dw=0, dh=0):
1061        """ Correct(dx=0, dy=0, dw=0, dh=0)
1062
1063        Correct the position by suplying a delta amount of pixels.
1064        The correction is only applied if the attribute is in pixels.
1065
1066        """
1067
1068        # get fractionals
1069        fractionals = self._GetFractionals()
1070
1071        # apply correction if we can
1072        if dx and not fractionals[0]:
1073            self._x += int(dx)
1074        if dy and not fractionals[1]:
1075            self._y += int(dy)
1076        if dw and not fractionals[2]:
1077            self._w += int(dw)
1078        if dh and not fractionals[3]:
1079            self._h += int(dh)
1080
1081        # we need an update now
1082        self._Update()
1083
1084
1085    def __getitem__(self,index):
1086        if not isinstance(index,int):
1087            raise IndexError("Position only accepts single indices!")
1088        if index==0: return self._x
1089        elif index==1: return self._y
1090        elif index==2: return self._w
1091        elif index==3: return self._h
1092        else:
1093            raise IndexError("Position only accepts indices 0,1,2,3!")
1094
1095
1096    def __setitem__(self,index, value):
1097        if not isinstance(index,int):
1098            raise IndexError("Position only accepts single indices!")
1099        if index==0: self._x = value
1100        elif index==1: self._y = value
1101        elif index==2: self._w = value
1102        elif index==3: self._h = value
1103        else:
1104            raise IndexError("Position only accepts indices 0,1,2,3!")
1105        # we need an update now
1106        self._Update()
1107
1108
1109    def Draw(self):
1110        # Redraw owner
1111        owner = self._owner()
1112        if owner is not None:
1113            owner.Draw()
1114
1115
1116    @misc.PropWithDraw
1117    def x():
1118        """ Get/Set the x-element of the position. This value can be
1119        an integer value or a float expressing the x-position as a fraction
1120        of the parent's width. The value can also be negative.
1121        """
1122        def fget(self):
1123            return self._x
1124        def fset(self,value):
1125            self._x = value
1126            self._Update()
1127        return locals()
1128
1129    @misc.PropWithDraw
1130    def y():
1131        """ Get/Set the y-element of the position. This value can be
1132        an integer value or a float expressing the y-position as a fraction
1133        of the parent's height. The value can also be negative.
1134        """
1135        def fget(self):
1136            return self._y
1137        def fset(self,value):
1138            self._y = value
1139            self._Update()
1140        return locals()
1141
1142    @misc.PropWithDraw
1143    def w():
1144        """ Get/Set the w-element of the position. This value can be
1145        an integer value or a float expressing the width as a fraction
1146        of the parent's width. The value can also be negative, in which
1147        case it's subtracted from the parent's width.
1148        """
1149        def fget(self):
1150            return self._w
1151        def fset(self,value):
1152            self._w = value
1153            self._Update()
1154        return locals()
1155
1156    @misc.PropWithDraw
1157    def h():
1158        """ Get/Set the h-element of the position. This value can be
1159        an integer value or a float expressing the height as a fraction
1160        of the parent's height. The value can also be negative, in which
1161        case it's subtracted from the parent's height.
1162        """
1163        def fget(self):
1164            return self._h
1165        def fset(self,value):
1166            self._h = value
1167            self._Update()
1168        return locals()
1169
1170    ## Long names properties expressed in pixels
1171
1172    @property
1173    def left(self):
1174        """ Get the x-element of the position, expressed in pixels.
1175        """
1176        tmp = self._inpixels
1177        return tmp[0]
1178
1179    @property
1180    def top(self):
1181        """ Get the y-element of the position, expressed in pixels.
1182        """
1183        tmp = self._inpixels
1184        return tmp[1]
1185
1186    @property
1187    def width(self):
1188        """ Get the w-element of the position, expressed in pixels.
1189        """
1190        tmp = self._inpixels
1191        return tmp[2]
1192
1193    @property
1194    def height(self):
1195        """ Get the h-element of the position, expressed in pixels.
1196        """
1197        tmp = self._inpixels
1198        return tmp[3]
1199
1200    @property
1201    def right(self):
1202        """ Get left+width.
1203        """
1204        tmp = self._inpixels
1205        return tmp[0] + tmp[2]
1206
1207    @property
1208    def bottom(self):
1209        """ Get top+height.
1210         """
1211        tmp = self._inpixels
1212        return tmp[1] + tmp[3]
1213
1214    @property
1215    def topLeft(self):
1216        """ Get a tuple (left, top).
1217        """
1218        tmp = self._inpixels
1219        return tmp[0], tmp[1]
1220
1221    @property
1222    def bottomRight(self):
1223        """ Get a tuple (right, bottom).
1224        """
1225        tmp = self._inpixels
1226        return tmp[0] + tmp[2], tmp[1] + tmp[3]
1227
1228    @property
1229    def size(self):
1230        """ Get a tuple (width, height).
1231        """
1232        tmp = self._inpixels
1233        return tmp[2], tmp[3]
1234
1235    ## More long names for absolute position
1236
1237    @property
1238    def absLeft(self):
1239        """ Get the x-element of the position, expressed in absolute pixels
1240        instead of relative to the parent.
1241        """
1242        tmp = self._absolute
1243        return tmp[0]
1244
1245    @property
1246    def absTop(self):
1247        """ Get the y-element of the position, expressed in absolute pixels
1248        instead of relative to the parent.
1249        """
1250        tmp = self._absolute
1251        return tmp[1]
1252
1253    @property
1254    def absTopLeft(self):
1255        """ Get a tuple (absLeft, absTop).
1256        """
1257        tmp = self._absolute
1258        return tmp[0], tmp[1]
1259
1260    @property
1261    def absRight(self):
1262        """ Get absLeft+width.
1263        """
1264        tmp = self._absolute
1265        return tmp[0] + tmp[2]
1266
1267    @property
1268    def absBottom(self):
1269        """ Get absTop+height.
1270        """
1271        tmp = self._absolute
1272        return tmp[1] + tmp[3]
1273
1274    @property
1275    def absBottomRight(self):
1276        """ Get a tuple (right, bottom).
1277        """
1278        tmp = self._absolute
1279        return tmp[0] + tmp[2], tmp[1] + tmp[3]
1280