Package AccessEngine :: Package AEState :: Module Setting
[hide private]
[frames] | no frames]

Source Code for Module AccessEngine.AEState.Setting

  1  ''' 
  2  Defines a class representing a configurable setting in a L{AEState} object. 
  3   
  4  @author: Peter Parente 
  5  @author: Eitan Isaacson 
  6  @organization: IBM Corporation 
  7  @copyright: Copyright (c) 2005, 2007 IBM Corporation 
  8  @license: The BSD License 
  9   
 10  All rights reserved. This program and the accompanying materials are made 
 11  available under the terms of the BSD license which accompanies 
 12  this distribution, and is available at 
 13  U{http://www.opensource.org/licenses/bsd-license.php} 
 14  ''' 
 15   
 16  import weakref, new, copy 
 17  import os.path 
 18   
19 -class Proxy(object):
20 ''' 21 Our own proxy object which enables weak references to bound and unbound 22 methods and arbitrary callables. Pulls information about the function, 23 class, and instance out of a bound method. Stores a weak reference to the 24 instance to support garbage collection. 25 '''
26 - def __init__(self, cb):
27 try: 28 try: 29 self.inst = weakref.ref(cb.im_self) 30 except TypeError: 31 self.inst = None 32 self.func = cb.im_func 33 self.klass = cb.im_class 34 except AttributeError: 35 self.inst = None 36 self.func = cb.im_func 37 self.klass = None
38
39 - def __call__(self, *args, **kwargs):
40 ''' 41 Proxy for a call to the weak referenced object. Take arbitrary params to 42 pass to the callable. 43 44 @raise ReferenceError: When the weak reference refers to a dead object 45 ''' 46 if self.inst is not None and self.inst() is None: 47 raise ReferenceError 48 elif self.inst is not None: 49 # build a new instance method with a strong reference to the instance 50 mtd = new.instancemethod(self.func, self.inst(), self.klass) 51 else: 52 # not a bound method, just return the func 53 mtd = self.func 54 # invoke the callable and return the result 55 return mtd(*args, **kwargs)
56
57 - def __eq__(self, other):
58 ''' 59 Compare the held function and instance with that held by another proxy. 60 61 @param other: Another proxy object 62 @type other: L{Proxy} 63 @return: Whether this func/inst pair is equal to the one in the other proxy 64 object or not 65 @rtype: boolean 66 ''' 67 try: 68 return self.func == other.func and self.inst() == other.inst() 69 except Exception: 70 return False
71
72 - def __ne__(self, other):
73 ''' 74 Inverse of __eq__. 75 ''' 76 return not self.__eq__(other)
77
78 -class RawRangeView(object):
79 ''' 80 Exposes the raw value, min, and max properties of a L{NumericSetting}. 81 '''
82 - def __init__(self, setting):
83 self.setting = setting
84
85 - def _getValue(self):
86 return self.setting.value
87
88 - def _setValue(self, val):
89 self.setting.value = val
90 91 min = property(lambda self: self.setting.min) 92 max = property(lambda self: self.setting.max) 93 precision = property(lambda self: self.setting.precision) 94 value = property(_getValue, _setValue)
95
96 -class PercentRangeView(RawRangeView):
97 ''' 98 Exposes the percent value, min, and max of a L{PercentRangeSetting} 99 '''
100 - def _setPercent(self, val):
101 ''' 102 Converts the given percentage to a raw value and stores it. 103 104 @param val: New percent to store 105 @type val: integer 106 @raise TypeError: When the value cannot be converted to a integer 107 ''' 108 # convert percent to value 109 val = (val/100.0 * (self.setting.max - self.setting.min))+self.setting.min 110 self.setting.value = val
111
112 - def _getPercent(self):
113 ''' 114 Gets the current value as a percentage. 115 116 @return: Current value as % 117 @rtype: integer 118 ''' 119 # converts value to percent 120 val = self.setting.value 121 return int(round(val/float(self.setting.max - self.setting.min)*100, 0))
122 123 precision = property(lambda self: 0) 124 min = property(lambda self: 0) 125 max = property(lambda self: 100) 126 value = property(_getPercent, _setPercent)
127
128 -class RelRawRangeView(RawRangeView):
129 ''' 130 Exposes the raw value, min, and max properties of a L{RelNumericSetting}. 131 '''
132 - def _getMin(self):
133 if self.setting.parent is None: 134 return self.setting.min 135 else: 136 return self.setting.rel_min
137
138 - def _getMax(self):
139 if self.setting.parent is None: 140 return self.setting.max 141 else: 142 return self.setting.rel_max
143 144 min = property(_getMin) 145 max = property(_getMax)
146
147 -class RelPercentRangeView(PercentRangeView):
148 ''' 149 Exposes the percent value, min, and max of a L{RelPercentRangeSetting}. 150 '''
151 - def _getMin(self):
152 if self.setting.parent is None: 153 return 0 154 else: 155 return self.setting.rel_min
156
157 - def _getMax(self):
158 if self.setting.parent is None: 159 return 100 160 else: 161 return self.setting.rel_max
162 163 min = property(_getMin) 164 max = property(_getMax)
165
166 -class Group(list):
167 ''' 168 Named collection of related L{Setting}s. 169 170 Both the name and the state of the group should be considered public 171 readable. 172 173 @ivar state: Object whose properties are contained within this group 174 @type state: L{AEState.AEState} 175 @ivar name: Name of the group 176 @type name: string 177 '''
178 - def __init__(self, state, name=None):
179 ''' 180 Stores the L{Group} state and its name. 181 182 @param state: Object whose properties are contained within this group 183 @type state: L{AEState.AEState} 184 @param name: Name of the group 185 @type name: string 186 ''' 187 self.state = state 188 self.name = name
189
190 - def __str__(self):
191 return 'Group %s: %s' % (self.name, list.__str__(self))
192
193 - def newGroup(self, name):
194 ''' 195 Adds a subgroup to this group with the given name. 196 197 @param name: Name of the new group 198 @type name: string 199 @return: The newly created group 200 @rtype: L{Group} 201 ''' 202 grp = Group(self.state, name) 203 self.append(grp) 204 return grp
205
206 - def iterSettings(self, reverse=False):
207 ''' 208 Iterates over the L{Setting}s and L{Group}s stored in this L{Group}. Yield 209 both the index of the item and the item itself. 210 211 @param reverse: Iterate in reverse direction? 212 @type reverse: boolean 213 @return: Next index and setting object on each iteration 214 @rtype: 2-tuple of (integer, L{Setting} or L{Group}) 215 ''' 216 if reverse: self.reverse() 217 for i, obj in enumerate(self): 218 if isinstance(obj, Group): 219 yield (i, obj) 220 else: 221 yield (i, self.state.getSettingObj(obj)) 222 if reverse: self.reverse()
223
224 -class Setting(object):
225 ''' 226 A wrapper around a value with a strict type in a L{AEState.AEState} object. 227 Provides a label and longer description of the value. Has a flag indicating 228 whether the value should be persisted or not. Defines methods for caching the 229 current value and restoring it later. Supports the observer pattern to notify 230 listeners of changes to the value. 231 232 All instance variables should be considered public readable except for those 233 prefixed with an underscore. The L{value} property should also be considered 234 public writable. L{value} is a property which will correctly notify observers 235 when changed. 236 237 Subclasses may define new value properties that perform conversions or do 238 other tasks. The data retrieved via the value property must always be the 239 data that should be persisted to disk for a setting. 240 241 @ivar state: Object in which this setting resides 242 @type state: weakref.proxy to L{AEState.AEState} 243 @ivar name: Name of the setting 244 @type name: string 245 @ivar default: Default value of the setting 246 @type default: object 247 @ivar label: Label of the setting 248 @type label: string 249 @ivar description: Extended description of the setting 250 @type description: string 251 @ivar persist: Should this setting value be persisted to disk? 252 @type persist: boolean 253 @ivar _value: Arbitrary protected value of this setting. 254 @type _value: object 255 @ivar _cached: Cached value from the last L{save} invocation 256 @type _cached: object 257 @ivar _observers: Callables to notify on a value change mapped to their 258 arguments 259 @type _observers: dictionary 260 '''
261 - def __init__(self, state, name, default, label, description, persist):
262 ''' 263 Initializes all instance variables. 264 ''' 265 self.state = weakref.proxy(state) 266 self.name = name 267 self.default = default 268 self.label = label 269 self.description = description 270 self.persist = persist 271 self._value = default 272 self._cached = None 273 self._observers = {} 274 275 # register the state object as a listener for value changes so it can 276 # update its dirty set 277 self.addObserver(self.state._makeDirty)
278
279 - def update(self, setting):
280 ''' 281 Updates this setting with the default value and current value of another 282 setting of the same kind. 283 284 @param setting: Setting of the same kind as this one 285 @type setting: L{Setting} 286 ''' 287 self.default = setting.default 288 self.value = setting.value
289
290 - def copy(self):
291 ''' 292 Makes a shallow copy of this object. 293 294 @return: Shallow copy of this setting 295 @rtype: L{Setting} 296 ''' 297 return copy.copy(self)
298
299 - def _getValue(self):
300 ''' 301 Gets the value. 302 303 @return: Current value of the setting 304 @rtype: object 305 ''' 306 return self._value
307
308 - def _setValue(self, val):
309 ''' 310 Sets the value and notifies all observers only if the new value is 311 different from the old. 312 313 @param val: New value to store in the setting 314 @type val: object 315 ''' 316 if val != self._value: 317 self._value = val 318 self._notify()
319 320 value = property(_getValue, _setValue) 321
322 - def _notify(self):
323 ''' 324 Notifies all observers of a change in value. If an observer is dead, as 325 indicated by a ReferenceError, removes it from the list of observers. 326 ''' 327 dead = [] 328 for ob, args in self._observers.items(): 329 try: 330 ob(self.state, self, *args) 331 except ReferenceError: 332 dead.append(ob) 333 # remove dead observer proxies 334 map(self._observers.pop, dead)
335
336 - def addObserver(self, ob, *args):
337 ''' 338 Stores a weak reference to the observer to be notified when the value 339 of this setting changes. Bound methods B{will} work as observers. 340 341 Additional arguments will be passed to the observer on notification. Weak 342 reference proxies to arguments will be used whenever possible to avoid 343 inhibiting garbage collection. 344 345 @note: Because weak references are used, a reference may be dead when it 346 is later delivered to an observer. The observer may safely ignore this 347 error as it will be caught by the L{_notify} method. Notification will 348 cease immediately as soon as the first ReferenceError is encountered. 349 @param ob: The observer 350 @type ob: callable 351 ''' 352 weak_args = [] 353 for a in args: 354 try: 355 weak_args.append(weakref.proxy(a)) 356 except TypeError: 357 weak_args.append(a) 358 self._observers[Proxy(ob)] = weak_args
359
360 - def removeObserver(self, ob):
361 ''' 362 Removes a weak reference to the observer. Pass a strong reference to the 363 observer to remove, not a weak reference. 364 365 @param ob: The observer 366 @type ob: callable 367 @raise KeyError: When the given callable is not an observer of this 368 setting 369 ''' 370 del self._observers[Proxy(ob)]
371
372 - def clearObservers(self):
373 ''' 374 Remove all observers. 375 ''' 376 self._observers = []
377
378 - def save(self):
379 ''' 380 Caches the current value for later restoration. Only one value may be 381 cached at a time. It's a box, not a stack. 382 ''' 383 self._cached = self._value
384
385 - def restore(self):
386 ''' 387 Restores the cached value. Sends notification only if the value in the 388 cache is different from the current value. 389 ''' 390 if self._cached is not None: 391 if self._value != self._cached: 392 # this will send notification only if we're not restoring the same 393 # value as the current value 394 self.value = self._cached 395 self.cached = None
396
397 - def serialize(self):
398 ''' 399 Gets the current L{_value} as the data to serialize. This default 400 implementation bypasses the property fget method. 401 402 @return: Data value to serialize 403 @rtype: object 404 ''' 405 return self._value
406
407 - def unserialize(self, val):
408 ''' 409 Sets the current L{_value} from data that was unserialized. This default 410 implementation bypasses the property fset method. 411 412 @param val: Data value to store 413 @type val: object 414 ''' 415 self._value = val
416
417 -class BoolSetting(Setting):
418 '''Represents a boolean setting.'''
419 - def _setValue(self, val):
420 ''' 421 Stores the given value as a boolean. 422 423 @param val: New value 424 @type val: boolean 425 @raise TypeError: When the value cannot be converted to a boolean 426 ''' 427 Setting._setValue(self, bool(val))
428 429 value = property(Setting._getValue, _setValue)
430
431 -class StringSetting(Setting):
432 '''Represents a string setting.'''
433 - def _setValue(self, val):
434 ''' 435 Stores the given value as a string. 436 437 @param val: New value 438 @type val: string 439 @raise TypeError: When the value cannot be converted to a string 440 ''' 441 Setting._setValue(self, str(val))
442 443 value = property(Setting._getValue, _setValue)
444
445 -class FilenameSetting(StringSetting):
446 ''' 447 Represents a string filename setting. 448 449 @ivar relative_path: Absolute path to which '.' or a filename with no path 450 refers (i.e. the root of the starting filename path) 451 @type relative_path: string 452 '''
453 - def __init__(self, state, name, default, label, description, persist, 454 relative_path):
455 Setting.__init__(self, state, name, default, label, description, persist) 456 self.relative_path = relative_path
457
458 - def update(self, setting):
459 ''' 460 Updates the relative path in addition to everything done in the parent 461 class method. 462 463 @param setting: Setting of the same kind as this one 464 @type setting: L{FilenameSetting} 465 ''' 466 super(FilenameSetting, self).update(setting) 467 self.relative_path = setting.relative_path
468
469 - def _getValue(self):
470 ''' 471 Gets the filename as a string. 472 473 @return: Current filename value 474 @rtype: string 475 ''' 476 val = Setting._getValue(self) 477 if not os.path.isfile(val): 478 val = os.path.join(self.relative_path, os.path.basename(val)) 479 return val
480
481 - def _setValue(self, val):
482 ''' 483 Stores the given value as a string. 484 485 @param val: New value 486 @type val: string 487 @raise TypeError: When the value cannot be converted to a string 488 ''' 489 val = str(val) 490 if os.path.dirname(val) == self.relative_path: 491 new = os.path.basename(val) 492 StringSetting._setValue(self, val)
493 494 value = property(_getValue, _setValue)
495
496 -class NumericSetting(Setting):
497 ''' 498 Represents a numeric setting where the bounds are arbitrarily chosen. 499 500 @ivar min: Minimum value in the range 501 @type min: number 502 @ivar max: Maximum value in the range 503 @type max: number 504 @ivar precision: Number of decimal places 505 @type precision: integer 506 '''
507 - def __init__(self, state, name, default, label, description, persist, min, 508 max, precision):
509 Setting.__init__(self, state, name, default, label, description, persist) 510 self.min = min 511 self.max = max 512 self.precision = precision
513
514 - def update(self, setting):
515 ''' 516 Updates the min, max, and precision in addition to everything done in the 517 parent class method. 518 519 @param setting: Setting of the same kind as this one 520 @type setting: L{NumericSetting} 521 ''' 522 super(NumericSetting, self).update(setting) 523 self.min = setting.min 524 self.max = setting.max 525 self.precision = setting.precision
526
527 - def _fixBounds(self, val, min, max, prec):
528 ''' 529 Snaps values to the specific range and precision. 530 531 @param val: Value to bound 532 @type val: numeric 533 @param min: Minimum of range 534 @type min: numeric 535 @param max: Maximum of range 536 @type max: numeric 537 @param prec: Number of decimal places of precision 538 @type prec: integer 539 @return: Number snapped to the appropriate range and rounded or truncated 540 to the proper precision 541 @rtype: numeric 542 @raise TypeError: When the value is no a numeric 543 ''' 544 # snap values to range 545 if val > max: 546 val = max 547 elif val < min: 548 val = min 549 # take precision into account 550 if prec == 0: 551 val = int(val) 552 else: 553 val = round(val, prec) 554 return val
555
556 - def _setValue(self, val):
557 ''' 558 Stores the given value as an integer or float with the given precision. 559 Uses round to store floats and int to store integers. 560 561 @param val: New value to store 562 @type val: numeric 563 @raise TypeError: When the value is not a numeric 564 ''' 565 Setting._setValue(self,self._fixBounds(val, self.min, self.max, 566 self.precision))
567 568 value = property(Setting._getValue, _setValue) 569
570 - def getView(self):
571 ''' 572 Returns an object that proxies as a view for the value, min, max, and 573 precision for this object. Having this separate view permits other ranges 574 to override those values with their own conversions (e.g. percent, relative 575 ranges). 576 577 @return: A raw view of this range 578 @rtype: L{RawRangeView} 579 ''' 580 return RawRangeView(self)
581
582 -class RangeSetting(NumericSetting):
583 ''' 584 Represents a numeric setting where the bounds are critical. 585 ''' 586 pass
587
588 -class PercentRangeSetting(RangeSetting):
589 ''' 590 Represents a numeric setting that should be scaled for presentation to a 591 user as a percentage between 0% and 100%. The value property of this setting 592 returns the raw value while the percent property returns the raw value 593 converted to a percentage. 594 '''
595 - def getView(self):
596 ''' 597 Returns a view that converts raw values to percentages. 598 599 @return: A percent range view of this range 600 @rtype: L{PercentRangeView} 601 ''' 602 return PercentRangeView(self)
603
604 -class ChoiceSetting(Setting):
605 ''' 606 Represents a one of many choice setting where the labels to be displayed to 607 a user and the values are the same. 608 609 @ivar values: Collection of choice values 610 @type values: list 611 '''
612 - def __init__(self, state, name, default, label, description, persist, 613 choices):
614 Setting.__init__(self, state, name, default, label, description, persist) 615 self.values = choices
616
617 - def addChoice(self, choice):
618 ''' 619 Add a new choice possibility. 620 621 @param choice: New option 622 @type choice: string 623 ''' 624 self.values.append(choice)
625
626 - def update(self, setting):
627 ''' 628 Updates the choices in addition to everything done in the parent class 629 method. 630 631 @param setting: Setting of the same kind as this one 632 @type setting: L{ChoiceSetting} 633 ''' 634 super(ChoiceSetting, self).update(setting) 635 self.values = setting.values
636
637 - def _setValue(self, val):
638 ''' 639 Stores the given choice value as the active selection. If the value is not 640 one of the available choices, defaults to the first choice. 641 642 @param val: New value 643 @type val: object 644 ''' 645 if val not in self.values: 646 val = self.default 647 Setting._setValue(self, val)
648 649 value = property(Setting._getValue, _setValue) 650
651 - def unserialize(self, val):
652 ''' 653 Does not bypass the fset property method so that when data is restored, all 654 invalid values are set back to the default. 655 656 @param val: Data value to store 657 @type val: object 658 ''' 659 self.value = val
660
661 -class EnumSetting(ChoiceSetting):
662 ''' 663 Represents a one of many choice setting where text labels describe an 664 arbitrary value. The labels collection is intended to be presented to the 665 user while the values collection is intended to stored values to be used 666 internally only. 667 668 @ivar labels: Collection of choice labels 669 @type labels: list 670 '''
671 - def __init__(self, state, name, default, label, description, persist, 672 choices):
673 # sort alphabetically 674 self.labels = choices.keys() 675 self.labels.sort() 676 ChoiceSetting.__init__(self, state, name, default, label, description, 677 persist, [choices[key] for key in self.labels])
678
679 - def getLabel(self, choice):
680 ''' 681 Gets the label for a given choice. 682 683 @param choice: Value for which to retrieve the label 684 @type choice: object 685 @raise ValueError: When L{choice} is not a valid value 686 ''' 687 return self.labels[self.values.index(choice)]
688
689 - def addChoice(self, label, choice):
690 ''' 691 Add a new choice possibility with the given label. 692 693 @param label: Human readable name 694 @type label: string 695 @param choice: New option 696 @type choice: object 697 ''' 698 super(EnumSetting, self).addChoice(choice) 699 self.labels.append(label)
700
701 - def update(self, setting):
702 ''' 703 Updates the labels in addition to everything done in the parent class 704 method. 705 706 @param setting: Setting of the same kind as this one 707 @type setting: L{EnumSetting} 708 ''' 709 super(EnumSetting, self).update(setting) 710 self.labels = setting.labels
711
712 -class ColorSetting(Setting):
713 ''' 714 Represents a color choice setting, with our without an alpha channel. The 715 value must be a collection with 3 or 4 values in it representing RGBA. 716 '''
717 - def _setValue(self, val):
718 ''' 719 Stores the given color and (optionally) alpha information. 720 721 @param val: New value 722 @type val: collection 723 @raise TypeError: When the value is not a 3 or 4 element collection 724 ''' 725 if len(val) != 3 and len(val) != 4: 726 raise TypeError 727 Setting._setValue(self, val)
728 729 value = property(Setting._getValue, _setValue)
730
731 -class RelNumericSetting(NumericSetting):
732 ''' 733 Defines a numeric setting that specifies a value relative to another one. If 734 the parent style is None, acts just like a regular 735 L{AEState.Setting.NumericSetting}. 736 737 The relative value is persisted to disk for settings that have an equivalent 738 in the parent style. The absolute value is persisted otherwise. 739 740 @ivar parent: Parent style of the stored style, None if no parent 741 @type parent: L{AEOutput.Style} 742 @ivar rel_min: Relative minimum based off of the true -(max - min) 743 @type rel_min: numeric 744 @ivar rel_max: Relative maximum bassed off of the true max - min 745 @type rel_max: numeric 746 '''
747 - def __init__(self, style, name, default, label, description, 748 persist, min, max, precision):
749 if style.getDefault() is not None: 750 # we have a parent style, set the default to zero, or no different from 751 # the parent 752 default = 0 753 NumericSetting.__init__(self, style, name, default, label, 754 description, persist, min, max, precision) 755 self.parent = style.getDefault() 756 self.rel_min = -(max-min) 757 self.rel_max = max-min
758
759 - def update(self, setting):
760 ''' 761 Updates the relative min and max in addition to everything done in the 762 parent class method. 763 764 @param setting: Setting of the same kind as this one 765 @type setting: L{RelNumericSetting} 766 ''' 767 super(RelNumericSetting, self).update(setting) 768 self.rel_min = setting.rel_min 769 self.rel_max = setting.rel_max
770
771 - def _getValue(self):
772 ''' 773 Gets the current absolute value. 774 775 @return: Relative value 776 @rtype: numeric 777 ''' 778 # get relative value in this setting object 779 val = NumericSetting._getValue(self) 780 if self.parent is None: 781 # return value as-is, we have no relative parent 782 return val 783 else: 784 # return relative value + parent value bound to true value range 785 val += self.parent.getSettingVal(self.name) 786 return self._fixBounds(val, self.min, self.max, self.precision)
787
788 - def _setValue(self, val):
789 ''' 790 Stores the given absolute value as an integer or float with the given 791 precision. Uses round to store floats and int to store integers. 792 793 @param val: New absolute value to store 794 @type val: numeric 795 @raise TypeError: When the value is not a numeric 796 ''' 797 if self.parent is None: 798 # store using Numeric set value method, no relative parent 799 val = NumericSetting._setValue(self, val) 800 else: 801 # store absolute value - parent value bound to the relative value range 802 val -= self.parent.getSettingVal(self.name) 803 self._value = self._fixBounds(val, self.rel_min, self.rel_max, 804 self.precision)
805 806 value = property(_getValue, _setValue) 807
808 - def getView(self):
809 ''' 810 Gets a relative range view that returns the proper min and max values 811 depending on if the value is relative or not. 812 813 @return: A relative view of this range 814 @rtype: L{RelRawRangeView} 815 ''' 816 return RelRawRangeView(self)
817
818 -class RelRangeSetting(RelNumericSetting):
819 ''' 820 Defines a range setting that specifies a value relative to another one. If 821 the parent style is None, acts just like a regular 822 L{RangeSetting}. 823 ''' 824 pass
825
826 -class RelPercentRangeSetting(RelRangeSetting):
827 ''' 828 Defines a percent setting that specifies a value relative to another one. If 829 the parent style is None, acts just like a regular 830 L{PercentRangeSetting}. 831 832 The relative percentage of this setting is additive, 833 '''
834 - def getView(self):
835 ''' 836 Gets a relative range view that returns the proper min and max values 837 depending on if the value is relative or not. 838 839 @return: A relative percent view of this range 840 @rtype: L{RelPercentRangeView} 841 ''' 842 return RelPercentRangeView(self)
843