The Apple documentation says that the sender passed to the NSMenuItem's action can be set to some custom object, but I can't seem to figure out how to do this. Is there a method I'm not seeing someplace in the documentation?
I'm not sure what piece of documentation you're referring to (a link would help).
You can use the -setRepresentedObject: method of NSMenuItem to associate an arbitrary object with a menu item:
//assume "item" is an NSMenuItem object:
NSString* someObj = #"Some Arbitrary Object";
[item setRepresentedObject:someObj];
[item setAction:#selector(doSomething:)];
Then when the menu item sends its action message you can obtain the object:
- (IBAction)doSomething:(id)sender
{
NSLog(#"The menu item's object is %#",[sender representedObject]);
}
Related
I'm using a custom view for NSMenu items so I can control the background colour via isHighlighted.
The issue is, if you use a combination of mouse and keyboard to navigate the menu, it's possible to have two items selected at once. This is because drawRect isn't being called on some items to dehighlight them
Has anyone else run into this?
NSMenuItems should be created using:
NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:#"" action:#selector(menuItemSelected:) keyEquivalent:#""];
where the selector menuItemSelected: is a valid method. isHighlighted won't be toggled if a valid action selector is not provided
Conform NSMenuDelegate and implement following function like this:
func menu(_ menu: NSMenu, willHighlight item: NSMenuItem?) {
for item in menu.items.compactMap({ return $0.isHighlighted ? $0 : nil }) {
item.view?.needsDisplay = true
}
}
I have a View-based NSOutlineView, and have in the class a selection change event:
- (void)outlineViewSelectionDidChange:(NSNotification *)notification
{
NSLog(#"Selected Row inside:%ld",[self selectedRow]);
}
This is the way I create my NSOutlineView:
ovc = [[OutlineViewController alloc] init];
[myOutlineView setDelegate:(id<NSOutlineViewDelegate>)ovc];
[myOutlineView setDataSource:(id<NSOutlineViewDataSource>)ovc];
MyOutlineView is created in IB.
Every time I click on a row, the event gets fired, however the result is always -1.
NSLog(#"Item 0:%#",[self viewAtColumn:1 row:0 makeIfNecessary:YES]);
Always returns NULL.
Is there something specific I should do ? Thanks.
=== EDIT ===
I have published my simplified code showing the issue: http://www.petits-suisses.ch/OutlineView.zip
Instead of checking the selectedRow of self object, which is just a object initialized in AppController which is a wrong instance. You need to check on the notification object as shown below.
NSLog(#"Selected Row:%ld",[[notification object] selectedRow]);
Also clickedRow is meaningful in the target’s implementation of the action. So the clickedRow gives correct value if checked inside a Action or DoubleAction method.
Your NSOutlineView "Controller" class is actually a subclass of NSOutlineView, this is different then the NSOutLineView in your XIB file. If you look at the notification object being sent it is an instance of NSOutlineView, not "OutlineViewController", therefore you are calling selectedRow on an incorrect instance.
This code should be place in a subclass of NSViewController as opposed to NSOutlineView. Then create an outlet from the outlineView to the controller.
I have a NSPopUpButton configured with bindings and coredata. Everything is working perfectly, however I would like to add a item that implements an action to "edit the list", like
Item 1
Item 2
Item 3
Item 4
------
Edit List..
Is this Possible to do with Bindings?
I think that the answer is NO, at least not completely. I thought I would provide the content to the button programatically and maintain bindings for the Selected Value , so this is what I came up with
- (void)updateSectorPopupItems
{
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Sector"];
NSSortDescriptor *sortPosition = [[NSSortDescriptor alloc] initWithKey:#"position" ascending:YES];
[request setSortDescriptors:#[sortPosition]];
NSError *anyError = nil;
NSArray *fetchObjects = [_gdcManagedObjectContext executeFetchRequest:request
error:&anyError];
if (fetchObjects == nil) {
DLog(#"Error:%#", [anyError localizedDescription]);
}
NSMutableArray *sectorNames = [NSMutableArray array];
for (NSManagedObject *sector in fetchObjects) {
[sectorNames addObject:[sector valueForKey:#"sectorCatagory"]];
}
[_sectorPopUpBotton addItemsWithTitles:sectorNames];
NSInteger items = [[_sectorPopUpBotton menu] numberOfItems];
if (![[_sectorPopUpBotton menu] itemWithTag:1] ) {
NSMenuItem *editList = [[NSMenuItem alloc] initWithTitle:#"Edit List..." action:#selector(showSectorWindow:) keyEquivalent:#""];
[editList setTarget:self];
[editList setTag:1];
[[_sectorPopUpBotton menu] insertItem:editList atIndex:items];
}
A couple of problems I'm having with this
1) When adding the Menu Item using
[_sectorPopUpBotton menu] insertItem:editList atIndex:items];
no matter what value is entered in atIndex, the item always appears at the top of the Menu list.
2) I just want the "Edit List..." menuitem to initiate the action, how do I prevent this from being selected as a value?
You might as well do that using an NSMenuDelegate method.
Actually in this way you can also keep the bindings for getting the NSPopUpButton content objects (in your case from the NSArrayController bound to the CoreData stack).
1) Set an object as delegate for the NSPopUpButton internal menu, you can do that in the Interface Builder by drilling down the NSPopUpButton to reveal its internal menu. Select it and then set its delegate in the Connections Inspector panel to the object you have designated to this task. As such delegate you might for example provide the same ViewController object which manages the view where the NSPopUpButton exists.
You'll then need to have the object provided as delegate adhere to the NSMenuDelegate informal protocol.
2) Implement the NSMenuDelegate method menuNeedsUpdate: there you'll add the NSmenuItem(s) (and eventually separators) you want to provide in addition to those already fetched by the NSPopButton's bindings.
An example code would be:
#pragma mark NSMenuDelegate
- (void)menuNeedsUpdate:(NSMenu *)menu {
if ([_thePopUpButton menu] == menu && ![[menu itemArray] containsObject:_editMenuItem]) {
[menu addItem:[NSMenuItem separatorItem]];
[menu addItem:_editMenuItem];
}
}
In this example the _editMenuItem is an NSMenuItem property provided by the object implementing this NSMenuDelegate method. Eventually it could be something as this:
_editMenuItem = [[NSMenuItem alloc] initWithTitle:#"Edit…" action:#selector(openEditPopUpMenuVC:) keyEquivalent:#""];
// Eventually also set the target for the action: where the selector is implemented.
_editMenuItem.target = self;
You'll then implement the method openEditPopUpMenuVC: to present to the user the view responsible for editing the content of the popUpButton (in your case the CoreData objects provided via bindings).
The only problem I haven't yet solved with this approach is that when getting back from the view where the edit happens, the NSPopUpButton will have the new item "Edit…" selected, rather than another "valid" one, which is very inconvenient.
Is there a way to change (for the purpose of localization) the null placeholder in a binding in Cocoa?
The bindings are set up in Interface Builder for a popup button. The two-way nature of the bindings as set up in IB is needed, so doing it programmatically is not really appealing.
I am aware that the standard way of handling localizations of a nib file is by making one for each language, but since this is the only difference in the whole nib file between the language versions, it seems a bit excessive for a single string.
If there is a way to modify a binding created in IB, I was thinking about doing it in the file's owner's awakeFromNib method.
In the controller object to which you bind, such as your NSDocument class, override -bind:toObject:withKeyPath:options:. This needs to be the target of that method invocation – the object you select under Bind to: in the nib.
If you bind to an NSObjectController or NSArrayController, you'll need a subclass.
That method should rewrite the options dictionary and invoke super, replacing the value for NSNullPlaceholderBindingOption with your localized string.
I would omit the null placeholder from the nib and that key value in code, though you could of course take the passed-in value for that key and translate it, instead.
The other answer no longer seems to work so I've come up with a slightly different solution which modifies an existing binding to use the given null placeholder string:
I have this method in my view controller:
- (void)rebind:(NSString *)binding of:(id)object withNullPlaceholder:(NSString *)nullPlaceholder {
// Possibly a bad idea, but Xcode doesn't localize the null placeholder so we have do it manually.
NSDictionary *bindingInfo = [object infoForBinding:binding];
id bindObject = bindingInfo[NSObservedObjectKey];
NSString *keyPath = bindingInfo[NSObservedKeyPathKey];
NSMutableDictionary *options = [bindingInfo[NSOptionsKey] mutableCopy];
options[NSNullPlaceholderBindingOption] = nullPlaceholder;
[object unbind:binding];
[object bind:binding toObject:bindObject withKeyPath:keyPath options:options];
}
I call this in awakeFromNib for all the bindings that need it and pass in a localized string:
- (void)awakeFromNib {
// Hacky hack hack: Xcode is stupid and doesn't localize the null placeholders so we have to do it.
[self rebind:#"contentValues" of:self.fooPopup withNullPlaceholder:NSLocalizedString(#"No foos available", #"foo popup null placeholder")];
[self rebind:#"contentValues" of:self.barPopup withNullPlaceholder:NSLocalizedString(#"No bars available", #"bar popup null placeholder")];
}
The localized strings are then localized normally as part of the Localizable.strings file.
I was able to change the null placeholder string (i.e. "No Value") in a NSPopUpButton that uses bindings.
Specifically, I wanted to have an popup button menu item that had a title other than "No Value" with a represented object of nil. An empty NSString or nil should be saved in the user defaults when the null placeholder menu item is selected.
NSPopUpButton Bindings:
Content is bound to an NSArrayController.arrangedObjects
Content Objects is bound NSArrayController.arrangedObjects.exampleContentObject (NSString), this is the object that the menu item represents and is the Selected Object,
Content Values is bound to NSArrayController.arrangedObjects.exampleContentValue (NSString), this is the title shown in the popup button's menu item.
Selected Object of the popup button is bound to NSSharedUserDefaultsController.values.ExampleUserDefaultsKey which is the same object type as the Content Objects and Selected Object (NSString). This object should match the object type of the NSUserDefault's key specified in the binding. When an item from the popup button is selected, it will save the Selected Object to the user defaults.
To change the null placeholder string from "No Value" to something else, subclass NSPopUpButton and override -[NSPopUpButton bind:toObject:withKeyPath:options:].
#interface CustomPopUpButton : NSPopUpButton
#end
#implementation CustomPopUpButton
- (void)bind:(NSString *)binding toObject:(id)observable withKeyPath:(NSString *)keyPath options:(NSDictionary<NSString *,id> *)options {
NSMutableDictionary *mutableOptions = options ? [options mutableCopy] : [NSMutableDictionary dictionaryWithCapacity:1];
mutableOptions[NSInsertsNullPlaceholderBindingOption] = #YES;
mutableOptions[NSNullPlaceholderBindingOption] = #"Custom Null Placeholder Text";
[super bind:binding toObject:observable withKeyPath:keyPath options:[mutableOptions copy]];
}
#end
Finally, select the NSPopUpButton in Interface Builder and under Custom Class in the Xcode Identity Inspector to your the class to the NSPopUpButton subclass.
I am having a problem. My contextual menu is getting displayed but the menu items are not activated.
so my new code for displaying the menu is as follows:
NSMenu *defMenu = [[[NSMenu alloc] initWithTitle:#"default Contextual Menu"] autorelease];
[defMenu insertItemWithTitle:#"Open" action:#selector(openFile) keyEquivalent:#"" atIndex:0];
[defMenu insertItemWithTitle:#"Delete" action:#selector(deleteFile) keyEquivalent:#"" atIndex:1];
return defMenu;
and function declaratons of deleteFile and openFile are as follows:
-(int)openFile;
-(int)deleteFile;
and i am calling my contextual menu as follows:
-(void)doSingleClick
{
if([[NSApp currentEvent] modifierFlags] & NSControlKeyMask)
{
NSLog(#"control clicked.......");
[NSMenu popUpContextMenu:[self defaultMenu] withEvent:[NSApp currentEvent] forView:tableView];
return;
}
}
my contextual menu items are all shaded and cannot be clicked. Please can you tell where i am going wrong.
Thanks
Your openFile: method takes an int as a parameter. Since insertItemWithTitle:action:withObject:keyEquivalent:atIndex: takes an object, the selector you give it must also take an object.
You can use NSNumber to wrap your int as an object, and simply change your openFile: method to take an NSNumber rather than an int. Like so:
[defMenu insertItemWithTitle:#"Open" action:#selector(openFile:) withObject:[NSNumber numberWithInt:5] keyEquivalent:#"" atIndex:0];
- (void)openFile:(NSNumber *)fileNumber {
int rowClicked = [fileNumber intValue];
// Do whatever your old method did here
}
EDIT: To answer your updated question:
The reason your menu items are disabled is that you've only told them what method name to call. You never told the items on which object instance those methods should actually be called. To fix this, you need to set the items' target:
NSMenuItem *openItem = [defMenu insertItemWithTitle:#"Open" action:#selector(openFile:) withObject:[NSNumber numberWithInt:5] keyEquivalent:#"" atIndex:0];
[openItem setTarget:self];
And so forth for each item you've got.
You can't define such an action. An action is a method that takes one object argument representing the object that triggered the action message. You need to create an action in your controller that calls through to the underlying openFile: method.