Keyboard Shortcuts for NSSegmentedControl Button in COCOA Mac Application - macos

In my COCOA Mac Application, there is one NSSegmentedControl Button for Queue ON/OFF functionality. Now I need to provide keyboard shortcut for the same. For example, for Queue ON if already it was Off and vice versa.
Is anyone did this type of functionality?
Thanks and Regards,
Barun

Since I didn't find a way to add a keyboard binding to the NSSegmentedControl via interface builder, I did id programmatically. I created a custom Window class named PMWindow:
#implementation PMWindow
- (BOOL)acceptsFirstResponder {
return YES;
}
- (void)keyDown:(NSEvent *)theEvent {
if([self.viewController.lastNextControl isEnabled]) {
if([theEvent keyCode] == 123) {
[self.viewController last];
} else if([theEvent keyCode] == 124) {
[self.viewController next];
}
}
}
#end
I added my view controller named PMViewController via the interface builder to the Window class PMWindow. In this example I'm reacting to the key strokes < left arrow > (123) and < right arrow > (124).
For completeness of this example I added here the implementation of the method which gets called when somebody hits a cell of my NSSegmentedControl element:
- (IBAction)lastOrNext:(id)sender {
switch ([sender selectedSegment]) {
case 0:
[self last];
break;
case 1:
[self next];
break;
}
}

Related

Prevent QLPreviewView from grabbing focus

I have a list of files. Next to it I have a QLPreviewView which shows the currently selected file.
Unfortunately QLPreviewView loads a web view to preview bookmark files. Some web pages can grab keyboard focus. E.g. the Gmail login form places the insertion point into the user name field.
This breaks the flow of my application. I want to navigate my list using arrow keys. This is disrupted when keyboard focus is taken away from the table view.
So far the best I could come up with is to override - [NSWindow makeFirstResponder:] and not call super for instances of classes named with a QL prefix. Yuck.
Is there a more reasonable way to
Prevent unwanted changes of first responder?
or prevent user interaction on QLPreviewView and its subviews?
I ended up using a NSWindow subclass that allows QLPreviewViews and its private subviews to become first responder on user interaction, but prevents these views from simply stealing focus.
- (BOOL)makeFirstResponder:(NSResponder *)aResponder
{
NSString *classname = NSStringFromClass([aResponder class]);
// This is a hack to prevent Quick Look from stealing first responder
if ([classname hasPrefix:#"QL"]) {
BOOL shouldMakeFirstRespnder = NO;
NSEvent *currentEvent = [[NSApplication sharedApplication] currentEvent] ;
NSEventType eventType = currentEvent.type;
if ((eventType == NSLeftMouseDown) || (eventType == NSRightMouseDown) || (eventType == NSMouseEntered)) {
if ([aResponder isKindOfClass:[NSView class]]) {
NSView *view = (NSView *)aResponder;
NSPoint locationInWindow = currentEvent.locationInWindow;
NSPoint locationInView = [view convertPoint:locationInWindow fromView:nil];
BOOL pointInRect = NSPointInRect(locationInView, [view bounds]);
shouldMakeFirstRespnder = pointInRect;
}
}
if (!shouldMakeFirstRespnder) {
return NO;
}
}
return [super makeFirstResponder:aResponder];
}
Maybe you can subclass QLPreviewView and override its becomeFirstResponder so that you can either enable or disable it when your application should allow it to accept focus.
Header
#interface MyQLPreviewView : QLPreviewView
#end
Implementation
#implementation
- (BOOL)becomeFirstResponder
{
return NO;
}
#end

How to deal with first responder and a NSPopover

I’m trying to replicate the behaviour of the search field in iTunes, for looking up stock symbols and names. Specifically, as you start typing in the search field a popover appears with the filtered items. For the most part I have this working however what I can’t replicate is the way it handles first responder
I have my popover appear after three characters are entered. At this point the NSSearchField would lose first responder status and therefore I could no longer continue typing. The behaviour I would like is
the ability to continue typing after the popover appears
if scrolling through the items with the arrow keys, and then resume typing, you would continue from the last character in the Search field.
What I tried is subclassing NSTextView (use this as the custom field editor for the NSSearchField) and overriding
- (BOOL)resignFirstResponder
By simply returning NO, I can continue typing once the popover appears, but obviously I can’t select any of the items in the popover. So i tried the following, which returns YES if the down arrow or a mousedown event occurs.
#interface SBCustomFieldEditor ()
{
BOOL resignFirstRepond;
}
#end
#implementation SBCustomFieldEditor
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
resignFirstRepond = NO;
}
return self;
}
- (BOOL)resignFirstResponder
{
return resignFirstRepond;
}
- (void)keyDown:(NSEvent *)theEvent
{
if ([theEvent keyCode] == 125) {
resignFirstRepond = YES;
[self resignFirstResponder];
}
[super keyDown:theEvent];
}
- (void)mouseDown:(NSEvent *)theEvent
{
resignFirstRepond = YES;
[self resignFirstResponder];
}
This works for the mousedown event, but not the keydown event, furthermore this doesn’t address the issue, when the user resumes typing.
Any suggestions?
In the meantime I found an easy fix. Subclass your text view and implement - (BOOL)canBecomeKeyView. Always return NO there. It will be called only once when the popover is shown. You can work with the text view any time still.

