// // ICeCoffEEServicePrefController.m // ICeCoffEE APE // // Created by Nicholas Riley on Fri Jun 06 2003. // Copyright (c) 2003 Nicholas Riley. All rights reserved. // #import "ICeCoffEEShared.h" #import "ICeCoffEEServices.h" #import "ICeCoffEEServicePrefController.h" #import "ICeCoffEENonHighlightingButtonCell.h" #import "ICeCoffEENonHighlightingTextFieldCell.h" #import "ICeCoffEELabeledIconCell.h" #import #import static NSNumber *ICCF_SERVICE_SHOWN, *ICCF_SERVICE_HIDDEN, *ICCF_SERVICE_MIXED; static NSDictionary *ICCF_SERVICE_OPTION_HIDDEN; static float ICCF_TableViewCellHeight(NSTableView *tableView) { return ([tableView rowHeight] + [tableView intercellSpacing].height); } static NSMutableDictionary *keyEquivalents; static void ICCF_RemoveSingleKeyEquivalents() { NSMutableArray *singleKeys = [[NSMutableArray alloc] init]; NSEnumerator *e = [[keyEquivalents allKeys] objectEnumerator]; NSString *keyEquivalent; ICLog(@"before ICCF_RemoveSingleKeyEquivalents: %@", keyEquivalents); while ( (keyEquivalent = [e nextObject]) != nil) { if ([[keyEquivalents objectForKey: keyEquivalent] count] == 1) [singleKeys addObject: keyEquivalent]; } [keyEquivalents removeObjectsForKeys: singleKeys]; [singleKeys release]; ICLog(@"after ICCF_RemoveSingleKeyEquivalents: %@", keyEquivalents); } static inline unsigned ICCF_CountForKeyEquivalent(NSString *keyEquivalent) { if (keyEquivalent == nil) return 0; NSMutableSet *setOrNil = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent]; return (setOrNil == nil) ? nil : [setOrNil count]; } static inline void ICCF_AddKeyEquivalentForItem(NSMenuItem *item) { NSString *keyEquivalent = [item toolTip]; if (keyEquivalent == nil) return; NSMutableSet *setOrNil = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent]; if (setOrNil == nil) return; [setOrNil addObject: item]; } static inline void ICCF_RemoveKeyEquivalentForItem(NSMenuItem *item) { NSString *keyEquivalent = [item toolTip]; if (keyEquivalent == nil) return; NSMutableSet *setOrNil = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent]; if (setOrNil == nil) return; [setOrNil removeObject: item]; } static inline void ICCF_UpdateKeyEquivalentForItem(NSMenuItem *item, id state) { NSNumber *oldState = [item representedObject]; if ((oldState == nil || oldState == ICCF_SERVICE_SHOWN) && state == ICCF_SERVICE_HIDDEN) ICCF_RemoveKeyEquivalentForItem(item); else if (oldState == ICCF_SERVICE_HIDDEN && (state == ICCF_SERVICE_SHOWN || state == nil)) ICCF_AddKeyEquivalentForItem(item); } static inline NSCellStateValue ICCF_ServiceItemState(NSMenuItem *item) { id state = [item representedObject]; return (state == nil) ? NSOnState : [state intValue]; } static void ICCF_PropagateServiceStateChange(NSMenu *menu, id state) { NSEnumerator *e = [[menu itemArray] objectEnumerator]; NSMenuItem *item; NSMenu *submenu; while ( (item = [e nextObject]) != nil) { submenu = [item submenu]; if (submenu != nil) ICCF_PropagateServiceStateChange(submenu, state); else ICCF_UpdateKeyEquivalentForItem(item, state); [item setRepresentedObject: state]; } } static NSCellStateValue ICCF_PropagateServiceState(NSMenuItem *item, NSMenuItem *changedItem) { NSMenu *submenu = [item submenu]; if (submenu == nil) return ICCF_ServiceItemState(item); if (item == changedItem) ICCF_PropagateServiceStateChange(submenu, [item representedObject]); BOOL areOn = NO, areOff = NO; NSEnumerator *e = [[submenu itemArray] objectEnumerator]; NSMenuItem *subItem; while ( (subItem = [e nextObject]) != nil) { switch (ICCF_PropagateServiceState(subItem, changedItem)) { case NSOnState: if (!areOff) { areOn = YES; continue; } break; case NSOffState: if (!areOn) { areOff = YES; continue; } break; case NSMixedState: break; } [item setRepresentedObject: ICCF_SERVICE_MIXED]; return NSMixedState; } if (areOn) { [item setRepresentedObject: ICCF_SERVICE_SHOWN]; return NSOnState; } else { [item setRepresentedObject: ICCF_SERVICE_HIDDEN]; return NSOffState; } } static NSMutableDictionary *ICCF_RetainedServiceOptionsDictionary(NSMenu *menu) { NSEnumerator *e = [[menu itemArray] objectEnumerator]; NSMenuItem *item; NSMenu *submenu; NSMutableDictionary *dict = nil, *subDict = nil, *submenuDict = nil; while ( (item = [e nextObject]) != nil) { submenu = [item submenu]; if (ICCF_ServiceItemState(item) == NSOffState) { subDict = [ICCF_SERVICE_OPTION_HIDDEN retain]; } else if (submenu != nil) { submenuDict = ICCF_RetainedServiceOptionsDictionary(submenu); if (submenuDict == nil) continue; subDict = [[NSDictionary alloc] initWithObjectsAndKeys: submenuDict, kICServiceSubmenu, nil]; [submenuDict release]; } else continue; if (dict == nil) { dict = [[NSMutableDictionary alloc] init]; } [dict setObject: subDict forKey: [item title]]; [subDict release]; } return dict; } static void ICCF_RestoreServiceOptionsDictionary(NSMenu *menu, NSDictionary *dict) { NSEnumerator *e = [dict keyEnumerator]; NSString *itemTitle; NSDictionary *subDict, *submenuDict; NSMenuItem *item; NSMenu *submenu; // XXX handle exceptions while ( (itemTitle = [e nextObject]) != nil) { item = (NSMenuItem *)[menu itemWithTitle: itemTitle]; if (item == nil) continue; subDict = [dict objectForKey: itemTitle]; if ([[subDict objectForKey: (NSString *)kICServiceHidden] boolValue]) { [item setRepresentedObject: ICCF_SERVICE_HIDDEN]; ICCF_RemoveKeyEquivalentForItem(item); } if ( (submenu = [item submenu]) != nil) { submenuDict = [subDict objectForKey: (NSString *)kICServiceSubmenu]; if ([submenuDict count] == 0) ICCF_PropagateServiceStateChange(submenu, ICCF_SERVICE_HIDDEN); else ICCF_RestoreServiceOptionsDictionary(submenu, submenuDict); } } } static void ICCF_AddServiceKeyEquivalentsAndIcons(NSMenu *menu, NSDictionary *serviceInfo) { if (serviceInfo == nil) return; NSEnumerator *enumerator = [[menu itemArray] objectEnumerator]; NSMenuItem *menuItem; NSMenu *submenu; NSDictionary *itemInfo = nil; while ( (menuItem = [enumerator nextObject]) != nil) { itemInfo = [serviceInfo objectForKey: [menuItem title]]; if (itemInfo == nil) continue; if ( (submenu = [menuItem submenu]) != nil) { ICCF_AddServiceKeyEquivalentsAndIcons(submenu, [itemInfo objectForKey: (NSString *)kICServiceSubmenu]); } else { NSString *keyEquivalent = (NSString *)[itemInfo objectForKey: (NSString *)kICServiceShortcut]; if (keyEquivalent != nil) { [menuItem setToolTip: keyEquivalent]; NSMutableSet *equivalentItems = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent]; if (equivalentItems == nil) { equivalentItems = [[NSMutableSet alloc] initWithObjects: menuItem, nil]; [keyEquivalents setObject: equivalentItems forKey: keyEquivalent]; [equivalentItems release]; } else { [equivalentItems addObject: menuItem]; } } } NSString *bundlePath = (NSString *)[itemInfo objectForKey: (NSString *)kICServiceBundlePath]; if (bundlePath == NULL) continue; IconRef serviceIcon = ICCF_CopyIconRefForPath(bundlePath); if (serviceIcon == NULL) continue; [menuItem _setIconRef: serviceIcon]; ReleaseIconRef(serviceIcon); } } @implementation ICeCoffEEServicePrefController #pragma mark class initialization + (void)initialize; { ICCF_SERVICE_SHOWN = [[NSNumber alloc] initWithInt: NSOnState]; ICCF_SERVICE_HIDDEN = [[NSNumber alloc] initWithInt: NSOffState]; ICCF_SERVICE_MIXED = [[NSNumber alloc] initWithInt: NSMixedState]; ICCF_SERVICE_OPTION_HIDDEN = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithBool: YES], kICServiceHidden, nil]; } #pragma mark initialize-release - (id)initWithParentWindow:(NSWindow *)parent; { if ( (self = [self initWithWindowNibName: @"Select services"])) { NSWindow *window = [self window]; // connect outlets [serviceOutline setAutoresizesOutlineColumn: NO]; NSButtonCell *checkBoxCell = [[ICeCoffEENonHighlightingButtonCell alloc] init]; [checkBoxCell setButtonType: NSSwitchButton]; [checkBoxCell setImagePosition: NSImageOnly]; [checkBoxCell setAllowsMixedState: YES]; [[serviceOutline tableColumnWithIdentifier: @"show"] setDataCell: checkBoxCell]; [checkBoxCell release]; NSTextFieldCell *textFieldCell = [[serviceOutline tableColumnWithIdentifier: @"service"] dataCell]; [textFieldCell setWraps: YES]; [[serviceOutline tableColumnWithIdentifier: @"service"] setDataCell: [ICeCoffEELabeledIconCell copyFromTextFieldCell: textFieldCell]]; textFieldCell = [[serviceOutline tableColumnWithIdentifier: @"key"] dataCell]; ((struct objc_object *)textFieldCell)->isa = [ICeCoffEENonHighlightingTextFieldCell class]; [window setResizeIncrements: NSMakeSize(1, ICCF_TableViewCellHeight(serviceOutline))]; if (parent != nil) { [NSApp beginSheet: window modalForWindow: parent modalDelegate: self didEndSelector: nil contextInfo: nil]; } else { [window center]; [window makeKeyAndOrderFront: nil]; } } return self; } - (void)dealloc; { [keyEquivalents release]; [servicesMenu release]; [super dealloc]; } #pragma mark actions - (IBAction)showAll:(NSButton *)sender; { ICCF_PropagateServiceStateChange(servicesMenu, nil); [serviceOutline reloadData]; } - (IBAction)hideAll:(NSButton *)sender; { ICCF_PropagateServiceStateChange(servicesMenu, ICCF_SERVICE_HIDDEN); [serviceOutline reloadData]; } - (void)closeWithReturnCode:(int)returnCode; { if ([[self window] isSheet]) { [NSApp endSheet: [self window] returnCode: NSRunAbortedResponse]; } [self close]; } - (IBAction)cancel:(NSButton *)sender; { [self closeWithReturnCode: NSRunAbortedResponse]; } - (IBAction)saveChanges:(NSButton *)sender; { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSMutableDictionary *icDefaults = [[defaults persistentDomainForName: (NSString *)kICBundleIdentifier] mutableCopy]; NSDictionary *serviceOptions = ICCF_RetainedServiceOptionsDictionary(servicesMenu); if (serviceOptions != nil) { [icDefaults setObject: serviceOptions forKey: (NSString *)kICServiceOptions]; [serviceOptions release]; } else { [icDefaults removeObjectForKey: (NSString *)kICServiceOptions]; } [defaults setPersistentDomain: icDefaults forName: (NSString *)kICBundleIdentifier]; [defaults synchronize]; APEMessageBroadcast(kICBundleIdentifier, kICPreferencesChanged, NULL); [icDefaults release]; [self closeWithReturnCode: NSRunStoppedResponse]; } @end @implementation ICeCoffEEServicePrefController (NSOutlineViewDataSource) - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item; { if (servicesMenu == nil) { keyEquivalents = [[NSMutableDictionary alloc] init]; servicesMenu = [[NSMenu alloc] initWithTitle: @""]; ICCF_SetServicesMenu(servicesMenu); ICCF_AddServiceKeyEquivalentsAndIcons(servicesMenu, ICCF_GetServicesInfo()); ICCF_RemoveSingleKeyEquivalents(); NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSDictionary *icDefaults = [defaults persistentDomainForName: (NSString *)kICBundleIdentifier]; NSDictionary *serviceOptions = [icDefaults objectForKey: (NSString *)kICServiceOptions]; ICCF_RestoreServiceOptionsDictionary(servicesMenu, serviceOptions); } return [(item == nil ? servicesMenu : [item submenu]) numberOfItems]; } - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item; { return (item == nil ? YES : [item submenu] != nil); } - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item; { return [(item == nil ? servicesMenu : [item submenu]) itemAtIndex: index]; } NSAttributedString *ICCF_KeyEquivalentAttributedStringWithModifierFlags(NSString *self, unsigned int modifierFlags, unsigned count); - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item; { if ([[tableColumn identifier] isEqualToString: @"service"]) { static NSDictionary *attrDict = nil; if (attrDict == nil) { // XXX we leak this, but so does the Apple sample code... NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; [style setLineBreakMode: NSLineBreakByTruncatingMiddle]; attrDict = [[NSDictionary alloc] initWithObjectsAndKeys: style, NSParagraphStyleAttributeName, nil]; [style release]; } return [[[NSAttributedString alloc] initWithString: [item title] attributes: attrDict] autorelease]; } else if ([[tableColumn identifier] isEqualToString: @"show"]) { id state = [item representedObject]; if ([item submenu] != nil && state == nil) { return [NSNumber numberWithInt: ICCF_PropagateServiceState(item, nil)]; } return (state == nil) ? ICCF_SERVICE_SHOWN : state; } else if ([[tableColumn identifier] isEqualToString: @"key"]) { NSString *equivalent = [item toolTip]; if (equivalent == nil || [equivalent length] != 1) return nil; // XXX Inconsistency between Cocoa and Carbon: always command-shift in Carbon, not in Cocoa. Since we only patch Cocoa for the moment, keep as is. return ICCF_KeyEquivalentAttributedStringWithModifierFlags(equivalent, [item keyEquivalentModifierMask] | ([[NSCharacterSet uppercaseLetterCharacterSet] characterIsMember: [equivalent characterAtIndex: 0]] ? NSShiftKeyMask : 0), ICCF_CountForKeyEquivalent(equivalent)); } return nil; } - (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item; { if ([[tableColumn identifier] isEqualToString: @"show"]) { NSNumber *newState = [object boolValue] ? nil : ICCF_SERVICE_HIDDEN; ICCF_UpdateKeyEquivalentForItem(item, newState); [item setRepresentedObject: newState]; NSMenu *submenu = [item menu]; if (submenu == servicesMenu) { ICCF_PropagateServiceState(item, item); } else { NSMenu *supermenu; while ( (supermenu = [submenu supermenu]) != servicesMenu) { submenu = supermenu; } ICCF_PropagateServiceState((NSMenuItem *)[supermenu itemAtIndex: [supermenu indexOfItemWithSubmenu: submenu]], item); } [outlineView reloadData]; } } @end @implementation ICeCoffEEServicePrefController (NSOutlineViewDelegate) - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item; { if (![[tableColumn identifier] isEqualToString: @"service"]) return; [(ICeCoffEELabeledIconCell *)cell setIconRef: [item _iconRef]]; } @end @implementation ICeCoffEEServicePrefController (NSWindowDelegate) - (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame; { NSWindow *window = [serviceOutline window]; NSRect frame = [window frame]; NSScrollView *scrollView = [serviceOutline enclosingScrollView]; float displayedHeight = [[scrollView contentView] bounds].size.height; float heightChange = [[scrollView documentView] bounds].size.height - displayedHeight; float heightExcess; if (heightChange >= 0 && heightChange <= 1) { // either the window is already optimal size, or it's too big float rowHeight = ICCF_TableViewCellHeight(serviceOutline); heightChange = (rowHeight * [serviceOutline numberOfRows]) - displayedHeight; } frame.size.height += heightChange; if ( (heightExcess = [window minSize].height - frame.size.height) > 1 || (heightExcess = [window maxSize].height - frame.size.height) < 1) { heightChange += heightExcess; frame.size.height += heightExcess; } frame.origin.y -= heightChange; return frame; } @end @implementation ICeCoffEEServicePrefController (NSWindowNotifications) - (void)windowWillClose:(NSNotification *)notification; { [self autorelease]; } @end