Charlie
Charlie

Reputation: 11787

showWindow but wait for window to load?

I have two .xib files, MainMenu and PreferenceMenu. I set the File's Owner of PreferenceMenu to be a subclass of NSWindowController, so that I can modify how the window is opened, modified, etc.

Normally I open PreferenceMenu through IB, sending the showWindow: action to the Preferences Controller object. This has been working fine, and the window has been opening flawlessly. I also implemented NSTabView and NSToolBar in my PreferenceMenu as different tabs house different preferences.

Anyways, I now have to prompt the user if they want to open up the PreferenceMenu after first loading the application. My process is check to see if a NSUserDefaults key is set, then prompts and sets the key if it isn't. If the returned NSAlert button is a specific button, I then open up PreferenceMenu and switch to the correct tab.

The gist of this process is to:

  1. Import CCPreferencesController.h in CCAppDelegate.h
  2. Create a strong reference to the CCPreferencesController window (for ARC)
  3. Prompt with NSAlert, and if returned button is correct, run the following:

    self.windowController = [[CCPreferencesController alloc] initWithWindowNibName:@"PreferenceMenu"];
    [self.windowController showWindow:self];
    
  4. Import CCAppDelegate.h in CCPreferencesController.m

  5. Deal with the NSAlert response in CCPreferencesController.m:

    - (void)showWindow:(id)sender{
        if ([CCAppDelegate class] == [sender class]){
            [self openLogin];
        }
        [super showWindow:sender];
    }
    
  6. Switch the TabView in the openLogin method.

This more of less works fine, except showWindow is called before the window's contents have actually loaded. This means that calling self.toolbar, self.tabView, etc. returns NULL. My solution to this is to simply use a timer to wait for the elements to load, but this is nowhere near elegant.

My question, therefore, is how can I avoid having to use a delay, and instead have my openLogin method get called on showWindow but wait for the window's contents to be loaded?

I'm also 99% sure that my code is awful, including how I'm importing .h files and subclassing File's Owner, so any tips to make this better are greatly appreciated.


For those who want to see the code in more detail, here are the relevant parts

CCAppDelegate.h:

#import <Cocoa/Cocoa.h>
#import "CCPreferencesController.h"

@class WebView;

@interface CCAppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate, NSSharingServiceDelegate>

@property (strong) CCPreferencesController *windowController;
...

@end 

CCAppDelegate.m:

- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame{
    bool loginPrompted = [[NSUserDefaults standardUserDefaults] boolForKey:@"loginPrompted"];
    if (!loginPrompted){
        NSAlert *alert = [NSAlert alertWithMessageText:@"Login"
                                         defaultButton:@"Login"
                                       alternateButton:@"Cancel"
                                           otherButton:nil
                             informativeTextWithFormat:@"Would you like to login to enable voting?"];
        // Display alert
        [alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
            // Return focus to window
            [[alert window] orderOut:self];
            NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
            [prefs setBool:YES forKey:@"loginPrompted"];
            [prefs synchronize];
            if (returnCode == 1){
                self.windowController = [[CCPreferencesController alloc] initWithWindowNibName:@"PreferenceMenu"];
                [self.windowController showWindow:self];
            }
        }];

    }

}

CCPreferencesController.h:

#import <Cocoa/Cocoa.h>

@interface CCPreferencesController : NSWindowController {}

...

@end

CCPreferencesController.m

#import "CCPreferencesController.h"
#import "CCAppDelegate.h"

@implementation CCPreferencesController

- (id)init{
    if(self = [super initWithWindowNibName:@"PreferenceMenu"]) {}
    return self;
}

- (void)awakeFromNib{
    ... 
    [self.toolbar setSelectedItemIdentifier:@"general"];
}

- (void)showWindow:(id)sender{
    if ([CCAppDelegate class] == [sender class]){
        [self openLogin];
    }
    [super showWindow:sender];
}

// This is gross
- (void)openLogin{
    dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 0.1);
    dispatch_after(delay, dispatch_get_main_queue(), ^(void){
        [self.toolbar setSelectedItemIdentifier:@"tab"];
        [self.tabView selectTabViewItemAtIndex:1];
        NSRect frame = [self.window frame];
        frame.size.height += 55;
        frame.origin.y -= 55;
        [self.window setFrame:frame display:YES animate:YES];
    });

}

