"Tab" between controls without tab key - objective-c

I have a window which happily tabs between its controls using the tab and and shift-tab keys. However, I also want to be able to move between controls with the up and down arrow keys (in this case). How can this be achieved?

Tell the window to make the current view's next/previous key view the first responder.

For non-text controls, you should be able to get by with something like this somewhere in the responder chain (e.g., your NSWindowController):
- (void)keyDown:(NSEvent *)event {
NSWindow *window = [self window];
switch ([[event characters] objectAtIndex:0]) {
case NSUpArrowFunctionKey:
[window makeFirstResponder:[[window firstResponder] previousValidKeyView]];
break;
case NSDownArrowFunctionKey:
[window makeFirstResponder:[[window firstResponder] nextValidKeyView]];
break;
default:
[super keyDown:event];
break;
}
}
And in text field delegates:
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command {
NSWindow *window = [control window];
if (command == #selector(moveUp:)) {
[window makeFirstResponder:[[window firstResponder] previousValidKeyView]];
return YES;
} else if (command == #selector(moveDown:)) {
[window makeFirstResponder:[[window firstResponder] nextValidKeyView]];
return YES;
}
return NO;
}
(There's a similar method for text view delegates.)

Related

NSOutlineView reloadData close my NSPopOver

I have a NSOutlineView (View based) with two columns. In the second column i have a NSButton connected to one NSPopOver.
When i click the button, it show the NSPopOver as expected => Popover is visible.
The problem: If i reloadData of NSOutlineView it hide the NSPopover !
Is it the normal behavior ? How to avoid this ?
In other word, popoverWillClose delegate message is called after each reloadData
//OMN_Object.m
#pragma mark - Actions
- (IBAction)togglePopover:(id)sender
{
...
Call App_delegate togglePopover:withTextLog:withTextInputFileDetails:withTextOutputFileDetails:fromObject:
//App_delegate.m
...
[self.myPopOver setDelegate:self];
...
#pragma mark Popover delegate
- (void)popoverWillClose:(NSNotification *)notification
{
NSLog(#"%s *** popoverWillClose %#",__PRETTY_FUNCTION__, self.myPopOver);
}
-(void)togglePopover:(id)sender withTextLog:(NSString*)textLog
withTextInputFileDetails:(NSString*) textInfoInput
withTextOutputFileDetails:(NSString*) textInfoOutput
fromObject:(id)object
{
id v = [[self.myPopOver contentViewController] view] ;
NSTextView *t1 = [v textViewLog];
[t1 setString:textLog];
NSTextView *t2 = [v textViewInputFileDetails];
[t2 setString:textInfoInput];
NSTextView *t3 = [v textViewOutputFileDetailsLastPassFileOnly];
[t3 setString:textInfoOutput];
if (self.myPopOver.isShown == 0) {
NSLog(#"%s Displaying popover %#", __PRETTY_FUNCTION__, self.myPopOver);
[self.myPopOver showRelativeToRect:[sender bounds]
ofView:sender
preferredEdge:NSMaxYEdge];
self.objectOwnerOfPopOver = object;
}
else {
NSLog(#"%s Closing popover %#", __PRETTY_FUNCTION__, self.myPopOver);
[self.myPopOver close];
self.objectOwnerOfPopOver = nil;
}
}
// Outline_view_delegate.m
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item{
...
else if ([[tableColumn identifier] isEqualToString:#"Status"]){
if ([item isKindOfClass:[OMN_Object class]])
{
OMN_Object *o = item;
ObjectStatusTableCellView *v = [outlineView makeViewWithIdentifier:#"StatusCell" owner:self];
....
[v.buttonRevealInFinder setAction:#selector(buttonRevealInFinderClicked:)];
[v.buttonRevealInFinder setTarget:o];
[v.buttonInfo setTarget:o];
[v.buttonInfo setAction:#selector(togglePopover:)];
return v;
}
...
}
When you call reloadData on the outline view the outline view removes all of its subviews (including the button that the popover is attached to) and recreates them. When the button is removed from its superview the popover is closed.
Do you really need to reload all of the outline view data? Could you reload specific rows instead using -reloadDataForRowIndexes:columnIndexes: or -reloadItem:?
Alternatively, rather than passing the button (sender) to -showRelativeToRect:ofView:preferredEdge:m you could try passing the outline view itself.
Solved. I attached the popover to NSWindow view and used NSMouseLocation for popover's position.
NSPoint mouseLoc2 = [self.window mouseLocationOutsideOfEventStream];
NSRect r4 = NSMakeRect(mouseLoc2.x -10, mouseLoc2.y -10, 16, 16);
[self.myPopOver showRelativeToRect:r4
ofView:self.window.contentView
preferredEdge:NSMaxYEdge];

UIMenuController not responding to first selection, only second

I have a view with a long press gesture recognizer in it:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressDetected:)];
[self addGestureRecognizer:longPress];
[longPress release];
}
return self;
}
When the long press is detected, I want to show a UIMenuViewController above the view with a single action in it, and when that menu item is tapped I want to execute a block:
- (void)longPressDetected:(UILongPressGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self becomeFirstResponder];
UIMenuController *menuController = [UIMenuController sharedMenuController];
UIMenuItem *actionItem = [[UIMenuItem alloc] initWithTitle:#"Action" action:#selector(someActionSelector)];
[menuController setMenuItems:[NSArray arrayWithObject:actionItem]];
[actionItem release];
[menuController setTargetRect:self.frame inView:self.superview];
[menuController setMenuVisible:YES animated:YES];
}
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if (action == #selector(copy:) || action == #selector(cut:) || action == #selector(delete:) ||
action == #selector(paste:) || action == #selector(select:) || action == #selector(selectAll:)) {
return NO;
}
else if (action == #selector(someActionSelector)) {
return YES;
}
else {
return [super canPerformAction:action withSender:sender];
}
}
- (void)someActionSelector {
if (self.actionBlock) {
self.actionBlock();
}
}
Problem is, this only works after the second long-press and tap combo. The first time I long-press on the view I see the menu, but tapping the menu does nothing. The second time I see the menu again, I tap it, then the block is executed.
Debugger shows that a breakpoint in someActionSelector is only reached on the second tap. Any idea why this is?
I figured it out. The view listening for a long press is contained inside a view that repositions some subviews when its frame is changed (by overriding setFrame:, which seems like a bad idea but I couldn't think of another way). So when the long press happened it triggered a layoutSubviews in the parent of the parent of the listening view, which set the frame of the parent of the listening view, which repositioned the listening view, which appears to break either the responder chain or deactivate the menu. The solution was to add a condition inside the overridden setFrame: to only trigger the layout if the frame actually changes, which it doesn't on the long press. I'm sure there's a better alternative for listening to frame changes that would avoid this problem entirely—feel free to suggest them in comments.

Override keydownevent in NSTextfield

I made a subclass of the nstextfield and i override the keydown event but my code doesn't work, then i override de keyup event and the code works perfectly.
My keydown code(doesn't work):
-(void)keyDown:(NSEvent *)event {
NSLog(#"Key released: %hi", [event keyCode]);
if ([event keyCode]==125){
[[self window] selectKeyViewFollowingView:self];
}
if ([event keyCode]==126){
[[self window] selectKeyViewPrecedingView:self];
}
}
My keyup code (it works):
-(void)keyUp:(NSEvent*)event
{if ([event keyCode]==125){
[[self window] selectKeyViewFollowingView:self];
}
if ([event keyCode]==126){
[[self window] selectKeyViewPrecedingView:self];
}
if ([event keyCode]==36){
[[self window] selectKeyViewFollowingView:self];
}
}
I don't see where is the problem with my keydown code. Any suggest will be accepted
EDIT:
I have read that you have to subclass NSTextView instead of NSTextField.
You can do it without subclassing by using NSTextFieldDelegate methods:
As #Darren Inksetter said, you can use control:textView:doCommandBySelector:
First, declare NSTextFieldDelegate in your interface tag.
Then implement the method:
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
{
if( commandSelector == #selector(moveUp:) ){
// Do yourthing here, like selectKeyViewFollowingView
return YES; // Return YES means don't pass it along responders chain. Return NO if you still want system's action on this key.
}
if( commandSelector == #selector(moveDown:) ){
// Do the same with the keys you want to track
return YES;
}
return NO;
}
The keydown event can't be overriden in a NSTextField, if you want , you could override the keydown event of the super view or you could use a NSTextView or just override the keyup event in the NSTextField
You will want to look at
control:textView:doCommandBySelector:
http://developer.apple.com/library/mac/#documentation/cocoa/reference/NSControlTextEditingDelegate_Protocol/Reference/Reference.html
Swift 5 example.
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
switch commandSelector {
case #selector(moveUp(_:)):
impl.tableView.doCommand(by: commandSelector)
return true
case #selector(moveDown(_:)):
impl.tableView.doCommand(by: commandSelector)
return true
default: return false
}
}

Click through NSView

I have an NSView containing multiple subviews. One of those subviews is transparent and layered on top.
I need to be able to click through this view down to the subviews below (so that the view below gets first responder status), but all the mouse events get stuck on the top view (alpha is 1, because I draw stuff in it - so it should only click through transparent areas).
I actually expected this to work, since normally it does. What's wrong?
Here's another approach. It doesn't require creating a new window object and is simpler (and probably a bit more efficient) than the findNextSiblingBelowEventLocation: method above.
- (NSView *)hitTest:(NSPoint)aPoint
{
// pass-through events that don't hit one of the visible subviews
for (NSView *subView in [self subviews]) {
if (![subView isHidden] && [subView hitTest:aPoint])
return subView;
}
return nil;
}
I circumvented the issue with this code snippet.
- (NSView *)findNextSiblingBelowEventLocation:(NSEvent *)theEvent {
// Translate the event location to view coordinates
NSPoint location = [theEvent locationInWindow];
NSPoint convertedLocation = [self convertPointFromBase:location];
// Find next view below self
NSArray *siblings = [[self superview] subviews];
NSView *viewBelow = nil;
for (NSView *view in siblings) {
if (view != self) {
NSView *hitView = [view hitTest:convertedLocation];
if (hitView != nil) {
viewBelow = hitView;
}
}
}
return viewBelow;
}
- (void)mouseDown:(NSEvent *)theEvent {
NSView *viewBelow = [self findNextSiblingBelowEventLocation:theEvent];
if (viewBelow) {
[[self window] makeFirstResponder:viewBelow];
}
[super mouseDown:theEvent];
}
Here's a Swift 5 version of figelwump's answer:
public override func hitTest(_ point: NSPoint) -> NSView? {
// pass-through events that don't hit one of the visible subviews
return subviews.first { subview in
!subview.isHidden && nil != subview.hitTest(point)
}
}
Here's a Swift 5 version of Erik Aigner's answer:
public override func mouseDown(with event: NSEvent) {
// Translate the event location to view coordinates
let convertedLocation = self.convertFromBacking(event.locationInWindow)
if let viewBelow = self
.superview?
.subviews // Find next view below self
.lazy
.compactMap({ $0.hitTest(convertedLocation) })
.first
{
self.window?.makeFirstResponder(viewBelow)
}
super.mouseDown(with: event)
}
Put your transparent view in a child window of its own.

Is there a way to make a custom NSWindow work with Spaces

I'm writing an app that has a custom, transparent NSWindow created using a NSWindow subclass with the following:
- (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)aStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)flag
{
self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:bufferingType defer:flag];
if (self)
{
[self setOpaque:NO];
[self setBackgroundColor:[NSColor clearColor]];
}
return self;
}
- (BOOL)canBecomeKeyWindow
{
return YES;
}
- (BOOL)canBecomeMainWindow
{
return YES;
}
I have everything working perfectly, including dragging and resizing, except the window doesn't work with Spaces. I cannot move the window to another space by either holding the window while switching spaces via keyboard shortcut, or by dragging to the bottom/top/left/right of the window. Is there anyway to have a custom window behave exactly like a normal window with regards to Spaces?
After a long time I found a solution to this annoying problem.
Indeed [window setMovableByWindowBackground:YES]; conflicts with my own resizing methods, the window trembles, it looks awful!
But overriding mouse event methods like below solved the problem in my case :)
- (void)mouseMoved:(NSEvent *)event
{
//set movableByWindowBackground to YES **ONLY** when the mouse is on the title bar
NSPoint mouseLocation = [event locationInWindow];
if (NSPointInRect(mouseLocation, [titleBar frame])){
[self setMovableByWindowBackground:YES];
}else{
[self setMovableByWindowBackground:NO];
}
//This is a good place to set the appropriate cursor too
}
- (void)mouseDown:(NSEvent *)event
{
//Just in case there was no mouse movement before the click AND
//is inside the title bar frame then setMovableByWindowBackground:YES
NSPoint mouseLocation = [event locationInWindow];
if (NSPointInRect(mouseLocation, [titleBar frame])){
[self setMovableByWindowBackground:YES];
}else if (NSPointInRect(mouseLocation, bottomRightResizingCornerRect)){
[self doBottomRightResize:event];
}//... do all other resizings here. There are 6 more in OSX 10.7!
}
- (void)mouseUp:(NSEvent *)event
{
//movableByBackground must be set to YES **ONLY**
//when the mouse is inside the titlebar.
//Disable it here :)
[self setMovableByWindowBackground:NO];
}
All my resizing methods start in mouseDown:
- (void)doBottomRightResize:(NSEvent *)event {
//This is a good place to push the appropriate cursor
NSRect r = [self frame];
while ([event type] != NSLeftMouseUp) {
event = [self nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
//do a little bit of maths and adjust rect r
[self setFrame:r display:YES];
}
//This is a good place to pop the cursor :)
//Dispatch unused NSLeftMouseUp event object
if ([event type] == NSLeftMouseUp) {
[self mouseUp:event];
}
}
Now I have my Custom window and plays nice with Spaces :)
Two things here.
You need to set the window to allow dragging by background, [window setMovableByWindowBackground:YES];
And If your custom window areas you expect to be draggable are custom NSView subclasses, you must override the method - (BOOL)mouseDownCanMoveWindow to return YES in any NSView subclass that needs to be able to move the window by dragging.
Did you override isMovable?
The Apple documentation says, that it changes Spaces behavior:
If a window returns NO, that means it
can only be dragged between spaces in
F8 mode, ...
Another method that might be related:
NSWindow setCollectionBehavior