Add bindings for custom menu items' isHidden to (an attribute of) a custom object?
Add bindings for custom menu items' isHidden to (an attribute of) a custom object?
- Subject: Add bindings for custom menu items' isHidden to (an attribute of) a custom object?
- From: Daryle Walker <email@hidden>
- Date: Mon, 08 Sep 2014 01:12:49 -0400
Yesterday, I had a thread (<email@hidden> “Bindings to enable a menu item based on an array's element count”) on this list on how to add a Binding to a menu item’s Hidden flag based on the length of a custom object’s array-based property. I got the code working.
There is a menu item for each day that has WebHistoryItem instances. (Each menu item has a submenu with items for each web-history entry.) The custom object is pointed to one of those menu items’ submenu and creates two arrays of menu items, one has copies of the first few menu items of the source menu, the other copies of the remaining trailing menu items. With KVO signaling, the first array’s items are directly dumped in the top-level menu and the second array’s items are dumped into a submenu of the menu item following the direct items. The Binding from the previous thread hid that menu item when the corresponding array was empty.
Now whenever the custom object has at least one non-empty array, I want to hide the menu item that contains the source submenu, so I don’t have two copies visible. Right now, I handle hiding and un-hiding manually:
> - (void)prepareTodayHistoryMenu {
> NSMenu * const browseMenu = self.earlierToday.menu;
> NSMenuItem * const beyondEarlierTodayMenuItem = [browseMenu itemAtIndex:(1 + [browseMenu indexOfItem:self.earlierToday])];
>
> self.todayHistoryHandler.sourceMenu = (beyondEarlierTodayMenuItem.isSeparatorItem || ![[NSCalendar autoupdatingCurrentCalendar]
> isDateInToday:beyondEarlierTodayMenuItem.representedObject]) ? nil : beyondEarlierTodayMenuItem.submenu;
> [beyondEarlierTodayMenuItem setHidden:!!self.todayHistoryHandler.sourceMenu];
> }
I grouped the source-menu assignment and the hiding of the original menu item together.
> - (void)notifyOnNewDay:(NSNotification *)notification {
> NSMenu * const browseMenu = self.earlierToday.menu;
> NSInteger const beyondEarlierTodayIndex = [browseMenu indexOfItem:self.earlierToday] + 1;
>
> [[browseMenu itemAtIndex:beyondEarlierTodayIndex] setHidden:NO];
> // If the "today" menu item shifts, we'll lose track of this and therefore can't restore it.
> [self prepareTodayHistoryMenu];
> }
This makes sure to disconnect the menu item’s submenu from being the source menu, since it no longer belongs to Today. I assume that the latest per-day menu item has the source submenu. If there are no per-day items, then the following separator item gets a no-op set-visible action. Note that the affected menu item isn’t tracked, so I have to make it (which may be a no-op on the following separator item) re-visible before the prepare action sets its source menu to NIL. (A menu item and submenu for the new Today get created as-needed in the following method.)
> - (void)rebuildHistoryMenusDueToChange:(NSDictionary *)change {
> NSMenu * const browseMenu = self.earlierToday.menu;
> NSInteger beyondEarlierTodayIndex = [browseMenu indexOfItem:self.earlierToday] + 1;
> NSIndexSet * const indexesChanged = change[NSKeyValueChangeIndexesKey]; // May be nil, depending on 'changeType'.
>
> NSParameterAssert(beyondEarlierTodayIndex != -1 + 1);
> switch ((NSKeyValueChange)[change[NSKeyValueChangeKindKey] unsignedIntegerValue]) {
> default:
> case NSKeyValueChangeSetting: {
> // Do wholesale replacement; get rid of the current menu items and install the new ones.
> //...
> [self prepareTodayHistoryMenu]; // Rebuild today's history menus.
> break;
> }
>
> case NSKeyValueChangeRemoval: {
> // Purge the menus of the deleted indexes.
> NSParameterAssert(indexesChanged);
> [indexesChanged enumerateIndexesWithOptions:NSEnumerationReverse usingBlock:^(NSUInteger idx, BOOL *stop) {
> [browseMenu removeItemAtIndex:(beyondEarlierTodayIndex + (NSInteger)idx)];
> }];
> if ([indexesChanged containsIndex:0u]) { // Only if today was deleted,...
> [self prepareTodayHistoryMenu]; // ...rebuild today's history menus.
> }
> break;
> }
>
> case NSKeyValueChangeInsertion: {
> // Place the menus of the added indexes.
> NSParameterAssert(indexesChanged);
> [[browseMenu itemAtIndex:beyondEarlierTodayIndex] setHidden:NO];
> // If index 0 shifts, we'll lose track of this and therefore can't restore it.
> [indexesChanged enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { /*…*/ }];
> if ([indexesChanged containsIndex:0u]) { // Gained or still have a today menu item, so just update.
> [self prepareTodayHistoryMenu];
> } else {
> [[browseMenu itemAtIndex:beyondEarlierTodayIndex] setHidden:YES]; // Undo non-hiding from second line.
> }
> break;
> }
>
> case NSKeyValueChangeReplacement: {
> // Since NSMenu doesn't have a replacement API, do a removal & insert.
> NSParameterAssert(indexesChanged);
> [[browseMenu itemAtIndex:beyondEarlierTodayIndex] setHidden:NO];
> // If index 0 shifts, we'll lose track of this and therefore can't restore it.
> [indexesChanged enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { /*…*/ }];
> if ([indexesChanged containsIndex:0u]) { // Still have a today menu item, so just update.
> [self prepareTodayHistoryMenu];
> } else {
> [[browseMenu itemAtIndex:beyondEarlierTodayIndex] setHidden:YES]; // Undo non-hiding from second line.
> }
> break;
> }
> }
> }
I was thinking that if I used Bindings for one menu item, could I add Bindings between the new menu items and some attribute of the custom object, so the menu items hide themselves when being mirrored. (At most one will be hidden.) I added a NSValueTransformer to my custom object.
> @interface MyOverflowMenuController : NSObject
>
> //! Starts as nil; when set, this instance stores copies of the menu’s
> //! items and tracks the menu for item insertions, removals, and renames.
> @property (nonatomic) NSMenu * sourceMenu;
> //! Starts as zero; if the menu has more menu items that this value,
> //! the copies of the menu's latter items are stored in the overflow
> //! array instead of the direct array.
> @property (nonatomic, assign) NSUInteger maxDirectCount;
>
> //! Starts as empty; updated to mirror the source menu's menu-items.
> //! Keeps at most 'maxDirectCount' items. KVO-compliant.
> @property (nonatomic, readonly) NSArray * directMenuItems;
> //! Starts as empty; updated to mirror the source menu's menu-items.
> //! Keeps the overflow from 'directMenuItems'. KVO-compliant.
> @property (nonatomic, readonly) NSArray * overflowMenuItems;
>
> //! Transforms a NSMenu to a NSNumber with a BOOL value that's YES
> //! when the given menu is self.sourceMenu.
> @property (nonatomic, readonly) NSValueTransformer * isSourceMenuTransformer;
>
> @end
That last property is of a custom NSValueTransformer subclass. (Is not using a new .h/.m file pair for the subclass OK?) The subclass holds a pointer to the source menu, and gets updated when the outer class changes that property in the setter. The custom object should out-live the per-day history menu items. So how would connect each menu item’s Hidden attribute to the custom object and the value transformer? Am I using the right kind of transformer? The source-menu attribute should be KVO-compliant since I use the automatic getter and a custom setter (that mutate the two arrays).
—
Daryle Walker
Mac, Internet, and Video Game Junkie
darylew AT mac DOT com
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden