NSPopover - Hide when focus lost? (clicked outside of popover) - objective-c

I'm using the doubleClickAction of a NSTableView to display a NSPopover. Something like this:
NSInteger selectedRow = [dataTableView clickedRow];
NSInteger selectedColumn = [dataTableView clickedColumn];
// If something was not selected, then we cannot display anything.
if(selectedRow < 0 || selectedColumn < 0)
{
NSLog(#"Invalid selected (%ld,%ld)", selectedRow, selectedColumn);
return;
} // End of something was not selected
// Setup our view controller, make sure if there was already a popover displayed, that we kill that one off first. Finally create and display our new popover.
DataInspectorViewController * controller =
[[DataInspectorViewController alloc] initWithNibName: #"DataInspectorViewController"
bundle: nil];
if(nil != dataPreviewPopover)
{
[dataPreviewPopover close];
} // End of popover was already visible
dataPreviewPopover = [[NSPopover alloc] init];
[dataPreviewPopover setContentSize:NSMakeSize(400.0f, 400.0f)];
[dataPreviewPopover setContentViewController:controller];
[dataPreviewPopover setAnimates:YES];
[dataPreviewPopover showRelativeToRect: [dataTableView frameOfCellAtColumn: selectedColumn row: selectedRow]
ofView: dataTableView
preferredEdge: NSMinYEdge];
Which works just fine. My popovers get created and removed on the cells that I double click on . The problem is, I want to the popover to go away if I click anywhere outside of it (like a single click on another cell). I have been looking around, but for the life of me cannot figure out how to do it.
This is something I would assume is built into a popover, (I'm fairly certain it was in the iOS UIPopoverController class) so I'm just wondering if im missing something simple.

You need to change the property behavior of your popover (in code or on interface builder) to:
popover.behavior = NSPopover.Behavior.transient;
NSPopover.Behavior.transient
The system will close the popover when the user interacts with a user interface element outside the popover.
Read more about this in Apple's documentation.

the .transient flag doesn't work for me.
However I can make things work by the following:
1) Whenever I show my popover I make sure I activate the app
(my app is a menu-bar app, so this doesn't happen automatically)
NSApp.activate(ignoringOtherApps: true)
2) When I click outside the app, then my app will be deactivated. I can detect this in the AppDelegate
func applicationWillResignActive(_ notification: Notification) {
print("resign active")
}
and act accordingly

After calling show(relativeTo:of:preferredEdge:) method,
Add below line
popover.contentViewController?.view.window?.makeKey()
And make sure you set
popover.behavior = .transient
Sorry, I've added solution in Swift.

While transient worked for most cases, it was an issue when the user interacted with elements outside of the application, as the popover would hide but not close.
What finally ended working for me was:
popover.behavior = .semitransient
Now the popover closes when changing app, or interacting with any other element outside of the app. But will not close when interacting with a NSMenu, and maybe won't close either with other interactions.
Quoting from the documentation for NSPopover.Behavior.semitransient:
The exact interactions that cause semi-transient popovers to close are not specified.
Similar to the documentation for NSPopover.Behavior.transient:
The exact interactions that will cause transient popovers to close are not specified.

Related

Show NSPopover when the application starts

I am using Pyobjc to create a NSStatusItem. When it is clicked, I am showing an NSPopOver. This is working fine. However, my requirement is to show the popover as soon as the application starts without any action by the user. Calling the callback directly in finishLaunching is not working. Is there any way to achieve this? It will be good enough even if can just simulate the click on NSStatusView.
class TestApp(NSApplication):
def finishLaunching(self):
# Make statusbar item
statusbar = NSStatusBar.systemStatusBar()
self.statusitem = statusbar.statusItemWithLength_(NSVariableStatusItemLength)
self.statusitem.setTarget_(self)
self.statusitem.setAction_('statusItemClicked:')
self.icon = NSImage.alloc().initByReferencingFile_('app-icon.png')
self.icon.setScalesWhenResized_(True)
self.icon.setSize_((20, 20))
self.statusitem.setImage_(self.icon)
self.statusitem.setHighlightMode_(1)
# self.statusItemClicked_(None)
def statusItemClicked_(self, notification):
self.viewController = SimpleXibDemoController.alloc().initWithWindowNibName_("Login")
# Show the window
self.viewController.showWindow_(self.viewController)
rect = self.statusitem.valueForKey_('button').frame()
self.viewController.popover.showRelativeToRect_ofView_preferredEdge_(rect, self.statusitem.valueForKey_('button'), NSMaxYEdge)
I finally got a somewhat sketchy solution. I have a method which positions the popover like so:
- (IBAction)showPopover:(id)sender {
[popover showRelativeToRect:self.statusItemView.bounds ofView:self.statusItemView preferredEdge:NSMinYEdge];
}
In applicationDidFinishLaunching, or finishLaunching in your case, instead of calling the method directly I called it with performSelector instead:
[self performSelector:#selector(showPopover:) withObject:self afterDelay:0];
Even setting the delay to 0 works, which I do not know why, and it now positions the popover correctly.
Edit: This seems to only work situationally. Even by creating a view controller and calling it from viewDidAppear, the popover only gets positioned half of the time.
Edit 2: I added a window controller to the status item's window, and overrode windowDidLoad. However as it turned out, the window is loaded before I can even set the controller's window so windowDidLoad is not called.
You have to wait until the view appears to present the NSPopover. Override viewDidAppear in your NSViewController subclass and present it there. (Or override loadView if your minimum deployment target is less than OS X Yosemite.)

iOS7 Keyboard Behavior with SearchBar/UITableView: Offset on secondary view appearances

Problem
I am having a rather big issue with the iOS7 keyboard appearance. I have a Searchbar on a UIViewController with TableView Delegation/Data Source setup (I am using the self.searchDisplayController delegates as well). I segue from this scene to a prototype tableview to show the results.
Here is the issue:
On first load I can see the keyboard being displayed when I tap into the text field of the UISearchBar. I can type and perform a search with the results being shown in the next scene.
I've added NSNotifications to view the keyboard properties in local methods keyboardWillShow and keyboardWasShown. I can see on the first scene appearance (after the view is completely loaded):
I segue to the result tableview at this point and when I navigate back and touch the text field, my keyboard shows up either fully or partially off-screen:
When I look at the keyboardWillShow notification at this point I can see that my keyboard values are incorrect:
I've researched and tried many possibilities including:
Added the following to my main view controller:
-(BOOL)canResignFirstResponder
{
return YES;
}
-(BOOL)canBecomeFirstResponder
{
return YES;
}
Configured the following in my view did load
self.searchDisplayController.searchBar.spellCheckingType = UITextSpellCheckingTypeNo;
self.searchDisplayController.searchBar.autocapitalizationType= UITextAutocapitalizationTypeNone;
self.searchDisplayController.searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
self.searchDisplayController.searchBar.keyboardType = UIKeyboardTypeDefault;
Put in standard stubs for:
-(void)searchDisplayController:(UISearchDisplayController *)controller didShowSearchResultsTableView:(UITableView *)tableView
-(void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
I've noticed that if I choose a Partial Curl as my segue mode, the keyboard remains accessible when I roll back to the main view controller (but then it was never fully off screen in that case). However if I move from the results tableview to a detail scene and then navigate back to the main view controller, the keyboard appears off-screen again.
Question
Is there a method I can use to intercept the misplaced keyboard so that it displays in the default location?
NB: Along these lines, I have created a NSDictionary property to hold the initial userInfo values with the correct keyboard placement. I am not sure how to reassign these values to get the keyboard to return to it's original placement.
BTW - This seems a bit of a hack to get the keyboard fixed due to a bug in IB, is there some other way that I can try to remedy the situation?
Thanks in advance for any feedback!
Solution
This was such an obscure issue that I'm sharing the solution to save the next person some effort. Like most programming issues, it turns out this one was self-inflicted. In my original iteration of this project I had turned off rotational support as I am learning auto-layout and I wanted to ease into the transition from Springs and Struts. Somehow between the start of the project and the code release I ended up with this bit of code in the Main Scenes' View Controller.
//BAD
- (NSUInteger) supportedInterfaceOrientations
{
return !UIInterfaceOrientationPortraitUpsideDown;
}
instead of returning a valid enumeration like...
//OK
- (NSUInteger) supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskAll;
}

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];
}

