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