2

I try to write my own keychanger.

So if I write "k" I get a russian "к"

[NSEvent addGlobalMonitorForEventsMatchingMask:(NSKeyDownMask) handler:^(NSEvent *event){
        NSMutableString *buffer = [event.characters mutableCopy];
        CFMutableStringRef bufferRef = (__bridge CFMutableStringRef)buffer;
        CFStringTransform(bufferRef, NULL, kCFStringTransformLatinCyrillic, false);
        NSLog(@"%@", buffer);
    }];

How to modify the output of keyDown Event in other applications.

For example, I am typing an email in chrome, gmail... my keyboard is set to english, but I get russian characters.

like this: translit.ru

Is there a way to modify the output?

JWWalker
  • 22,385
  • 6
  • 55
  • 76
Alexander_F
  • 2,831
  • 3
  • 28
  • 61
  • It's not clear to me what you're asking -- perhaps you can provide some more context, or an example showing what you're looking for? Be sure also to check the [question checklist](http://meta.stackexchange.com/questions/156810/stack-overflow-question-checklist). Thanks! – Christian Ternus Oct 28 '13 at 22:19
  • @ChristianTernus what I need is a native application like this: http://translit.ru/ but working in background. Every time I write and the application is running I get russian output in all my apps (safari, chrome etc) – Alexander_F Oct 28 '13 at 22:21
  • 1
    Looks like answer to this question will help you: http://stackoverflow.com/questions/6421718/how-to-trap-global-keydown-keyup-events-in-cocoa – Nickolay Olshevsky Oct 28 '13 at 22:40
  • @NickolayOlshevsky I don't think so, he is exactly so far as me... – Alexander_F Oct 29 '13 at 08:14
  • Wait, is your question about OS X or iOS? – TotoroTotoro Nov 09 '13 at 21:49
  • 1
    what he maybe wants: a program (daemon or any other background app) which maps the buttons to different keys (symbols, kyrillic...), a tool like this maybe: https://www.macupdate.com/app/mac/25141/keyremap4macbook – Gotschi Nov 13 '13 at 22:57

1 Answers1

4

I quickly threw this code together so be sure to vet it for memory leaks etc. You will see that you need to add the other characters you want to handle as well as the case (I added k -> к only).

This example was build as a "command line" utility so it has no UI and is suitable for running in the background via launchd. Note that you either need to run this as root or enable assistive device support in system preferences and add permission for this app.

Note that you can run as root while developing in Xcode by going to Product -> Scheme -> Edit and in the "run" section change the radio for "debug process as" to "root".

Edit: Update with autorelease pool to release temporary objects.

// compile and run from the commandline with:
//    clang -fobjc-arc -framework Cocoa  ./foo.m  -o foo
//    sudo ./foo 

#import <Foundation/Foundation.h>
#import <AppKit/NSEvent.h>

typedef CFMachPortRef EventTap;

@interface KeyChanger : NSObject
{
@private
    EventTap _eventTap;
    CFRunLoopSourceRef _runLoopSource;
    CGEventRef _lastEvent;
}
@end

CGEventRef _tapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, KeyChanger* listener);

@implementation KeyChanger

- (BOOL)tapEvents
{
    if (!_eventTap) {
        NSLog(@"Initializing an event tap.");

        _eventTap = CGEventTapCreate(kCGSessionEventTap,
                                     kCGTailAppendEventTap,
                                     kCGEventTapOptionDefault,
                                     CGEventMaskBit(kCGEventKeyDown),
                                     (CGEventTapCallBack)_tapCallback,
                                     (__bridge void *)(self));
        if (!_eventTap) {
            NSLog(@"unable to create event tap. must run as root or add privlidges for assistive devices to this app.");
            return NO;
        }
    }
    CGEventTapEnable(_eventTap, TRUE);

    return [self isTapActive];
}

- (BOOL)isTapActive
{
    return CGEventTapIsEnabled(_eventTap);
}

