Cocoa: NSPanel loses parent and other strange behaviour - objective-c

I have an issue with an NSPanel acting strangely and have created a Sample App to demonstrate this behaviour.
The App was generated from Xcode 4's template and simply creates a panel and then opens and closes it based on button presses:
The strange behaviour I have observed:
Under Lion, after opening the panel for the first time, the panel follows the main window around, which is correct behaviour. However after closing it and then re-opening it no longer follows the main window around.
Under Snow Leopard, when closing the panel the main window is also closed!
EDIT: Just to be clear; the behaviour I expect is for the panel to follow the main window around when the main window is moved; and for that to be true after the panel is closed and subsequently re-opened. Also I expected the panel and main window to behave the same way under Snow Leopard and Lion.
The important part of the code is here:
#implementation MyAppDelegate
- (void)dealloc
{
[_panel release];
[super dealloc];
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
_panel = [[NSPanel alloc]
initWithContentRect:NSMakeRect(400, 400, 200, 100)
styleMask:NSUtilityWindowMask|NSClosableWindowMask|NSTitledWindowMask
backing:NSBackingStoreBuffered
defer:YES];
[_panel setTitle:#"A Panel"];
}
- (IBAction)openPanel:(id)sender
{
[_panel setParentWindow:[self window]];
[_panel makeKeyAndOrderFront:sender];
}
- (IBAction)closePanel:(id)sender
{
[_panel orderOut:sender];
}
#end
Note: I added the unnecessary setParentWindow call to the openPanel method to assert that the parent window is reset every time the panel was opened; it made no difference however.
Can someone please explain what I'm doing wrong?
EDIT: There is some confusion about the missing #synthesize window = _window from the implementation file, but I have just opened another project sample that I created to test for memory leak behaviour and it's not in there either. I am using Xcode 4.4, so it's possibly a bug in that, however I don't think the project templates have changed.

You're not supposed to set the parent-child relationship from the child, but from the parent. The setParentWindow: docs say:
This method should be called from a subclass when it is overridden by a subclass’s implementation. It should not be called otherwise.
Instead, use addChildWindow:ordered:, like so:
- (IBAction)openPanel:(id)sender
{
[[self window] addChildWindow:_panel ordered:NSWindowAbove];
//[_panel setParentWindow:[self window]];
[_panel makeKeyAndOrderFront:sender];
}
I didn't test this under Snow Leopard, but it fixes the behavior for me when run on Lion.
Rob Keniger notes below that on Snow Leopard you should also do [[self window] removeChildWindow:_panel] before ordering the panel out. (I assume this is also a good idea on Lion.)

I'm a little confused about your question. I downloaded and checked out your project. First, somehow you're missing the #synthesize command for the application's main window. You said you used the template but somehow it's missing. Since the AppDelegate header file has a #property for the window, you need an #synthesize command in the implementation file for it. I'm not sure how you lost that line in your project but add this just after the #implementation line...
#synthesize window = _window;
Second, why do you state the following?
Under Lion, after opening the panel for the first time, the panel
follows the main window around, which is correct behavior.
What you're saying makes no sense. When you create your panel you use "initWithContentRect:NSMakeRect(400, 400, 200, 100)". Notice that you create it at position (400, 400) with a size of (200, 100). So the first time it opens it opens at the screen location (400, 400). It has nothing to do with the position of the main window.
Anyway, after adding the #synthesize part I could compile and run the application with no errors. It's working as expected in 10.7 for me.

Related

Adding NSTouchBar support after main window has been created

I'm trying to add support for exposing NSTouchBar buttons via a plugin to an application that I cannot otherwise modify. The plugin, a shared library, is loaded at runtime after the main window has been created. I've created an AppDelegate as follows:
#interface AppDelegate : NSResponder <NSTouchBarDelegate>
#end
With an #implmentation that implements the makeTouchBar and touchBar functions:
- (NSTouchBar *) makeTouchBar
- (nullable NSTouchBarItem *) touchBar:(NSTouchBar *)touchBar
makeItemForIdentifier: (NSTouchBarItemIdentifier)identifier
I finally try to inject it into the application, I have the following in the onload function that's called in the dynamic library when it's loaded into the application.
NSWindow *w = [NSApp mainWindow];
AppDelegate *a = [[AppDelegate alloc] init];
a.nextResponder = w.nextResponder;
w.nextResponder = a;
// Re-setting FirstResponder will trigger AppDelegate::makeTouchBar
// that was attached above.
NSResponder *n = w.firstResponder;
[w makeFirstResponder: nil];
[w makeFirstResponder: n];
however... AppDelegate::touchBar will never be called, thus the touchbar will never be populated with my test buttons.
If I create a new project in Xcode, and use the exact same AppDelegate implementation (copy-paste of the same #interface and #implementation), I get functional buttons that are both shown and responds to press events, so it's the injection part that seems to be broken. (In Xcode everything is hooked up via MainMenu.xib I guess)
Update:
The main problem was that the application was compiled in a way that prevented TouchBar to work. Perhaps built on an older version of Mac OS.
If this object really should act as the application delegate (as the name suggests) -- rather than inserting it in the main window's responder chain, it would be better to actually set it as the NSApp's delegate. The set delegate's touchBar is also used when building the touch bar (see NSTouchBar Discovery and the Responder Chain).
If that doesn't work for your plugin (maybe the application already has a specific app delegate it needs), you could insert it into NSApp's responder chain instead of the mainWindow's.
Both of these also have the benefit of making the touch bar be associated at the application level rather than window. So if the main/key window changes, the provided touch bar will always be used.

NSWindow closing and controlling issues in Mac OS X

I met the issues of NSWindow regarding closing it when the application starts. There are plenty of examples, however, I can not get the proper effect, perhaps I missing something.
Firstly, in many examples there is the recommendation to use "[self window]" but I get the error like "No visible #interface for 'ViewController' declares the selector 'window'".
Then I use the round way: "[[self view] window]". Anyway, the window is not closed after the execution of the code:
NSWindow *win = [[self view] window];
[win performClose:self];
or
NSWindow *win = [[self view] window];
[win close];
The next one also does not give any results as I expect, according to the documentation:
[win orderOut:self];
The code compiles but I can see the window. Of course, I tried:
NSLog(#"%#", [win.windowController windowShouldClose:self] ? #"YES" : #"NO" );
It outputs "NO", so, it means that the window, which appear when I run my application, cannot be closed? Is there any way how to work around it? Why I cannot control that main window following the way the documentation suggests?
I checked for the import "#import AppKit/AppKit.h;" as well.
All of this suggests that win is nil. The view of your view controller is not in a window.

NSPanel - Why can I not set my Panel title and data values before showing the window?

Simple structure:
exampleController.h :
#import <Foundation/Foundation.h>
#interface exampleController : NSWindowController {
#public
IBOutlet NSPanel *entryPanel;
#property (nonatomic, strong) IBOutlet NSPanel *entryPanel;
#end
exampleController.m :
#import "exampleController.h"
#implementation exampleController
#synthesize entryPanel;
- (id)init {
self = [super initWithWindowNibName:#"ExamplePanel"];
if (self) {
// Initialization code here.
NSLog(#"entryPanel: %#", entryPanel);
[self.entryPanel setTitle:#"TESTING!"];
}
return self;
}
randomController.m :
...
- (id) init {
self = [super init];
if (self) {
// loading our example controller if it isn't loaded yet.
if (!ourExampleController) {
ourExampleController = [exampleController alloc] init];
}
}
return self;
}
...and then later in the random controller within a method I show the NSPanel via:
[ourExampleController showWindow:self];
[ourExampleController window] makeKeyAndOrderFront:self];
My problem is that no matter what, the first time the NSPanel displays and shows itself the title is always still set to the title that it has in Interface Builder! Even though I explicitly set the title in the exampleController init method.
I've also tried throwing an NSLog(#"entryPanel: %#", entryPanel) in the init method for exampleController and at launch it is always NULL. I do not have to ALLOC all my IBOutlets in the init because I am already synthesizing them?
I've double checked everything in interface builder. The File Owner for the ExamplePanel.xib is set to the exampleController class. The window AND entryPanel outlets are both referencing the NSPanel in our xib file. What am I missing ??
Thanks in advance!
EDIT: Just to add. If I open the window (..and see the default IB title) and then close it and reopen it with a method that changes the title - it seems to work! This problem seems to only reside with the window first opening. It seems like my properties are not being alloc'd until the window first opens?
EUREKA!
As per discussion here:
IBOutlet instances are (null) after loading from NIB
I learnt that the window itself is not loaded when my controller is initialized. Found that surprising since I figured using initWithWindowNibName:#"myNibFile" would also alloc and initialize all outlet properties but since I'm new to OSX Obj-C that appears to not be the case. All the outlet properties are only alloc'd once the window itself is loaded too.
It's easy to just show the window (which also loads the window if it's not loaded yet) and then quickly set all the outlets to my desired values BUT this was an issue for me since I wanted to avoid that ever so slight "screen flicker" (for lack of a better description) that occurs as the values adjust to their new settings.
The solution was to find a way to load the controller and load the window without actually showing it first! Then I discovered this:
Can you force a NSWindow to load, i.e. before it is presented onscreen?
Steps to make that happen:
Add the following to my NSWindowController subclass init method:
// this loads the window as per link/description above
[self window]
The key seems to be though to ensure that in your NIB/XIB file that the Visible At Launch is unchecked. If it is checked (default behavior) then the [self window] call above will still show your window when your app launches. Unchecking the above option ensures the above call does not show your window until you explicitly show it yourself!
E.g. You can define an action button which loads your window:
[exampleController showWindow:self];
[[exampleController window] makeKeyAndOrderFront:self];
Hope this helps someone else out. Was a head scratcher for a couple hours!
You should set the title, etc. in -awakeFromNib or -windowDidLoad instead of an -init… method. That way the values will be set before the window is shown and you won't get the flicker.

Show Window without activating (keep application below it active)

I need to show a window (without title bar) above third party applications without my window taking focus.
I have tried using an NSPanel and setting enabling non-activating, but that didn't help.
I tried orderFront:self, but that didn't help either.
I always needed to add [NSApp activateIgnoringOtherApps:YES]; because the window wouldn't show otherwise.
I have here a sample project for just this functionality:
http://users.telenet.be/prullen/TopW2.zip
UIElement is set to true in the application's plist file, so there is no dock. You can activate the window by pressing ALT + SPACE at the same time. You will see that the app below it looses focus. Any thoughts on how to fix this? I've seen other apps do it so I know it's possible.
Edit: here's the code so far. Remember the window is a non-activating NSPanel.
I still need that last NSApp activateIgnoringOtherApps line or otherwise it doesn't display. But of course that makes the window the active one.
_windowController = [[MyWindowController alloc] initWithWindowNibName:#"MyWindowController"];
[[_windowController window] setLevel:NSNormalWindowLevel+1];
[[_windowController window] orderFrontRegardless];
[_windowController showWindow:self];
[NSApp activateIgnoringOtherApps:YES];
I've also subclassed NSPanel and added two methods:
- (BOOL)canBecomeKeyWindow
{
return YES;
}
- (BOOL)canBecomeMainWindow
{
return YES;
}
Edit: OK, unchecking setHidesOnDeactivate fixes this, but now the window will never hide. I need it to hide when the user presses the app below it or switches to another app.
Edit 2: OK, this seems to fix the above issue:
- (void)awakeFromNib
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(hideWindow) name:NSWindowDidResignKeyNotification object:nil];
}
- (void)hideWindow {
[self setHidesOnDeactivate:YES];
}
Not sure if there's a better way.
And for those that want to know how to display the window:
[[_windowController window] setLevel:NSPopUpMenuWindowLevel];
[[_windowController window] orderFrontRegardless];
[[_windowController window] makeKeyWindow];
[_windowController showWindow:self];
Either one of these should do the trick:
Use -[NSWindow orderFrontRegardless] to get a normal level window to the front without activating the corresponding app, or
Use -[NSWindow setLevel:] to increase the window level to something higher than NSNormalWindowLevel
Not to take away from #puzzle's useful answer, but it sounds like your problem has something to do with using an NSPanel instead of an NSWindow.
The "How Panels Work" docs say:
Onscreen panels, except for alert dialogs, are removed from the screen when the application isn’t active and are restored when the application again becomes active. This reduces screen clutter.
Specifically, the NSWindow implementation of the hidesOnDeactivate method returns NO, but the NSPanel implementation of the same method returns YES.
So perhaps you could override hidesOnDeactivate to return NO, or change to NSWindow

Super slow lag/delay on initial keyboard animation of UITextField

Alright, this problem has been driving me nuts.
It takes roughly 3-4 seconds for the keyboard to pop up after I touch my UITextField. This only occurs on the first time the keyboard pops up since the app launched, afterwards the animation starts instantly.
At first I thought it was problem of loading too many images, or my UITableView, but I just created a brand new project with only a UITextField, and I still experience this problem. I'm using iOS 5, Xcode ver 4.2, and running on an iPhone 4S.
This is my code:
#import "ViewController.h"
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(20, 20, 280, 30)];
textField.borderStyle = UITextBorderStyleRoundedRect;
textField.delegate = self;
[self.view addSubview:textField];
}
#end
Is this a common problem for all apps?
Right now, the only way I can make it somewhat better is by having textField become/resign first responder in viewDidAppear, but that doesn't solve the problem entirely - it just loads the delay onto when the view loads instead. If I click on textField immediately when the view loads, I still get the problem; if I wait 3-4 seconds after the view loads before touching the textField, I don't get the delay.
Before you implement any exotic hacks to get around this problem, try this: stop the debug session, close the app from multitasking, unplug your device from the computer and run the app normally by tapping its icon. I have seen at least two cases in which the delay only occurs while the device is plugged in.
So the problem is NOT just limited to the first install as I had previously thought, but happens every time the app is launched. Here's my solution that solves the issue completely.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Preloads keyboard so there's no lag on initial keyboard appearance.
UITextField *lagFreeField = [[UITextField alloc] init];
[self.window addSubview:lagFreeField];
[lagFreeField becomeFirstResponder];
[lagFreeField resignFirstResponder];
[lagFreeField removeFromSuperview];
}
Yeah, I also got a few seconds delay on the latest iPhone 4s. Don't panic. For some reasons, it only happens the first time the app is loaded from Xcode in Debug. When I did Release, I don't get the delay. Just forget it...
This is a known issue.
Preloading keyboard seems promising. Check Preloading the UIKeyboard.
Some additional reading material:
Initial iPhone virtual keyboard display is slow for a UITextField. Is this hack around required?
UITextField keyboard blocks runloop while loading?
http://www.iphonedevsdk.com/forum/iphone-sdk-development/12114-uitextfield-loooong-delay-when-first-tapped.html
You can use Vadoff's solution in Swift by adding this to didFinishLaunchingWithOptions:
// Preloads keyboard so there's no lag on initial keyboard appearance.
let lagFreeField: UITextField = UITextField()
self.window?.addSubview(lagFreeField)
lagFreeField.becomeFirstResponder()
lagFreeField.resignFirstResponder()
lagFreeField.removeFromSuperview()
It is working for me in iOS 8.
Code in block added to main queue and run asynchronously. (don't locked main thread)
dispatch_async(dispatch_get_main_queue(), ^(void){
[textField becomeFirstResponder];
});
See this answer. They suggest UIResponder+KeyboardCache. It's simple and awesome. Tested on iOS 7.
A related problem, where a UIViewController would be slow to present, was solved by using the system font instead of a custom font on the UITextField. Perhaps using the system font might also work for this problem?
This bug seems to be fixed in iOS 9.2.1. Since upgrading my device, I no longer have a delay between tapping a text field and the keyboard appearing when my device is connected to my computer.
This selected answer causes BAD_EXC crash on iOS 11 - remove from app to fix
You can add below code when viewController's view did loaded, like viewDidAppear.Not just application:didFinishLaunchingWithOptions:
UITextField *lagFreeField = [[UITextField alloc] init];
[self.window addSubview:lagFreeField];
[lagFreeField becomeFirstResponder];
[lagFreeField resignFirstResponder];
[lagFreeField removeFromSuperview];