Home | History | Annotate | Download | only in plat-mac
      1 """Tools for use in AppleEvent clients and servers.
      2 
      3 pack(x) converts a Python object to an AEDesc object
      4 unpack(desc) does the reverse
      5 
      6 packevent(event, parameters, attributes) sets params and attrs in an AEAppleEvent record
      7 unpackevent(event) returns the parameters and attributes from an AEAppleEvent record
      8 
      9 Plus...  Lots of classes and routines that help representing AE objects,
     10 ranges, conditionals, logicals, etc., so you can write, e.g.:
     11 
     12     x = Character(1, Document("foobar"))
     13 
     14 and pack(x) will create an AE object reference equivalent to AppleScript's
     15 
     16     character 1 of document "foobar"
     17 
     18 Some of the stuff that appears to be exported from this module comes from other
     19 files: the pack stuff from aepack, the objects from aetypes.
     20 
     21 """
     22 
     23 
     24 from warnings import warnpy3k
     25 warnpy3k("In 3.x, the aetools module is removed.", stacklevel=2)
     26 
     27 from types import *
     28 from Carbon import AE
     29 from Carbon import Evt
     30 from Carbon import AppleEvents
     31 import MacOS
     32 import sys
     33 import time
     34 
     35 from aetypes import *
     36 from aepack import packkey, pack, unpack, coerce, AEDescType
     37 
     38 Error = 'aetools.Error'
     39 
     40 # Amount of time to wait for program to be launched
     41 LAUNCH_MAX_WAIT_TIME=10
     42 
     43 # Special code to unpack an AppleEvent (which is *not* a disguised record!)
     44 # Note by Jack: No??!? If I read the docs correctly it *is*....
     45 
     46 aekeywords = [
     47     'tran',
     48     'rtid',
     49     'evcl',
     50     'evid',
     51     'addr',
     52     'optk',
     53     'timo',
     54     'inte', # this attribute is read only - will be set in AESend
     55     'esrc', # this attribute is read only
     56     'miss', # this attribute is read only
     57     'from'  # new in 1.0.1
     58 ]
     59 
     60 def missed(ae):
     61     try:
     62         desc = ae.AEGetAttributeDesc('miss', 'keyw')
     63     except AE.Error, msg:
     64         return None
     65     return desc.data
     66 
     67 def unpackevent(ae, formodulename=""):
     68     parameters = {}
     69     try:
     70         dirobj = ae.AEGetParamDesc('----', '****')
     71     except AE.Error:
     72         pass
     73     else:
     74         parameters['----'] = unpack(dirobj, formodulename)
     75         del dirobj
     76     # Workaround for what I feel is a bug in OSX 10.2: 'errn' won't show up in missed...
     77     try:
     78         dirobj = ae.AEGetParamDesc('errn', '****')
     79     except AE.Error:
     80         pass
     81     else:
     82         parameters['errn'] = unpack(dirobj, formodulename)
     83         del dirobj
     84     while 1:
     85         key = missed(ae)
     86         if not key: break
     87         parameters[key] = unpack(ae.AEGetParamDesc(key, '****'), formodulename)
     88     attributes = {}
     89     for key in aekeywords:
     90         try:
     91             desc = ae.AEGetAttributeDesc(key, '****')
     92         except (AE.Error, MacOS.Error), msg:
     93             if msg[0] != -1701 and msg[0] != -1704:
     94                 raise
     95             continue
     96         attributes[key] = unpack(desc, formodulename)
     97     return parameters, attributes
     98 
     99 def packevent(ae, parameters = {}, attributes = {}):
    100     for key, value in parameters.items():
    101         packkey(ae, key, value)
    102     for key, value in attributes.items():
    103         ae.AEPutAttributeDesc(key, pack(value))
    104 
    105 #
    106 # Support routine for automatically generated Suite interfaces
    107 # These routines are also useable for the reverse function.
    108 #
    109 def keysubst(arguments, keydict):
    110     """Replace long name keys by their 4-char counterparts, and check"""
    111     ok = keydict.values()
    112     for k in arguments.keys():
    113         if k in keydict:
    114             v = arguments[k]
    115             del arguments[k]
    116             arguments[keydict[k]] = v
    117         elif k != '----' and k not in ok:
    118             raise TypeError, 'Unknown keyword argument: %s'%k
    119 
    120 def enumsubst(arguments, key, edict):
    121     """Substitute a single enum keyword argument, if it occurs"""
    122     if key not in arguments or edict is None:
    123         return
    124     v = arguments[key]
    125     ok = edict.values()
    126     if v in edict:
    127         arguments[key] = Enum(edict[v])
    128     elif not v in ok:
    129         raise TypeError, 'Unknown enumerator: %s'%v
    130 
    131 def decodeerror(arguments):
    132     """Create the 'best' argument for a raise MacOS.Error"""
    133     errn = arguments['errn']
    134     err_a1 = errn
    135     if 'errs' in arguments:
    136         err_a2 = arguments['errs']
    137     else:
    138         err_a2 = MacOS.GetErrorString(errn)
    139     if 'erob' in arguments:
    140         err_a3 = arguments['erob']
    141     else:
    142         err_a3 = None
    143 
    144     return (err_a1, err_a2, err_a3)
    145 
    146 class TalkTo:
    147     """An AE connection to an application"""
    148     _signature = None   # Can be overridden by subclasses
    149     _moduleName = None  # Can be overridden by subclasses
    150     _elemdict = {}      # Can be overridden by subclasses
    151     _propdict = {}      # Can be overridden by subclasses
    152 
    153     __eventloop_initialized = 0
    154     def __ensure_WMAvailable(klass):
    155         if klass.__eventloop_initialized: return 1
    156         if not MacOS.WMAvailable(): return 0
    157         # Workaround for a but in MacOSX 10.2: we must have an event
    158         # loop before we can call AESend.
    159         Evt.WaitNextEvent(0,0)
    160         return 1
    161     __ensure_WMAvailable = classmethod(__ensure_WMAvailable)
    162 
    163     def __init__(self, signature=None, start=0, timeout=0):
    164         """Create a communication channel with a particular application.
    165 
    166         Addressing the application is done by specifying either a
    167         4-byte signature, an AEDesc or an object that will __aepack__
    168         to an AEDesc.
    169         """
    170         self.target_signature = None
    171         if signature is None:
    172             signature = self._signature
    173         if type(signature) == AEDescType:
    174             self.target = signature
    175         elif type(signature) == InstanceType and hasattr(signature, '__aepack__'):
    176             self.target = signature.__aepack__()
    177         elif type(signature) == StringType and len(signature) == 4:
    178             self.target = AE.AECreateDesc(AppleEvents.typeApplSignature, signature)
    179             self.target_signature = signature
    180         else:
    181             raise TypeError, "signature should be 4-char string or AEDesc"
    182         self.send_flags = AppleEvents.kAEWaitReply
    183         self.send_priority = AppleEvents.kAENormalPriority
    184         if timeout:
    185             self.send_timeout = timeout
    186         else:
    187             self.send_timeout = AppleEvents.kAEDefaultTimeout
    188         if start:
    189             self._start()
    190 
    191     def _start(self):
    192         """Start the application, if it is not running yet"""
    193         try:
    194             self.send('ascr', 'noop')
    195         except AE.Error:
    196             _launch(self.target_signature)
    197             for i in range(LAUNCH_MAX_WAIT_TIME):
    198                 try:
    199                     self.send('ascr', 'noop')
    200                 except AE.Error:
    201                     pass
    202                 else:
    203                     break
    204                 time.sleep(1)
    205 
    206     def start(self):
    207         """Deprecated, used _start()"""
    208         self._start()
    209 
    210     def newevent(self, code, subcode, parameters = {}, attributes = {}):
    211         """Create a complete structure for an apple event"""
    212 
    213         event = AE.AECreateAppleEvent(code, subcode, self.target,
    214                   AppleEvents.kAutoGenerateReturnID, AppleEvents.kAnyTransactionID)
    215         packevent(event, parameters, attributes)
    216         return event
    217 
    218     def sendevent(self, event):
    219         """Send a pre-created appleevent, await the reply and unpack it"""
    220         if not self.__ensure_WMAvailable():
    221             raise RuntimeError, "No window manager access, cannot send AppleEvent"
    222         reply = event.AESend(self.send_flags, self.send_priority,
    223                                   self.send_timeout)
    224         parameters, attributes = unpackevent(reply, self._moduleName)
    225         return reply, parameters, attributes
    226 
    227     def send(self, code, subcode, parameters = {}, attributes = {}):
    228         """Send an appleevent given code/subcode/pars/attrs and unpack the reply"""
    229         return self.sendevent(self.newevent(code, subcode, parameters, attributes))
    230 
    231     #
    232     # The following events are somehow "standard" and don't seem to appear in any
    233     # suite...
    234     #
    235     def activate(self):
    236         """Send 'activate' command"""
    237         self.send('misc', 'actv')
    238 
    239     def _get(self, _object, asfile=None, _attributes={}):
    240         """_get: get data from an object
    241         Required argument: the object
    242         Keyword argument _attributes: AppleEvent attribute dictionary
    243         Returns: the data
    244         """
    245         _code = 'core'
    246         _subcode = 'getd'
    247 
    248         _arguments = {'----':_object}
    249         if asfile:
    250             _arguments['rtyp'] = mktype(asfile)
    251 
    252         _reply, _arguments, _attributes = self.send(_code, _subcode,
    253                 _arguments, _attributes)
    254         if 'errn' in _arguments:
    255             raise Error, decodeerror(_arguments)
    256 
    257         if '----' in _arguments:
    258             return _arguments['----']
    259             if asfile:
    260                 item.__class__ = asfile
    261             return item
    262 
    263     get = _get
    264 
    265     _argmap_set = {
    266         'to' : 'data',
    267     }
    268 
    269     def _set(self, _object, _attributes={}, **_arguments):
    270         """set: Set an object's data.
    271         Required argument: the object for the command
    272         Keyword argument to: The new value.
    273         Keyword argument _attributes: AppleEvent attribute dictionary
    274         """
    275         _code = 'core'
    276         _subcode = 'setd'
    277 
    278         keysubst(_arguments, self._argmap_set)
    279         _arguments['----'] = _object
    280 
    281 
    282         _reply, _arguments, _attributes = self.send(_code, _subcode,
    283                 _arguments, _attributes)
    284         if _arguments.get('errn', 0):
    285             raise Error, decodeerror(_arguments)
    286         # XXXX Optionally decode result
    287         if '----' in _arguments:
    288             return _arguments['----']
    289 
    290     set = _set
    291 
    292     # Magic glue to allow suite-generated classes to function somewhat
    293     # like the "application" class in OSA.
    294 
    295     def __getattr__(self, name):
    296         if name in self._elemdict:
    297             cls = self._elemdict[name]
    298             return DelayedComponentItem(cls, None)
    299         if name in self._propdict:
    300             cls = self._propdict[name]
    301             return cls()
    302         raise AttributeError, name
    303 
    304 # Tiny Finder class, for local use only
    305 
    306 class _miniFinder(TalkTo):
    307     def open(self, _object, _attributes={}, **_arguments):
    308         """open: Open the specified object(s)
    309         Required argument: list of objects to open
    310         Keyword argument _attributes: AppleEvent attribute dictionary
    311         """
    312         _code = 'aevt'
    313         _subcode = 'odoc'
    314 
    315         if _arguments: raise TypeError, 'No optional args expected'
    316         _arguments['----'] = _object
    317 
    318 
    319         _reply, _arguments, _attributes = self.send(_code, _subcode,
    320                 _arguments, _attributes)
    321         if 'errn' in _arguments:
    322             raise Error, decodeerror(_arguments)
    323         # XXXX Optionally decode result
    324         if '----' in _arguments:
    325             return _arguments['----']
    326 #pass
    327 
    328 _finder = _miniFinder('MACS')
    329 
    330 def _launch(appfile):
    331     """Open a file thru the finder. Specify file by name or fsspec"""
    332     _finder.open(_application_file(('ID  ', appfile)))
    333 
    334 
    335 class _application_file(ComponentItem):
    336     """application file - An application's file on disk"""
    337     want = 'appf'
    338 
    339 _application_file._propdict = {
    340 }
    341 _application_file._elemdict = {
    342 }
    343 
    344 # Test program
    345 # XXXX Should test more, really...
    346 
    347 def test():
    348     target = AE.AECreateDesc('sign', 'quil')
    349     ae = AE.AECreateAppleEvent('aevt', 'oapp', target, -1, 0)
    350     print unpackevent(ae)
    351     raw_input(":")
    352     ae = AE.AECreateAppleEvent('core', 'getd', target, -1, 0)
    353     obj = Character(2, Word(1, Document(1)))
    354     print obj
    355     print repr(obj)
    356     packevent(ae, {'----': obj})
    357     params, attrs = unpackevent(ae)
    358     print params['----']
    359     raw_input(":")
    360 
    361 if __name__ == '__main__':
    362     test()
    363     sys.exit(1)
    364