Upvotes: 0

Views: 1174

Answers (3)

Volodymyr Sapsai
Volodymyr Sapsai

Reputation: 413

What if you call [super showWindow:sender] before [self openLogin]? Like

- (void)showWindow:(id)sender {
    [super showWindow:sender];
    if ([sender isKindOfClass:[CCAppDelegate class]]) {
        [self openLogin];
    }
}

Added:

Sorry, my suggestion to move [super showWindow:sender] before [self openLogin] doesn't explain your problems. self.toolbar and self.tabView are nil because they are IBOutlets and outlets aren't set until the corresponding nib file is loaded. The delay is just a way to wait until nib file is loaded and you admit yourself it's a fishy approach. Another solution is to load nib before calling [self openLogin] and I've offered to call -showWindow: because -showWindow: indirectly loads nib if it isn't loaded already. I strongly encourage you to add breakpoints in -[CCPreferencesController awakeFromNib] and -[CCPreferencesController windowDidLoad]. Thus you can see in debugger when nib file is loaded and when IBOutlets are ready to be used.

I don't know anything about -[NSWindowController showWindow:] scheduling something on the runloop. Unfortunately, @Daij-Djan hasn't provided any details about what exactly is scheduled on the runloop. @Daij-Djan has suggested to perform -openLogin actions in -windowDidLoad, but I see a sequence before_showWindow:, windowDidLoad, after_showWindow:. I make a conclusion that nib is loaded immediately, not scheduled on the runloop. That's why it is safe to call -openLogin without any delays.

Once again, I encourage you to check yourself in debugger how nib file is loaded. It is better to check it yourself, than rely on somebody's words.

The answer to question how to avoid using delay: call -openLogin after window is loaded and in this case call -openLogin after -showWindow: because it causes nib loading.

Upvotes: 0

stevesliva
stevesliva

Reputation: 5665

Looking at the other options, in my opinion, you ought to just do this:

        if (returnCode == 1){
            self.windowController = [[CCPreferencesController alloc] initWithWindowNibName:@"PreferenceMenu"];
            [self.windowController showWindow:self];
            __weak CCAppDelegate* weakSelf = self;
            dispatch_async(dispatch_get_main_queue(), ^{
                [weakSelf.windowController openLogin];
           }

And this:

- (void)openLogin{
    [self.toolbar setSelectedItemIdentifier:@"tab"];
    [self.tabView selectTabViewItemAtIndex:1];
    NSRect frame = [self.window frame];
    frame.size.height += 55;
    frame.origin.y -= 55;
    [self.window setFrame:frame display:YES animate:YES];
}

You are already instructing the WC to do something. It's not out of line to instruct it to show a particular tab, if the WC provides a public method to do such a thing.

It is essentially what I do when a user clicks a notification. I show the window, and have the window controller react tho what's in the userInfo dictionary in the notification on the next runloop iteration.

[edit] -- now schedules openLogin on the runloop with dispatch_async

Upvotes: 0

Daij-Djan
Daij-Djan

Reputation: 50099

I'd not go this way but maybe it is right in your situation because you do it bloc kingly but before you call showWindow you could force load the window. just call self.window before you call super showWindow

NOTE that calling a getter to achieve side-effects is also far from clean!

It'd be best to move the code that needs the window in something like windowDidLoad or so

MAYBE

@implementation MyWindowController {
    NSDictionary *_options;
}

- (void)showWindow:(id)sender{
    if ([CCAppDelegate class] == [sender class]){
        _options = @{@"Login": @YES};
    } else {
        _options = nil; 
    }

    if(self.isWindowLoaded) {
        [self applyOptions];
    }
    [super showWindow:sender];
}

- (void)windowDidLoad {
    [self applyOptions];
}

- (void)applyOptions {
    if([_options[@"Login"] boolValue]) {
        //if logged in ... blablabla
    }
}

Upvotes: 1

Related Questions