DirectGuiBase.py 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094
  1. """
  2. Base class for all DirectGui items. Handles composite widgets and
  3. command line argument parsing.
  4. Code overview:
  5. 1) Each widget defines a set of options (optiondefs) as a list of tuples
  6. of the form ``('name', defaultValue, handler)``.
  7. 'name' is the name of the option (used during construction of configure)
  8. handler can be: None, method, or INITOPT. If a method is specified, it
  9. will be called during widget construction (via initialiseoptions), if the
  10. Handler is specified as an INITOPT, this is an option that can only be set
  11. during widget construction.
  12. 2) :func:`~DirectGuiBase.defineoptions` is called. defineoption creates:
  13. self._constructorKeywords = { keyword: [value, useFlag] }
  14. A dictionary of the keyword options specified as part of the
  15. constructor keywords can be of the form 'component_option', where
  16. component is the name of a widget's component, a component group or a
  17. component alias.
  18. self._dynamicGroups
  19. A list of group names for which it is permissible to specify options
  20. before components of that group are created.
  21. If a widget is a derived class the order of execution would be::
  22. foo.optiondefs = {}
  23. foo.defineoptions()
  24. fooParent()
  25. fooParent.optiondefs = {}
  26. fooParent.defineoptions()
  27. 3) :func:`~DirectGuiBase.addoptions` is called. This combines options
  28. specified as keywords to the widget constructor (stored in
  29. self._constructorKeywords) with the default options (stored in optiondefs).
  30. Results are stored in
  31. ``self._optionInfo = { keyword: [default, current, handler] }``.
  32. If a keyword is of the form 'component_option' it is left in the
  33. self._constructorKeywords dictionary (for use by component constructors),
  34. otherwise it is 'used', and deleted from self._constructorKeywords.
  35. Notes:
  36. - constructor keywords override the defaults.
  37. - derived class default values override parent class defaults
  38. - derived class handler functions override parent class functions
  39. 4) Superclass initialization methods are called (resulting in nested calls
  40. to define options (see 2 above)
  41. 5) Widget components are created via calls to
  42. :func:`~DirectGuiBase.createcomponent`. User can specify aliases and groups
  43. for each component created.
  44. Aliases are alternate names for components, e.g. a widget may have a
  45. component with a name 'entryField', which itself may have a component
  46. named 'entry', you could add an alias 'entry' for the 'entryField_entry'
  47. These are stored in self.__componentAliases. If an alias is found,
  48. all keyword entries which use that alias are expanded to their full
  49. form (to avoid conversion later)
  50. Groups allow option specifications that apply to all members of the group.
  51. If a widget has components: 'text1', 'text2', and 'text3' which all belong
  52. to the 'text' group, they can be all configured with keywords of the form:
  53. 'text_keyword' (e.g. ``text_font='comic.rgb'``). A component's group
  54. is stored as the fourth element of its entry in self.__componentInfo.
  55. Note: the widget constructors have access to all remaining keywords in
  56. _constructorKeywords (those not transferred to _optionInfo by
  57. define/addoptions). If a component defines an alias that applies to
  58. one of the keywords, that keyword is replaced with a new keyword with
  59. the alias expanded.
  60. If a keyword (or substituted alias keyword) is used during creation of the
  61. component, it is deleted from self._constructorKeywords. If a group
  62. keyword applies to the component, that keyword is marked as used, but is
  63. not deleted from self._constructorKeywords, in case it applies to another
  64. component. If any constructor keywords remain at the end of component
  65. construction (and initialisation), an error is raised.
  66. 5) :func:`~DirectGuiBase.initialiseoptions` is called. This method calls any
  67. option handlers to respond to any keyword/default values, then checks to
  68. see if any keywords are left unused. If so, an error is raised.
  69. """
  70. __all__ = ['DirectGuiBase', 'DirectGuiWidget']
  71. from panda3d.core import *
  72. from direct.showbase import ShowBaseGlobal
  73. from direct.showbase.ShowBase import ShowBase
  74. from direct.showbase.MessengerGlobal import messenger
  75. from . import DirectGuiGlobals as DGG
  76. from .OnscreenText import *
  77. from .OnscreenGeom import *
  78. from .OnscreenImage import *
  79. from direct.directtools.DirectUtil import ROUND_TO
  80. from direct.showbase import DirectObject
  81. from direct.task import Task
  82. from direct.task.TaskManagerGlobal import taskMgr
  83. guiObjectCollector = PStatCollector("Client::GuiObjects")
  84. _track_gui_items = ConfigVariableBool('track-gui-items', False)
  85. class DirectGuiBase(DirectObject.DirectObject):
  86. """Base class of all DirectGUI widgets."""
  87. def __init__(self):
  88. # Default id of all gui object, subclasses should override this
  89. self.guiId = 'guiObject'
  90. # List of all post initialization functions
  91. self.postInitialiseFuncList = []
  92. # To avoid doing things redundantly during initialisation
  93. self.fInit = 1
  94. # Mapping from each megawidget option to a list of information
  95. # about the option
  96. # - default value
  97. # - current value
  98. # - function to call when the option is initialised in the
  99. # call to initialiseoptions() in the constructor or
  100. # modified via configure(). If this is INITOPT, the
  101. # option is an initialisation option (an option that can
  102. # be set by the call to the constructor but can not be
  103. # used with configure).
  104. # This mapping is not initialised here, but in the call to
  105. # defineoptions() which precedes construction of this base class.
  106. #
  107. # self._optionInfo = {}
  108. # Mapping from each component name to a tuple of information
  109. # about the component.
  110. # - component widget instance
  111. # - configure function of widget instance
  112. # - the class of the widget (Frame, EntryField, etc)
  113. # - cget function of widget instance
  114. # - the name of the component group of this component, if any
  115. self.__componentInfo = {}
  116. # Mapping from alias names to the names of components or
  117. # sub-components.
  118. self.__componentAliases = {}
  119. # Contains information about the keywords provided to the
  120. # constructor. It is a mapping from the keyword to a tuple
  121. # containing:
  122. # - value of keyword
  123. # - a boolean indicating if the keyword has been used.
  124. # A keyword is used if, during the construction of a megawidget,
  125. # - it is defined in a call to defineoptions() or addoptions(), or
  126. # - it references, by name, a component of the megawidget, or
  127. # - it references, by group, at least one component
  128. # At the end of megawidget construction, a call is made to
  129. # initialiseoptions() which reports an error if there are
  130. # unused options given to the constructor.
  131. #
  132. # self._constructorKeywords = {}
  133. # List of dynamic component groups. If a group is included in
  134. # this list, then it not an error if a keyword argument for
  135. # the group is given to the constructor or to configure(), but
  136. # no components with this group have been created.
  137. # self._dynamicGroups = ()
  138. def defineoptions(self, keywords, optionDefs, dynamicGroups = ()):
  139. """ defineoptions(keywords, optionDefs, dynamicGroups = {}) """
  140. # Create options, providing the default value and the method
  141. # to call when the value is changed. If any option created by
  142. # base classes has the same name as one in <optionDefs>, the
  143. # base class's value and function will be overriden.
  144. # keywords is a dictionary of keyword/value pairs from the constructor
  145. # optionDefs is a dictionary of default options for the widget
  146. # dynamicGroups is a tuple of component groups for which you can
  147. # specify options even though no components of this group have
  148. # been created
  149. # This should be called before the constructor of the base
  150. # class, so that default values defined in the derived class
  151. # override those in the base class.
  152. if not hasattr(self, '_constructorKeywords'):
  153. tmp = {}
  154. for option, value in keywords.items():
  155. tmp[option] = [value, 0]
  156. self._constructorKeywords = tmp
  157. self._optionInfo = {}
  158. # Initialize dictionary of dynamic groups
  159. if not hasattr(self, '_dynamicGroups'):
  160. self._dynamicGroups = ()
  161. self._dynamicGroups = self._dynamicGroups + tuple(dynamicGroups)
  162. # Reconcile command line and default options
  163. self.addoptions(optionDefs, keywords)
  164. def addoptions(self, optionDefs, optionkeywords):
  165. """ addoptions(optionDefs) - add option def to option info """
  166. # Add additional options, providing the default value and the
  167. # method to call when the value is changed. See
  168. # "defineoptions" for more details
  169. # optimisations:
  170. optionInfo = self._optionInfo
  171. optionInfo_has_key = optionInfo.__contains__
  172. keywords = self._constructorKeywords
  173. keywords_has_key = keywords.__contains__
  174. FUNCTION = DGG._OPT_FUNCTION
  175. for name, default, function in optionDefs:
  176. if '_' not in name:
  177. default = optionkeywords.get(name, default)
  178. # The option will already exist if it has been defined
  179. # in a derived class. In this case, do not override the
  180. # default value of the option or the callback function
  181. # if it is not None.
  182. if not optionInfo_has_key(name):
  183. if keywords_has_key(name):
  184. # Overridden by keyword, use keyword value
  185. value = keywords[name][0]
  186. optionInfo[name] = [default, value, function]
  187. # Delete it from self._constructorKeywords
  188. del keywords[name]
  189. else:
  190. # Use optionDefs value
  191. optionInfo[name] = [default, default, function]
  192. elif optionInfo[name][FUNCTION] is None:
  193. # Only override function if not defined by derived class
  194. optionInfo[name][FUNCTION] = function
  195. else:
  196. # This option is of the form "component_option". If this is
  197. # not already defined in self._constructorKeywords add it.
  198. # This allows a derived class to override the default value
  199. # of an option of a component of a base class.
  200. if not keywords_has_key(name):
  201. keywords[name] = [default, 0]
  202. def initialiseoptions(self, myClass):
  203. """
  204. Call all initialisation functions to initialize widget
  205. options to default of keyword value
  206. """
  207. # This is to make sure this method class is only called by
  208. # the most specific class in the class hierarchy
  209. if self.__class__ is myClass:
  210. # Call the configuration callback function for every option.
  211. FUNCTION = DGG._OPT_FUNCTION
  212. self.fInit = 1
  213. for info in self._optionInfo.values():
  214. func = info[FUNCTION]
  215. if func is not None and func is not DGG.INITOPT:
  216. func()
  217. self.fInit = 0
  218. # Now check if anything is left over
  219. unusedOptions = []
  220. keywords = self._constructorKeywords
  221. for name in keywords:
  222. used = keywords[name][1]
  223. if not used:
  224. # This keyword argument has not been used. If it
  225. # does not refer to a dynamic group, mark it as
  226. # unused.
  227. index = name.find('_')
  228. if index < 0 or name[:index] not in self._dynamicGroups:
  229. unusedOptions.append(name)
  230. self._constructorKeywords = {}
  231. if len(unusedOptions) > 0:
  232. if len(unusedOptions) == 1:
  233. text = 'Unknown option "'
  234. else:
  235. text = 'Unknown options "'
  236. raise KeyError(text + ', '.join(unusedOptions) + \
  237. '" for ' + myClass.__name__)
  238. # Can now call post init func
  239. self.postInitialiseFunc()
  240. def postInitialiseFunc(self):
  241. for func in self.postInitialiseFuncList:
  242. func()
  243. def isinitoption(self, option):
  244. """
  245. Is this opition one that can only be specified at construction?
  246. """
  247. return self._optionInfo[option][DGG._OPT_FUNCTION] is DGG.INITOPT
  248. def options(self):
  249. """
  250. Print out a list of available widget options.
  251. Does not include subcomponent options.
  252. """
  253. options = []
  254. if hasattr(self, '_optionInfo'):
  255. for option, info in self._optionInfo.items():
  256. isinit = info[DGG._OPT_FUNCTION] is DGG.INITOPT
  257. default = info[DGG._OPT_DEFAULT]
  258. options.append((option, default, isinit))
  259. options.sort()
  260. return options
  261. def configure(self, option=None, **kw):
  262. """
  263. configure(option = None)
  264. Query or configure the megawidget options.
  265. """
  266. #
  267. # If not empty, *kw* is a dictionary giving new
  268. # values for some of the options of this gui item
  269. # For options defined for this widget, set
  270. # the value of the option to the new value and call the
  271. # configuration callback function, if any.
  272. #
  273. # If *option* is None, return all gui item configuration
  274. # options and settings. Options are returned as standard 3
  275. # element tuples
  276. #
  277. # If *option* is a string, return the 3 element tuple for the
  278. # given configuration option.
  279. # First, deal with the option queries.
  280. if len(kw) == 0:
  281. # This configure call is querying the values of one or all options.
  282. # Return 3-tuples:
  283. # (optionName, default, value)
  284. if option is None:
  285. rtn = {}
  286. for option, config in self._optionInfo.items():
  287. rtn[option] = (option,
  288. config[DGG._OPT_DEFAULT],
  289. config[DGG._OPT_VALUE])
  290. return rtn
  291. else:
  292. config = self._optionInfo[option]
  293. return (option, config[DGG._OPT_DEFAULT], config[DGG._OPT_VALUE])
  294. # optimizations:
  295. optionInfo = self._optionInfo
  296. optionInfo_has_key = optionInfo.__contains__
  297. componentInfo = self.__componentInfo
  298. componentInfo_has_key = componentInfo.__contains__
  299. componentAliases = self.__componentAliases
  300. componentAliases_has_key = componentAliases.__contains__
  301. VALUE = DGG._OPT_VALUE
  302. FUNCTION = DGG._OPT_FUNCTION
  303. # This will contain a list of options in *kw* which
  304. # are known to this gui item.
  305. directOptions = []
  306. # This will contain information about the options in
  307. # *kw* of the form <component>_<option>, where
  308. # <component> is a component of this megawidget. It is a
  309. # dictionary whose keys are the configure method of each
  310. # component and whose values are a dictionary of options and
  311. # values for the component.
  312. indirectOptions = {}
  313. indirectOptions_has_key = indirectOptions.__contains__
  314. for option, value in kw.items():
  315. if optionInfo_has_key(option):
  316. # This is one of the options of this gui item.
  317. # Check it is an initialisation option.
  318. if optionInfo[option][FUNCTION] is DGG.INITOPT:
  319. print('Cannot configure initialisation option "' \
  320. + option + '" for ' + self.__class__.__name__)
  321. break
  322. #raise KeyError, \
  323. # 'Cannot configure initialisation option "' \
  324. # + option + '" for ' + self.__class__.__name__
  325. optionInfo[option][VALUE] = value
  326. directOptions.append(option)
  327. else:
  328. index = option.find('_')
  329. if index >= 0:
  330. # This option may be of the form <component>_<option>.
  331. # e.g. if alias ('efEntry', 'entryField_entry')
  332. # and option = efEntry_width
  333. # component = efEntry, componentOption = width
  334. component = option[:index]
  335. componentOption = option[(index + 1):]
  336. # Expand component alias
  337. if componentAliases_has_key(component):
  338. # component = entryField, subcomponent = entry
  339. component, subComponent = componentAliases[component]
  340. if subComponent is not None:
  341. # componentOption becomes entry_width
  342. componentOption = subComponent + '_' \
  343. + componentOption
  344. # Expand option string to write on error
  345. # option = entryField_entry_width
  346. option = component + '_' + componentOption
  347. # Does this component exist
  348. if componentInfo_has_key(component):
  349. # Get the configure func for the named component
  350. # component = entryField
  351. componentConfigFuncs = [componentInfo[component][1]]
  352. else:
  353. # Check if this is a group name and configure all
  354. # components in the group.
  355. componentConfigFuncs = []
  356. # For each component
  357. for info in componentInfo.values():
  358. # Check if it is a member of this group
  359. if info[4] == component:
  360. # Yes, append its config func
  361. componentConfigFuncs.append(info[1])
  362. if len(componentConfigFuncs) == 0 and \
  363. component not in self._dynamicGroups:
  364. raise KeyError('Unknown option "' + option + \
  365. '" for ' + self.__class__.__name__)
  366. # Add the configure method(s) (may be more than
  367. # one if this is configuring a component group)
  368. # and option/value to dictionary.
  369. for componentConfigFunc in componentConfigFuncs:
  370. if not indirectOptions_has_key(componentConfigFunc):
  371. indirectOptions[componentConfigFunc] = {}
  372. # Create a dictionary of keyword/values keyed
  373. # on configuration function
  374. indirectOptions[componentConfigFunc][componentOption] \
  375. = value
  376. else:
  377. raise KeyError('Unknown option "' + option + \
  378. '" for ' + self.__class__.__name__)
  379. # Call the configure methods for any components.
  380. # Pass in the dictionary of keyword/values created above
  381. for func, options in indirectOptions.items():
  382. func(**options)
  383. # Call the configuration callback function for each option.
  384. for option in directOptions:
  385. info = optionInfo[option]
  386. func = info[DGG._OPT_FUNCTION]
  387. if func is not None:
  388. func()
  389. # Allow index style references
  390. def __setitem__(self, key, value):
  391. self.configure(**{key: value})
  392. def cget(self, option):
  393. """
  394. Get current configuration setting for this option
  395. """
  396. # Return the value of an option, for example myWidget['font'].
  397. if option in self._optionInfo:
  398. return self._optionInfo[option][DGG._OPT_VALUE]
  399. else:
  400. index = option.find('_')
  401. if index >= 0:
  402. component = option[:index]
  403. componentOption = option[(index + 1):]
  404. # Expand component alias
  405. if component in self.__componentAliases:
  406. component, subComponent = self.__componentAliases[
  407. component]
  408. if subComponent is not None:
  409. componentOption = subComponent + '_' + componentOption
  410. # Expand option string to write on error
  411. option = component + '_' + componentOption
  412. if component in self.__componentInfo:
  413. # Call cget on the component.
  414. componentCget = self.__componentInfo[component][3]
  415. return componentCget(componentOption)
  416. else:
  417. # If this is a group name, call cget for one of
  418. # the components in the group.
  419. for info in self.__componentInfo.values():
  420. if info[4] == component:
  421. componentCget = info[3]
  422. return componentCget(componentOption)
  423. # Option not found
  424. raise KeyError('Unknown option "' + option + \
  425. '" for ' + self.__class__.__name__)
  426. # Allow index style refererences
  427. __getitem__ = cget
  428. def createcomponent(self, componentName, componentAliases, componentGroup,
  429. widgetClass, *widgetArgs, **kw):
  430. """
  431. Create a component (during construction or later) for this widget.
  432. """
  433. # Check for invalid component name
  434. if '_' in componentName:
  435. raise ValueError('Component name "%s" must not contain "_"' % componentName)
  436. # Get construction keywords
  437. if hasattr(self, '_constructorKeywords'):
  438. keywords = self._constructorKeywords
  439. else:
  440. keywords = {}
  441. for alias, component in componentAliases:
  442. # Create aliases to the component and its sub-components.
  443. index = component.find('_')
  444. if index < 0:
  445. # Just a shorter name for one of this widget's components
  446. self.__componentAliases[alias] = (component, None)
  447. else:
  448. # An alias for a component of one of this widget's components
  449. mainComponent = component[:index]
  450. subComponent = component[(index + 1):]
  451. self.__componentAliases[alias] = (mainComponent, subComponent)
  452. # Remove aliases from the constructor keyword arguments by
  453. # replacing any keyword arguments that begin with *alias*
  454. # with corresponding keys beginning with *component*.
  455. alias = alias + '_'
  456. aliasLen = len(alias)
  457. for option in keywords.copy():
  458. if len(option) > aliasLen and option[:aliasLen] == alias:
  459. newkey = component + '_' + option[aliasLen:]
  460. keywords[newkey] = keywords[option]
  461. del keywords[option]
  462. # Find any keyword arguments for this component
  463. componentPrefix = componentName + '_'
  464. nameLen = len(componentPrefix)
  465. # First, walk through the option list looking for arguments
  466. # than refer to this component's group.
  467. for option in keywords:
  468. # Check if this keyword argument refers to the group
  469. # of this component. If so, add this to the options
  470. # to use when constructing the widget. Mark the
  471. # keyword argument as being used, but do not remove it
  472. # since it may be required when creating another
  473. # component.
  474. index = option.find('_')
  475. if index >= 0 and componentGroup == option[:index]:
  476. rest = option[(index + 1):]
  477. kw[rest] = keywords[option][0]
  478. keywords[option][1] = 1
  479. # Now that we've got the group arguments, walk through the
  480. # option list again and get out the arguments that refer to
  481. # this component specifically by name. These are more
  482. # specific than the group arguments, above; we walk through
  483. # the list afterwards so they will override.
  484. for option in keywords.copy():
  485. if len(option) > nameLen and option[:nameLen] == componentPrefix:
  486. # The keyword argument refers to this component, so add
  487. # this to the options to use when constructing the widget.
  488. kw[option[nameLen:]] = keywords[option][0]
  489. # And delete it from main construction keywords
  490. del keywords[option]
  491. # Return None if no widget class is specified
  492. if widgetClass is None:
  493. return None
  494. # Get arguments for widget constructor
  495. if len(widgetArgs) == 1 and isinstance(widgetArgs[0], tuple):
  496. # Arguments to the constructor can be specified as either
  497. # multiple trailing arguments to createcomponent() or as a
  498. # single tuple argument.
  499. widgetArgs = widgetArgs[0]
  500. # Create the widget
  501. widget = widgetClass(*widgetArgs, **kw)
  502. componentClass = widget.__class__.__name__
  503. self.__componentInfo[componentName] = (widget, widget.configure,
  504. componentClass, widget.cget, componentGroup)
  505. return widget
  506. def component(self, name):
  507. # Return a component widget of the megawidget given the
  508. # component's name
  509. # This allows the user of a megawidget to access and configure
  510. # widget components directly.
  511. # Find the main component and any subcomponents
  512. index = name.find('_')
  513. if index < 0:
  514. component = name
  515. remainingComponents = None
  516. else:
  517. component = name[:index]
  518. remainingComponents = name[(index + 1):]
  519. # Expand component alias
  520. # Example entry which is an alias for entryField_entry
  521. if component in self.__componentAliases:
  522. # component = entryField, subComponent = entry
  523. component, subComponent = self.__componentAliases[component]
  524. if subComponent is not None:
  525. if remainingComponents is None:
  526. # remainingComponents = entry
  527. remainingComponents = subComponent
  528. else:
  529. remainingComponents = subComponent + '_' \
  530. + remainingComponents
  531. # Get the component from __componentInfo dictionary
  532. widget = self.__componentInfo[component][0]
  533. if remainingComponents is None:
  534. # Not looking for subcomponent
  535. return widget
  536. else:
  537. # Recursive call on subcomponent
  538. return widget.component(remainingComponents)
  539. def components(self):
  540. # Return a list of all components.
  541. names = list(self.__componentInfo.keys())
  542. names.sort()
  543. return names
  544. def hascomponent(self, component):
  545. return component in self.__componentInfo
  546. def destroycomponent(self, name):
  547. # Remove a megawidget component.
  548. # This command is for use by megawidget designers to destroy a
  549. # megawidget component.
  550. self.__componentInfo[name][0].destroy()
  551. del self.__componentInfo[name]
  552. def destroy(self):
  553. # Clean out any hooks
  554. self.ignoreAll()
  555. del self._optionInfo
  556. del self.__componentInfo
  557. del self.postInitialiseFuncList
  558. def bind(self, event, command, extraArgs = []):
  559. """
  560. Bind the command (which should expect one arg) to the specified
  561. event (such as ENTER, EXIT, B1PRESS, B1CLICK, etc.)
  562. See DirectGuiGlobals for possible events
  563. """
  564. # Need to tack on gui item specific id
  565. gEvent = event + self.guiId
  566. if ConfigVariableBool('debug-directgui-msgs', False):
  567. from direct.showbase.PythonUtil import StackTrace
  568. print(gEvent)
  569. print(StackTrace())
  570. self.accept(gEvent, command, extraArgs = extraArgs)
  571. def unbind(self, event):
  572. """
  573. Unbind the specified event
  574. """
  575. # Need to tack on gui item specific id
  576. gEvent = event + self.guiId
  577. self.ignore(gEvent)
  578. def toggleGuiGridSnap():
  579. DirectGuiWidget.snapToGrid = 1 - DirectGuiWidget.snapToGrid
  580. def setGuiGridSpacing(spacing):
  581. DirectGuiWidget.gridSpacing = spacing
  582. class DirectGuiWidget(DirectGuiBase, NodePath):
  583. # Toggle if you wish widget's to snap to grid when draggin
  584. snapToGrid = 0
  585. gridSpacing = 0.05
  586. # Determine the default initial state for inactive (or
  587. # unclickable) components. If we are in edit mode, these are
  588. # actually clickable by default.
  589. guiEdit = ConfigVariableBool('direct-gui-edit', False)
  590. if guiEdit:
  591. inactiveInitState = DGG.NORMAL
  592. else:
  593. inactiveInitState = DGG.DISABLED
  594. guiDict = {}
  595. def __init__(self, parent = None, **kw):
  596. # Direct gui widgets are node paths
  597. # Direct gui widgets have:
  598. # - stateNodePaths (to hold visible representation of widget)
  599. # State node paths can have:
  600. # - a frame of type (None, FLAT, RAISED, GROOVE, RIDGE)
  601. # - arbitrary geometry for each state
  602. # They inherit from DirectGuiWidget
  603. # - Can create components (with aliases and groups)
  604. # - Can bind to mouse events
  605. # They inherit from NodePath
  606. # - Can position/scale them
  607. optiondefs = (
  608. # Widget's constructor
  609. ('pgFunc', PGItem, None),
  610. ('numStates', 1, None),
  611. ('invertedFrames', (), None),
  612. ('sortOrder', 0, None),
  613. # Widget's initial state
  614. ('state', DGG.NORMAL, self.setState),
  615. # Widget's frame characteristics
  616. ('relief', DGG.FLAT, self.setRelief),
  617. ('borderWidth', (.1, .1), self.setBorderWidth),
  618. ('borderUvWidth', (.1, .1), self.setBorderUvWidth),
  619. ('frameSize', None, self.setFrameSize),
  620. ('frameColor', (.8, .8, .8, 1), self.setFrameColor),
  621. ('frameTexture', None, self.setFrameTexture),
  622. ('frameVisibleScale', (1, 1), self.setFrameVisibleScale),
  623. ('pad', (0, 0), self.resetFrameSize),
  624. # Override button id (beware! your name may not be unique!)
  625. ('guiId', None, DGG.INITOPT),
  626. # Initial pos/scale of the widget
  627. ('pos', None, DGG.INITOPT),
  628. ('hpr', None, DGG.INITOPT),
  629. ('scale', None, DGG.INITOPT),
  630. ('color', None, DGG.INITOPT),
  631. # Do events pass through this widget?
  632. ('suppressMouse', 1, DGG.INITOPT),
  633. ('suppressKeys', 0, DGG.INITOPT),
  634. ('enableEdit', 1, DGG.INITOPT),
  635. )
  636. # Merge keyword options with default options
  637. self.defineoptions(kw, optiondefs)
  638. # Initialize the base classes (after defining the options).
  639. DirectGuiBase.__init__(self)
  640. NodePath.__init__(self)
  641. # Create a button
  642. self.guiItem = self['pgFunc']('')
  643. # Override automatically generated guiId
  644. if self['guiId']:
  645. self.guiItem.setId(self['guiId'])
  646. self.guiId = self.guiItem.getId()
  647. if ShowBaseGlobal.__dev__:
  648. guiObjectCollector.addLevel(1)
  649. guiObjectCollector.flushLevel()
  650. # track gui items by guiId for tracking down leaks
  651. if _track_gui_items:
  652. if not hasattr(ShowBase, 'guiItems'):
  653. ShowBase.guiItems = {}
  654. if self.guiId in ShowBase.guiItems:
  655. ShowBase.notify.warning('duplicate guiId: %s (%s stomping %s)' %
  656. (self.guiId, self,
  657. ShowBase.guiItems[self.guiId]))
  658. ShowBase.guiItems[self.guiId] = self
  659. # Attach button to parent and make that self
  660. if parent is None:
  661. parent = ShowBaseGlobal.aspect2d
  662. self.assign(parent.attachNewNode(self.guiItem, self['sortOrder']))
  663. # Update pose to initial values
  664. if self['pos']:
  665. self.setPos(self['pos'])
  666. if self['hpr']:
  667. self.setHpr(self['hpr'])
  668. if self['scale']:
  669. self.setScale(self['scale'])
  670. if self['color']:
  671. self.setColor(self['color'])
  672. # Initialize names
  673. # Putting the class name in helps with debugging.
  674. self.setName("%s-%s" % (self.__class__.__name__, self.guiId))
  675. # Create
  676. self.stateNodePath = []
  677. for i in range(self['numStates']):
  678. self.stateNodePath.append(NodePath(self.guiItem.getStateDef(i)))
  679. # Initialize frame style
  680. self.frameStyle = []
  681. for i in range(self['numStates']):
  682. self.frameStyle.append(PGFrameStyle())
  683. # For holding bounds info
  684. self.ll = Point3(0)
  685. self.ur = Point3(0)
  686. # Is drag and drop enabled?
  687. if self['enableEdit'] and self.guiEdit:
  688. self.enableEdit()
  689. # Set up event handling
  690. suppressFlags = 0
  691. if self['suppressMouse']:
  692. suppressFlags |= MouseWatcherRegion.SFMouseButton
  693. suppressFlags |= MouseWatcherRegion.SFMousePosition
  694. if self['suppressKeys']:
  695. suppressFlags |= MouseWatcherRegion.SFOtherButton
  696. self.guiItem.setSuppressFlags(suppressFlags)
  697. # Bind destroy hook
  698. self.guiDict[self.guiId] = self
  699. # self.bind(DGG.DESTROY, self.destroy)
  700. # Update frame when everything has been initialized
  701. self.postInitialiseFuncList.append(self.frameInitialiseFunc)
  702. # Call option initialization functions
  703. self.initialiseoptions(DirectGuiWidget)
  704. def frameInitialiseFunc(self):
  705. # Now allow changes to take effect
  706. self.updateFrameStyle()
  707. if not self['frameSize']:
  708. self.resetFrameSize()
  709. def enableEdit(self):
  710. self.bind(DGG.B2PRESS, self.editStart)
  711. self.bind(DGG.B2RELEASE, self.editStop)
  712. self.bind(DGG.PRINT, self.printConfig)
  713. # Can we move this to showbase
  714. # Certainly we don't need to do this for every button!
  715. #mb = base.mouseWatcherNode.getModifierButtons()
  716. #mb.addButton(KeyboardButton.control())
  717. #base.mouseWatcherNode.setModifierButtons(mb)
  718. def disableEdit(self):
  719. self.unbind(DGG.B2PRESS)
  720. self.unbind(DGG.B2RELEASE)
  721. self.unbind(DGG.PRINT)
  722. #mb = base.mouseWatcherNode.getModifierButtons()
  723. #mb.removeButton(KeyboardButton.control())
  724. #base.mouseWatcherNode.setModifierButtons(mb)
  725. def editStart(self, event):
  726. taskMgr.remove('guiEditTask')
  727. vWidget2render2d = self.getPos(ShowBaseGlobal.render2d)
  728. vMouse2render2d = Point3(event.getMouse()[0], 0, event.getMouse()[1])
  729. editVec = Vec3(vWidget2render2d - vMouse2render2d)
  730. if base.mouseWatcherNode.getModifierButtons().isDown(
  731. KeyboardButton.control()):
  732. t = taskMgr.add(self.guiScaleTask, 'guiEditTask')
  733. t.refPos = vWidget2render2d
  734. t.editVecLen = editVec.length()
  735. t.initScale = self.getScale()
  736. else:
  737. t = taskMgr.add(self.guiDragTask, 'guiEditTask')
  738. t.editVec = editVec
  739. def guiScaleTask(self, state):
  740. mwn = base.mouseWatcherNode
  741. if mwn.hasMouse():
  742. vMouse2render2d = Point3(mwn.getMouse()[0], 0, mwn.getMouse()[1])
  743. newEditVecLen = Vec3(state.refPos - vMouse2render2d).length()
  744. self.setScale(state.initScale * (newEditVecLen/state.editVecLen))
  745. return Task.cont
  746. def guiDragTask(self, state):
  747. mwn = base.mouseWatcherNode
  748. if mwn.hasMouse():
  749. vMouse2render2d = Point3(mwn.getMouse()[0], 0, mwn.getMouse()[1])
  750. newPos = vMouse2render2d + state.editVec
  751. self.setPos(ShowBaseGlobal.render2d, newPos)
  752. if DirectGuiWidget.snapToGrid:
  753. newPos = self.getPos()
  754. newPos.set(
  755. ROUND_TO(newPos[0], DirectGuiWidget.gridSpacing),
  756. ROUND_TO(newPos[1], DirectGuiWidget.gridSpacing),
  757. ROUND_TO(newPos[2], DirectGuiWidget.gridSpacing))
  758. self.setPos(newPos)
  759. return Task.cont
  760. def editStop(self, event):
  761. taskMgr.remove('guiEditTask')
  762. def setState(self):
  763. if isinstance(self['state'], int):
  764. self.guiItem.setActive(self['state'])
  765. elif self['state'] == DGG.NORMAL or self['state'] == 'normal':
  766. self.guiItem.setActive(1)
  767. else:
  768. self.guiItem.setActive(0)
  769. def resetFrameSize(self):
  770. if not self.fInit:
  771. self.setFrameSize(fClearFrame = 1)
  772. def setFrameSize(self, fClearFrame = 0):
  773. # Use ready state to determine frame Type
  774. frameType = self.getFrameType()
  775. if self['frameSize']:
  776. # Use user specified bounds
  777. self.bounds = self['frameSize']
  778. #print "%s bounds = %s" % (self.getName(), self.bounds)
  779. bw = (0, 0)
  780. else:
  781. if fClearFrame and frameType != PGFrameStyle.TNone:
  782. self.frameStyle[0].setType(PGFrameStyle.TNone)
  783. self.guiItem.setFrameStyle(0, self.frameStyle[0])
  784. # To force an update of the button
  785. self.guiItem.getStateDef(0)
  786. # Clear out frame before computing bounds
  787. self.getBounds()
  788. # Restore frame style if necessary
  789. if frameType != PGFrameStyle.TNone:
  790. self.frameStyle[0].setType(frameType)
  791. self.guiItem.setFrameStyle(0, self.frameStyle[0])
  792. if frameType != PGFrameStyle.TNone and \
  793. frameType != PGFrameStyle.TFlat:
  794. bw = self['borderWidth']
  795. else:
  796. bw = (0, 0)
  797. # Set frame to new dimensions
  798. self.guiItem.setFrame(
  799. self.bounds[0] - bw[0],
  800. self.bounds[1] + bw[0],
  801. self.bounds[2] - bw[1],
  802. self.bounds[3] + bw[1])
  803. def getBounds(self, state = 0):
  804. self.stateNodePath[state].calcTightBounds(self.ll, self.ur)
  805. # Scale bounds to give a pad around graphics
  806. vec_right = Vec3.right()
  807. vec_up = Vec3.up()
  808. left = (vec_right[0] * self.ll[0]
  809. + vec_right[1] * self.ll[1]
  810. + vec_right[2] * self.ll[2])
  811. right = (vec_right[0] * self.ur[0]
  812. + vec_right[1] * self.ur[1]
  813. + vec_right[2] * self.ur[2])
  814. bottom = (vec_up[0] * self.ll[0]
  815. + vec_up[1] * self.ll[1]
  816. + vec_up[2] * self.ll[2])
  817. top = (vec_up[0] * self.ur[0]
  818. + vec_up[1] * self.ur[1]
  819. + vec_up[2] * self.ur[2])
  820. self.ll = Point3(left, 0.0, bottom)
  821. self.ur = Point3(right, 0.0, top)
  822. self.bounds = [self.ll[0] - self['pad'][0],
  823. self.ur[0] + self['pad'][0],
  824. self.ll[2] - self['pad'][1],
  825. self.ur[2] + self['pad'][1]]
  826. return self.bounds
  827. def getWidth(self):
  828. return self.bounds[1] - self.bounds[0]
  829. def getHeight(self):
  830. return self.bounds[3] - self.bounds[2]
  831. def getCenter(self):
  832. x = self.bounds[0] + (self.bounds[1] - self.bounds[0])/2.0
  833. y = self.bounds[2] + (self.bounds[3] - self.bounds[2])/2.0
  834. return (x, y)
  835. def getFrameType(self, state = 0):
  836. return self.frameStyle[state].getType()
  837. def updateFrameStyle(self):
  838. if not self.fInit:
  839. for i in range(self['numStates']):
  840. self.guiItem.setFrameStyle(i, self.frameStyle[i])
  841. def setRelief(self, fSetStyle = 1):
  842. relief = self['relief']
  843. # Convert None, and string arguments
  844. if relief is None:
  845. relief = PGFrameStyle.TNone
  846. elif isinstance(relief, str):
  847. # Convert string to frame style int
  848. relief = DGG.FrameStyleDict[relief]
  849. # Set style
  850. if relief == DGG.RAISED:
  851. for i in range(self['numStates']):
  852. if i in self['invertedFrames']:
  853. self.frameStyle[1].setType(DGG.SUNKEN)
  854. else:
  855. self.frameStyle[i].setType(DGG.RAISED)
  856. elif relief == DGG.SUNKEN:
  857. for i in range(self['numStates']):
  858. if i in self['invertedFrames']:
  859. self.frameStyle[1].setType(DGG.RAISED)
  860. else:
  861. self.frameStyle[i].setType(DGG.SUNKEN)
  862. else:
  863. for i in range(self['numStates']):
  864. self.frameStyle[i].setType(relief)
  865. # Apply styles
  866. self.updateFrameStyle()
  867. def setFrameColor(self):
  868. # this might be a single color or a list of colors
  869. colors = self['frameColor']
  870. if isinstance(colors[0], (int, float)):
  871. colors = (colors,)
  872. for i in range(self['numStates']):
  873. if i >= len(colors):
  874. color = colors[-1]
  875. else:
  876. color = colors[i]
  877. self.frameStyle[i].setColor(color[0], color[1], color[2], color[3])
  878. self.updateFrameStyle()
  879. def setFrameTexture(self):
  880. # this might be a single texture or a list of textures
  881. textures = self['frameTexture']
  882. if textures is None or \
  883. isinstance(textures, (Texture, str)):
  884. textures = (textures,) * self['numStates']
  885. for i in range(self['numStates']):
  886. if i >= len(textures):
  887. texture = textures[-1]
  888. else:
  889. texture = textures[i]
  890. if isinstance(texture, str):
  891. texture = base.loader.loadTexture(texture)
  892. if texture:
  893. self.frameStyle[i].setTexture(texture)
  894. else:
  895. self.frameStyle[i].clearTexture()
  896. self.updateFrameStyle()
  897. def setFrameVisibleScale(self):
  898. scale = self['frameVisibleScale']
  899. for i in range(self['numStates']):
  900. self.frameStyle[i].setVisibleScale(scale[0], scale[1])
  901. self.updateFrameStyle()
  902. def setBorderWidth(self):
  903. width = self['borderWidth']
  904. for i in range(self['numStates']):
  905. self.frameStyle[i].setWidth(width[0], width[1])
  906. self.updateFrameStyle()
  907. def setBorderUvWidth(self):
  908. uvWidth = self['borderUvWidth']
  909. for i in range(self['numStates']):
  910. self.frameStyle[i].setUvWidth(uvWidth[0], uvWidth[1])
  911. self.updateFrameStyle()
  912. def destroy(self):
  913. if hasattr(self, "frameStyle"):
  914. if ShowBaseGlobal.__dev__:
  915. guiObjectCollector.subLevel(1)
  916. guiObjectCollector.flushLevel()
  917. if hasattr(ShowBase, 'guiItems'):
  918. ShowBase.guiItems.pop(self.guiId, None)
  919. # Destroy children
  920. for child in self.getChildren():
  921. childGui = self.guiDict.get(child.getName())
  922. if childGui:
  923. childGui.destroy()
  924. else:
  925. # RAU since we added the class to the name, try
  926. # it with the original name
  927. parts = child.getName().split('-')
  928. simpleChildGui = self.guiDict.get(parts[-1])
  929. if simpleChildGui:
  930. simpleChildGui.destroy()
  931. # messenger.send(DESTROY + child.getName())
  932. del self.guiDict[self.guiId]
  933. del self.frameStyle
  934. # Get rid of node path
  935. self.removeNode()
  936. for nodePath in self.stateNodePath:
  937. nodePath.removeNode()
  938. del self.stateNodePath
  939. del self.guiItem
  940. # Call superclass destruction method (clears out hooks)
  941. DirectGuiBase.destroy(self)
  942. def printConfig(self, indent = 0):
  943. space = ' ' * indent
  944. print('%s%s - %s' % (space, self.guiId, self.__class__.__name__))
  945. print('%sPos: %s' % (space, tuple(self.getPos())))
  946. print('%sScale: %s' % (space, tuple(self.getScale())))
  947. # Print out children info
  948. for child in self.getChildren():
  949. messenger.send(DGG.PRINT + child.getName(), [indent + 2])
  950. def copyOptions(self, other):
  951. """
  952. Copy other's options into our self so we look and feel like other
  953. """
  954. for key, value in other._optionInfo.items():
  955. self[key] = value[1]
  956. def taskName(self, idString):
  957. return idString + "-" + str(self.guiId)
  958. def uniqueName(self, idString):
  959. return idString + "-" + str(self.guiId)
  960. def setProp(self, propString, value):
  961. """
  962. Allows you to set a property like frame['text'] = 'Joe' in
  963. a function instead of an assignment.
  964. This is useful for setting properties inside function intervals
  965. where must input a function and extraArgs, not an assignment.
  966. """
  967. self[propString] = value