0

The function CGEventKeyboardSetUnicodeString takes a UniCharCount and const UniChar unicodeString[]. I am having trouble figuring out how I can call this from python using pyobjc. Ideally, I'd like to be able to just pass in a python unicode string. Is there a way to modify the metadata for this function so I can do that? Alternatively, is there a way for me to convert a python unicode string into an array of UNICHAR and a length using pyobjc?

To clarify:

I am using version 2.5.1 of pyobjc

the metadata of CGEventKeyboardSetUnicodeString:

>>> pprint(CGEventKeyboardSetUnicodeString.__metadata__())
{'arguments': ({'already_cfretained': False,
            'already_retained': False,
            'null_accepted': True,
            'type': '^{__CGEvent=}'},
           {'already_cfretained': False,
            'already_retained': False,
            'type': 'L'},
           {'already_cfretained': False,
            'already_retained': False,
            'null_accepted': True,
            'type': '^T'}),
'retval': {'already_cfretained': False,
        'already_retained': False,
        'type': 'v'},
'variadic': False}

I've tried the following:

>>> CGEventKeyboardSetUnicodeString(event, 1, 'a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: depythonifying 'pointer', got 'str'

>>> CGEventKeyboardSetUnicodeString(event, 1, u'a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: depythonifying 'pointer', got 'unicode'

>>> CGEventKeyboardSetUnicodeString(event, 1, [ord('a')])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: depythonifying 'pointer', got 'list'

>>> Quartz.CGEventKeyboardSetUnicodeString(event, 1, struct.unpack('>{}H'.format(1), u'a'.encode('utf-16-le')))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: depythonifying 'pointer', got 'tuple'
Hesky Fisher
  • 1,145
  • 7
  • 14
  • OK, it looks like PyObjC 2.5.1 (which you must have installed manually, because the latest Apple release is 2.3.2a0) has the fix I was talking about… and apparently is isn't the right fix. Let me take a look at it. – abarnert Aug 05 '13 at 18:13
  • When I test this in 2.4.0 or top-of-tree PyObjC, with Python 3.3.0 or 2.7.5, I get different—and correct—metadata for the argument. The type is `n^T`, and it has `c_array_length_in_arg=1`. And passing it a Python `unicode` object works fine. You are using the function out of `Quartz`, not `CoreGraphics`, right? – abarnert Aug 05 '13 at 18:24
  • Looking at this further… it looks like PyObjC 2.4.0 fixed these functions, 2.5.0 may have broken them again (in a different way), and 2.6.0 may fix them again… it's hard to be sure, but I'm installing all the versions I can to test, and I'll report back when I know. If I'm right, the best fix may or may not be to copy the 2.6 bridgesupport file to 2.5.1… but again, I'll let you know once I do. – abarnert Aug 05 '13 at 18:43

2 Answers2

2

Functions that take UniChar * strings have never been easy to call, but there is a workaround. Unfortunately, for 2.5, even that workaround doesn't work unless you first work around another problem. First, I'll explain the workaround for earlier versions, then I'll come back to 2.5.

In earlier versions of PyObjC, functions taking UniChar * had their param type declared as unsigned short * (that's '^S' in the metadata); in later versions, it's correctly UniChar * (that's '^T'). But either way, PyObjC doesn't see this as a string type, it sees it as a sequence of integers from 0-65535. So, that's what you have to pass it.


If you've got a unicode all of whose characters are in the BMP,* that's just the ord of each character in the string. Which is easy:

>>> s = u'abc'
>>> e = Quartz.CGEventCreateKeyboardEvent(None, 0, True)
>>> Quartz.CGEventKeyboardSetUnicodeString(e, len(s), map(ord, s))
>>> slen, swords = Quartz.CGEventKeyboardGetUnicodeString(e, 100, None, None)
>>> slen
3
>>> u''.join(map(unichr, swords))
u'abc'

Note that the Get function also returned us a sequence of short ints, not a unicode.


But what if you have, or might have, some non-BMP characters? A UniChar * is a sequence of UTF-16 code points, not a sequence of Unicode characters, so you need to encode to UTF-16. You can do that explicitly (and then struct.unpack to get a list of H values), but there's an easier way: a CFString or NSString is also a sequence of UTF-16 code points. So, if you just use the original code, but with an NSString instead of a unicode, it works:

>>> s = u'abc\U00010000d'
>>> nss = Foundation.NSString.stringWithString_(s)
>>> len(s), len(nss)
(5, 6, 6)
>>> Quartz.CGEventKeyboardSetUnicodeString(e, len(nss), map(ord, nss))

If you find yourself doing this a lot, you probably want to write a simple wrapper:

def to_unichr(s):
    nss = Foundation.NSStringWithString_(s)
    return len(nss), map(ord, nss)

So you can do this:

Quartz.CGEventKeyboardSetUnicodeString(e, *to_unichr(s))

All of the above is for Python 2.x, but should work the same in 3.x, as long as you drop the excess u prefixes (for 3.2 and earlier) and use list(map(…)) instead of map(…) when you want something you can print out.


Meanwhile, PyObjC 2.5 changed the way bridge support metadata is declared, and changed the details of much of the metadata. As far as I can tell, it's effectively impossible to call this function with 2.5.0 or 2.5.1. See issue #64 for details.

The problem seems to be that the _C_IN ('n') type modifier has been dropped. You can pass a sequence to a function expecting an input-argument pointer and PyObjC will convert it appropriately automatically, but for obvious reasons that doesn't work for output, in-out, or unspecified pointers.

You can work around this by modifying the metadata yourself. In 2.5.1, you'll find this in file Quartz/CoreGraphics/_metadata.py, at char 1351 of line 29 (or, more simply, search for CGEventKeyboardSetUnicodeString). You have to add two values to the tuple. Instead of this:

'CGEventKeyboardSetUnicodeString': (sel32or64(b'v^{__CGEvent=}L^T', b'v^{__CGEvent=}Q^T'),)

You need this:

'CGEventKeyboardSetUnicodeString': (sel32or64(b'v^{__CGEvent=}L^T', b'v^{__CGEvent=}Q^T'), '', {'arguments': {2: {'type_modifier': 'n'}}})

With that change, the workaround above works with 2.5.0 or 2.5.1.


* It also works with a str/bytes object that has only ASCII characters, but don't do that.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • @abarnet Thanks for the response. I tried all of the above options but unfortunately, they didn't work in the version of pyobjc that I am using. When I pass in a list of shorts or even an NSString I get: `ValueError: depythonifying 'pointer', got 'list'` or `ValueError: depythonifying 'pointer', got 'objc.pyobjc_unicode'` In the end you suggested changing the type to `^T` but it seems to already be that way. – Hesky Fisher Aug 03 '13 at 16:28
  • The metadata for the third argument is: `{'already_cfretained': False, 'already_retained': False, 'null_accepted': True, 'type': '^T'}` – Hesky Fisher Aug 03 '13 at 16:32
  • These comments have a very limited amount of space so I've updated the question above. – Hesky Fisher Aug 03 '13 at 17:18
0

I've figured out that I could use ctypes to make the call and still work nicely with pyobjc. But if anybody knows how to do this entirely with pyobjc then I'll gladly accept that answer.

My solution looks like this:

import ctypes
import ctypes.util
import objc
import Quartz
import sys

event = Quartz.CGEventCreateKeyboardEvent(None, 0, True)
cf = ctypes.cdll.LoadLibrary(ctypes.util.find_library('ApplicationServices'))
cf.CGEventKeyboardSetUnicodeString.restype = None
event_id = objc.pyobjc_id(event)
s = u'\U0001F600'
utf16_native = 'utf-16-le' if sys.byteorder == 'little' else 'utf-16-be'
buf = s.encode(utf16_native)
cf.CGEventKeyboardSetUnicodeString(event_id, len(buf) / 2, buf)
Quartz.CGEventPost(Quartz.kCGSessionEventTap, event) 
Hesky Fisher
  • 1,145
  • 7
  • 14
  • Besides the fact that this _shouldn't_ be necessary (which is irrelevant if it actually _is_ necessary due to a deficiency in pyobjc…), this isn't actually correct. An OS X `wchar_t` is a 4-byte type, which `UniChar` is a 2-byte type. Effectively you're converting to UTF-32, then passing it to code that expects UTF-16. Of course for this test case that's fine, because it's going to see `a` followed by `\u0000`, but for a multiple-character string (or a single character that's not in the BMP) it won't. – abarnert Aug 05 '13 at 18:19
  • Thanks! I noticed that it wasn't working with characters outside the BMP but I didn't know why. I've updated the example. – Hesky Fisher Aug 06 '13 at 04:14
  • OK, I think I've found the problem, and I've got a workaround if you're willing to modify the function metadata, and I've filed a bug. Based on past history, Ronald Oussoren will likely get to soon and either fix it, or, as is often the case, explain what I got wrong and why it doesn't need to be fixed, which is also good enough for me. But if you need to work with 2.5.x, you'll need some kind of workaround. – abarnert Aug 06 '13 at 17:18
  • I've fixed this in the repository, the call will work correctly in PyObjC 3.0 – Ronald Oussoren Feb 20 '14 at 12:01