Get current view

I have an app which has split view inside a tab bar, and these split views often have navigation hierarchy and then sometimes modal views are presents on top of them, and it all works fine, but...
I am trying to display a passcode lock whenever the app goes into background, so I put
[self.window.rootViewController presentModalViewController:lockView animated:YES];
in my AppDelegate's method
- (void)applicationWillResignActive:(UIApplication *)application
...which works fine unless a modal view is displayed.
the passcode does not display if a modal view is open.
Is there a way to retrieve the currently active view controller so I can present this lock view?
Thanks in advance
Cheerio
Code that worked was as follows:
BOOL hasKids = YES;
UIViewController *topViewController = (UIViewController*)[[(UITabBarController*)self.window.rootViewController viewControllers] objectAtIndex:((UITabBarController*)self.window.rootViewController).selectedIndex];
while (hasKids) {
if (topViewController.presentedViewController) {
hasKids = YES;
topViewController = topViewController.presentedViewController;
} else {
hasKids = NO;
}
}
[topViewController presentModalViewController:lockView animated:YES];`
I think the easiest way is to keep track of which tab is currently active (there are a number of ways to do this, but I'd recommend implementing the UITabBarControllerDelegate and handling its tabBarController:didSelectViewController: method).
Once that's done, you'll probably need to manage a property in each view controller that holds any modal view controllers you present. If, however, you're on iOS 5 or higher, look into the UIViewController property presentedViewController. It appears that this is a new way to do exactly what you want.

Showing a different XIB/NIB in an iOS app

I have multiple nib (xib) files and I want the user to see a different one when they tap a button. What I am looking for:
- (IBAction)buttonTap {
//Code for showing a different nib goes here
}
I can't figure out how to do this. I can show a different view within the nib file just fine, but I can't get it to show a different nib. How do I show a different nib when the user taps a button?
Any help is appreciated! Thanks!
The way I handle switching between actual xib's, and I'm sure there are a multitude of ways to accomplish the same thing, is to have my App Delegate act as a routing center between my views.
I subscribe my App Delegate to recieve events from button presses for existing views. When it recieves a request to switch views, such as a button press, I do something like this:
- (void) showLogin
{
LoginViewController *loginViewController = [[LoginViewController alloc]
initWithNibName:#"LoginViewController" bundle:nil];
// Show
self.loginViewController = loginViewController;
[loginViewController release];
self.window.rootViewController = self.loginViewController;
}
I set my rootViewController to the view I am attempting to display. It doesn't release the old controller, but simply replaces the view being displayed. You can place more logic in to determine if it's already displayed, close out other views, etc. In most simplistic terms, this works for me.