System-wide hotkey for an application - cocoa

I have a simple window with 3 buttons and I am trying to add a system-wide hot key so i can "press" those buttons without having to switch to that app, press a button and then go back to what I was doing.
Something like Cmd + Shift + 1 press button 1, Cmd + Shift + 2 press button 2, etc.
Is there any way to achieve this in Cocoa (with Objective-C)?
Thanks, code is appreciated since I am a total newbie on Cocoa.

I also didn't like PTHotKey, so I ended up writing a new wrapper, available here:
http://github.com/davedelong/DDHotKey
edit
The 2 files you'd need are:
DDHotKeyCenter.h
DDHotKeyCenter.m
And you'd use it something like this:
- (IBAction) registerHotkey:(id)sender {
DDHotKeyCenter * c = [[DDHotKeyCenter alloc] init];
if (![c registerHotKeyWithKeyCode:kVK_ANSI_1 modifierFlags:(NSCommandKeyMask | NSShiftKeyMask) target:self action:#selector(hotkeyWithEvent:) object:nil]) {
NSLog(#"unable to register hotkey");
} else {
NSLog(#"registered hotkey");
}
[c release];
}
- (void) hotkeyWithEvent:(NSEvent *)hkEvent {
NSLog(#"Hotkey event: %#", hkEvent);
}

PTHotKey is old and busted (generates reams of warnings) on modern SDKs. Use SGHotKeysLib instead.
Both SGHotKeysLib and PTHotKey are reusable source code. You need only add the classes to your own project, then use them from your own classes.

There is a library called PTHotKey that makes this fairly easy. You can google PTHotKey or just grab it from http://code.google.com/p/shortcutrecorder/source/browse/trunk/Demo/HotKey/?r=2

Related

UITextInputDelegate methods not functioning correctly

I'm working on iOS 8 custom keyboard extension right now, and there are some issues in using UITextInputDelegate Methods.
Does this right: selectionWillChange: and selectionDidChange: methods should be called when user long-presses typing area? And textWillChange: and textDidChange: methods should be called whenever the text is literally changing?
Actually, what I observed is that, when I changed selection in text input area, textWillChange: and textDidChange: are called, and I cannot get a clue that the other two methods are called in what condition. If anyone knows about the usage of these delegate methods, please let me know.
It's not working for me either... what I am currently doing is just using textWillChange and textDidChange which does get called, as you mentioned, when you change your selection... (they get called BEFORE and AFTER) And then comparing the: self.textDocumentProxy.documentContextBeforeInputself.textDocumentProxy.documentContextAfterInput From BEFORE (textWillChange) to the AFTER (textDidChange) to see if selection range or length changed at all.
Something like this (set the 4 NSStrings below in your .h file of course... haven't tested this exact snippet because I wrote it from scratch just now on SO.com but I'm sure the principle works if I made any errors)
- (void)textWillChange:(id<UITextInput>)textInput {
beforeStringOfTextBehindCursor = self.textDocumentProxy.documentContextBeforeInput;
beforeStringOfTextAfterCursor = self.textDocumentProxy.documentContextAfterInput;
}
- (void)textDidChange:(id<UITextInput>)textInput {
afterStringOfTextBehindCursor = self.textDocumentProxy.documentContextBeforeInput;
afterStringOfTextAfterCursor = self.textDocumentProxy.documentContextAfterInput;
BOOL didSelectionChange = NO;
if (![beforeStringOfTextBehindCursor isEqualToString:afterStringOfTextBehindCursor]) {
didSelectionChange = YES;
}
if (![beforeStringOfTextAfterCursor isEqualToString:afterStringOfTextAfterCursor]) {
didSelectionChange = YES;
}
if (didSelectionChange) {
NSLog(#"Selection Changed!");
}
}
I had the same problem with the functions specified in the UITextInput protocol not being called. The reason as far as I can discern is due to the fact that the inputDelegate is set at runtime. According to the ios docs:
The UIKit provides a private text input delegate, which it assigns at runtime to the inputDelegate property of the object whose class adopts the UITextInput protocol. link
The fix which works in my case is to reset the inputDelegate in the function:
textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string
by the following line:
[myUITextField setInputDelegate:self];
where self implements the UITextInputDelegate protocol.

NSTextField with auto-suggestions like Safari's address bar?

What's the easiest way to have an NSTextField with a "recommendation list" dynamically shown below it as the user types? Just like Safari's address bar that has a menu of some sorts (I'm pretty confident Safari's address bar suggestions is menu since it has rounded corners, blue gradient selection, and background blurring).
I've tried using NSTextView's autocompletion facility but found it was inadequate:
It tries to complete words instead of the whole text fields – in other words, selecting an autocomplete suggestion will only replace the current word.
It nudges the autocompletion list forward and align it with the insertion point instead of keeping it align with the text field.
In the sample screenshot above whenever I selected the autocomplete suggestion the text field only replaces K with the suggested item in the list, which results in Abadi Abadi Kurniawan.
These are what I'd like to achieve:
Whenever a suggestion is selected, the entire text field is replaced with the suggestion.
Keep the suggestion list aligned with the text field's left side.
Note: This is not a question about adding progress indicator behind a text field.
The Safari address bar uses a separate window. Apple has example project CustomMenus and it only takes an hour or two to customize it.
Developer session explaining what has to be done Key Event Handling in Cocoa Applications
If you want to be able to select multiple words you need to provide own FieldEditor (credits should go for someone else)
- (id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(nullable id)client;
{
if ([client isKindOfClass:[NSSearchField class]])
{
if (!_mlFieldEditor)
{
_mlFieldEditor = [[MLFieldEditor alloc] init];
[_mlFieldEditor setFieldEditor:YES];
}
return _mlFieldEditor;
}
return nil;
}
- (void)insertCompletion:(NSString *)word forPartialWordRange:(NSRange)charRange movement:(NSInteger)movement isFinal:(BOOL)flag
{
// suppress completion if user types a space
if (movement == NSRightTextMovement) return;
// show full replacements
if (charRange.location != 0) {
charRange.length += charRange.location;
charRange.location = 0;
}
[super insertCompletion:word forPartialWordRange:charRange movement:movement isFinal:flag];
if (movement == NSReturnTextMovement)
{
[[NSNotificationCenter defaultCenter] postNotificationName:#"MLSearchFieldAutocompleted" object:self userInfo:nil];
}
}
This only addresses half of your answer, but I believe you need to subclass NSTextView and implement the - (NSRange)rangeForUserCompletion method, returning the range of the entire string in the text field. This should make sure that it doesn't just autocomplete the most recently entered word.
If you want a custom menu, you're going to have to do that yourself, probably by implementing the -controlTextDidChange: method and displaying a custom view with a table when appropriate.

UICollectionView effective drag and drop

I am currently trying to implement the UITableView reordering behavior using UICollectionView.
Let's call a UItableView TV and a UICollectionView CV (to clarify the following explanation)
I am basically trying to reproduce the drag&drop of the TV, but I am not using the edit mode, the cell is ready to be moved as soon as the long press gesture is triggered. It works prefectly, I am using the move method of the CV, everything is fine.
I update the contentOffset property of the CV to handle the scroll when the user is dragging a cell. When a user goes to a particular rect at the top and the bottom, I update the contentOffset and the CV scroll. The problem is when the user stop moving it's finger, the gesture doesn't send any update which makes the scroll stop and start again as soon as the user moves his finger.
This behavior is definitely not natural, I would prefer continu to scroll until the user release the CV as it is the case in the TV. The TV drag&drop experience is awesome and I really want to reproduce the same feeling. Does anyone know how they manage the scroll in TV during reordering ?
I tried using a timer to trigger a scroll action repeatedly as long as the gesture position is in the right spot, the scroll was awful and not very productive (very slow and jumpy).
I also tried using GCD to listen the gesture position in another thread but the result is even worst.
I ran out of idea about that, so if someone has the answer I would marry him!
Here is the implementation of the longPress method:
- (void)handleLongPress:(UILongPressGestureRecognizer *)sender
{
ReorganizableCVCLayout *layout = (ReorganizableCVCLayout *)self.collectionView.collectionViewLayout;
CGPoint gesturePosition = [sender locationInView:self.collectionView];
NSIndexPath *selectedIndexPath = [self.collectionView indexPathForItemAtPoint:gesturePosition];
if (sender.state == UIGestureRecognizerStateBegan)
{
layout.selectedItem = selectedIndexPath;
layout.gesturePoint = gesturePosition; // Setting gesturePoint invalidate layout
}
else if (sender.state == UIGestureRecognizerStateChanged)
{
layout.gesturePoint = gesturePosition; // Setting gesturePoint invalidate layout
[self swapCellAtPoint:gesturePosition];
[self manageScrollWithReferencePoint:gesturePosition];
}
else
{
[self.collectionView performBatchUpdates:^
{
layout.selectedItem = nil;
layout.gesturePoint = CGPointZero; // Setting gesturePoint invalidate layout
} completion:^(BOOL completion){[self.collectionView reloadData];}];
}
}
To make the CV scroll, I am using that method:
- (void)manageScrollWithReferencePoint:(CGPoint)gesturePoint
{
ReorganizableCVCLayout *layout = (ReorganizableCVCLayout *)self.collectionView.collectionViewLayout;
CGFloat topScrollLimit = self.collectionView.contentOffset.y+layout.itemSize.height/2+SCROLL_BORDER;
CGFloat bottomScrollLimit = self.collectionView.contentOffset.y+self.collectionView.frame.size.height-layout.itemSize.height/2-SCROLL_BORDER;
CGPoint contentOffset = self.collectionView.contentOffset;
if (gesturePoint.y < topScrollLimit && gesturePoint.y - layout.itemSize.height/2 - SCROLL_BORDER > 0)
contentOffset.y -= SCROLL_STEP;
else if (gesturePoint.y > bottomScrollLimit &&
gesturePoint.y + layout.itemSize.height/2 + SCROLL_BORDER < self.collectionView.contentSize.height)
contentOffset.y += SCROLL_STEP;
[self.collectionView setContentOffset:contentOffset];
}
This might help
https://github.com/lxcid/LXReorderableCollectionViewFlowLayout
This is extends the UICollectionView to allow each of the UICollectionViewCells to be rearranged manually by the user with a long touch (aka touch-and-hold). The user can drag the Cell to any other position in the collection and the other cells will reorder automatically. Thanks go to lxcid for this.
Here is an alternative:
The differences between DraggableCollectionView and LXReorderableCollectionViewFlowLayout are:
The data source is only changed once. This means that while the user is dragging an item the cells are re-positioned without modifying the data source.
It's written in such a way that makes it possible to use with custom layouts.
It uses a CADisplayLink for smooth scrolling and animation.
Animations are canceled less frequently while dragging. It feels more "natural".
The protocol extends UICollectionViewDataSource with methods similar to UITableViewDataSource.
It's a work in progress. Multiple sections are now supported.
To use it with a custom layout see DraggableCollectionViewFlowLayout. Most of the logic exists in LSCollectionViewLayoutHelper. There is also an example in CircleLayoutDemo showing how to make Apple's CircleLayout example from WWDC 2012 work.
As of iOS 9, UICollectionView now supports reordering.
For UICollectionViewControllers, just override collectionView(collectionView: UICollectionView, moveItemAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath)
For UICollectionViews, you'll have to handle the gestures yourself in addition to implementing the UICollectionViewDataSource method above.
Here's the code from the source:
private var longPressGesture: UILongPressGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
longPressGesture = UILongPressGestureRecognizer(target: self, action: "handleLongGesture:")
self.collectionView.addGestureRecognizer(longPressGesture)
}
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
guard let selectedIndexPath = self.collectionView.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case UIGestureRecognizerState.Changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case UIGestureRecognizerState.Ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
Sources:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UICollectionView_class/#//apple_ref/doc/uid/TP40012177-CH1-SW67
http://nshint.io/blog/2015/07/16/uicollectionviews-now-have-easy-reordering/
If you want to experiment rolling out your own, I just wrote a Swift based tutorial you can look. I tried to build the most basic of cases so as to be easier to follow this.
Here is another approach:
Key difference is that this solution does not require a "ghost" or "dummy" cell to provide the drag and drop functionality. It simply uses the cell itself. Animations are in line with UITableView. It works by adjusting the collection view layout's private datasource while moving around. Once you let go, it will tell your controller that you can commit the change to your own datasource.
I believe it's a bit simpler to work with for most use cases. Still a work in progress, but yet another way to accomplish this. Most should find this pretty easy to incorporate into their own custom UICollectionViewLayouts.

NSMenuBar mouse down notification?

i'm trying to find notification/way to check if NSStatusItem from NSStatusBar (System wide bar) was clicked.
Does anybody knows how to achive this ?
Thanks
EDITED 20120111
I meant "any" item from whole status bar.
do something like:
statusItem.target = self;
statusItem.action = #selector(mouseDown:);
statusItem.sendActionOn = NSLeftMouseDownMask; // try without this at first - i can't remember what the default it so you probrably don't need it
then:
- (void)mouseDown:(id)sender {
NSLog(#"click click");
}

How do you implement global keyboard hooks in Mac OS X?

I know this can be done for Windows and that XGrabKey can be used for X11, but what about Mac OS X? I want create a class that allows setting shortcut keys that can be invoked even when the application windows are inactive.
This is not (yet?) supported in Cocoa. You can still use the old Carbon library for this (which is 64 bit compatible), but unfortunately Apple decided to remove all documentation on the subject.
There's a nice blog article here: http://dbachrach.com/blog/2005/11/program-global-hotkeys-in-cocoa-easily/
The article is a bit lengthy for my taste, so here is the short version:
- (id)init {
self = [super init];
if (self) {
EventHotKeyRef hotKeyRef;
EventHotKeyID hotKeyId;
EventTypeSpec eventType;
eventType.eventClass = kEventClassKeyboard;
eventType.eventKind = kEventHotKeyPressed;
InstallApplicationEventHandler(&mbHotKeyHandler, 1, &eventType, NULL, NULL);
hotKeyId.signature = 'hotk';
hotKeyId.id = 1337;
RegisterEventHotKey(kVK_ANSI_C, cmdKey + shiftKey, hotKeyCopyId, GetApplicationEventTarget(), 0, &hotKeyRef);
}
}
OSStatus mbHotKeyHandler(EventHandlerCallRef nextHandler, EventRef event, void *userData) {
// Your hotkey was pressed!
return noErr;
}
The hotkey is registered with the RegisterEventHotKey(…) call. In this case it registers CMD + Shift + C.
The ANSI keys are defined in HIToolbox/Events.h, so you can look around there for other keys (just press CMD + Shift + O in XCode, and type Events.h to find it).
You have to do a bit more work if you want multiple hotkeys or if you want to call methods from your handler, but that's all in the link near the top of this answer.
I've been looking for a simple answer to this question, so I hope this helps someone else...
Take a look at addGlobalMonitorForEventsMatchingMask:handler: class methods of NSEvent. Also you may find Shortcut Recorder handy.

Resources