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