PDA

View Full Version : Document-based menu




MrFusion
Aug 22, 2006, 06:16 AM
Hey,

There is something I don't know or understand about document-based app's.
A new project has two nibs, one is MyDocument, the other one is MainMenu.
So the main things of my application are either in the MyDocument subclass or in other classes that I made.

Now I want to hook up some extra menu's. But how do I do this?
I know how to add menu's in a non-document-based app.
I know how to use bindings, or hook up action methods in IB. That's not the question. I need to know in which class to put the actions (e.g. -(IBAction) testMenu:(id)sender;).
And will this class be loaded at start-up? Do I need to do an alloc and init?

Do I have to change the file owner of the MainMenu, or create a subclass of ... and make an instance from it?

Thanks



robbieduncan
Aug 22, 2006, 07:32 AM
Lets assume we have created a document subclass called MyDocument (the default) and we have a method:


- (IBAction) doSomething:(id)sender;


in that class.

We can create a new menu item in MainMenu.nib titled "Do Something" on one of the menus. The double click on the First Responder and add an action doSomething: Set the target/action to the First Responder/doSomething: (via the normal drag method). And that's it.

This works as the First Responder will automatically pass any action it does not handle itself down the responder chain. In a document based app the document class is in the responder chain. Even cooler if you have a window that does not have a document (say your preferences window) then this item will automatically be de-activated as there is no valid responder for it in the chain!

slooksterPSV
Aug 22, 2006, 09:39 AM
Hey,

There is something I don't know or understand about document-based app's.
A new project has two nibs, one is MyDocument, the other one is MainMenu.
So the main things of my application are either in the MyDocument subclass or in other classes that I made.

Now I want to hook up some extra menu's. But how do I do this?
I know how to add menu's in a non-document-based app.
I know how to use bindings, or hook up action methods in IB. That's not the question. I need to know in which class to put the actions (e.g. -(IBAction) testMenu:(id)sender;).
And will this class be loaded at start-up? Do I need to do an alloc and init?

Do I have to change the file owner of the MainMenu, or create a subclass of ... and make an instance from it?

Thanks

In a document-based app, you can create an NSObject class in the MainMenu.nib that will handle your menu items, so for example, the user chooses View - Set Window Size or some other menu you made up you can add an action to that menu.

So for example:
You choose Classes tab you push enter when NSObject is the only thing selected, name that class (usually AppController is what a lot use) and add actions to that AppController. Now you can create the files and instaniate it. Once you've done that, go back to your Instances tab and link the menu item to the AppController and choose which item you want it to control.

Now Robbie mentioned a way that you're probably asking how to do, I don't know about that way - never tried it - but he has a lot of Cocoa programming experience. -- Very good resource.

robbieduncan
Aug 22, 2006, 10:31 AM
You choose Classes tab you push enter when NSObject is the only thing selected, name that class (usually AppController is what a lot use) and add actions to that AppController. Now you can create the files and instaniate it. Once you've done that, go back to your Instances tab and link the menu item to the AppController and choose which item you want it to control.

There are a couple of downsides to doing it this way (which I used to do until not that long ago, look at old version of Poster Paint which use this method, I used the AppDelegate)

Downsides:

1) You might need to create an additional class you don't use for anything else.

2) Your menu actions connect to a class that simply forwards on to another class. This means that the AppControler has to figure out which NSDocument object to send the message to (not hard) and forward it on. This is an additional runtime lookup and will add (a very small) extra amount of time to the action.

3. You don't get automatic menu item validation. You will have to write your own validateMenuItem: (iirc) method to disable the menu item when non-document windows are key.

If I remember later (possible after some beer) I'll post a really small sample that shows the method I described above.

slooksterPSV
Aug 22, 2006, 11:40 AM
There are a couple of downsides to doing it this way (which I used to do until not that long ago, look at old version of Poster Paint which use this method, I used the AppDelegate)

Downsides:

1) You might need to create an additional class you don't use for anything else.

2) Your menu actions connect to a class that simply forwards on to another class. This means that the AppControler has to figure out which NSDocument object to send the message to (not hard) and forward it on. This is an additional runtime lookup and will add (a very small) extra amount of time to the action.

