I have a document based cocoa application with an item in the application menu hooked up to an IBAction. Clicking the item needs to perform a task that uses an IBOutlet in the main nib file which is using another class, MyDocument. Creating 2 objects of the same class, one in each nib seems to not be working. How can I access the outlet?
Actions for menu items are often sent to the first responder so that whatever is currently selected can act on it.
It sounds like this action is something that works on the current document, then it should be implemented by the document. In this case have the menu send it's action to the first responder and then put the action method in the MyDocument class.
If the action you are trying to send is a custom one: in the Main Menu nib select the First Responder item, add your method name, then connect the menu item's selector to the action.
Read the Responders section of the Cocoa Event-Handling Guide for more info.
To summarize the above, in your NIB/XIB file, in interface builder make the connection to the First Responder object, not to Files Owner or anything else. You'll still be offered a lit of actions across potential first responders.
Cocoa then takes that selector and looks for it, starting with the NSView (if any) that's currently the first responder, then with the NSDocument that's currently in use, then with it's window controller etc etc all the way up to the Application delegate. The first object it checks that actually implements that method, it will use that object (after validating it with that same object).
So:
#interface MyDocumentTypeA : NSDocument {
}
-(void)myMenuAction:(id)sender;
-
#interface MyDocumntTypeB : NSDocument {
}
// -myMenuAction: not implemented here
-
#interface MyApplicationDelegate ... {
}
-(void)myMenuAction:(id)sender;
-
In Interface builder (or even programmatically), if you've linked the "action" of the menu item to a selector named "myMenuAction:" on the First Responder (which equates to not specifying a target when done programmatically), for the above two document subclasses the following will happen.
For MyDocumentTypeA, when the user selects that menu item, MyDocumentTypeA's -myMenuAction: will be invoked. Since MyDocumentTypeB does not implement this action, Cocoa will continue to look up the responder chain until it gets to your application delegate, which does implement it, so it will be invoked here instead.
If Cocoa finds no objects in the responder chain that implement the method, the menu item remains disabled.
There is a way how to do this, I've posted the answer in a similar thread: Access IBOutlet from other class (ObjC)
Related
I'm creating my first NSDocument based application. I'm able to create new documents, both from scratch and by importing legacy files.
This app will allow multiple windows per document, so I am overriding makeWindowControllers. This method is currently very simple:
- (void) makeWindowControllers
{
if (documentDatabase == nil) return;
DataSheetWindowController * dswc = [[DataSheetWindowController alloc] initWithDatabase:documentDatabase];
[self addWindowController: dswc];
}
The window appears as expected, however, the Save, Revert to Save, and other document enabled menus are disabled, as if the document was not in the responder chain.
As an experiment, I tried adding this method to my NSWindowController class:
- (void)saveDocument:(id)sender {
[[self document] saveDocument:sender];
}
With this method in place, the Save menu item is enabled, and selecting it causes the document's save methods to be invoked.
From reading the documentation and other questions on Stack Overflow, it's clear that something is wrong -- I should NOT have to put this method in the NSWindowController class. I'm sure I've overlooked something silly, but for the life of me I cannot figure out what it is, or any other mention of this problem here or elsewhere on the web.
Some additional information that may be useful -- in IB, the window's owner and delegate are set to the NSWindowController. I created a method to display the responder chain (see How to inspect the responder chain?) and the document was not listed. Here is the output of the responder chain (however, since NSDocument is not a subclass of NSResponder, I'm not sure if it is supposed to be listed anyway).
RESPONDER CHAIN:
<NSClipView: 0x102344350>
<NSScrollView: 0x102344480>
<NSView: 0x102345040>
<NSWindow: 0x10234e090>
Since the saveDocument method I put into the NSWindowController class does work, that indicates to me that the window controller does know that it is associated with the document.
So -- any thoughts as to why the document is behaving as if it is not in the responder chain?
Updated info: After setting up a new document, the initWithType method includes this temporary line to make sure that the document status is edited:
[self updateChangeCount:NSChangeDone];
I have verified that isDocumentEdited returns true.
I'm going to suggest that the solution is the one pointed to here:
https://stackoverflow.com/a/9349636/341994
In the nib containing the window that the window controller will be loading, the File's Owner proxy needs to be of the window controller's class (select the File's Owner proxy and examine the Identity inspector to confirm / configure that), and its window outlet must be hooked to the window and the window's delegate outlet must be hooked to the File's Owner proxy (select the File's Owner proxy and examine the Connections inspector to confirm that).
I have a Document.xib and a MainMenu.xib, and a class MainController. I added an instance of MainController to Document.xib by dragging the NSObject from the object library to Document.xib's instance tree and setting the class in the property to MainController. I added a button and connected it to one of the actions provided by MainController.
So far, so clear. Now I basically want to call the same action from a menu item. Obviously, I can't just add another instance of MainController to the MainMenu.xib, because I'd end up with two instances. There should be just one per document, and the menu item should call the action in the active document's MainController. How do I do this?
This is what the First Responder proxy icon is for. You can connect menu items to the First Responder proxy, and messages will be sent up the responder chain until they reach an object that handles the message. Your document, along with views and other objects, will participate in the responder chain, and so will have an opportunity to handle the message provided an object earlier in the chain hasn't already done so. The responder chain concept also ensures that the message is delivered to the active document -- if you have more than one document open, you naturally want menu commands to be handled by the document that the user is working on.
So, just make the First Responder icon the target for your menu items and the right thing will happen.
I have a Document based application. I want to add a contextual menu that displays context-sensitive info when the user right-clicks selected text in an NSTextView.
I have followed the advice in the Apple documentation and
Added an NSMenu as a root object in my XIB file.
Connected the NSMenu instance to the menu outlet of the NSTextView.
Connected an IBAction to the NSMenuItem inside the NSMenu.
So far so good. Every thing works as expected: the menu item appears and the action is called when it is selected.
I need to get the selected text from the NSTextView before the menu appears so that I can configure my menu item appropriately. According to the docs
If you need to customize the contextual menu, you can do so by setting
an appropriate object as the menu’s delegate and implementing the
menuWillOpen: method to customize the menu as you see fit just before
it appears.
I connect the delegate of the NSMenu to File's Owner. None of the delegate methods are called. ( menuWillOpen: is the only one I need, but I've tried others, too).
I set a breakpoint inside the IBAction that gets called when the menu item is selected. If I inspect the menu with the debugger I can see that the delegate is correctly set to the object that implements the delegate method.
Is there anything else to check? Anything I'm doing blatantly wrong?
Xcode v4.6.3
SDK v10.8
Deployment target 10.7
After some digging, this is what I found: NSTextView builds a different NSMenu instance to use as the contextual menu, probably by overriding -menuForEvent: or some similar internal method. This new menu copies the menu items from the menu you created in Interface Builder (in fact, it creates new menu item instances whose attributes are copied from the original menu items) but it does not copy the menu delegate, which is why your menu delegate does not receive -menuWillOpen:. I am not sure whether this is intentional or not. Reading that documentation quote you posted, it seems to be a bug.
What you can do is to set the delegate of your NSTextView instance to an object whose class conforms to NSTextViewDelegate (maybe your File’s Owner, which already conforms to NSMenuDelegate) and implement the following method:
- (NSMenu *)textView:(NSTextView *)view menu:(NSMenu *)menu forEvent:(NSEvent *)event atIndex:(NSUInteger)charIndex
{
// if the menu delegate is not self, set another object
[menu setDelegate:self];
return menu;
}
This will make sure that the contextual menu created by the text view uses your delegate.
NB: since NSTextView creates a different contextual menu, it could be the case that it might want to set the menu delegate to itself or some other internal object. In my tests the delegate is nil, so it looks like it’s safe. Alternatively, you could discard the proposed menu argument and return your own NSMenu instance with the delegate correctly set.
Finding this thread saved me a lot of time...thanks! Here's an implementation that works in an NSView in Swift. myNSMenu is an outlet from Storyboard to appDelegate and a subclass of NSMenu. Without the assignment of the delegate in the code below, the NSMenuDelegate functions were not called.
let appDelegate = NSApplication.sharedApplication().delegate as! AppDelegate
appDelegate.myNSMenu.delegate = appDelegate.myNSMenu
NSMenu.popUpContextMenu(appDelegate.myNSMenu, withEvent: theEvent, forView: self)
I have a menu item whose state should depend on whichever NSDocument is open. From my understanding, to make its state change dynamically I should use the NSMenu delegate method menuNeedsUpdate:.
It seems like I would want to have the menu's delegate be the First Responder in MainMenu.xib. However, Interface Builder won't let me set it as the Main Menu's delegate. How can I make a delegate which will be able to access the currently active document?
I generally make such changes in the validateMenuItem: method being called before the menu is shown. The receiver of the action is asked whether the item is to be enabled or not. But you can do pretty much any change there. Since 10.5 it is also safe to add and remove items during such a call.
I'm having a problem with my Cocoa app. I'm using my app delegate as a controller, and opening one window in a NIB file. Clicking a toolbar button opens another window from another NIB. Clicking save on this second window calls a method on the app delegate/controller. All this works fine.
The strange thing is that I can't figure out is that the app delegate points to one memory location when I click the toolbar button and to a different memory location after clicking save on the second window. It's as if a second app delegate/controller is being created, though stepping through the code doesn't give me any indication of that occurring.
Is there a better way to architect this type of application? Any idea of where I'm going wrong?
It sounds like you're creating a second instance of your AppController class in your window's nib file. You can't do that, each instance of an object in a nib file will be instantiated when a nib is unarchived at runtime. This means if you have an AppController instance in MainMenu.xib and also one your MyWindow.xib file, the AppController object will be alloced and initialized twice.
Normally the way you'd handle this is by using the responder chain. In your Window nib, you assign First Responder as the target of your actions. This means that when the action method is called, the app will ask the currently focused view/control (the one that has first responder status) if it responds to the method by calling the -respondsToSelector: method and passing in the action selector.
If the first responder doesn't respond to the method, the message travels up the responder chain until an object that does respond to the method is found. If no object responds to the method, the NSApplication instance handles it and calls NSBeep().
Just before the method is sent to the NSApplication instance, the application delegate is asked if it responds to the selection. In this case, if your AppController object is set as the application delegate, it will receive the message sent as the action from your object in the window nib.
If this isn't clear enough, it's worth reading the Event Handling guide
You don't have to use the responder chain. You can call a method on the application delegate by calling [[NSApp delegate] yourMethod]. You could also store a reference to the app controller by adding it as an instance variable to your NSWindowController object that loads the nib and setting it at creation time, like so:
- (id)initWithAppController:(id)aController
{
self=[super initWithWindowNibName:#"YourWindowNibName"];
if(self)
{
appController = [aController retain];
}
return self;
}
Your window controller can then call methods of your AppController directly.