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

Last change on this file since 319 was 319, checked in by Nicholas Riley, 13 years ago

VERSION: Starting with 1.5d1.

ICeCoffEEKeyEquivalents.m: Support "collision font" for displaying key
equivalent conflicts.

ICeCoffEE.m: Increase debug ICCF_MAX_URL_LEN to 120 for testing. Set
icons in ICCF_ConsolidateServicesMenu (needs better caching).

ICeCoffEEServicePrefController.m: Display icons, proper key
equivalents (instead of #, what was I thinking?!) and conflicts. Fix
a dumb bug in ICCF_PropagateServiceStateChange. Ellipsize long menu
items rather than chopping them off. Fix key equivalent column
getting moved when expanding disclosure triangles.

ICeCoffEELabeledIconCell.[hm]: An IconRef?-displaying text cell.

Info-APE Module.plist: Update version to 1.5d1.

ICeCoffEE.xcodeproj: Added files, no significant changes.

English.lproj/InfoPlist.strings: Update version to 1.5d1.

English.lproj/APEInfo.rtfd/TXT.rtf: Some overdue documentation
updates.

ICeCoffEEShared.[hm]: Enable debugging; we're now using
kICServiceShortcut (though not yet for customizable shortcuts) so
define its data type.

ICeCoffEETerminal.m: Remove some useless code to "extend to beginning
of string" which seems to have been stolen from the NSTextView
implementation and not well understood. Handle common uses of
parentheses in URLs; still need to do this for NSTextView.

ICeCoffEESetServicesMenu.[hm]: Needs renaming; now with icon
extraction functionality and semi-working code to create a service
info dictionary.

Info-APEManagerPrefPane.plist: Update version to 1.5d1.

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 "ICeCoffEESetServicesMenu.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);
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.