source: trunk/ICeCoffEE/ICeCoffEE/ICeCoffEEServicePrefController.m @ 428

Last change on this file since 428 was 428, checked in by Nicholas Riley, 12 years ago

Compiler fixes; first pass at menu-style highlighting

File size: 19.2 KB
Line 
1//
2//  ICeCoffEEServicePrefController.m
3//  ICeCoffEE APE
4//
5//  Created by Nicholas Riley on Fri Jun 06 2003.
6//  Copyright (c) 2003 Nicholas Riley. All rights reserved.
7//
8
9#import "ICeCoffEEShared.h"
10#import "ICeCoffEEServices.h"
11#import "ICeCoffEEServicePrefController.h"
12#import "ICeCoffEENonHighlightingButtonCell.h"
13#import "ICeCoffEEInvertingTextFieldCell.h"
14#import "ICeCoffEELabeledIconCell.h"
15#import <objc/objc.h>
16#import <ApplicationEnhancer/ApplicationEnhancer.h>
17#import <QuartzCore/QuartzCore.h>
18
19const int ICCF_SERVICE_UNKNOWN = 0;
20const int ICCF_SERVICE_SHOWN = 1;
21const int ICCF_SERVICE_HIDDEN = 2;
22const int ICCF_SERVICE_MIXED = 3;
23
24static NSDictionary *ICCF_SERVICE_OPTION_HIDDEN;
25
26static float ICCF_TableViewCellHeight(NSTableView *tableView) {
27    return ([tableView rowHeight] + [tableView intercellSpacing].height);
28}
29
30static NSMutableDictionary *keyEquivalents;
31
32static void ICCF_RemoveSingleKeyEquivalents() {
33    NSMutableArray *singleKeys = [[NSMutableArray alloc] init];
34    NSEnumerator *e = [[keyEquivalents allKeys] objectEnumerator];
35    NSString *keyEquivalent;
36    ICLog(@"before ICCF_RemoveSingleKeyEquivalents: %@", keyEquivalents);
37    while ( (keyEquivalent = [e nextObject]) != nil) {
38        if ([[keyEquivalents objectForKey: keyEquivalent] count] == 1)
39            [singleKeys addObject: keyEquivalent];
40    }
41    [keyEquivalents removeObjectsForKeys: singleKeys];
42    [singleKeys release];
43    ICLog(@"after ICCF_RemoveSingleKeyEquivalents: %@", keyEquivalents);
44}
45
46static inline unsigned ICCF_CountForKeyEquivalent(NSString *keyEquivalent) {
47    if (keyEquivalent == nil) return 0;
48    NSMutableSet *setOrNil = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent];
49    return (setOrNil == nil) ? 0 : [setOrNil count];
50}
51
52static inline void ICCF_AddKeyEquivalentForItem(NSMenuItem *item) {
53    NSString *keyEquivalent = [item toolTip];
54    if (keyEquivalent == nil) return;
55    NSMutableSet *setOrNil = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent];
56    if (setOrNil == nil) return;
57    [setOrNil addObject: item];
58}
59
60static inline void ICCF_RemoveKeyEquivalentForItem(NSMenuItem *item) {
61    NSString *keyEquivalent = [item toolTip];
62    if (keyEquivalent == nil) return;
63    NSMutableSet *setOrNil = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent];
64    if (setOrNil == nil) return;
65    [setOrNil removeObject: item];
66}
67
68static int ICCF_GetServiceState(NSMenuItem *item) {
69    return [item tag];
70}
71
72static void ICCF_SetServiceState(NSMenuItem *item, int state) {
73    [item setTag: state];
74}
75
76static inline void ICCF_UpdateKeyEquivalentForItem(NSMenuItem *item, int state) {
77    int oldState = ICCF_GetServiceState(item);
78    if ((oldState == ICCF_SERVICE_UNKNOWN || oldState == ICCF_SERVICE_SHOWN) && state == ICCF_SERVICE_HIDDEN)
79        ICCF_RemoveKeyEquivalentForItem(item);
80    else if (oldState == ICCF_SERVICE_HIDDEN && (state == ICCF_SERVICE_SHOWN || state == ICCF_SERVICE_UNKNOWN))
81        ICCF_AddKeyEquivalentForItem(item);
82}
83
84
85static inline NSCellStateValue ICCF_ServiceItemState(NSMenuItem *item) {
86    int state = ICCF_GetServiceState(item);
87    if (state == ICCF_SERVICE_HIDDEN)
88        return NSOffState;
89    if (state == ICCF_SERVICE_MIXED)
90            return NSMixedState;
91    return NSOnState;
92}
93
94static void ICCF_PropagateServiceStateChange(NSMenu *menu, int state) {
95    NSEnumerator *e = [[menu itemArray] objectEnumerator];
96    NSMenuItem *item;
97    NSMenu *submenu;
98
99    while ( (item = [e nextObject]) != nil) {
100        submenu = [item submenu];
101        if (submenu != nil)
102            ICCF_PropagateServiceStateChange(submenu, state);
103        else
104            ICCF_UpdateKeyEquivalentForItem(item, state);
105       
106        ICCF_SetServiceState(item, state);
107    }
108}
109
110static NSCellStateValue ICCF_PropagateServiceState(NSMenuItem *item, NSMenuItem *changedItem) {
111    NSMenu *submenu = [item submenu];
112    if (submenu == nil) return ICCF_ServiceItemState(item);
113
114    if (item == changedItem) ICCF_PropagateServiceStateChange(submenu, [item tag]);
115
116    BOOL areOn = NO, areOff = NO;
117    NSEnumerator *e = [[submenu itemArray] objectEnumerator];
118    NSMenuItem *subItem;
119    while ( (subItem = [e nextObject]) != nil) {
120        switch (ICCF_PropagateServiceState(subItem, changedItem)) {
121            case NSOnState: if (!areOff) { areOn = YES; continue; }
122                break;
123            case NSOffState: if (!areOn) { areOff = YES; continue; }
124                break;
125            case NSMixedState:
126                break;
127        }
128        ICCF_SetServiceState(item, ICCF_SERVICE_MIXED);
129        return NSMixedState;
130    }
131    if (areOn) {
132        ICCF_SetServiceState(item, ICCF_SERVICE_SHOWN);
133        return NSOnState;
134    } else {
135        ICCF_SetServiceState(item, ICCF_SERVICE_HIDDEN);
136        return NSOffState;
137    }
138}
139
140static NSMutableDictionary *ICCF_RetainedServiceOptionsDictionary(NSMenu *menu) {
141    NSEnumerator *e = [[menu itemArray] objectEnumerator];
142    NSMenuItem *item;
143    NSMenu *submenu;
144    NSMutableDictionary *dict = nil, *subDict = nil, *submenuDict = nil;
145
146    while ( (item = [e nextObject]) != nil) {
147        submenu = [item submenu];
148        if (ICCF_ServiceItemState(item) == NSOffState) {
149            subDict = [ICCF_SERVICE_OPTION_HIDDEN retain];
150        } else if (submenu != nil) {
151            submenuDict = ICCF_RetainedServiceOptionsDictionary(submenu);
152            if (submenuDict == nil)
153                continue;
154            subDict = [[NSDictionary alloc] initWithObjectsAndKeys: submenuDict, kICServiceSubmenu, nil];
155            [submenuDict release];
156        } else continue;
157        if (dict == nil) {
158            dict = [[NSMutableDictionary alloc] init];
159        }
160        [dict setObject: subDict forKey: [item title]];
161        [subDict release];
162    }
163    return dict;
164}
165
166static void ICCF_RestoreServiceOptionsDictionary(NSMenu *menu, NSDictionary *dict) {
167    NSEnumerator *e = [dict keyEnumerator];
168    NSString *itemTitle;
169    NSDictionary *subDict, *submenuDict;
170    NSMenuItem *item;
171    NSMenu *submenu;
172
173    // XXX handle exceptions
174    while ( (itemTitle = [e nextObject]) != nil) {
175        item = (NSMenuItem *)[menu itemWithTitle: itemTitle];
176        if (item == nil) continue;
177        subDict = [dict objectForKey: itemTitle];
178        if ([[subDict objectForKey: (NSString *)kICServiceHidden] boolValue]) {
179            ICCF_SetServiceState(item, ICCF_SERVICE_HIDDEN);
180            ICCF_RemoveKeyEquivalentForItem(item);
181        }
182        if ( (submenu = [item submenu]) != nil) {
183            submenuDict = [subDict objectForKey: (NSString *)kICServiceSubmenu];
184            if ([submenuDict count] == 0)
185                ICCF_PropagateServiceStateChange(submenu, ICCF_SERVICE_HIDDEN);
186            else
187                ICCF_RestoreServiceOptionsDictionary(submenu, submenuDict);
188        }
189    }
190}
191
192static void ICCF_AddServiceKeyEquivalentsAndIcons(NSMenu *menu, NSDictionary *serviceInfo) {
193    if (serviceInfo == nil) return;
194    NSEnumerator *enumerator = [[menu itemArray] objectEnumerator];
195    NSMenuItem *menuItem;
196    NSMenu *submenu;
197    NSDictionary *itemInfo = nil;
198    while ( (menuItem = [enumerator nextObject]) != nil) {
199        itemInfo = [serviceInfo objectForKey: [menuItem title]];
200        if (itemInfo == nil) continue;
201       
202        if ( (submenu = [menuItem submenu]) != nil) {
203            ICCF_AddServiceKeyEquivalentsAndIcons(submenu, [itemInfo objectForKey: (NSString *)kICServiceSubmenu]);
204        } else {
205            NSString *keyEquivalent = (NSString *)[itemInfo objectForKey: (NSString *)kICServiceShortcut];
206            if (keyEquivalent != nil) {
207                [menuItem setToolTip: keyEquivalent];
208                NSMutableSet *equivalentItems = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent];
209                if (equivalentItems == nil) {
210                    equivalentItems = [[NSMutableSet alloc] initWithObjects: menuItem, nil];
211                    [keyEquivalents setObject: equivalentItems forKey: keyEquivalent];
212                    [equivalentItems release];
213                } else {
214                    [equivalentItems addObject: menuItem];
215                }
216            }
217        }
218
219        NSString *bundlePath = (NSString *)[itemInfo objectForKey: (NSString *)kICServiceBundlePath];
220        if (bundlePath == NULL) continue;
221        IconRef serviceIcon = ICCF_CopyIconRefForPath(bundlePath);
222        if (serviceIcon == NULL) continue;
223        [menuItem _setIconRef: serviceIcon];
224        ReleaseIconRef(serviceIcon);
225    }
226}
227
228@implementation ICeCoffEEServicePrefController
229
230#pragma mark class initialization
231
232+ (void)initialize;
233{
234    ICCF_SERVICE_OPTION_HIDDEN = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithBool: YES], kICServiceHidden, nil];
235}
236
237#pragma mark initialize-release
238
239- (id)initWithParentWindow:(NSWindow *)parent;
240{
241    if ( (self = [self initWithWindowNibName: @"Select services"])) {
242        NSWindow *window = [self window]; // connect outlets
243        [serviceOutline setAutoresizesOutlineColumn: NO];
244       
245        NSButtonCell *checkBoxCell = [[ICeCoffEENonHighlightingButtonCell alloc] init];
246        [checkBoxCell setButtonType: NSSwitchButton];
247        [checkBoxCell setImagePosition: NSImageOnly];
248        [checkBoxCell setAllowsMixedState: YES];
249       
250        [[serviceOutline tableColumnWithIdentifier: @"show"] setDataCell: checkBoxCell];
251        [checkBoxCell release];
252
253        NSTextFieldCell *textFieldCell = [[serviceOutline tableColumnWithIdentifier: @"service"] dataCell];
254        [textFieldCell setWraps: YES];
255        [[serviceOutline tableColumnWithIdentifier: @"service"] setDataCell:
256            [ICeCoffEELabeledIconCell copyFromTextFieldCell: textFieldCell]];
257       
258        textFieldCell = [[serviceOutline tableColumnWithIdentifier: @"key"] dataCell];
259        ((struct objc_object *)textFieldCell)->isa = [ICeCoffEEInvertingTextFieldCell class];
260       
261        [serviceOutline noteNumberOfRowsChanged]; // or we get no active scroll bar or initial selection
262
263        [window setResizeIncrements: NSMakeSize(1, ICCF_TableViewCellHeight(serviceOutline))];
264        if (parent != nil) {
265            [NSApp beginSheet: window modalForWindow: parent modalDelegate: self didEndSelector: nil contextInfo: nil];
266        } else {
267            [window center];
268            [window makeKeyAndOrderFront: nil];
269        }
270    }
271    return self;
272}
273
274- (void)dealloc;
275{
276    [keyEquivalents release];
277    [servicesMenu release];
278    [closedTriangle release];
279    [selectedClosedTriangle release];
280    [openTriangle release];
281    [selectedOpenTriangle release];
282    [super dealloc];
283}
284
285#pragma mark actions
286
287- (IBAction)showAll:(NSButton *)sender;
288{
289    ICCF_PropagateServiceStateChange(servicesMenu, 0);
290    [serviceOutline reloadData];
291}
292
293- (IBAction)hideAll:(NSButton *)sender;
294{
295    ICCF_PropagateServiceStateChange(servicesMenu, ICCF_SERVICE_HIDDEN);
296    [serviceOutline reloadData];
297}
298
299- (void)closeWithReturnCode:(int)returnCode;
300{
301    if ([[self window] isSheet]) {
302        [NSApp endSheet: [self window] returnCode: NSRunAbortedResponse];
303    }
304    [self close];
305}
306
307- (IBAction)cancel:(NSButton *)sender;
308{
309    [self closeWithReturnCode: NSRunAbortedResponse];
310}
311
312- (IBAction)saveChanges:(NSButton *)sender;
313{
314    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
315    NSMutableDictionary *icDefaults = [[defaults persistentDomainForName: (NSString *)kICBundleIdentifier] mutableCopy];
316    NSDictionary *serviceOptions = ICCF_RetainedServiceOptionsDictionary(servicesMenu);
317    if (serviceOptions != nil) {
318        [icDefaults setObject: serviceOptions forKey: (NSString *)kICServiceOptions];
319        [serviceOptions release];
320    } else {
321        [icDefaults removeObjectForKey: (NSString *)kICServiceOptions];
322    }
323    [defaults setPersistentDomain: icDefaults forName: (NSString *)kICBundleIdentifier];
324    [defaults synchronize];
325    APEMessageBroadcast(kICBundleIdentifier, kICPreferencesChanged, NULL);
326    [icDefaults release];
327    [self closeWithReturnCode: NSRunStoppedResponse];
328}
329
330@end
331
332@implementation ICeCoffEEServicePrefController (NSOutlineViewDataSource)
333
334- (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item;
335{
336    if (servicesMenu == nil) {
337        keyEquivalents = [[NSMutableDictionary alloc] init];
338        servicesMenu = [[NSMenu alloc] initWithTitle: @""];
339        ICCF_SetServicesMenu(servicesMenu);
340        ICCF_AddServiceKeyEquivalentsAndIcons(servicesMenu, ICCF_GetServicesInfo());
341        ICCF_RemoveSingleKeyEquivalents();
342       
343        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
344        NSDictionary *icDefaults = [defaults persistentDomainForName: (NSString *)kICBundleIdentifier];
345        NSDictionary *serviceOptions = [icDefaults objectForKey: (NSString *)kICServiceOptions];
346        ICCF_RestoreServiceOptionsDictionary(servicesMenu, serviceOptions);
347    }
348    return [(item == nil ? servicesMenu : [item submenu]) numberOfItems];
349}
350
351- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item;
352{
353    return (item == nil ? YES : [item submenu] != nil);
354}
355
356- (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item;
357{
358    return [(item == nil ? servicesMenu : [item submenu]) itemAtIndex: index];
359}
360
361NSAttributedString *ICCF_KeyEquivalentAttributedStringWithModifierFlags(NSString *self, unsigned int modifierFlags, unsigned count);
362
363- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item;
364{
365    if ([[tableColumn identifier] isEqualToString: @"service"]) {
366        static NSDictionary *attrDict = nil;
367        if (attrDict == nil) { // XXX we leak this, but so does the Apple sample code...
368            NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
369            [style setLineBreakMode: NSLineBreakByTruncatingMiddle];
370            attrDict = [[NSDictionary alloc] initWithObjectsAndKeys: style, NSParagraphStyleAttributeName, nil];
371            [style release];
372        }
373        return [[[NSAttributedString alloc] initWithString: [item title] attributes: attrDict] autorelease];
374    } else if ([[tableColumn identifier] isEqualToString: @"show"]) {
375        int state = ICCF_GetServiceState(item);
376        if ([item submenu] != nil && state == ICCF_SERVICE_UNKNOWN) {
377            return [NSNumber numberWithInt: ICCF_PropagateServiceState(item, nil)];
378        }
379        return [NSNumber numberWithInt: ICCF_ServiceItemState(item)];
380    } else if ([[tableColumn identifier] isEqualToString: @"key"]) {
381        NSString *equivalent = [item toolTip];
382        if (equivalent == nil || [equivalent length] != 1) return nil;
383        // 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.
384        return ICCF_KeyEquivalentAttributedStringWithModifierFlags(equivalent, [item keyEquivalentModifierMask] | ([[NSCharacterSet uppercaseLetterCharacterSet] characterIsMember: [equivalent characterAtIndex: 0]] ? NSShiftKeyMask : 0), ICCF_CountForKeyEquivalent(equivalent));
385    }
386    return nil;
387}
388
389- (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item;
390{
391    if ([[tableColumn identifier] isEqualToString: @"show"]) {
392        int newState = [object boolValue] ? ICCF_SERVICE_SHOWN : ICCF_SERVICE_HIDDEN;
393        ICCF_UpdateKeyEquivalentForItem(item, newState);
394        ICCF_SetServiceState(item, newState);
395       
396        NSMenu *submenu = [item menu];
397        if (submenu == servicesMenu) {
398            ICCF_PropagateServiceState(item, item);
399        } else {
400            NSMenu *supermenu;
401            while ( (supermenu = [submenu supermenu]) != servicesMenu) {
402                submenu = supermenu;
403            }
404            ICCF_PropagateServiceState((NSMenuItem *)[supermenu itemAtIndex: [supermenu indexOfItemWithSubmenu: submenu]], item);
405        }
406        [outlineView reloadData];
407    }
408}
409
410@end
411
412@implementation ICeCoffEEServicePrefController (NSOutlineViewDelegate)
413
414- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item;
415{
416    if (![[tableColumn identifier] isEqualToString: @"service"])
417        return;
418    [(ICeCoffEELabeledIconCell *)cell setIconRef: [item _iconRef]];
419}
420
421static NSImage *ICCF_InvertedImage(NSImage *image) {
422    NSImage *invertedImage = [[NSImage alloc] initWithSize: [image size]];
423    NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData: [image TIFFRepresentation]];
424    CIImage *source = [[CIImage alloc] initWithBitmapImageRep: imageRep];
425    CIColor *black = [[CIColor alloc] initWithColor: [NSColor blackColor]];
426                                                             
427    CIFilter *monochromeFilter = [CIFilter filterWithName: @"CIColorMonochrome"];
428    [monochromeFilter setValue: source forKey: @"inputImage"];
429    [monochromeFilter setValue: [NSNumber numberWithFloat: 1.0] forKey: @"inputIntensity"];
430    [monochromeFilter setValue: black forKey: @"inputColor"];
431   
432    CIFilter *invertFilter = [CIFilter filterWithName: @"CIColorInvert"];
433    [invertFilter setValue: [monochromeFilter valueForKey: @"outputImage"] forKey: @"inputImage"];
434   
435    CIFilter *maskToAlphaFilter = [CIFilter filterWithName: @"CIMaskToAlpha"];
436    [maskToAlphaFilter setValue: [invertFilter valueForKey: @"outputImage"] forKey: @"inputImage"];
437   
438    [invertedImage lockFocus];
439    CIContext *context = [CIContext contextWithCGContext: [[NSGraphicsContext currentContext] graphicsPort]
440                                                 options: nil];
441    CIImage *result = [maskToAlphaFilter valueForKey: @"outputImage"];
442    [context drawImage: result atPoint: CGPointZero fromRect: [result extent]];
443    [invertedImage unlockFocus];
444
445    [source release];
446    [black release];
447
448    return invertedImage;
449}
450
451- (void)outlineView:(NSOutlineView *)outlineView willDisplayOutlineCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item;
452{
453    static BOOL isInverted = NO;
454
455    if (closedTriangle == nil) {
456        closedTriangle = [[cell image] retain];
457        openTriangle = [[cell alternateImage] retain];
458        selectedClosedTriangle = ICCF_InvertedImage(closedTriangle);
459        selectedOpenTriangle = ICCF_InvertedImage(openTriangle);
460    }
461   
462    if (![outlineView isRowSelected: [outlineView rowForItem: item]] ||
463        [[outlineView window] firstResponder] != outlineView || ![[outlineView window] isKeyWindow]) {
464        if (!isInverted)
465            return;
466       
467        [cell setImage: closedTriangle];
468        [cell setAlternateImage: openTriangle];
469        isInverted = NO;
470        return;
471    }
472   
473    // not checking for isInverted is intentional - images reset when triangle is flipped
474    [cell setImage: selectedClosedTriangle];
475    [cell setAlternateImage: selectedOpenTriangle];
476    isInverted = YES;
477}
478
479@end
480
481@implementation ICeCoffEEServicePrefController (NSWindowDelegate)
482
483- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame;
484{
485    NSWindow *window = [serviceOutline window];
486    NSRect frame = [window frame];
487    NSScrollView *scrollView = [serviceOutline enclosingScrollView];
488    float displayedHeight = [[scrollView contentView] bounds].size.height;
489    float heightChange = [[scrollView documentView] bounds].size.height - displayedHeight;
490    float heightExcess;
491
492    if (heightChange >= 0 && heightChange <= 1) {
493        // either the window is already optimal size, or it's too big
494        float rowHeight = ICCF_TableViewCellHeight(serviceOutline);
495        heightChange = (rowHeight * [serviceOutline numberOfRows]) - displayedHeight;
496    }
497
498    frame.size.height += heightChange;
499
500    if ( (heightExcess = [window minSize].height - frame.size.height) > 1 ||
501         (heightExcess = [window maxSize].height - frame.size.height) < 1) {
502        heightChange += heightExcess;
503        frame.size.height += heightExcess;
504    }
505
506    frame.origin.y -= heightChange;
507
508    return frame;
509}
510
511@end
512
513@implementation ICeCoffEEServicePrefController (NSWindowNotifications)
514
515- (void)windowWillClose:(NSNotification *)notification;
516{
517    [self autorelease];
518}
519
520@end
Note: See TracBrowser for help on using the repository browser.