Capture the space key in a window containing several NSTextFields - objective-c

I have a large window with a number of NSTextField fields on it, and I would like to intercept the space bar even while any of the text fields have focus, so that pressing space at any time will cause a separate action.
Subclassing NSTextField and overriding -keyUp: allows me to detect the space key, but a blank is still inserted in the text field. -keyDown: does not fire at all.
For other keys like Return and the arrow keys, I could use the control:textView:doCommandBySelector: delegate method, but it does not fire with the space bar.
There's a lot of advice out there for NSTextView, but I have found none for NSTextField.

Have you experimented with adding an event monitor? You create them via the NSEvent class method addLocalMonitorForEventsMatchingMask(_:handler:)
and they give you first dibs on the events you specify. In your case you'd add a monitor for keyDown events, then in the associated block you decide what to do with them. At the end of the block you can return the event just as it arrived, in which case it will behave normally, you can swallow the event (return nil), or you can modify the event by creating a new event object and returning that instead. Here's an example:
// You can add monitors anywhere - I've chosen the AppDelegate for
// this simple example.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask
handler:^NSEvent * (NSEvent * theEvent) {
if (theEvent.keyCode == 49) {
NSLog(#"swallowing spacebar");
theEvent = nil;
}
return theEvent;
}];
}

Since the behavior you're trying to implement is at the level of the window (or perhaps a view that's an ancestor of all of the text fields), you should consider implementing -performKeyEquivalent: at that level instead of trying to capture the space key in each individual text field.
In your implementation, check the event's modifierFlags to make sure none of Control, Option, or Command are pressed (assuming you only want to handle unmodified space key presses) and if its charactersIgnoringModifiers equals #" ". If so, do what you want and then return YES to indicate you've handled the key event. Otherwise, return NO to allow it to be handled normally.

If you know how to do it for NSTextView, then you know how to do it for NSTextField, because NSTextField uses an NSTextView called the "field editor" for editing. You can get the window's default field editor like so:
NSTextView* fieldEd = [myWindow fieldEditor: YES forObject: nil];
You can also use a window delegate to provide a custom field editor.

So you might want to look into ReactiveCocoa
Go to the video on this page and look at 5:14 to see something similar to what you might like.
ReactiveCocoa can be hooked into all your text fields and then any time a space is hit a signal handler could pick it up. (Probably, however, more than you want to get into).

Related

Detecting when NSTextField becomes active

To be specific I would like to receive notifications when an NSTextField gets focus, or when the user clicks on it and is about to start editing.
I've tried the textDidBeginEditing: method but this is only fired when the user actually starts to type and I need it when the text field becomes the first responder.
I've tried overriding -(BOOL)becomeFirstResponder but that's only called when the text field first becomes the first responder and not any time after that.
I've tried registering for NSControlDidBeginEditing notifications but never got any.
Is this possible?
Implement the window's delegate method windowWillReturnFieldEditor:toObject:. This tells you that the field editor is switching to a different object. Look to see if this object is the text field.
Alternatively, subclass NSWindow and override makeFirstResponder:. Call super, look to see what responder is becoming first responder and whether the call to super returned YES, and return the result of the call to super.
Just to be clear #matt's answer put me on the right path; I thought I just ought to clarify exactly how you can do this.
Aim
So I wanted to have an NSTextField subclass that would know when it became active (i.e. first responder), and then notify it's delegate.
Problem
It turns out the under the hood of OS X text editing is a messy world and you can't really rely on NSTextField to do it all. Basically when an object that is involved in text editing becomes the first responder, something (the window, the system, the NSApplication) gives it an _NSKeyboardClipView (I think it's called that...) as a subview. In turn the _NSKeyboardClipView has an NSTextView as a subview, and it's the NSTextView that becomes the first responder.
Solution
• Subclass (or extend) NSWindow and override the makeFirstResponder: method.
• Fire a notification using NSNotificationCenter who's object is the responder object that is passed to the makeFirstResponder: method.
• Catch the notification in your NSTextField subclass.
• Here's the horrible bit: you need to check that the notification.object is, a) a subclass of NSView and, b) the notification.object.superview.superview == self (you will have to cast to NSView here because the object will be of type id). e.g:
- (void)didBecomeFirstResponder:(NSNotification *)note
{
if ([note.object isKindOfClass:[NSView class]] && [[(NSView *)note.object superview] superview] == self) {
// You just became the first responder
}
}
It's horrible and tacky/hacky but it does work.

How to correctly handle <Return> + modifier keys in a NSTextField?