3. You don't get automatic menu item validation. You will have to write your own validateMenuItem: (iirc) method to disable the menu item when non-document windows are key.

If I remember later (possible after some beer) I'll post a really small sample that shows the method I described above.

If you would, that'd be great, cause if that method works, I'd use that method. I can see how doing the way that i only know how to do it, would put a lot of unnecessary code in an app.

robbieduncan
Aug 22, 2006, 04:10 PM
So as promised (after a few pints) here is a short example. The 10 steps to create this example are:

1) Create new Project (ResponderDemo)
2) Open MainMenu.nib
3) Double click First Responder
4) Add new actions (drinkGuinness: and drinkBitter:)
5) Add new menu items
6) Connect items to First Reponder
7) Save and close MainMenu.nib
8) Create MyDocument window layout
9) Have IB create MyDocument files
10) Create actions for drinkGuinness and drinkBitter:

The project is attached.

I hope this is reasonably clear. For a bit of extra wow factor try creating a preferences window that can become key (or you show as key) and watch the menu items get auto-disabled.

robbieduncan
Aug 22, 2006, 04:15 PM
OK. It's only a couple of minutes extra effort. Here's a version with a (blank) preferences window as well to show the auto-disabling of the menu items.

MrFusion
Aug 23, 2006, 02:25 AM
OK. It's only a couple of minutes extra effort. Here's a version with a (blank) preferences window as well to show the auto-disabling of the menu items.

Ok, thank you. That helped a lot.
Now I also know I have to read the stuff about the responder chain. I am taking it one step at the time.

<edit>
An additionnel question. What do to do if the menu or submenu depends on bindings? e.g. adding or removing entries from an NSArrarycontroller (coredata). In this case the menu has to be in the nib of the document window, or the bindings can't be connected.

robbieduncan
Aug 23, 2006, 07:20 AM
An additionnel question. What do to do if the menu or submenu depends on bindings? e.g. adding or removing entries from an NSArrarycontroller (coredata). In this case the menu has to be in the nib of the document window, or the bindings can't be connected.

I've never wanted to do that so I can't say for sure. The main menu should always be in the MainMenu.nib. You could bind the menu to an array controller in main menu then programtically bind that a path in the document controller to get to the correct array perhaps...

slooksterPSV
Aug 23, 2006, 11:59 AM
Robbie, that is amazing, oh my gosh, that is unreal - it's that easy with a docu based? omg, so my way was really complex and unecessary holy cow

MrFusion
Aug 24, 2006, 05:47 AM
Ok, thank you. That helped a lot.
Now I also know I have to read the stuff about the responder chain. I am taking it one step at the time.

<edit>
An additionnel question. What do to do if the menu or submenu depends on bindings? e.g. adding or removing entries from an NSArrarycontroller (coredata). In this case the menu has to be in the nib of the document window, or the bindings can't be connected.

Ok. For those who have the same question someday.

The MyDocument.nib has an arraycontroller (bound to coredata for example) and you have a menu whose enabled mode depends on the "canAdd" of the arraycontroller.

The following seems to work.
Add the menuitems to the MainMenu.nib, and the actions to the First responder. Just like Robbie says.

In the MyDocument.nib add an outlet to the file owner (thus MyDocument class) for an NSArrayController, and hook it up to the arraycontroller.

Now you have acces the "canAdd" and "canRemove" with
[myArraycontroller valueForKey:@"canRemove"]

In the MyDocument class, add
-(BOOL)validateMenuItem:(NSMenuItem *)theMenuItem;
and
-(BOOL)validateMenuItem:(NSMenuItem *)theMenuItem
{ BOOL enable = [self respondsToSelector:[theMenuItem action]]; //handle general case.
if ([theMenuItem action] == @selector(remove:))
{
enable = [[myArraycontroller valueForKey:@"canRemove"] boolValue];
} else if ([theMenuItem action] == @selector(add:))
{
enable = [[myArraycontroller valueForKey:@"canAdd"] boolValue]
}
return enable;
}
(from:http://www.cocoadev.com/index.pl?NSMenuValidation)

<edit>
It works fine, but there is a noticeable delay in adding an entry when using the menu, compared to a button that hooks up directly to my "addEntry" routine. Removing an entry is instant in both cases.