Search code examples
objective-cmacoscocoansopenpanel

Trouble calling NSOpenPanel from another class and attaching it to its parent window


I am running into a problem with the NSOpenPanel and calling it form another class.

I currently have one main window with a button that opens up a second window that is setup as an image editor using ImageKit. That works well. I would also like for when that image editor window opens up (as a result of the button push) the NSOpenPanel is launched. Basically I want to bypass making the user click a button to open the image editor and then click "Open" in the menu or command-O to open an image. We know that if the user is opening the image editor they will need to open an image to edit... I'd like the open panel to open when the window is displayed.

In my appDelegate.m I have this code to launch the image editor window "_imageWindow" and call the "openImage" method:

[_imageWindow makeKeyAndOrderFront:self];
Controller *controllerOpenImage = [[Controller alloc] init];
[controllerOpenImage openImage];

This works EXCEPT that the open panel which is supposed to be modal is launched as a separate window and not attached to the Image Editor window (_imageWindow) so when a user selects an image it's not opened... I've tried adding a delay to allow the _imageWindow window time to open to no avail. I've tried both IBAction(openImage) and void(openImage) with and without sender with the same result...

Here's the code to open an image in my Controller.m:

- (IBAction)openImage:(id)sender
{
    // present open panel...
    NSString *    extensions = @"tiff/tif/TIFF/TIF/jpg/jpeg/JPG/JPEG";
    NSArray *     types = [extensions pathComponents];

    NSString *url=[[NSUserDefaults standardUserDefaults] objectForKey:@"photoPath"];
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"yyyy"];
    NSString *yearString = [formatter stringFromDate:[NSDate date]];

    NSString *photoUrl =[NSString stringWithFormat:@"%@%@%@",url,@"/",yearString];

    // Let the user choose an output file, then start the process of writing samples
    NSOpenPanel *openPanel = [NSOpenPanel openPanel];
    [openPanel setDirectoryURL:[NSURL fileURLWithPath:photoUrl]];
    [openPanel setAllowedFileTypes:types];
    [openPanel setCanSelectHiddenExtension:YES];
    [openPanel beginSheetModalForWindow:_imageWindow completionHandler:^(NSInteger result) {
        if (result)
        {
                // user did select an image...
                [self openImageURL: [openPanel URL]];
        }
        [_imageView zoomImageToFit: self];
    }];
}

Is there an issue with the sender being null when called from my appDelegate.m as opposed to sender having an identity when called from the Image Editor window (_imageEditor) or am I asking to do something that just can't be done.


Solution

  • Let's look at your code. First in some method/function you execute:

    [_imageWindow makeKeyAndOrderFront:self];
    

    This accesses _imageWindow which by common naming convention is probably an instance variable or property backing variable, but could be a global - you don't say.

    Controller *controllerOpenImage = [[Controller alloc] init];
    

    This creates a brand new object storing a reference to it in a local variable.

    [controllerOpenImage openImage];
    

    This calls a method on this brand new object, and in that method you do:

    [openPanel beginSheetModalForWindow:_imageWindow ...
    

    So where does _imageWindow come from? By common naming convention it is an instance/property variable, and if so it belongs to the Controller instance and is in no way connected with the similarly named variable in the first line above. As you've not shown the init method we've no idea if you initialised this variable, if you didn't it is nil.

    On the other hand this could reference a global, and if it does it is presumably the same global referenced in this first code line above. If this is the case your code would probably work, but it doesn't...

    So it's a fair guess that both references are to distinct, but similarly named instance/property variables, that they are connected in no way, that the second is nil, and that therefore openPanel is passed a nil window reference and opens a standalone dialog.

    Addendum

    In your fourth comment you end with:

    does that make any sense?

    Unfortunately not.

    When you link an object, such as your window, to an IBOutlet of another object, such as an instance of your Controller class, then you are making a connection between the specific instances created by your NIB (the IB document).

    When the system evaluates your NIB at runtime it creates a window, being an instance of NSWindow, and an instance of your Controller class and places a reference to that specific window instance into the IBOutlet variable/property of that specific Controller instance.

    This has no impact on any other windows you might create, they are not automatically linked to Controller instances; or on any other Controller instances, they are not automatically linked to the window.

    When you write:

    Controller *controllerOpenImage = [[Controller alloc] init];
    

    You are creating a new Controller instance, which is in no way connected to the Controller instance created by your NIB. Initially the latter instance, the one you are not using, might reference your window; the former, which you are using, certainly does not.

    If you are going to create a Controller instance in your NIB then in your code you need to use that specific instance, not create a new one. To use it you need a reference to it. One way to do that would be to add a property to your appDelegate class which references it, setting up the link in your NIB.

    In an earlier comment you wrote:

    but I'm not really sure how to extrapolate a solution from the second comment

    The comment you refer to was suggesting you get your window reference into your Controller class by passing it as a parameter. This is a very basic mechanism in any programming language. In this case it is suggesting you write a method such as:

    - (instancetype) initWithWindow:(NSWindow *)aWindow { ... }
    

    And create a Controller instance using:

    [[Controller alloc] initWithWindow:_imageWindow];
    

    It sounds like you need to go back and study the basics of instances, methods, parameter passing; and then how a NIB is evaluated.

    HTH