I want to be able to handle three different key combinations when a user is editing a NSTextField:
↩
⌘↩
⇧↩
Each of these combinations will be responded to differently.
Ideally, I would like to achieve this through IB alone. However, my understanding is that Cocoa only allows one sent action per control.
I can bind this sent action to an IBAction and set the NSTextField to only send it when the return key is pressed, but I can't find a way to get which modifier keys are currently held down (as the action only receives (id)sender, not an event object). Is that possible?
Alternatively, I guess it would be possible to subclass NSTextField, override keyDown:, and manually run the appropriate actions. However, I can't think of how to wire this up correctly in IB. (I could use an IBOutlet for the target, but I can't think of a way to wire up a specific method.) I'm also worried about the efficiency of this approach.
What is the normal way of handling certain key events on an NSTextField?
The approach which I'm leaning towards at the moment is writing a custom protocol and putting an outlet on the text field subclass:
#property (weak, nonatomic) IBOutlet id<TextFieldReturnHandler> target
which could then be called from the keypress event. This seems really convoluted, though.
You can wire the sent action up to a method on your controller:
- (IBAction)cellDidEndEditing:(id)sender;
If you only want this method to be called when the return key is pressed, and not, eg, when the user clicks away, you can set the Action property on the NSTextField in IB to 'Send On Enter Only' instead of 'Send On End Editing'.
Within this method, you can access the event with:
NSEvent *evt = [NSApp currentEvent];
After checking whether the event is valid, you can obtain and act on the necessary modifier flags:
if (evt && evt.type == NSKeyDown) {
evt.modifierFlags // NSShiftKeyMask, NSCommandKeyMas, NSAlternateKeyMask
// Perform actions
}

Scrolling process status window