How can I handle ESC key in Cocoa app?

I made an app switching to full screen mode. I want to use ESC key to escaping fullscreen mode, but binding menu item to ESC key in IB is removed at runtime. How can I keep ESC key binding to a menu item?
Preferred way to handle escape key in Cocoa is this as like #Josh Caswell said.
#pragma mark - NSResponder
- (void)cancelOperation:(id)sender
{
[self exitFullScreen];
}
One way to capture keyboard events involves subclassing:
Subclass your full-screen class (e.g.) NSView.
Add the method - (void) keyDown:(NSEvent *)theEvent to the subclass implementation.
Open up InterfaceBuilder and select the full-screen class that you previously created.
Change its class to your new subclass.
The subclass looks something like:
MySubclass.h
#interface MySubclass : NSView {
}
#end
MySubclass.m
#import <Carbon/Carbon.h>
#implementation MySubclass
- (void)keyDown:(NSEvent *)theEvent
{
switch([theEvent keyCode]) {
case kVK_Escape:
NSLog(#"ESC");
// Call the full-screen mode method
break;
default:
[super keyDown:theEvent];
}
}
#end
This doesn't bind the ESC key to the menu item, but it does give you equivalent functionality (and a bit more flexability since you can intercept all keyboard events).
Many people try to implement esc key functionality. There is cancelOperation in the responder chain to handle escape events.
//WRONG
- (void)keyDown:(NSEvent *)event
{
//unichar character = 0;
//if ([event type] == NSEventTypeKeyDown) {
// if ([[event charactersIgnoringModifiers] length] == 1) {
// character = [[event characters] characterAtIndex:0];
// }
//}
switch (character) {
//THIS IS WRONG correct is to implement interpretKeyEvents+moveRight
//case NSRightArrowFunctionKey:
// [self moveSelectedIndexRight];
// break;
//THIS IS WRONG correct is to implement interpretKeyEvents+ moveLeft
//case NSLeftArrowFunctionKey:
// [self moveSelectedIndexLeft];
// break;
//THIS IS WRONG correct is to implement interpretKeyEvents+ moveLeft
//case NSCarriageReturnCharacter:
// [self dismissWithCurrentlySelectedToken];
// break;
default:
[self interpretKeyEvents:#[event]];
[super keyDown:event]
break;
}
}
//CORRECT
- (void)keyDown:(NSEvent *)event
{
[self interpretKeyEvents:#[event]];
[super keyDown:event];
}
`/* Catch the commands interpreted by interpretKeyEvents:. Normally, if we don't implement (or any other view in the hierarchy implements) the selector, the system beeps. Menu navigation generally doesn't beep, so stop doCommandBySelector: from calling up t`he hierarchy just to stop the beep.
*/
- (void)doCommandBySelector:(SEL)selector {
if ( selector == #selector(moveRight:)
|| selector == #selector(moveLeft:)
|| selector == #selector(cancelOperation:)
|| selector == #selector(insertNewline:) )
{
[super doCommandBySelector:selector];
}
// do nothing, let the menu handle it (see call to super in -keyDown:)
// But don't call super to prevent the system beep
}
- (void)cancelOperation:(id)sender
{
//do your escape stuff
}
- (void)insertNewline:(id)sender
{
//do your enter stuff
}
- (void)moveRight:(nullable id)sender
{
[self moveSelectedIndexRight];
}
- (void)moveLeft:(nullable id)sender
{
[self moveSelectedIndexLeft];
}
I needed to dodge WKWebView crashes when ESC is pressed(?) so I sub-class it, and added:
import Carbon.HIToolbox
override func keyDown(with event: NSEvent) {
if event.keyCode == UInt16(kVK_Escape) {
// We crash otherwise, so just close window
self.window?.performClose(event)
}
else
{
// still here?
super.keyDown(with: event)
}
}

NSTableView & NSOutlineView editing on tab key

My app has an NSOutlineView and an NSTableView, and I'm having the same problem with both. With a row in either selected, pressing the tab key puts the first column into edit mode instead of making the next key view first responder. To get to the next key view, you need to tab through all of the columns.
Also, shift-tabbing into either view results in the last column going into edit mode, necessitating more shift-tabs to get into its previous key view.
In case it matters, I'm using the autocalculated key view loop, not my own, with my NSWindow set to autorecalculatesKeyViewLoop = YES. I would like tabbing between the columns once the user elects to edit a column, but I don't think it's standard behavior for the tab key to trigger edit mode.
Update
Thanks to the helpful responses below, I worked it out. Basically, I override -keyDown in my custom table view class, which handles tabbing and shift-tabbing out of the table view. It was tougher to solve shift-tabbing into the table view, however. I set a boolean property to YES in the custom table view's -acceptsFirstResponder if it's accepting control from another view.
The delegate's -tableView:shouldEditTableColumn:row checks for that when the current event is a shift-tab keyDown event. -tableView:shouldEditTableColumn:row is called and it's not a shift-tab event, it sets the table view's property back to NO so it can still be edited as usual.
I've pasted the full solution below.
/* CustomTableView.h */
#interface CustomTableView : NSTableView {}
#property (assign) BOOL justFocused;
#end
/* CustomTableView.m */
#implementation CustomTableView
#synthesize justFocused;
- (BOOL)acceptsFirstResponder {
if ([[self window] firstResponder] != self) {
justFocused = YES;
}
return YES;
}
- (void)keyDown:(NSEvent *)theEvent
{
// Handle the Tab key
if ([[theEvent characters] characterAtIndex:0] == NSTabCharacter) {
if (([theEvent modifierFlags] & NSShiftKeyMask) != NSShiftKeyMask) {
[[self window] selectKeyViewFollowingView:self];
} else {
[[self window] selectKeyViewPrecedingView:self];
}
}
else {
[super keyDown:theEvent];
}
}
#end
/* TableViewDelegate.m */
. . .
- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn
row:(NSInteger)row
{
NSEvent *event = [NSApp currentEvent];
BOOL shiftTabbedIn = ([event type] == NSKeyDown
&& [[event characters] characterAtIndex:0] == NSBackTabCharacter);
if (shiftTabbedIn && ((CustomTableView *)tableView).justFocused == YES) {
return NO;
} else {
((CustomTableView *)tableView).justFocused = NO;
}
return YES;
}
. . .
This is the default behavior. If there's no row selected, the table view as a whole has focus, and the Tab key switches to the next key view. If there is a row selected, the table view begins editing or moves to the next cell if already editing.
From AppKit Release Notes:
Tables now support inter-cell
navigation as follows:
Tabbing forward to a table focuses the entire table.
Hitting Space will attempt to 'performClick:' on a NSButtonCell in
the selected row, if there is only one
instance in that row.
Tabbing again focuses the first "focusable" (1) cell, if there is one.
If the newly focused cell can be edited, editing will begin.
Hitting Space calls 'performClick:' on the cell and sets the datasource
value afterwards, if changed. (2)
If a text cell is editing, hitting Enter will commit editing and focus
will be returned to the tableview, and
Tab/Shift-tab will commit the editing
and then perform the new tab-loop
behavior.
Tabbing will only tab through a single row
Once the last cell in a row is reached, tab will take the focus to
the next focusable control.
Back tabbing into a table will select the last focusable cell.
If you want to change this behavior, the delegate method tableView:shouldEditTableColumn:row: may be helpful. You may also have to subclass NSTableView if you really want to affect only the behavior of the Tab key.
The solution using keyDown didn't work for me. Perhaps because it is for cell-based table view.
My solution for a view-based table view, in Swift, looks like this:
extension MyTableView: NSTextFieldDelegate {
func controlTextDidEndEditing(_ obj: Notification) {
guard
let view = obj.object as? NSView,
let textMovementInt = obj.userInfo?["NSTextMovement"] as? Int,
let textMovement = NSTextMovement(rawValue: textMovementInt) else { return }
let columnIndex = column(for: view)
let rowIndex = row(for: view)
let newRowIndex: Int
switch textMovement {
case .tab:
newRowIndex = rowIndex + 1
if newRowIndex >= numberOfRows { return }
case .backtab:
newRowIndex = rowIndex - 1
if newRowIndex < 0 { return }
default: return
}
DispatchQueue.main.async {
self.editColumn(columnIndex, row: newRowIndex, with: nil, select: true)
}
}
}
You also need to set the cell.textField.delegate so that the implementation works.
My blog post on this tricky workaround: https://samwize.com/2018/11/13/how-to-tab-to-next-row-in-nstableview-view-based-solution/
I've had to deal with this before as well. My solution was to subclass NSTableView or NSOutlineView and override keyDown: to catch the tab key presses there, then act on them.
How convenient! I was just looking at this myself yesterday, and it's good to see some confirmation of the approach I took - keyDown: handling.
However, I have one small possible refinement to your approach: I worked out that the method triggering editing on shift-tabbing back to the table was the becomeFirstResponder call. So what I did on a NSTableView subclass was:
Add a synthesized property to control whether tab-editing behaviour was disabled
On keydown, check the first character (also check for [[theEvent characters] length] to avoid exceptions for dead keys!) for tab; if tab editing is disabled, move on to the next/previous view, as per your code sample.
Override becomeFirstResponder:
- (BOOL)becomeFirstResponder {
if (tabEditingDisabled) {
[self display];
return YES;
}
return [super becomeFirstResponder];
}
This keeps all the code in the tableview subclass, keeping the delegate cleaner :)
The only danger is I don't know what else NSTableView does in becomeFirstResponder; I didn't notice anything breaking, but...
This worked for me:
- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
NSEvent *e = [NSApp currentEvent];
if (e.type == NSKeyDown && e.keyCode == 48) return NO;
return YES;
}

How to force NSToolBar validation?

I'm geting this strange behavior. I'm using a panel with text to show to the user when the app is waiting for some info. This panel is show modally to prevent the user to click something.
When the loading panel is hidden all the items on the toolbar are disabled and the validateToolbarItem method is not called.
I'm showing the panel in this way:
- (void)showInWindow:(NSWindow *)mainWindow {
sheetWindow = [self window];
[self sheetWillShow];
[NSApp beginSheet:sheetWindow modalForWindow:mainWindow modalDelegate:nil didEndSelector:nil contextInfo:nil];
[NSApp runModalForWindow:sheetWindow];
[NSApp endSheet:sheetWindow];
[sheetWindow orderOut:self];
}
- (void)dismissModal {
[sheetWindow close];
[NSApp stopModal];
}
How can I force the toolbar to validate in this case?
Edit after comment:
I have already tried:
[[[NSApp mainWindow] toolbar] validateVisibleItems]
[[NSApp mainWindow] update];
[NSApp updateWindows];
[NSApp setWindowsNeedUpdate:YES];
All after call dismissModal. I'm thinking that the problem is elsewhere....
The problem is that NSToolbar only sends validation messages to NSToolbarItem's that are of Image type, which none of mine were. In order to validate any or all NSToolbarItems's, create a custom subclass of NSToolBar and override the validateVisibleItems: method. This will send validation messages to ALL visible NSToolbarItem's. The only real difference is that instead of having the Toolbar class enable or disable the item with the returned BOOL, you need to enable or disable the item in the validation method itself.
#interface CustomToolbar : NSToolbar
#end
#implementation CustomToolbar
-(void)validateVisibleItems
{
for (NSToolbarItem *toolbarItem in self.visibleItems)
{
NSResponder *responder = toolbarItem.view;
while ((responder = [responder nextResponder]))
{
if ([responder respondsToSelector:toolbarItem.action])
{
[responder performSelector:#selector(validateToolbarItem:) withObject:toolbarItem];
}
}
}
}
#end
Now, assume you have a controller with an IBAction method that handles Actions for a NSSegmentedControl in your toolbar:
- (IBAction)backButton:(NSSegmentedControl*)sender
{
NSInteger segment = sender.selectedSegment;
if (segment == 0)
{
// Action for first button segment
}
else if (segment == 1)
{
// Action for second button segment
}
}
Place the following in the same controller that handles the toolbar item's Action:
-(BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem
{
SEL theAction = [toolbarItem action];
if (theAction == #selector(backButton:))
{
[toolbarItem setEnabled:YES];
NSSegmentedControl *backToolbarButton = (NSSegmentedControl *)toolbarItem.view;
[backToolbarButton setEnabled:YES forSegment:0];
[backToolbarButton setEnabled:NO forSegment:1];
}
return NO;
}
The result is that you have complete control over which segments are enabled or disabled.
This technique should be applicable to almost any other type of NSToolbarItem as long as the item's Received Action is being handled by a controller in the responder chain.
I hope this helps.
NSToolbar *toolbar; //Get this somewhere. If you have the window it is in, call [window toolbar];
[toolbar validateVisibleItems];

Resources