diff --git a/direct/src/gui/DirectButton.py b/direct/src/gui/DirectButton.py index 304c0c719c..1e07c2e87e 100644 --- a/direct/src/gui/DirectButton.py +++ b/direct/src/gui/DirectButton.py @@ -1,5 +1,9 @@ from DirectGuiBase import * +IMAGE_SORT_INDEX = 10 +GEOM_SORT_INDEX = 20 +TEXT_SORT_INDEX = 30 + class DirectButton(DirectGuiBase, NodePath): def __init__(self, parent = guiTop, **kw): # Pass in a background texture, and/or a geometry object, @@ -14,39 +18,57 @@ class DirectButton(DirectGuiBase, NodePath): # - a VBase4(L,R,B,T) # - a bounding box object optiondefs = ( + # Button can have: + # A background texture ('image', None, self.setImage), + # A midground geometry item ('geom', None, self.setGeom), - ('text', '', self.setText), + # A foreground text node + ('text', None, self.setText), + # Command to be called on button click ('command', None, None), + ('extraArgs', [], None), + # Which mouse buttons can be used to click the button ('commandButtons', (1,), self.setCommandButtons), + # Buttons initial state + ('state', NORMAL, self.setState), + # Button frame characteristics ('relief', FLAT, self.setRelief), ('frameColor', (1,1,1,1), self.setFrameColor), ('borderWidth', (.1,.1), self.setBorderWidth), ('frameSize', None, self.setFrameSize), - ('pressEffect', 1, None), - ('padSX', 1.2, None), - ('padSZ', 1.1, None), - ('pos', None, None), - ('scale', None, None), - ('state', NORMAL, self.setState), + ('pad', (.25,.15), self.resetFrameSize), + # Sounds to be used for button events ('rolloverSound', None, None), ('clickSound', None, None), + # Can only be specified at time of widget contruction + # Do the text/graphics appear to move when the button is clicked + ('pressEffect', 1, INITOPT), + # Initial pos/scale of the button + ('pos', None, INITOPT), + ('scale', None, INITOPT), ) - # Update options to reflect keyword parameters - apply(DirectGuiBase.__init__, (self, optiondefs, ('text',)), kw) - # Initialize the superclass + # Merge keyword options with default options + self.defineoptions(kw, optiondefs, + dynamicGroups = ('text', 'geom', 'image')) + + # Initialize superclasses + DirectGuiBase.__init__(self) NodePath.__init__(self) # Create a button self.guiItem = PGButton() self.guiId = self.guiItem.getId() # Attach button to parent and make that self self.assign(parent.attachNewNode( self.guiItem ) ) - # Set up names + # Initialize names self.guiItem.setName(self.guiId) self.setName(self.guiId + 'NodePath') + # Get a handle on the button's hidden node paths for each state self.stateNodePath = [] for i in range(4): self.stateNodePath.append(NodePath(self.guiItem.getStateDef(i))) + # If specifed, add scaling to the pressed state to make it look + # like the button is moving when you press it if self['pressEffect']: np = self.stateNodePath[1].attachNewNode('pressEffect') np.setScale(0.98) @@ -58,33 +80,38 @@ class DirectButton(DirectGuiBase, NodePath): # For holding bounds info self.ll = Point3(0) self.ur = Point3(0) - # Call initialization functions if necessary - # To avoid doing things redundantly + # Call option initialization functions + # To avoid doing things redundantly set fInit flag self.fInit = 1 self.initialiseoptions(DirectButton) self.fInit = 0 - # Allow changes to take effect + # Now allow changes to take effect self.updateFrameStyle() if not self['frameSize']: self.setFrameSize() - # Update pose + # Update pose to initial values if self['pos']: - if type(self['pos']) == type(()): - apply(self.setPos, self['pos']) + pos = self['pos'] + # Can either be a Point3 or a tuple of 3 values + if isintance(pos, Point3): + self.setPos(pos) else: - apply(self.setPos, (self['pos'],)) + apply(self.setPos, pos) if self['scale']: - if type(self['scale']) == type(()): - apply(self.setScale, self['scale']) + scale = self['scale'] + # Can either be a Vec3 or a tuple of 3 values + if (isinstance(scale, Vec3) or + (type(scale) == types.IntType) or + (type(scale) == types.FloatType)): + self.setScale(scale) else: - apply(self.setScale, (self['scale'],)) + apply(self.setScale, self['scale']) def updateFrameStyle(self): for i in range(4): self.guiItem.setFrameStyle(i, self.frameStyle[i]) def setRelief(self, fSetStyle = 1): - print 'setting Frame' relief = self['relief'] if relief == None: for i in range(4): @@ -104,7 +131,8 @@ class DirectButton(DirectGuiBase, NodePath): self.updateFrameStyle() def resetFrameSize(self): - self.setFrameSize(fClearFrame = 1) + if not self.fInit: + self.setFrameSize(fClearFrame = 1) def setFrameSize(self, fClearFrame = 0): if self['frameSize']: @@ -121,8 +149,10 @@ class DirectButton(DirectGuiBase, NodePath): # Clear out frame before computing bounds self.stateNodePath[0].calcTightBounds(self.ll, self.ur) # Scale bounds to give a pad around graphics - bounds = (self.ll[0] * self['padSX'], self.ur[0] * self['padSX'], - self.ll[2] * self['padSZ'], self.ur[2] * self['padSZ']) + bounds = (self.ll[0] - self['pad'][0], + self.ur[0] + self['pad'][0], + self.ll[2] - self['pad'][1], + self.ur[2] + self['pad'][1]) # Restore frame style if necessary if (frameType != PGFrameStyle.TNone): self.frameStyle[0].setType(frameType) @@ -160,6 +190,7 @@ class DirectButton(DirectGuiBase, NodePath): OnscreenText.OnscreenText, (), parent = self.stateNodePath[i], text = text[i], scale = 1, + sort = TEXT_SORT_INDEX, mayChange = 1) else: self[component + '_text'] = text[i] @@ -179,6 +210,7 @@ class DirectButton(DirectGuiBase, NodePath): component, (), 'geom', OnscreenGeom.OnscreenGeom, (), parent = self.stateNodePath[i], + sort = GEOM_SORT_INDEX, geom = geom[i], scale = 1) else: self[component + '_geom'] = geom[i] @@ -206,6 +238,7 @@ class DirectButton(DirectGuiBase, NodePath): component, (), 'image', OnscreenImage.OnscreenImage, (), parent = self.stateNodePath[i], + sort = IMAGE_SORT_INDEX, image = image[i], scale = 1) else: self[component + '_image'] = image[i] @@ -239,5 +272,10 @@ class DirectButton(DirectGuiBase, NodePath): def commandFunc(self, event): if self['command']: - self['command'](event) + # Pass any extra args to command + apply(self['command'], self['extraArgs']) + def destroy(self): + DirectGuiBase.destroy(self) + # Get rid of node path + self.removeNode() diff --git a/direct/src/gui/DirectGuiBase.py b/direct/src/gui/DirectGuiBase.py index 60380c13b3..530574eeba 100644 --- a/direct/src/gui/DirectGuiBase.py +++ b/direct/src/gui/DirectGuiBase.py @@ -1,14 +1,95 @@ from DirectGuiGlobals import * +""" +Base class for all Direct Gui items. Handles composite widgets and +command line argument parsing. +""" # Symbolic constants for the indexes into an optionInfo list. _OPT_DEFAULT = 0 _OPT_VALUE = 1 _OPT_FUNCTION = 2 +""" +Code Overview: + +1 Each widget defines a set of options (optiondefs) as a list of tuples + of the form ('name', defaultValue, handler). + 'name' is the name of the option (used during construction of configure) + handler can be: None, method, or INITOPT. If a method is specified, + it will be called during widget construction (via initialiseoptions), + if the Handler is specified as an INITOPT, this is an option that can + only be set during widget construction. + +2) DirectGuiBase.defineoptions is called. defineoption creates: + + self._constructorKeywords = { keyword : [value, useFlag] } + a dictionary of the keyword options specified as part of the constructor + keywords can be of the form 'component_option', where component is + the name of a widget's component, a component group or a component alias + + self._dynamicGroups, a list of group names for which it is permissible + to specify options before components of that group are created. + If a widget is a derived class the order of execution would be: + foo.optiondefs = {} + foo.defineoptions() + fooParent() + fooParent.optiondefs = {} + fooParent.definoptions() + +3) addoptions is called. This combines options specified as keywords to + the widget constructor (stored in self._constuctorKeywords) + with the default options (stored in optiondefs). Results are stored in + self._optionInfo = { keyword: [default, current, handler] } + If a keyword is of the form 'component_option' it is left in the + self._constructorKeywords dictionary (for use by component constructors), + otherwise it is 'used', and deleted from self._constructorKeywords. + Notes: - constructor keywords override the defaults. + - derived class default values override parent class defaults + - derived class handler functions override parent class functions + +4) Superclass initialization methods are called (resulting in nested calls + to define options (see 2 above) + +5) Widget components are created via calls to self.createcomponent. + User can specify aliases and groups for each component created. + + Aliases are alternate names for components, e.g. a widget may have a + component with a name 'entryField', which itself may have a component + named 'entry', you could add an alias 'entry' for the 'entryField_entry' + These are stored in self.__componentAliases. If an alias is found, + all keyword entries which use that alias are expanded to their full + form (to avoid conversion later) + + Groups allow option specifications that apply to all members of the group. + If a widget has components: 'text1', 'text2', and 'text3' which all belong + to the 'text' group, they can be all configured with keywords of the form: + 'text_keyword' (e.g. text_font = 'comic.rgb'). A component's group + is stored as the fourth element of its entry in self.__componentInfo + + Note: the widget constructors have access to all remaining keywords in + _constructorKeywords (those not transferred to _optionInfo by + define/addoptions). If a component defines an alias that applies to + one of the keywords, that keyword is replaced with a new keyword with + the alias expanded. + + If a keyword (or substituted alias keyword) is used during creation of the + component, it is deleted from self._constructorKeywords. If a group + keyword applies to the component, that keyword is marked as used, but is + not deleted from self._constructorKeywords, in case it applies to another + component. If any constructor keywords remain at the end of component + construction (and initialisation), an error is raised. + +5) initialiseoptions is called. This method calls any option handlers to + respond to any keyword/default values, then checks to see if any keywords + are left unused. If so, an error is raised. +""" + class DirectGuiBase(PandaObject): - def __init__(self, optiondefs, dynamicGroups, **kw): + def __init__(self): # Default id of all gui object, subclasses should override this self.guiId = 'guiObject' + # List of all active hooks + self.hookDict = {} # Mapping from each megawidget option to a list of information # about the option # - default value @@ -58,14 +139,19 @@ class DirectGuiBase(PandaObject): # no components with this group have been created. # self._dynamicGroups = () - self.defineoptions(kw, optiondefs, dynamicGroups) - def defineoptions(self, keywords, optionDefs, dynamicGroups = ()): + """ defineoptions(keywords, optionDefs, dynamicGroups = {}) """ # Create options, providing the default value and the method # to call when the value is changed. If any option created by # base classes has the same name as one in , the # base class's value and function will be overriden. + # keywords is a dictionary of keyword/value pairs from the constructor + # optionDefs is a dictionary of default options for the widget + # dynamicGroups is a tuple of component groups for which you can + # specify options even though no components of this group have + # been created + # This should be called before the constructor of the base # class, so that default values defined in the derived class # override those in the base class. @@ -83,6 +169,7 @@ class DirectGuiBase(PandaObject): self.addoptions(optionDefs) def addoptions(self, optionDefs): + """ addoptions(optionDefs) - add option def to option info """ # Add additional options, providing the default value and the # method to call when the value is changed. See # "defineoptions" for more details @@ -105,12 +192,13 @@ class DirectGuiBase(PandaObject): # Overridden by keyword, use keyword value value = keywords[name][0] optionInfo[name] = [default, value, function] + # Delete it from self._constructorKeywords del keywords[name] else: # Use optionDefs value optionInfo[name] = [default, default, function] elif optionInfo[name][FUNCTION] is None: - # Override function + # Only override function if not defined by derived class optionInfo[name][FUNCTION] = function else: # This option is of the form "component_option". If this is @@ -121,11 +209,24 @@ class DirectGuiBase(PandaObject): keywords[name] = [default, 0] def initialiseoptions(self, myClass): + """ + initialiseoptions(myClass) - call all initialisation functions + to initialize widget options to default of keyword value + """ + # This is to make sure this method class is only called by + # the most specific class in the class hierarchy if self.__class__ is myClass: + # Call the configuration callback function for every option. + FUNCTION = _OPT_FUNCTION + for info in self._optionInfo.values(): + func = info[FUNCTION] + if func is not None and func is not INITOPT: + func() + + # Now check if anything is left over unusedOptions = [] keywords = self._constructorKeywords for name in keywords.keys(): - print name used = keywords[name][1] if not used: # This keyword argument has not been used. If it @@ -142,18 +243,20 @@ class DirectGuiBase(PandaObject): text = 'Unknown options "' raise KeyError, text + string.join(unusedOptions, ', ') + \ '" for ' + myClass.__name__ - - # Call the configuration callback function for every option. - FUNCTION = _OPT_FUNCTION - for info in self._optionInfo.values(): - func = info[FUNCTION] - if func is not None and func is not INITOPT: - func() def isinitoption(self, option): + """ + isinitoption(option) + Is this opition one that can only be specified at construction? + """ return self._optionInfo[option][_OPT_FUNCTION] is INITOPT def options(self): + """ + options() + Print out a list of available widget options. + Does not include subcomponent options. + """ options = [] if hasattr(self, '_optionInfo'): for option, info in self._optionInfo.items(): @@ -164,7 +267,10 @@ class DirectGuiBase(PandaObject): return options def configure(self, option=None, **kw): - # Query or configure the megawidget options. + """ + configure(option = None) + Query or configure the megawidget options. + """ # # If not empty, *kw* is a dictionary giving new # values for some of the options of this gui item @@ -232,29 +338,39 @@ class DirectGuiBase(PandaObject): index = string.find(option, '_') if index >= 0: # This option may be of the form _