As part of a Mac application I am working on, the user fills out a screen full of stuff and then presses a 'process' button. There are edits performed and if everything passes the edit, a couple of minute process is performed which either end ok or not. I would like to have that process spit out a series of status and processing messages into a separate scrolling window so that if something goes bad, the user can go back through the log and see if anything shows up there.
What would be the best objects and methods for me to review and use for this type of processing?
Added 11/24/2011
As per the first suggestion, I created a second XIB, created a NSWindowController to match and put it all together as some prep work. When the button in pressed in the app delegate, I have this thing do the following:
- (IBAction)runButtonPressed:(id)sender {
RunResultWindow *wc;
wc = [[RunResultWindow alloc] initWithWindowNibName:#"RunResultWindow"];
[wc showWindow:self];
}
RunResultWindow is the name of the XIB and the NSWindowController class that controls it. I also added a finish button and wired that up with the intention of having the results of the process fill up the text window and then hang there until the user presses 'done' or 'finish' or whatever I wind up calling the button.
It actually shows the window when I press the button on the main window but when the code for the button finishes, the window vanishes. Clearly I am leaving out (an important) step.
Once I get the window then I can add the text view etc.... and get that working. What I would like is for the new Window to get focus and then close out when the user presses the 'done' button.
Additionally, I got the window for the window controller from the window method (it returned an address) and tried a couple of window focus methods in the windowDidLoad method of the NSWindowController but no dice.
Thanks again for whatever info I can get on this.
Added 11/25/2011
Duh. Maybe if I make the class instance an ivar instead of embedding it in the button method it will work and, lo, it did. Le Oops.
Sounds like you want to drop a NSTextView into a window where one can select & scroll the text but not edit the contents.
You can insert text as easily as using the insertText: method.
So... The NSTextView and insertText combination worked out somewhat ok but I don't' think it is the final answer. First, my understanding is that insertText is really only meant for user input and not background 'system' input to a NXTextStorage object. I'm not sure why but that's fine so I'll avoid it. There are other options. I did find the beginEdit and endEdit methods and it works pretty much about the same way though I have some more work to do on some detail delegate methods.
The part that doesn't work so well is getting the NSScrollView in the window to update on demand. I do the beginEdit and endEdit stuff and am able to update the NSTextStorage object properly. I can do this multiple times in the same method (a test button on the window containing the scroll view). I can tell that because print-object in debug shows me what I'm expecting at the right times. However, I'd like to be able to show an updated NSScrollView multiple times during the court of the windowDidLoad method. The scroll view updates properly when the button push method ends.
Here is some sample code. I do mix insertText and the begin/end edit methods in here but it was more of a test thing than any code I would use for real....
(IBAction)FinishButtonPush:(id)sender {
NSString *teststring;
teststring = [NSString stringWithString: #"show"];
[RunResultWindowTextView setString:teststring];
teststring = [NSString stringWithString: #"show show"];
[RunResultWindowTextView setString:teststring];
teststring = [NSString stringWithString: #"show show show show"];
[RunResultWindowTextView setString:teststring];
[RunResultWindowTextView insertText:#"123"];
NSTextStorage *tempTextStorage;
tempTextStorage = [RunResultWindowTextView textStorage];
[tempTextStorage beginEditing];
[tempTextStorage replaceCharactersInRange:NSMakeRange(5,13)
withString:#"Hello to you!"];
[tempTextStorage endEditing];
[tempTextStorage beginEditing];
[tempTextStorage replaceCharactersInRange:NSMakeRange(10,13)
withString:#"second change"];
[tempTextStorage endEditing];
[RunResultWindowTextView insertText:#"xxx123"];
[RunResultWindowTextView insertText:#"xxx123567"];
}
Even though the NSTextStorage object is updated properly, the scroll view only updates when the method completes. My understanding is that processEdit is called automatically during endEdit. I added processEdit in there just to see and all I got was either abends or no change depending on where I put the command.
This got deleted and I'm not sure why. If you're going to gong the post, please let me know why you did so. Can't improve my post unless I have an idea what was wrong with it....

Trouble accessing NSTextField from method called by a Custom View

I'm almost done writing an image editing program, but ran into a problem that should have a simple solution.
Basically, I've built a set of buttons and NSTextFields and a Custom View into the main xib, just dropping them straight off the Library into the default window (and then, of course, linking them up with IBOutlets and IBActions). One of the buttons is an "Open" button that calls a function. The function does several things: it runs a NSOpenPanel, and then changes some of the NSTextFields (used for changing image name and path). That code is called as follows:
- (IBAction)openButtonPressed: (id)sender {
[self runOpenPanel];
}
Now I also happen to be running my keyDown handler from the Custom View, as I've told it to acceptFirstResponder. (I know, it's probably bad practice to not write a separate Controller class, but that's best left for another time.) So my keyDown event looks like this (simplified, as in the actual code I have has if statements to handle certain keys separately that don't pertain to this question):
- (void)keyDown: (NSEvent *)theEvent {
[self runOpenPanel];
}
So they both use [self runOpenPanel] but from different contexts. The problem I'm having is that "runOpenPanel" makes a few calls to change IBOutlets, like this:
-(void)runOpenPanel {
// Omitting some of the trivial NSOpenPanel code
[myTextField setStringValue: #"The file name NSString from the aforementioned omitted code."];
}
So myTextField updates when the function is run by an Interface Builder button, but not when run by the Custom View's keyDown handler. Is there some way to call runOpenPanel that will allow it to access myTextField? I've tried using [[super self] runOpenPanel] (don't laugh) and a number of other things. Thanks in advance!

NSMenuItem and bare esc key equiv

I'm trying to bind a bare esc key press to an NSMenuItem that toggles full screen (currently just a stub function). Manually selecting the menu item sends the desired IBAction. When I set the NSMenuItem's Key Equiv. in Interface Builder to an arbitrary bare key (eg. w) that key command sends the desired IBAction. When I set the Key Equiv. to command + esc, that key command sends the desired IBAction. But a bare esc key press is ignored.
I'm assuming the esc key is special-cased. Other applications (eg. Bannister's various emulators) are able to achieve this, any idea how?
I'm no Objective-C veteran, so apologies if I'm misunderstanding the question. But have you tried moving up the responder chain and grabbing the keyDown event in NSWindow? Something like:
- (void)keyDown: (NSEvent *) event {
if ([event keyCode] == 53) {
NSLog(#"Esc. pressed");
}
}
Of course, this solution will require that you subclass NSWindow.
The Escape key is tightly bound to the cancelOperation class of NSResponder.
Try to subclass your NSWindow and give it this method:
- (void)cancelOperation:(id)sender {
if (![SomeController doSomeAction]) {
[super cancelOperation:sender];
}
}
Your window will then react to the Escape key and evoke doSomeAction. That would be the method your NSMenuItem would have liked to call, but it refused to do so :)
In the doSomeAction method you should return a Boolean that indicates whether the action did actually do something. If it did, good. If it did not (and returned NO), your NSWindow will pass on the Escape key event to the next responder in the chain.
I like this solution because it gives the user audio feedback about whether his key press actually did something. Because if your method didn't do anything, and no other repsonder did do anything, there will be some "beep" sound.
Note that in an NSView without an attached window, you might have to use [self nextResponder] instead of super.
Please have a look at http://developer.apple.com/mac/library/documentation/UserExperience/Conceptual/AppleHIGuidelines/XHIGUserInput/XHIGUserInput.html
“… The Esc (Escape) key basically means “let me out of here.” It has specific meanings in certain contexts. The user can press Esc in the following situations: …”
Greetings