- (void)listen
{
    if (!_runLoopSource) {
        if (_eventTap) {//dont use [self tapActive]
            _runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault,
                                                           _eventTap, 0);
            // Add to the current run loop.
            CFRunLoopAddSource(CFRunLoopGetCurrent(), _runLoopSource,
                               kCFRunLoopCommonModes);

            NSLog(@"Registering event tap as run loop source.");
            CFRunLoopRun();
        }else{
            NSLog(@"No Event tap in place! You will need to call listen after tapEvents to get events.");
        }
    }
}

- (CGEventRef)processEvent:(CGEventRef)cgEvent
{
    NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];

    // TODO: add other cases and do proper handling of case
    if ([event.characters caseInsensitiveCompare:@"k"] == NSOrderedSame) {
        event = [NSEvent keyEventWithType:event.type
                                 location:NSZeroPoint
                            modifierFlags:event.modifierFlags
                                timestamp:event.timestamp
                             windowNumber:event.windowNumber
                                  context:event.context
                               characters:@"к"
              charactersIgnoringModifiers:@"к"
                                isARepeat:event.isARepeat
                                  keyCode:event.keyCode];
    }
    _lastEvent = [event CGEvent];
    CFRetain(_lastEvent); // must retain the event. will be released by the system
    return _lastEvent;
}

- (void)dealloc
{
    if (_runLoopSource){
        CFRunLoopRemoveSource(CFRunLoopGetCurrent(), _runLoopSource, kCFRunLoopCommonModes);
        CFRelease(_runLoopSource);
    }
    if (_eventTap){
        //kill the event tap
        CGEventTapEnable(_eventTap, FALSE);
        CFRelease(_eventTap);
    }
}

@end
CGEventRef _tapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, KeyChanger* listener) {
    //Do not make the NSEvent here.
    //NSEvent will throw an exception if we try to make an event from the tap timout type
    @autoreleasepool {
        if(type == kCGEventTapDisabledByTimeout) {
            NSLog(@"event tap has timed out, re-enabling tap");
            [listener tapEvents];
            return nil;
        }
        if (type != kCGEventTapDisabledByUserInput) {
            return [listener processEvent:event];
        }
    }
    return event;
}

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        KeyChanger* keyChanger = [KeyChanger new];
        [keyChanger tapEvents];
        [keyChanger listen];//blocking call.
    }
    return 0;
}
P i
  • 29,020
  • 36
  • 159
  • 267
Brad Allred
  • 7,323
  • 1
  • 30
  • 49
  • Hi, thank you for posting the code, I am not so fit in objective C. I have created a command line tool and pasted this code into main.m. trying to build a get a banch of errors. ndefined symbols for architecture x86_64: "_CGEventGetType", referenced from: -[KeyChanger processEvent:] in main.o "_CGEventTapCreate", referenced from: -[KeyChanger tapEvents] in main.o "_CGEventTapEnable", referenced from: – Alexander_F Nov 14 '13 at 22:27
  • @Fincha what settings did you choose for the command line tool? you should have selected "Foundation" for type and you will also need to add the AppKit framework to the target in the "link with libraries" section of "build phases" – Brad Allred Nov 14 '13 at 22:31
  • thanкs a lot, Appкit was missing – Alexander_F Nov 14 '13 at 22:39
  • @Fincha no problem. If you don't wan't to use AppKit you can just use `CGEvent` family of C functions. I decided `NSEvent` was more convenient :) – Brad Allred Nov 14 '13 at 22:53
  • @Fincha I updated my post with a known problem. I will update with a solution tomorrow. – Brad Allred Nov 15 '13 at 03:23
  • @is there a way to call NSNotification like growl from command line tool? Can I integrate the toolbar code from other app? I is it better to transport your code to a "normal" application? Thank your for your support. – Alexander_F Nov 15 '13 at 09:59
  • @Fincha it depends what you are trying to do. certainly you can transfer this code to a standard cocoa application, but there is generally no point unless you actually want a GUI, or behavior provided by `NSApplication` and its faculties. It would solve the autorelease pool problem, but that alone isn't a reason to do it IMO :p – Brad Allred Nov 15 '13 at 14:34
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/41281/discussion-between-fincha-and-brad-allred) – Alexander_F Nov 15 '13 at 17:23