menu:updateItem:atIndex:shouldCancel: Crazy instead of Lazy!
menu:updateItem:atIndex:shouldCancel: Crazy instead of Lazy!
- Subject: menu:updateItem:atIndex:shouldCancel: Crazy instead of Lazy!
- From: Jerry Krinock <email@hidden>
- Date: Thu, 20 Jul 2006 13:46:35 -0700
- Thread-topic: menu:updateItem:atIndex:shouldCancel: Crazy instead of Lazy!
SUMMARY:
Beginning with Panther, Cocoa provides an NSMenuItem method
-menu:updateItem:tIndex:shouldCancel: so that we can lazily-create a
hierarchical menu. By "lazy", I mean that additional submenus are created
only when the user clicks on their parent. This method gets a message from
Cocoa whenever it needs to update a submenu. But under certain other
circumstances, Cocoa will start sending this message with exponentially
increasing frequency; eventually the app stops responding. I cannot find
any "cycles" in my code which would cause this.
A workaround and code are given. I'm asking if anyone has ever seen this or
can find anything wrong with my code.
LONG STORY:
My app gives outline and table views of data objects which are organized
into a tree. I've just added a "Move to ->" contextual menu which is
attached to the rows one of an NSTableView showing search results. Under
"Move to ->" is a hierarchical menu which mirrors the entire tree, so that
the user can right-click an item and then move it anywhere (almost; I have
to check for incest and other unallowed moves).
I believe I've done this in the recommended way, by implementing the
NSMenuItem delegate method
menu:(NSMenu*)menu updateItem:(NSMenuItem*)item atIndex:(int)index
shouldCancel:(BOOL)shouldCancel
Now, the argument "item" is "blank" NSMenuItem which my implementation is
supposed to "fill in", knowing its parent "menu" and its "index". In my
implementation therefore, I set its title, target, action, representedObject
and, if item has children, a submenu. But this submenu is, in turn, also
"blank"; the only thing I have set is its delegate, so it can call me back
when needed.
My interpretation of Apple's documentation, and "old-time engineering common
sense", is that the system will send me this message only when needed as the
user clicks through the hierarchy. Slick.
It does send the message like this. But under certain other circumstances
Cocoa will start sending this message like crazy: If, after the contextual
menu has been shown once, the user changes the table selection using the up-
or down-arrow keys, this message gets sent extraneously, as much as needed
to fill the entire hierarchy! Note here that the user is simply stepping
through the table...the contextual menu is not even displayed. It gets
worse: With the second such up/down keypress, it sends this message
repeatedly for the same items, and the number of messages grows
exponentially each time an up/down arrow key is pressed. We get longer and
longer spinning kaleidescopes, and eventually the app stops responding.
Now, I've looked all through my code for any kind of closed cycles which
would cause this, but can't find anything. Here's the kicker: If the user
changes the selection by clicking in the table with the mouse instead of
using up/down arrow keys, I get no crazy extraneous messages.
I've worked around the problem by declaring a BOOL _beLazy. I test it
before doing anything in my implementation of
menu:updateItem:item:atIndex:index:shouldCancel: and if it is YES, do
nothing. To enable normal operation, I set it to NO in my implementation of
the table delegate's menuForTableColumnIndex:rowIndex: method, because this
always gets invoked when the user right-clicks on the table. Then, to
disable the craziness, I set it to YES in my implementation of the table
delegate's tableViewSelectionDidChange: method, since this gets invoked
whenever the up/down arrows are keyed. With this workaround, I get a single
burst of crazy extraneous messages the first time the user hits up/down
arrow. But when I ignore these messages, I don't get any more after that.
There are a couple other things that I don't understand about
menu:updateItem:atIndex:shouldCancel:. (1) What is the purpose of the
argument shouldCancel? I've never gotten any messages where it was YES.
It's always NO. (2) What is the purpose of allowing me to return NO? (It
does not stop the craziness.)
Has anyone else ever seen this behavior using
menu:updateItem:atIndex:shouldCancel:, or is there something I am doing
wrong?
Jerry Krinock
I've pasted in my implementation of this and the other NSMenu delegate
method below, although due to the complications of dealing with root vs.
non-root items, it is, like most "dataSource" methods, pretty convoluted;
items must be "cooked" before they are returned. SSMenu is my subclass of
NSMenu which has an additional instance variable, _owningMenuItem, which I
need to go backwards and get the representedObject of the NSMenuItem of
which the NSMenu is a submenu. (Another little thing I think Apple omitted,
but we can dicuss that in some other thread).
@implementation MenuOutlineDataSource
- (int)numberOfItemsInMenu:(SSMenu*)menu {
BmItem* itemRepresentedByMenu = [[menu owningMenuItem]
representedObject] ;
int answer ;
if (!itemRepresentedByMenu) {
// root item
answer = [[[NSApp delegate] openBmDocs] count] ;
}
else {
// not root item
answer = [itemRepresentedByMenu numberOfSubfolders] ;
}
return answer ;
}
- (BOOL)menu:(SSMenu*)menu
updateItem:(NSMenuItem*)item
atIndex:(int)index
shouldCancel:(BOOL)shouldCancel {
id target = [[NSApp delegate] searchController] ;
if (![target beLazy]) {
BmItem* itemRepresentedByMenu =
[[menu owningMenuItem] representedObject] ;
NSString* title ;
id rawRepresentedItem ;
BmItem* cookedRepresentedItem ;
if (!itemRepresentedByMenu) {
// itemRepresentedByMenu is a root
rawRepresentedItem =
[[[NSApp delegate] openBmDocs] objectAtIndex:index] ;
title = [rawRepresentedItem formattedName] ;
cookedRepresentedItem = [rawRepresentedItem root] ;
}
else {
// itemRepresentedByMenu is not a root
rawRepresentedItem =
[[itemRepresentedByMenu subfolders] objectAtIndex:index] ;
title = [rawRepresentedItem name] ;
cookedRepresentedItem = rawRepresentedItem ;
}
[item setRepresentedObject:cookedRepresentedItem] ;
[item setTitle:title] ;
[item setTarget:target] ;
SEL action ;
if ([[NSApp delegate]
checkOkToInsertItems:[target selectedObjects]
atBmLocation:[BmLocation
BmLocationWithParent:cookedRepresentedItem index:0]]) {
action = @selector(moveOrCopySelectionToSender:) ;
if ([cookedRepresentedItem numberOfSubfolders] > 0) {
// Create and add a submenu to item
SSMenu* submenu =
[[SSMenu alloc] initWithOwningMenuItem:item] ;
[submenu setDelegate:self] ;
[item setSubmenu:submenu] ;
[submenu release] ;
}
}
else {
// Disable (gray-out) item:
action = nil ;
}
[item setAction:action] ;
}
return YES ;
// If you return NO, it does not come back and ask to update any
// more items, so you can end up with "empty" menu items.
// I don't know understand why you'd ever return NO.
}
@end
_______________________________________________
Do not post admin requests to the list. They will be ignored.
Cocoa-dev mailing list (email@hidden)
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden