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

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

ICeCoffEE.m: I was wrong in [319] about the selection extension stuff
being dumb; explain why in code, even though it's not fixed.
Implement some of the same parens support I did in Terminal. Update
for more robust service info dictionary format.

ICeCoffEEServicePrefController.m: Update for more robust service info
dictionary format.

ICeCoffEEServices.[hm]: Renamed from ICeCoffEESetServicesMenu.[hm],
since we do more now. Service info dictionary-creating code now makes
more sense and no longer has naming collision issues.

ICeCoffEEWebKit.m: Some preliminary Safari 3 compatibility stuff, not
quite working yet. An outstanding question - is it better to rely on
"public" WebCore? API or private WebKit? API? So far it seems the
latter is more stable.

English.lproj/APEInfo.rtfd: The Bored Zo has a name: use it. Remove
now-erroneous reference to SimpleText? since TextEdit? support is gone.

File size: 16.5 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 "ICeCoffEENonHighlightingTextFieldCell.h"
14#import "ICeCoffEELabeledIconCell.h"
15#import <objc/objc.h>
16#import <ApplicationEnhancer/ApplicationEnhancer.h>
17
18static NSNumber *ICCF_SERVICE_SHOWN, *ICCF_SERVICE_HIDDEN, *ICCF_SERVICE_MIXED;
19static NSDictionary *ICCF_SERVICE_OPTION_HIDDEN;
20
21static float ICCF_TableViewCellHeight(NSTableView *tableView) {
22    return ([tableView rowHeight] + [tableView intercellSpacing].height);
23}
24
25static NSMutableDictionary *keyEquivalents;
26
27static void ICCF_RemoveSingleKeyEquivalents() {
28    NSMutableArray *singleKeys = [[NSMutableArray alloc] init];
29    NSEnumerator *e = [[keyEquivalents allKeys] objectEnumerator];
30    NSString *keyEquivalent;
31    ICLog(@"before ICCF_RemoveSingleKeyEquivalents: %@", keyEquivalents);
32    while ( (keyEquivalent = [e nextObject]) != nil) {
33        if ([[keyEquivalents objectForKey: keyEquivalent] count] == 1)
34            [singleKeys addObject: keyEquivalent];
35    }
36    [keyEquivalents removeObjectsForKeys: singleKeys];
37    [singleKeys release];
38    ICLog(@"after ICCF_RemoveSingleKeyEquivalents: %@", keyEquivalents);
39}
40
41static inline unsigned ICCF_CountForKeyEquivalent(NSString *keyEquivalent) {
42    if (keyEquivalent == nil) return 0;
43    NSMutableSet *setOrNil = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent];
44    return (setOrNil == nil) ? nil : [setOrNil count];
45}
46
47static inline void ICCF_AddKeyEquivalentForItem(NSMenuItem *item) {
48    NSString *keyEquivalent = [item toolTip];
49    if (keyEquivalent == nil) return;
50    NSMutableSet *setOrNil = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent];
51    if (setOrNil == nil) return;
52    [setOrNil addObject: item];
53}
54
55static inline void ICCF_RemoveKeyEquivalentForItem(NSMenuItem *item) {
56    NSString *keyEquivalent = [item toolTip];
57    if (keyEquivalent == nil) return;
58    NSMutableSet *setOrNil = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent];
59    if (setOrNil == nil) return;
60    [setOrNil removeObject: item];
61}
62
63static inline void ICCF_UpdateKeyEquivalentForItem(NSMenuItem *item, id state) {
64    NSNumber *oldState = [item representedObject];
65    if ((oldState == nil || oldState == ICCF_SERVICE_SHOWN) && state == ICCF_SERVICE_HIDDEN)
66        ICCF_RemoveKeyEquivalentForItem(item);
67    else if (oldState == ICCF_SERVICE_HIDDEN && (state == ICCF_SERVICE_SHOWN || state == nil))
68        ICCF_AddKeyEquivalentForItem(item);
69}
70
71static inline NSCellStateValue ICCF_ServiceItemState(NSMenuItem *item) {
72    id state = [item representedObject];
73    return (state == nil) ? NSOnState : [state intValue];
74}
75
76static void ICCF_PropagateServiceStateChange(NSMenu *menu, id state) {
77    NSEnumerator *e = [[menu itemArray] objectEnumerator];
78    NSMenuItem *item;
79    NSMenu *submenu;
80
81    while ( (item = [e nextObject]) != nil) {
82        submenu = [item submenu];
83        if (submenu != nil)
84            ICCF_PropagateServiceStateChange(submenu, state);
85        else
86            ICCF_UpdateKeyEquivalentForItem(item, state);
87       
88        [item setRepresentedObject: state];
89    }
90}
91
92static NSCellStateValue ICCF_PropagateServiceState(NSMenuItem *item, NSMenuItem *changedItem) {
93    NSMenu *submenu = [item submenu];
94    if (submenu == nil) return ICCF_ServiceItemState(item);
95
96    if (item == changedItem) ICCF_PropagateServiceStateChange(submenu, [item representedObject]);
97
98    BOOL areOn = NO, areOff = NO;
99    NSEnumerator *e = [[submenu itemArray] objectEnumerator];
100    NSMenuItem *subItem;
101    while ( (subItem = [e nextObject]) != nil) {
102        switch (ICCF_PropagateServiceState(subItem, changedItem)) {
103            case NSOnState: if (!areOff) { areOn = YES; continue; }
104                break;
105            case NSOffState: if (!areOn) { areOff = YES; continue; }
106                break;
107            case NSMixedState:
108                break;
109        }
110        [item setRepresentedObject: ICCF_SERVICE_MIXED];
111        return NSMixedState;
112    }
113    if (areOn) {
114        [item setRepresentedObject: ICCF_SERVICE_SHOWN];
115        return NSOnState;
116    } else {
117        [item setRepresentedObject: ICCF_SERVICE_HIDDEN];
118        return NSOffState;
119    }
120}
121
122static NSMutableDictionary *ICCF_RetainedServiceOptionsDictionary(NSMenu *menu) {
123    NSEnumerator *e = [[menu itemArray] objectEnumerator];
124    NSMenuItem *item;
125    NSMenu *submenu;
126    NSMutableDictionary *dict = nil, *subDict = nil, *submenuDict = nil;
127
128    while ( (item = [e nextObject]) != nil) {
129        submenu = [item submenu];
130        if (ICCF_ServiceItemState(item) == NSOffState) {
131            subDict = [ICCF_SERVICE_OPTION_HIDDEN retain];
132        } else if (submenu != nil) {
133            submenuDict = ICCF_RetainedServiceOptionsDictionary(submenu);
134            if (submenuDict == nil)
135                continue;
136            subDict = [[NSDictionary alloc] initWithObjectsAndKeys: submenuDict, kICServiceSubmenu, nil];
137            [submenuDict release];
138        } else continue;
139        if (dict == nil) {
140            dict = [[NSMutableDictionary alloc] init];
141        }
142        [dict setObject: subDict forKey: [item title]];
143        [subDict release];
144    }
145    return dict;
146}
147
148static void ICCF_RestoreServiceOptionsDictionary(NSMenu *menu, NSDictionary *dict) {
149    NSEnumerator *e = [dict keyEnumerator];
150    NSString *itemTitle;
151    NSDictionary *subDict, *submenuDict;
152    NSMenuItem *item;
153    NSMenu *submenu;
154
155    // XXX handle exceptions
156    while ( (itemTitle = [e nextObject]) != nil) {
157        item = (NSMenuItem *)[menu itemWithTitle: itemTitle];
158        if (item == nil) continue;
159        subDict = [dict objectForKey: itemTitle];
160        if ([[subDict objectForKey: (NSString *)kICServiceHidden] boolValue]) {
161            [item setRepresentedObject: ICCF_SERVICE_HIDDEN];
162            ICCF_RemoveKeyEquivalentForItem(item);
163        }
164        if ( (submenu = [item submenu]) != nil) {
165            submenuDict = [subDict objectForKey: (NSString *)kICServiceSubmenu];
166            if ([submenuDict count] == 0)
167                ICCF_PropagateServiceStateChange(submenu, ICCF_SERVICE_HIDDEN);
168            else
169                ICCF_RestoreServiceOptionsDictionary(submenu, submenuDict);
170        }
171    }
172}
173
174static void ICCF_AddServiceKeyEquivalentsAndIcons(NSMenu *menu, NSDictionary *serviceInfo) {
175    if (serviceInfo == nil) return;
176    NSEnumerator *enumerator = [[menu itemArray] objectEnumerator];
177    NSMenuItem *menuItem;
178    NSMenu *submenu;
179    NSDictionary *itemInfo = nil;
180    while ( (menuItem = [enumerator nextObject]) != nil) {
181        itemInfo = [serviceInfo objectForKey: [menuItem title]];
182        if (itemInfo == nil) continue;
183       
184        if ( (submenu = [menuItem submenu]) != nil) {
185            ICCF_AddServiceKeyEquivalentsAndIcons(submenu, [itemInfo objectForKey: (NSString *)kICServiceSubmenu]);
186        } else {
187            NSString *keyEquivalent = (NSString *)[itemInfo objectForKey: (NSString *)kICServiceShortcut];
188            if (keyEquivalent != nil) {
189                [menuItem setToolTip: keyEquivalent];
190                NSMutableSet *equivalentItems = (NSMutableSet *)[keyEquivalents objectForKey: keyEquivalent];
191                if (equivalentItems == nil) {
192                    equivalentItems = [[NSMutableSet alloc] initWithObjects: menuItem, nil];
193                    [keyEquivalents setObject: equivalentItems forKey: keyEquivalent];
194                    [equivalentItems release];
195                } else {
196                    [equivalentItems addObject: menuItem];
197                }
198            }
199        }
200
201        NSString *bundlePath = (NSString *)[itemInfo objectForKey: (NSString *)kICServiceBundlePath];
202        if (bundlePath == NULL) continue;
203        IconRef serviceIcon = ICCF_CopyIconRefForPath(bundlePath);
204        if (serviceIcon == NULL) continue;
205        [menuItem _setIconRef: serviceIcon];
206        ReleaseIconRef(serviceIcon);
207    }
208}
209
210@implementation ICeCoffEEServicePrefController
211
212#pragma mark class initialization
213
214+ (void)initialize;
215{
216    ICCF_SERVICE_SHOWN = [[NSNumber alloc] initWithInt: NSOnState];
217    ICCF_SERVICE_HIDDEN = [[NSNumber alloc] initWithInt: NSOffState];
218    ICCF_SERVICE_MIXED = [[NSNumber alloc] initWithInt: NSMixedState];
219    ICCF_SERVICE_OPTION_HIDDEN = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithBool: YES], kICServiceHidden, nil];
220}
221
222#pragma mark initialize-release
223
224- (id)initWithParentWindow:(NSWindow *)parent;
225{
226    if ( (self = [self initWithWindowNibName: @"Select services"])) {
227        NSWindow *window = [self window]; // connect outlets
228        [serviceOutline setAutoresizesOutlineColumn: NO];
229       
230        NSButtonCell *checkBoxCell = [[ICeCoffEENonHighlightingButtonCell alloc] init];
231        [checkBoxCell setButtonType: NSSwitchButton];
232        [checkBoxCell setImagePosition: NSImageOnly];
233        [checkBoxCell setAllowsMixedState: YES];
234        [[serviceOutline tableColumnWithIdentifier: @"show"] setDataCell: checkBoxCell];
235        [checkBoxCell release];
236
237        NSTextFieldCell *textFieldCell = [[serviceOutline tableColumnWithIdentifier: @"service"] dataCell];
238        [textFieldCell setWraps: YES];
239        [[serviceOutline tableColumnWithIdentifier: @"service"] setDataCell:
240            [ICeCoffEELabeledIconCell copyFromTextFieldCell: textFieldCell]];
241
242        textFieldCell = [[serviceOutline tableColumnWithIdentifier: @"key"] dataCell];
243        ((struct objc_object *)textFieldCell)->isa = [ICeCoffEENonHighlightingTextFieldCell class];
244
245        [window setResizeIncrements: NSMakeSize(1, ICCF_TableViewCellHeight(serviceOutline))];
246        if (parent != nil) {
247            [NSApp beginSheet: window modalForWindow: parent modalDelegate: self didEndSelector: nil contextInfo: nil];
248        } else {
249            [window center];
250            [window makeKeyAndOrderFront: nil];
251        }
252    }
253    return self;
254}
255
256- (void)dealloc;
257{
258    [keyEquivalents release];
259    [servicesMenu release];
260    [super dealloc];
261}
262
263#pragma mark actions
264
265- (IBAction)showAll:(NSButton *)sender;
266{
267    ICCF_PropagateServiceStateChange(servicesMenu, nil);
268    [serviceOutline reloadData];
269}
270
271- (IBAction)hideAll:(NSButton *)sender;
272{
273    ICCF_PropagateServiceStateChange(servicesMenu, ICCF_SERVICE_HIDDEN);
274    [serviceOutline reloadData];
275}
276
277- (void)closeWithReturnCode:(int)returnCode;
278{
279    if ([[self window] isSheet]) {
280        [NSApp endSheet: [self window] returnCode: NSRunAbortedResponse];
281    }
282    [self close];
283}
284
285- (IBAction)cancel:(NSButton *)sender;
286{
287    [self closeWithReturnCode: NSRunAbortedResponse];
288}
289
290- (IBAction)saveChanges:(NSButton *)sender;
291{
292    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
293    NSMutableDictionary *icDefaults = [[defaults persistentDomainForName: (NSString *)kICBundleIdentifier] mutableCopy];
294    NSDictionary *serviceOptions = ICCF_RetainedServiceOptionsDictionary(servicesMenu);
295    if (serviceOptions != nil) {
296        [icDefaults setObject: serviceOptions forKey: (NSString *)kICServiceOptions];
297        [serviceOptions release];
298    } else {
299        [icDefaults removeObjectForKey: (NSString *)kICServiceOptions];
300    }
301    [defaults setPersistentDomain: icDefaults forName: (NSString *)kICBundleIdentifier];
302    [defaults synchronize];
303    APEMessageBroadcast(kICBundleIdentifier, kICPreferencesChanged, NULL);
304    [icDefaults release];
305    [self closeWithReturnCode: NSRunStoppedResponse];
306}
307
308@end
309
310@implementation ICeCoffEEServicePrefController (NSOutlineViewDataSource)
311
312- (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item;
313{
314    if (servicesMenu == nil) {
315        keyEquivalents = [[NSMutableDictionary alloc] init];
316        servicesMenu = [[NSMenu alloc] initWithTitle: @""];
317        ICCF_SetServicesMenu(servicesMenu);
318        ICCF_AddServiceKeyEquivalentsAndIcons(servicesMenu, ICCF_GetServicesInfo());
319        ICCF_RemoveSingleKeyEquivalents();
320       
321        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
322        NSDictionary *icDefaults = [defaults persistentDomainForName: (NSString *)kICBundleIdentifier];
323        NSDictionary *serviceOptions = [icDefaults objectForKey: (NSString *)kICServiceOptions];
324        ICCF_RestoreServiceOptionsDictionary(servicesMenu, serviceOptions);
325    }
326    return [(item == nil ? servicesMenu : [item submenu]) numberOfItems];
327}
328
329- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item;
330{
331    return (item == nil ? YES : [item submenu] != nil);
332}
333
334- (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item;
335{
336    return [(item == nil ? servicesMenu : [item submenu]) itemAtIndex: index];
337}
338
339NSAttributedString *ICCF_KeyEquivalentAttributedStringWithModifierFlags(NSString *self, unsigned int modifierFlags, unsigned count);
340
341- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item;
342{
343    if ([[tableColumn identifier] isEqualToString: @"service"]) {
344        static NSDictionary *attrDict = nil;
345        if (attrDict == nil) { // XXX we leak this, but so does the Apple sample code...
346            NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
347            [style setLineBreakMode: NSLineBreakByTruncatingMiddle];
348            attrDict = [[NSDictionary alloc] initWithObjectsAndKeys: style, NSParagraphStyleAttributeName, nil];
349            [style release];
350        }
351        return [[[NSAttributedString alloc] initWithString: [item title] attributes: attrDict] autorelease];
352    } else if ([[tableColumn identifier] isEqualToString: @"show"]) {
353        id state = [item representedObject];
354        if ([item submenu] != nil && state == nil) {
355            return [NSNumber numberWithInt: ICCF_PropagateServiceState(item, nil)];
356        }
357        return (state == nil) ? ICCF_SERVICE_SHOWN : state;
358    } else if ([[tableColumn identifier] isEqualToString: @"key"]) {
359        NSString *equivalent = [item toolTip];
360        if (equivalent == nil || [equivalent length] != 1) return nil;
361        // 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.
362        return ICCF_KeyEquivalentAttributedStringWithModifierFlags(equivalent, [item keyEquivalentModifierMask] | ([[NSCharacterSet uppercaseLetterCharacterSet] characterIsMember: [equivalent characterAtIndex: 0]] ? NSShiftKeyMask : 0), ICCF_CountForKeyEquivalent(equivalent));
363    }
364    return nil;
365}
366
367- (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item;
368{
369    if ([[tableColumn identifier] isEqualToString: @"show"]) {
370        NSNumber *newState = [object boolValue] ? nil : ICCF_SERVICE_HIDDEN;
371        ICCF_UpdateKeyEquivalentForItem(item, newState);
372        [item setRepresentedObject: newState];
373       
374        NSMenu *submenu = [item menu];
375        if (submenu == servicesMenu) {
376            ICCF_PropagateServiceState(item, item);
377        } else {
378            NSMenu *supermenu;
379            while ( (supermenu = [submenu supermenu]) != servicesMenu) {
380                submenu = supermenu;
381            }
382            ICCF_PropagateServiceState((NSMenuItem *)[supermenu itemAtIndex: [supermenu indexOfItemWithSubmenu: submenu]], item);
383        }
384        [outlineView reloadData];
385    }
386}
387
388@end
389
390@implementation ICeCoffEEServicePrefController (NSOutlineViewDelegate)
391
392- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item;
393{
394    if (![[tableColumn identifier] isEqualToString: @"service"])
395        return;
396    [(ICeCoffEELabeledIconCell *)cell setIconRef: [item _iconRef]];
397}
398
399@end
400
401@implementation ICeCoffEEServicePrefController (NSWindowDelegate)
402
403- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame;
404{
405    NSWindow *window = [serviceOutline window];
406    NSRect frame = [window frame];
407    NSScrollView *scrollView = [serviceOutline enclosingScrollView];
408    float displayedHeight = [[scrollView contentView] bounds].size.height;
409    float heightChange = [[scrollView documentView] bounds].size.height - displayedHeight;
410    float heightExcess;
411
412    if (heightChange >= 0 && heightChange <= 1) {
413        // either the window is already optimal size, or it's too big
414        float rowHeight = ICCF_TableViewCellHeight(serviceOutline);
415        heightChange = (rowHeight * [serviceOutline numberOfRows]) - displayedHeight;
416    }
417
418    frame.size.height += heightChange;
419
420    if ( (heightExcess = [window minSize].height - frame.size.height) > 1 ||
421         (heightExcess = [window maxSize].height - frame.size.height) < 1) {
422        heightChange += heightExcess;
423        frame.size.height += heightExcess;
424    }
425
426    frame.origin.y -= heightChange;
427
428    return frame;
429}
430
431@end
432
433@implementation ICeCoffEEServicePrefController (NSWindowNotifications)
434
435- (void)windowWillClose:(NSNotification *)notification;
436{
437    [self autorelease];
438}
439
440@end
Note: See TracBrowser for help on using the repository browser.