Objective-C on macOS - get keyDown: for Shift+Tab in NSView - objective-c

I have an application that processes keystrokes via keyDown.
This works well so far, with the exception of two problems (I'll post/ask the other later).
The problem here is the Shift+Tab combo. I would like to process it myself, but as explained in the Mac OS keyboard event doc, it is used to move the focus forward.
I tried capturing it under
-(BOOL)performKeyEquivalent:(NSEvent *)nsevent
{
}
where I do get an event for it, returning TRUE or FALSE but it still does not appear in keyDown:
I also know about selectNextKeyView: but ideally I am looking for a way to get the system to pass the combination to keyDown: for normal handling (incidentally keyUp: is called correctly for it).
-(void)selectNextKeyView:(id)sender
{
// shift+tabbed
}

Answering my own question:
I solved it by intercepting selectNextKeyView and selectPreviousKeyView and forwarding the [NSApp currentEvent] to my keyboard handler (the one that is called on keyDown: for all other keys)
-(void)selectNextKeyView:(id)sender
{
// this is part of the keyboard handling. Cocoa swallows ctrl+Tab and
// calls sselectNextKeyView, so we create a matchin key event for the occasion
NSEvent *nsevent= [NSApp currentEvent];
[self handleKeyEvent:nsevent isRaw:FALSE isUp:FALSE];
}
-(void)selectPreviousKeyView:(id)sender
{
// this is part of the keyboard handling. Cocoa swallows ctrl+shift+Tab and
// calls selectPreviousKeyView, so we create a matchin key event for the occasion
NSEvent *nsevent= [NSApp currentEvent];
[self handleKeyEvent:nsevent isRaw:FALSE isUp:FALSE];
}

Related

How to send app to background

On tvOS, I've only been able to get the begin states of button presses from the Siri remote by overriding the pressesBegan method on the view. If I use gesture recognizers, it only returns the end state. The catch is, when I override pressesBegan, even if I only use it for the select button, it still overrides the default function of the menu button (to push the app to the background). So I was looking into how to send the app to the background and call that method for the menu button (as is default behavior), but it appears that it is not kosher per Apple's standards to do that.
Here is my code for reference:
-(void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
for (UIPress* press in presses) {
switch (press.type) {
case UIPressTypeSelect:
NSLog(#"press began");
break;
case UIPressTypeMenu:
// this is where I would call the send to background call if Apple would allow that
// removing this case also has no effect on what happens
break;
default:
break;
}
}
As an alternative, this ONLY sends button release signals, but nothing presses begin.
UITapGestureRecognizer* tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(gestureTap:)];
tapGesture.allowedPressTypes = #[[NSNumber numberWithInteger:UIPressTypeSelect]];
[view addGestureRecognizer:tapGesture];
When there's some behavior that happens if you don't override a method, and the behavior goes away in an empty override implementation, it stands to reason that behavior is provided by the superclass. (Cocoa is dynamic and complicated, so such inferences aren't true 100% of the time, but often enough.)
So, just call super for the cases where you don't want your override to change the default behavior:
case UIPressTypeMenu:
[super pressesBegan: presses withEvent: event];
break;

Menu Bar App Never Becomes Reactivated

I'm building a Mac app that only sits in the menu bar with no dock item and no key window and no main menu (it's LSUIElement in the info.plist is set to YES). When I first launch the app, applicationDidBecomeActive: is called, as I expect. However, once another app gains focus, applicationDidBecomeActive: is never called again.
This prevents a text field I have within my app from becoming the first responder. When I first open the app, the text field is editable:
But after another app comes to the foreground, the text field is not editable:
What I've tried:
When the menu is opened, menuWillOpen: is called on the NSMenu's delegate. I've tried placing the following with no success:
[NSApp unhide];
[NSApp arrangeInFront:self];
[NSApp activateIgnoringOtherApps:YES];
[NSApp requestUserAttention:NSCriticalRequest];
[[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
[[NSRunningApplication currentApplication] unhide];
I think the issue is probably related to not having any windows to bring to the front. I feel like I'm grasping at straws here. Any help would be greatly appreciated.
I think the issue is with that how the runloop operates when a NSMenu is open, so you should try activating the app before you display the menu. If you're having the NSStatusItem display it, I'd suggest doing it yourself like this:
- (void)toggleMenu:(id)sender
{
// App might already be active
if ([NSApp isActive]) {
[self.statusItem popUpStatusItemMenu:self.menu];
} else {
[NSApp activateIgnoringOtherApps:YES];
}
}
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
[self.statusItem popUpStatusItemMenu:self.menu];
}
That should work, but I think though in general you'll have better luck with an actual window instead of a menu.
You probably need to allow your input to -becomeFirstResponder, maybe by overriding -canBecomeFirstResponder or by calling the become method yourself.
You'd likely have to implement/call these methods for whatever view is housing your text input, or maybe tell your input view to become the first responder.
Either way, it smells like a responder chain issue.
Try calling -makeFirstResponder: on your window. NSWindow is usually the start of the NSResponder chain.
- (void)menuWillOpen:(NSMenu *)menu {
[[NSApp mainWindow] makeFirstResponder:yourTextInputField];
}
I'm assuming your text field already accepts first responder since you said your app launches initially with it as the first responder. If not, make sure your text field overrides -acceptsFirstResponder: to return YES
- (BOOL)acceptsFirstResponder {
return YES;
}
Edit: Ah, see that you don't have a key window. It looks like NSMenu actually has a window associated with it though, and it's safe to call -makeFirstResponder:. Some discussion here suggests overriding -viewDidMoveToWindow: on your view containing your text field in the NSMenu like so:
- (void)viewDidMoveToWindow {
[super viewDidMoveToWindow];
[[self window] makeFirstResponder:yourTextInputField];
}

keyDown:(NSEvent *)event is not invoked when the focus is on a text field

I've overridden - (void)keyDown:(NSEvent *)event in my NSPanel subclass.
However it is invoked only if the focus is not on a NSTextField inside my panel.
However I need to catch the event "Enter button pressed" regardless of if the focus is on the text field or the panel.
How to make sure it is always invoked?
Are you sure that you need to catch the key down event for that?
Apple state in the docs that fiddling with keyDown: for controls is kind of a last resort, to be used only if the normal Cocoa architecture around delegates does not do what you want.
If the purpose is to catch the enter button pressed, notice that this event in a text field triggers the textDidEndEditing delegate method (or notification, if you prefer that).
So if you implement controlTextDidEndEditing: in a delegate for your NSTextField you should be able to react to the event. This notification (and the relative delegate method) is sent when the field editor ends editing.
If you prefer to catch the event one step earlier (before the field editor ends editing), you can implement the delegate method control:textView:doCommandBySelector: which lets you intercept specific key events (such as the return key) and modify the behaviour of the editor.
An example could be the following:
- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
{
BOOL retval = NO;
if (commandSelector == #selector(insertNewline:)) {
retval = YES; // Handled
// Do stuff that needs to be done when newLine is pressed
}
return retval;
}
There is a lot of documentation on Apple's site about it, for instance an introduction here.

Can notification from addGlobalMonitorForEventsMatchingMask be canceled?

I have simple global monitor for mouse clicks:
[NSEvent addGlobalMonitorForEventsMatchingMask:NSLeftMouseDownMask handler:^(NSEvent *event){
if ([event type] == NSLeftMouseDown) {
[self mouseDown];
if (self.lockMouse) {
// Cancel event
}
}
}];
Is there any way to cancel global mouse events, so clicking only notifies my app?
I.e.: After clicking on any button on screen (that don't belong to my app) while "locked", event goes here, but not to button that was below cursor. Something like event.preventDefault() in JavaScript.
Not with this API, no. From the documentation, it says that the block is:
The event handler block object. It is passed the event to monitor. You are unable to change the event, merely observe it.
If you want to intercept the event and prevent it from propagating, you need to use a CGEventTap instead.

App modal NSPanel / sheet / dialog + NSThread == window hangs?

I'm in the midst of debugging an extremely unusual problem, and I was wondering if anybody might have any insight into what might be going wrong:
In a controller class from a NIB, I take an NSPanel from that same NIB, and then show it app modally on a NSWindow (that was created by hand in code):
[[NSApplication sharedApplication] beginSheet: myPanel
modalForWindow: window
modalDelegate: self
didEndSelector: #selector(sheetDidEnd:returnCode:contextInfo:)
contextInfo: nil];
[[NSApplication sharedApplication] runModalForWindow: myPanel];
Now, when the "finish" button on that sheet is clicked, I run some code to disable some buttons and fire off a thread to make sure the user input is valid (I have to validate with a remote service). This thread is fired from a separate validator object I create:
// controller calls:
[validator validateCreds: creds
notify: #selector(validationComplete:)
onObject: self];
// validator object
validateInfo: (NSDictionary *)parms
notify: (SEL)notifySelector
onObject: (id)notifyObject
{
// build up data with parms and notify info
[[NSThread detachNewThreadSelector: #selector(remotevalidate:)
toTarget: self withObject: data];
}
Next, when the validation is finished, the validator notifies my controller object:
[notifyObject performSelectorOnMainThread: notifySelector
withObject: results waitUntilDone: NO];
And then my controller object, in the method that the validator object calls, kills the dialog:
- (void)validationComplete: (id)data
{
[[NSApplication sharedApplication] stopModal];
[createTwitterPanel orderOut: nil];
[[NSApplication sharedApplication] endSheet: createTwitterPanel
returnCode: NSOKButton];
}
- (void)sheetDidEnd:(NSWindow *)sheet
returnCode:(int)returnCode
contextInfo:(void *)contextInfo
{
m_returnCode = returnCode;
}
My problem: Although the panel is closed / disappears, the top NSApp runModalForWindow: does not exit until some system event is sent to the window that was showing the dialog. Trying to move, resize, or do anything to the window, or otherwise switching away from the application suddenly causes the method to exit and execution to continue. No amount of waiting seems to help, otherwise, however.
I have verified that all methods being invoked on the controller class are all being invoked on the main app thread.
An even more interesting clue is that the dialog has two controls, a WebView, and an NSTextField: Even if I force the exit of runModalForWindow: by clicking on the window, TABbing between the two controls remains screwed up — it simply never works again. It's like my event loop is horked.
I've tried changing validationComplete: to instead post a notification to the main thread, and I've also played with the waitUntilDone on the performSelectorOnMainThread method, all to no effect.
Any ideas? Things I should try looking at?
From the NSApplication documentation:
abortModal must be used instead of
stopModal or stopModalWithCode: when
you need to stop a modal event loop
from anywhere other than a callout
from that event loop. In other words,
if you want to stop the loop in
response to a user’s actions within
the modal window, use stopModal;
otherwise, use abortModal. For
example, use abortModal when running
in a different thread from the
Application Kit’s main thread or when
responding to an NSTimer that you have
added to the NSModalPanelRunLoopMode
mode of the default NSRunLoop.
So, I learned something today.