PoserMenu.py
1    from __future__ import print_function
2    import os
3    from PoserMenuLink import Link as MenuLink
4    
5    try:
6        import poser
7    except ImportError:
8        from PoserLibs import POSER_FAKE as poser
9    
10   TITLE = "ADP-Menu"
11   BUTTON_KEYWORD = "BUTTON"
12   TEMPDIR = "."
13   SCENE = poser.Scene()
14   UNIVERSE = SCENE.Actor("UNIVERSE")
15   
16   
17   def restore_buttons():
18       """ 
19       Back to standard button menu. 
20       """
21       path = os.path.dirname(poser.AppLocation())
22       poser.ExecFile(os.path.join(path, "Runtime", "Python", "poserScripts", "mainButtons.py"))
23   
24   
25   ##############################################################################
26   # *** Script-template generated for each defined button
27   ##############################################################################
28   SCRIPT_BODY = """### Generated Poser Button 
29   import poser 
30   args = {args} 
31    
32   if args: 
33       MENU.MenuObj.{funcname}(*(globals().get(a, a) for a in args)) 
34   else: 
35       MENU.MenuObj.{funcname}() 
36   """
37   
38   
39   ##############################################################################
40   ##############################################################################
41   
42   
43   class MenuClass(MenuLink):
44       """ 
45       MenuClass 
46    
47       Use this class as baseclass. E.g: 
48    
49           from Menu import MenuClass 
50    
51           class MyMenu(MenuClass): 
52               def setup(self): 
53                   ... 
54               def BUTTON_my_button(*args): 
55                   ... 
56    
57       """
58   
59       # Some fixed class-variables.
60       __slots__ = ["_btnlist", "_tempdir", "menu_name"]
61   
62       def __init__(self, **kw):
63           super(MenuClass, self).__init__()
64           # <list>_btnlist holds the list of found button
65           # declarations from user class.
66           self._btnlist = list()
67           # <str>menu_name is used to create subdirs in tempdir.
68           self.menu_name = kw.pop("name", self.__class__.__name__)
69           # <str>_tempdir is the place where code snippets are
70           # created (Python code executed after pressing a button).
71           self._tempdir = os.path.join(TEMPDIR, "SCRIPTBUTTONS")
72   
73           self.clearButtons()
74           self.displayButtons()
75   
76       def setup(self):
77           """ 
78           Should be overwritten in user defined class. 
79           :return: None 
80           """
81           pass
82   
83       def add(self, menuclass):
84           if self.Next.__class__ != menuclass:
85               MENU.MenuObj = self.Next = menuclass()
86           else:
87               MENU.MenuObj = self.Next
88               MENU.MenuObj.clearButtons()
89               MENU.MenuObj.displayButtons()
90   
91       def back(self):
92           if self.Prev is None:
93               print("No Prev")
94           else:
95               MENU.MenuObj = self.Prev
96               MENU.MenuObj.clearButtons()
97               MENU.MenuObj.displayButtons()
98   
99       @staticmethod
100      def clearButtons():
101          """ 
102          Clear all Poser ScriptButtons. 
103          :return: None 
104          """
105          for idx in range(1, 11):
106              poser.DefineScriptButton(idx, "", "")
107  
108      @staticmethod
109      def clearTempDir(path=TEMPDIR):
110          if not os.path.exists(path):
111              return
112  
113          for entry in os.listdir(path):
114              p = os.path.join(path, entry)
115              if os.path.isdir(p):
116                  MenuClass.clearTempDir(p)
117                  try:
118                      os.remove(p)
119                  except Exception:
120                      pass
121                  continue
122  
123              if entry.startswith(BUTTON_KEYWORD):
124                  if os.path.isfile(p):
125                      try:
126                          os.remove(p)
127                      except Exception:
128                          pass
129  
130      def _extractBtnData(self, func):
131          """ 
132          Extract button-data from method __doc__ string. 
133          :return: btn_nr, scriptname, args, title or name 
134   
135          btn_nr = <int>, a buttons index number. 
136                   Indexnumber goes from 1 to 10. IndexError is raised if 
137                   button number is not in range. 
138          scriptname = <str>, absolute path-/filename (generated script to call). 
139                       An IOError is raised if script can't be created. 
140          args = <list>, array of arguments this button-function is called with. 
141                         Default: empty list. 
142                         Arguments may be a comma-seperated list. 
143          title or name = <str>, title/name displayed in button. 
144                          Default: empty string. 
145   
146          Each keyword in __doc__ string must be enclosed in ":" 
147          (:nr:, :buttonnr:, :title:, :name:, :args:) 
148          Substitutes :nr: == :buttonnr:, 
149                      :title: == :name:. 
150          Keywords are case-insensitive. 
151          Anything after '#' is ignored in __doc__. 
152          E.g.: 
153            :nr: 2 # Script button number 2. 
154            :buttonnr: 2 # Same as above. 
155            :title: List Actors # Description for this button. 
156            :name: List Actors # Same as above. 
157            :args: a, b, c # Comma seperated list of arguments. 
158                             If for instance 'a' is defined global, content 
159                             of variable a is returned, else the string 'a'. 
160          """
161  
162          def extract(s):
163              empty = None
164              # This one does the actual work.
165              if len(func.__doc__) == 0:
166                  return empty
167  
168              try:
169                  i = func.__doc__.lower().find(s.lower())
170              except AttributeError:
171                  # Maybe no __doc__? Or no keyword?
172                  return empty
173              if i < 0:
174                  return empty
175  
176              i += len(s)
177              eol = func.__doc__.find("\n", i)
178              ret = str(func.__doc__[i:eol].split("#", 1)[0] or "").strip()
179  
180              return ret or empty
181  
182          def nr_from_name():
183              nr = func.__name__.split("_", 1)[-1].split("_", 1)[0]
184              i = 0
185              while i < len(nr) and nr[i].isdigit():
186                  i += 1
187              return int(nr[:i]) if i else 0
188  
189          if not os.path.exists(self._tempdir):
190              os.makedirs(self._tempdir)
191  
192          button_nr = int(extract(":nr:") or extract(":buttonnr:") or nr_from_name())
193          button_ext = func.__name__.split(BUTTON_KEYWORD, 1)[-1]
194          args = extract(":args:")
195          #        print("FUNC: %s, ButtonNr: %s, ButtonExt: %s, Args: %s" % (func.__name__, button_nr, button_ext, args))
196          args = [a.strip() for a in args.split(",")] if args else list()
197  
198          return button_nr, \
199                 button_ext, \
200                 args, \
201                 extract(":title:") or extract(":name:") or func.__name__.rsplit("_", 1)[-1]
202  
203      def find_button_declarations(self):
204          """ 
205          Analyse class to find declared Buttons. 
206   
207          Each class method found with a name starting with predefined 
208          keyword ('Button' by default) is assumed to be a declaration of 
209          a Poser-Python Script-Button. 
210          The __doc__ part of these methods should have entries to 
211          describe this button. 
212   
213          :return: None 
214          """
215          for entry in dir(self):
216              if not entry.startswith(BUTTON_KEYWORD):
217                  continue
218              func = getattr(self, entry)
219              if not callable(func):
220                  continue
221              button_nr, func_ext, args, title = self._extractBtnData(func)
222              self._btnlist.append((func, int(button_nr), args, title, func_ext))
223  
224      def displayButtons(self):
225          """ 
226          Show all defined buttons from this class. 
227          :return: None 
228          """
229  
230          def substitute_args(args, locals=locals()):
231              new_args = args
232  
233              def repl(s, x):
234                  if s in new_args:
235                      i = new_args.index(s)
236                      new_args[i] = x
237  
238              repl("$index", button_nr)
239              repl("$name", func.__name__)
240              repl("$script", script)
241  
242              return new_args
243  
244          if len(self._btnlist) == 0:
245              self.find_button_declarations()
246  
247          occupied = set()  # Index-numbers already defined.
248          todo = list()  # List of buttons defined without index.
249          backnext = "back next".split()  # Used for auto-index.
250  
251          self.clearTempDir(self._tempdir)
252  
253          for func, button_nr, args, title, func_ext in self._btnlist:
254              # Use predefined list to actually create the buttons.
255              if button_nr > 10:
256                  raise SyntaxError("Too much buttons defined in class '%s'." % self.__class__.__name__)
257  
258              if button_nr < 1 and func_ext in backnext:
259                  # Buttons with index 0 and with 'next' or 'back' in name.
260                  idx = backnext.index(func_ext)
261                  button_nr = 10 - idx
262                  if not title:
263                      # Generate a title if none is declared.
264                      title = ("<<<", ">>>")[idx] + backnext[idx].capitalize()
265  
266              if button_nr > 0:
267                  # Buttons with index declared or computed.
268                  occupied.add(button_nr)
269                  script = os.path.abspath(os.path.join(self._tempdir, "BUTTON_%02d.py" % button_nr))
270                  poser.DefineScriptButton(button_nr, script, title)
271                  with open(script, "w") as fh:
272                      print(SCRIPT_BODY.format(args=substitute_args(args), funcname=func.__name__), file=fh)
273              else:
274                  # Buttons without index are stored to be processed later.
275                  todo.append((args, func, title))
276  
277          if todo:
278              # Process buttons without index.
279              for button_nr in range(1, 11):
280                  if button_nr not in occupied:
281                      args, func, title = todo.pop(0)
282                      script = os.path.abspath(os.path.join(self._tempdir, "BUTTON_%02d.py" % button_nr))
283                      poser.DefineScriptButton(button_nr, script, title)
284                      with open(script, "w") as fh:
285                          print(SCRIPT_BODY.format(args=substitute_args(args), funcname=func.__name__), file=fh)
286                      if len(todo) == 0:
287                          # all set.
288                          break
289              if todo:
290                  # still some buttons left?
291                  s = ",".join(t[1] for t in todo)
292                  raise SyntaxError("No more room for defined buttons [%s]" % s)
293  
294          # Make sure generated script can find us.
295          MENU.MenuObj = self
296  
297      def rename(self, index, newTitle):
298          for idx, entry in enumerate(self._btnlist):
299              func, button_nr, script, args, title, func_ext = entry
300              if button_nr == index:
301                  self._btnlist[idx] = func, button_nr, script, args, newTitle, func_ext
302                  poser.DefineScriptButton(button_nr, script, newTitle)
303  
304      def __del__(self):
305          # make sure back references are deleted.
306          try:
307              if MENU.MenuObj == self:
308                  MENU.MenuObj = None
309          except NameError:
310              pass
311          except Exception:
312              pass
313          self.clearTempDir(self._tempdir)
314  
315  
316  ##############################################################################
317  # *** Try to make global available variable MENU as resistent as possible
318  # *** if it not yet exists.
319  # *** This works by defining MENU as a builtin function.
320  ##############################################################################
321  
322  MENU = globals().get("MENU", None)
323  if MENU.__class__.__name__ != "__MENU__":
324      class __MENU__(object):
325          __slots__ = ["MenuObj", "MenuClass", "MenuList"]
326  
327          def __init__(self):
328              super(__MENU__, self).__init__()
329              self.MenuClass = MenuClass
330              self.MenuObj = None
331  
332      MENU = __MENU__()
333  try:
334      __builtins__.MENU = MENU
335  except AttributeError:
336      __builtins__["MENU"] = MENU
337  
338  class MenuError(Exception):
339      pass
340  
341  MENU.MenuClass = MenuClass
342