root/tags/2D16/JVAppearancePreferences.m

Revision 2687, 32.2 kB (checked in by timothy, 4 years ago)

Adjusted how we use enableFlushWindow. This improved the refresh on the style previews.

Line 
1 #import <ChatCore/NSColorAdditions.h>
2 #import <ChatCore/NSStringAdditions.h>
3
4 #import "JVAppearancePreferences.h"
5 #import "JVStyle.h"
6 #import "JVStyleView.h"
7 #import "JVEmoticonSet.h"
8 #import "JVFontPreviewField.h"
9 #import "JVColorWellCell.h"
10 #import "JVDetailCell.h"
11 #import "NSBundleAdditions.h"
12
13 #import <libxml/xinclude.h>
14 #import <libxslt/transform.h>
15 #import <libxslt/xsltutils.h>
16
17 @interface WebCoreCache
18 + (void) empty;
19 @end
20
21 #pragma mark -
22
23 @interface WebView (WebViewPrivate) // WebKit 1.3 pending public API
24 - (void) setDrawsBackground:(BOOL) draws;
25 - (BOOL) drawsBackground;
26 @end
27
28 #pragma mark -
29
30 @interface NSWindow (NSWindowPrivate) // new Tiger private method
31 - (void) _setContentHasShadow:(BOOL) shadow;
32 @end
33
34 #pragma mark -
35
36 @implementation JVAppearancePreferences
37 - (id) init {
38         if( ( self = [super init] ) ) {
39                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( colorWellDidChangeColor: ) name:JVColorWellCellColorDidChangeNotification object:nil];
40                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( updateChatStylesMenu ) name:JVStylesScannedNotification object:nil];
41                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( updateEmoticonsMenu ) name:JVEmoticonSetsScannedNotification object:nil];
42                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( reloadStyles: ) name:NSApplicationDidBecomeActiveNotification object:[NSApplication sharedApplication]];
43
44                 _style = nil;
45                 _styleOptions = nil;
46                 _userStyle = nil;
47         }
48         return self;
49 }
50
51 - (void) dealloc {
52         [[NSNotificationCenter defaultCenter] removeObserver:self];
53
54         [_style release];
55         _style = nil;
56
57         [super dealloc];
58 }
59
60 - (NSString *) preferencesNibName {
61         return @"JVAppearancePreferences";
62 }
63
64 - (BOOL) hasChangesPending {
65         return NO;
66 }
67
68 - (NSImage *) imageForPreferenceNamed:(NSString *) name {
69         return [[[NSImage imageNamed:@"AppearancePreferences"] retain] autorelease];
70 }
71
72 - (BOOL) isResizable {
73         return NO;
74 }
75
76 - (void) moduleWillBeRemoved {
77         [optionsDrawer close];
78 }
79
80 #pragma mark -
81
82 - (void) selectStyleWithIdentifier:(NSString *) identifier {
83         [self setStyle:[JVStyle styleWithIdentifier:identifier]];
84         [self changePreferences];
85 }
86
87 - (void) selectEmoticonsWithIdentifier:(NSString *) identifier {
88         [_style setDefaultEmoticonSet:[JVEmoticonSet emoticonSetWithIdentifier:identifier]];
89         [self updateEmoticonsMenu];
90 }
91
92 #pragma mark -
93
94 - (void) setStyle:(JVStyle *) style {
95         [_style autorelease];
96         _style = [style retain];
97
98         JVChatTranscript *transcript = [JVChatTranscript chatTranscriptWithContentsOfFile:[_style previewTranscriptFilePath]];
99         [preview setTranscript:transcript];
100
101         [preview setEmoticons:[_style defaultEmoticonSet]];
102         [preview setStyle:_style];
103
104         [[NSNotificationCenter defaultCenter] removeObserver:self name:JVStyleVariantChangedNotification object:nil];
105         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( updateVariant ) name:JVStyleVariantChangedNotification object:_style];
106 }
107
108 #pragma mark -
109
110 - (void) awakeFromNib {
111         [(NSClipView *)[preview superview] setBackgroundColor:[NSColor clearColor]]; // allows rgba backgrounds to see through to the Desktop
112         [(NSScrollView *)[(NSClipView *)[preview superview] superview] setBackgroundColor:[NSColor clearColor]];
113         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( styleDidReload: ) name:JVStyleViewDidChangeStylesNotification object:preview];
114
115 #ifdef NSAppKitVersionNumber10_3
116         if( floor( NSAppKitVersionNumber ) > NSAppKitVersionNumber10_3 )
117                 [[preview window] _setContentHasShadow:NO]; // this is new in Tiger
118 #endif
119 }
120
121 - (void) initializeFromDefaults {
122         [preview setPolicyDelegate:self];
123         [preview setUIDelegate:self];
124         [optionsTable setRefusesFirstResponder:YES];
125
126         [useStyleFont setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"JVChatInputUsesStyleFont"]];
127
128         NSTableColumn *column = [optionsTable tableColumnWithIdentifier:@"key"];
129         JVDetailCell *prototypeCell = [[JVDetailCell new] autorelease];
130         [prototypeCell setFont:[NSFont boldSystemFontOfSize:11.]];
131         [prototypeCell setAlignment:NSRightTextAlignment];
132         [column setDataCell:prototypeCell];
133
134         [JVStyle scanForStyles];
135         [self setStyle:[JVStyle defaultStyle]];
136
137         [self changePreferences];
138 }
139
140 - (IBAction) changeBaseFontSize:(id) sender {
141         int size = [sender intValue];
142         [baseFontSize setIntValue:size];
143         [baseFontSizeStepper setIntValue:size];
144         [[preview preferences] setDefaultFontSize:size];
145 }
146
147 - (IBAction) changeMinimumFontSize:(id) sender {
148         int size = [sender intValue];
149         [minimumFontSize setIntValue:size];
150         [minimumFontSizeStepper setIntValue:size];
151         [[preview preferences] setMinimumFontSize:size];
152 }
153
154 - (IBAction) changeDefaultChatStyle:(id) sender {
155         JVStyle *style = [[sender representedObject] objectForKey:@"style"];
156         NSString *variant = [[sender representedObject] objectForKey:@"variant"];
157
158         if( style == _style ) {
159                 [_style setDefaultVariantName:variant];
160
161                 [_styleOptions autorelease];
162                 _styleOptions = [[_style styleSheetOptions] mutableCopy];
163
164                 [self updateChatStylesMenu];
165
166                 if( _variantLocked ) [optionsTable deselectAll:nil];
167
168                 [self updateVariant];
169                 [self parseStyleOptions];
170         } else {
171                 [self setStyle:style];
172
173                 [JVStyle setDefaultStyle:_style];
174                 [_style setDefaultVariantName:variant];
175
176                 [self changePreferences];
177         }
178 }
179
180 - (void) changePreferences {
181         [self updateChatStylesMenu];
182         [self updateEmoticonsMenu];
183
184         [_styleOptions autorelease];
185         _styleOptions = [[_style styleSheetOptions] mutableCopy];
186
187         [preview setPreferencesIdentifier:[_style identifier]];
188
189         WebPreferences *prefs = [preview preferences];
190         [prefs setAutosaves:YES];
191
192         // disable the user style sheet for users of 2C4 who got this
193         // turned on, we do this different now and the user style can interfere
194         [prefs setUserStyleSheetEnabled:NO];
195
196         [standardFont setFont:[NSFont fontWithName:[prefs standardFontFamily] size:[prefs defaultFontSize]]];
197
198         [minimumFontSize setIntValue:[prefs minimumFontSize]];
199         [minimumFontSizeStepper setIntValue:[prefs minimumFontSize]];
200
201         [baseFontSize setIntValue:[prefs defaultFontSize]];
202         [baseFontSizeStepper setIntValue:[prefs defaultFontSize]];
203
204         if( _variantLocked ) [optionsTable deselectAll:nil];
205
206         [self parseStyleOptions];
207 }
208
209 - (IBAction) changeDefaultEmoticons:(id) sender {
210         [self selectEmoticonsWithIdentifier:[sender representedObject]];
211 }
212
213 #pragma mark -
214
215 - (IBAction) changeUseStyleFont:(id) sender {
216         [[NSUserDefaults standardUserDefaults] setBool:(BOOL)[sender state] forKey:@"JVChatInputUsesStyleFont"];
217 }
218
219 #pragma mark -
220
221 - (void) updateChatStylesMenu {
222         NSString *variant = [_style defaultVariantName];
223
224         _variantLocked = ! [_style isUserVariantName:variant];
225
226         NSMenu *menu = [[[NSMenu alloc] initWithTitle:@""] autorelease], *subMenu = nil;
227         NSMenuItem *menuItem = nil, *subMenuItem = nil;
228
229         NSEnumerator *enumerator = [[[[JVStyle styles] allObjects] sortedArrayUsingSelector:@selector( compare: )] objectEnumerator];
230         NSEnumerator *venumerator = nil;
231         JVStyle *style = nil;
232         id item = nil;
233
234         while( ( style = [enumerator nextObject] ) ) {
235                 menuItem = [[[NSMenuItem alloc] initWithTitle:[style displayName] action:@selector( changeDefaultChatStyle: ) keyEquivalent:@""] autorelease];
236                 [menuItem setTarget:self];
237                 [menuItem setRepresentedObject:[NSDictionary dictionaryWithObjectsAndKeys:style, @"style", nil]];
238                 if( [_style isEqualTo:style] ) [menuItem setState:NSOnState];
239                 [menu addItem:menuItem];
240
241                 NSArray *variants = [style variantStyleSheetNames];
242                 NSArray *userVariants = [style userVariantStyleSheetNames];
243
244                 if( [variants count] || [userVariants count] ) {
245                         subMenu = [[[NSMenu alloc] initWithTitle:@""] autorelease];
246
247                         subMenuItem = [[[NSMenuItem alloc] initWithTitle:[style mainVariantDisplayName] action:@selector( changeDefaultChatStyle: ) keyEquivalent:@""] autorelease];
248                         [subMenuItem setTarget:self];
249                         [subMenuItem setRepresentedObject:[NSDictionary dictionaryWithObjectsAndKeys:style, @"style", nil]];
250                         if( [_style isEqualTo:style] && ! variant ) [subMenuItem setState:NSOnState];
251                         [subMenu addItem:subMenuItem];
252
253                         venumerator = [variants objectEnumerator];
254                         while( ( item = [venumerator nextObject] ) ) {
255                                 subMenuItem = [[[NSMenuItem alloc] initWithTitle:item action:@selector( changeDefaultChatStyle: ) keyEquivalent:@""] autorelease];
256                                 [subMenuItem setTarget:self];
257                                 [subMenuItem setRepresentedObject:[NSDictionary dictionaryWithObjectsAndKeys:style, @"style", item, @"variant", nil]];
258                                 if( [_style isEqualTo:style] && [variant isEqualToString:item] )
259                                         [subMenuItem setState:NSOnState];
260                                 [subMenu addItem:subMenuItem];
261                         }
262
263                         if( [userVariants count] ) [subMenu addItem:[NSMenuItem separatorItem]];
264
265                         venumerator = [userVariants objectEnumerator];
266                         while( ( item = [venumerator nextObject] ) ) {
267                                 subMenuItem = [[[NSMenuItem alloc] initWithTitle:item action:@selector( changeDefaultChatStyle: ) keyEquivalent:@""] autorelease];
268                                 [subMenuItem setTarget:self];
269                                 [subMenuItem setRepresentedObject:[NSDictionary dictionaryWithObjectsAndKeys:style, @"style", item, @"variant", nil]];
270                                 if( [_style isEqualTo:style] && [variant isEqualToString:item] )
271                                         [subMenuItem setState:NSOnState];
272                                 [subMenu addItem:subMenuItem];
273                         }
274
275                         [menuItem setSubmenu:subMenu];
276                 }
277
278                 subMenu = nil;
279         }
280
281         [styles setMenu:menu];
282 }
283
284 - (void) updateEmoticonsMenu {
285         NSEnumerator *enumerator = [[[[JVEmoticonSet emoticonSets] allObjects] sortedArrayUsingSelector:@selector( compare: )] objectEnumerator];
286         NSMenu *menu = nil;
287         NSMenuItem *menuItem = nil;
288         JVEmoticonSet *defaultEmoticon = [_style defaultEmoticonSet];
289         JVEmoticonSet *emoticon = nil;
290
291         menu = [[[NSMenu alloc] initWithTitle:@""] autorelease];
292
293         emoticon = [JVEmoticonSet textOnlyEmoticonSet];
294         menuItem = [[[NSMenuItem alloc] initWithTitle:[emoticon displayName] action:@selector( changeDefaultEmoticons: ) keyEquivalent:@""] autorelease];
295         [menuItem setTarget:self];
296         [menuItem setRepresentedObject:[emoticon identifier]];
297         if( [defaultEmoticon isEqual:emoticon] ) [menuItem setState:NSOnState];
298         [menu addItem:menuItem];
299
300         [menu addItem:[NSMenuItem separatorItem]];
301
302         while( ( emoticon = [enumerator nextObject] ) ) {
303                 if( ! [[emoticon displayName] length] ) continue;
304                 menuItem = [[[NSMenuItem alloc] initWithTitle:[emoticon displayName] action:@selector( changeDefaultEmoticons: ) keyEquivalent:@""] autorelease];
305                 [menuItem setTarget:self];
306                 [menuItem setRepresentedObject:[emoticon identifier]];
307                 if( [defaultEmoticon isEqual:emoticon] ) [menuItem setState:NSOnState];
308                 [menu addItem:menuItem];
309         }
310
311         [emoticons setMenu:menu];
312 }
313
314 - (void) updateVariant {
315         [preview setStyleVariant:[_style defaultVariantName]];
316
317         if( [[preview window] isFlushWindowDisabled] ) [[preview window] enableFlushWindow];
318
319         [[preview window] disableFlushWindow];
320         [preview reloadCurrentStyle];
321 }
322
323 - (void) styleDidReload:(NSNotification *) notification {
324         if( [[preview window] isFlushWindowDisabled] ) [[preview window] enableFlushWindow];
325         [[preview window] displayIfNeeded];
326 }
327
328 #pragma mark -
329
330 - (void) fontPreviewField:(JVFontPreviewField *) field didChangeToFont:(NSFont *) font {
331         [[preview preferences] setStandardFontFamily:[font fontName]];
332         [[preview preferences] setFixedFontFamily:[font fontName]];
333         [[preview preferences] setSerifFontFamily:[font fontName]];
334         [[preview preferences] setSansSerifFontFamily:[font fontName]];
335 }
336
337 - (NSArray *) webView:(WebView *) sender contextMenuItemsForElement:(NSDictionary *) element defaultMenuItems:(NSArray *) defaultMenuItems {
338         return nil;
339 }
340
341 - (void) webView:(WebView *) sender decidePolicyForNavigationAction:(NSDictionary *) actionInformation request:(NSURLRequest *) request frame:(WebFrame *) frame decisionListener:(id <WebPolicyDecisionListener>) listener {
342         if( [[[actionInformation objectForKey:WebActionOriginalURLKey] scheme] isEqualToString:@"about"]  ) {
343                 [listener use];
344         } else {
345                 NSURL *url = [actionInformation objectForKey:WebActionOriginalURLKey];
346                 [[NSWorkspace sharedWorkspace] openURL:url];
347                 [listener ignore];
348         }
349 }
350
351 #pragma mark -
352
353 - (void) buildFileMenuForCell:(NSPopUpButtonCell *) cell andOptions:(NSMutableDictionary *) options {
354         NSMenu *menu = [[[NSMenu alloc] initWithTitle:@""] autorelease];
355         NSMenuItem *menuItem = [[[NSMenuItem alloc] initWithTitle:NSLocalizedString( @"None", "no background image label" ) action:NULL keyEquivalent:@""] autorelease];
356         [menuItem setRepresentedObject:@"none"];
357         [menu addItem:menuItem];
358
359         NSArray *files = [[_style bundle] pathsForResourcesOfType:nil inDirectory:[options objectForKey:@"folder"]];
360         NSEnumerator *enumerator = [files objectEnumerator];
361         NSString *resourcePath = [[[_style bundle] resourcePath] stringByAppendingPathComponent:[options objectForKey:@"folder"]];
362         NSString *path = nil;
363         BOOL matched = NO;
364
365         if( [files count] ) [menu addItem:[NSMenuItem separatorItem]];
366
367         while( ( path = [enumerator nextObject] ) ) {
368                 NSImage *icon = [[NSWorkspace sharedWorkspace] iconForFile:path];
369                 NSImageRep *sourceImageRep = [icon bestRepresentationForDevice:nil];
370                 NSImage *smallImage = [[[NSImage alloc] initWithSize:NSMakeSize( 12., 12. )] autorelease];
371                 [smallImage lockFocus];
372                 [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationLow];
373                 [sourceImageRep drawInRect:NSMakeRect( 0., 0., 12., 12. )];
374                 [smallImage unlockFocus];
375
376                 menuItem = [[[NSMenuItem alloc] initWithTitle:[[[NSFileManager defaultManager] displayNameAtPath:path] stringByDeletingPathExtension] action:NULL keyEquivalent:@""] autorelease];
377                 [menuItem setImage:smallImage];
378                 [menuItem setRepresentedObject:path];
379                 [menuItem setTag:5];
380                 [menu addItem:menuItem];
381
382                 NSString *fullPath = ( [[options objectForKey:@"path"] isAbsolutePath] ? [options objectForKey:@"path"] : [resourcePath stringByAppendingPathComponent:[options objectForKey:@"path"]] );
383                 if( [path isEqualToString:fullPath] ) {
384                         int index = [menu indexOfItemWithRepresentedObject:path];
385                         [options setObject:[NSNumber numberWithInt:index] forKey:@"value"];
386                         matched = YES;
387                 }
388         }
389
390         path = [options objectForKey:@"path"];
391         if( ! matched && [path length] ) {
392                 [menu addItem:[NSMenuItem separatorItem]];
393
394                 NSImage *icon = [[NSWorkspace sharedWorkspace] iconForFile:path];
395                 NSImageRep *sourceImageRep = [icon bestRepresentationForDevice:nil];
396                 NSImage *smallImage = [[[NSImage alloc] initWithSize:NSMakeSize( 12., 12. )] autorelease];
397                 [smallImage lockFocus];
398                 [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationLow];
399                 [sourceImageRep drawInRect:NSMakeRect( 0., 0., 12., 12. )];
400                 [smallImage unlockFocus];
401
402                 menuItem = [[[NSMenuItem alloc] initWithTitle:[[NSFileManager defaultManager] displayNameAtPath:path] action:NULL keyEquivalent:@""] autorelease];
403                 [menuItem setImage:smallImage];
404                 [menuItem setRepresentedObject:path];
405                 [menuItem setTag:10];
406                 [menu addItem:menuItem];
407
408                 int index = [menu indexOfItemWithRepresentedObject:path];
409                 [options setObject:[NSNumber numberWithInt:index] forKey:@"value"];
410         }
411
412         [menu addItem:[NSMenuItem separatorItem]];
413
414         menuItem = [[[NSMenuItem alloc] initWithTitle:NSLocalizedString( @"Other...", "other image label" ) action:@selector( selectImageFile: ) keyEquivalent:@""] autorelease];
415         [menuItem setTarget:self];
416         [menuItem setTag:10];
417         [menu addItem:menuItem];
418
419         [cell setMenu:menu];
420         [cell synchronizeTitleAndSelectedItem];
421         [optionsTable performSelector:@selector( reloadData ) withObject:nil afterDelay:0.];
422 }
423
424 #pragma mark -
425
426 // Called when Colloquy reactivates.
427 - (void) reloadStyles:(NSNotification *) notification {
428         if( ! [[preview window] isVisible] ) return;
429         [JVStyle scanForStyles];
430
431         if( ! [_userStyle length] ) return;
432         [self parseStyleOptions];
433         [self updateVariant];
434 }
435
436 // Parses the style options plist and reads the CSS files to figure out the current selected values.
437 - (void) parseStyleOptions {
438         [self setUserStyle:[_style contentsOfVariantStyleSheetWithName:[_style defaultVariantName]]];
439
440         NSString *css = _userStyle;
441         css = [css stringByAppendingString:[_style contentsOfMainStyleSheet]];
442
443         NSEnumerator *enumerator = [_styleOptions objectEnumerator];
444         NSMutableDictionary *info = nil;
445
446         // Step through each options.
447         while( ( info = [enumerator nextObject] ) ) {
448                 NSMutableArray *styleLayouts = [NSMutableArray array];
449                 NSArray *sarray = nil;
450                 NSEnumerator *senumerator = nil;
451                 if( ! [info objectForKey:@"style"] ) continue;
452                 if( [[info objectForKey:@"style"] isKindOfClass:[NSArray class]] && [[info objectForKey:@"type"] isEqualToString:@"list"] )
453                         sarray = [info objectForKey:@"style"];
454                 else sarray = [NSArray arrayWithObject:[info objectForKey:@"style"]];
455                 senumerator = [sarray objectEnumerator];
456
457                 [info removeObjectForKey:@"value"]; // Clear any old values, we will get the new value later on.
458
459                 // Step through each style choice per option, colors have only one; lists have one style per list item.
460                 int count = 0;
461                 NSString *style = nil;
462                 while( ( style = [senumerator nextObject] ) ) {
463                         // Parse all the selectors in the style.
464                         AGRegex *regex = [AGRegex regexWithPattern:@"(\\S.*?)\\s*\{([^\\}]*?)\\}" options:( AGRegexCaseInsensitive | AGRegexDotAll )];
465                         NSEnumerator *selectors = [regex findEnumeratorInString:style];
466                         AGRegexMatch *selector = nil;
467
468                         NSMutableArray *styleLayout = [NSMutableArray array];
469                         [styleLayouts addObject:styleLayout];
470
471                         // Step through the selectors.
472                         while( ( selector = [selectors nextObject] ) ) {
473                                 // Parse all the properties for the selector.
474                                 regex = [AGRegex regexWithPattern:@"(\\S*?):\\s*(.*?);" options:( AGRegexCaseInsensitive | AGRegexDotAll )];
475                                 NSEnumerator *properties = [regex findEnumeratorInString:[selector groupAtIndex:2]];
476                                 AGRegexMatch *property = nil;
477
478                                 // Step through all the properties and build a dictionary on this selector/property/value combo.
479                                 while( ( property = [properties nextObject] ) ) {
480                                         NSMutableDictionary *propertyInfo = [NSMutableDictionary dictionary];
481                                         NSString *p = [property groupAtIndex:1];
482                                         NSString *s = [selector groupAtIndex:1];
483                                         NSString *v = [property groupAtIndex:2];
484
485                                         [propertyInfo setObject:s forKey:@"selector"];
486                                         [propertyInfo setObject:p forKey:@"property"];
487                                         [propertyInfo setObject:v forKey:@"value"];
488                                         [styleLayout addObject:propertyInfo];
489
490                                         // Get the current value of this selector/property from the Variant CSS and the Main CSS to compare.
491                                         NSString *value = [self valueOfProperty:p forSelector:s inStyle:css];
492                                         if( [[info objectForKey:@"type"] isEqualToString:@"list"] ) {
493                                                 // Strip the "!important" flag to compare correctly.
494                                                 regex = [AGRegex regexWithPattern:@"\\s*!\\s*important\\s*$" options:AGRegexCaseInsensitive];
495                                                 NSString *compare = [regex replaceWithString:@"" inString:v];
496
497                                                 // Try to pick which option the list needs to select.
498                                                 if( ! [value isEqualToString:compare] ) { // Didn't match.
499                                                         NSNumber *value = [info objectForKey:@"value"];
500                                                         if( [value intValue] == count ) [info removeObjectForKey:@"value"];
501                                                 } else [info setObject:[NSNumber numberWithInt:count] forKey:@"value"]; // Matched for now.
502                                         } else if( [[info objectForKey:@"type"] isEqualToString:@"color"] ) {
503                                                 if( value && [v rangeOfString:@"%@"].location != NSNotFound ) {
504                                                         // Strip the "!important" flag to compare correctly.
505                                                         regex = [AGRegex regexWithPattern:@"\\s*!\\s*important\\s*$" options:AGRegexCaseInsensitive];
506
507                                                         // Replace %@ with (.*) so we can pull the color value out.
508                                                         NSString *expression = [regex replaceWithString:@"" inString:v];
509                                                         expression = [expression stringByEscapingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"^[]{}()\\.$*+?|"]];
510                                                         expression = [NSString stringWithFormat:expression, @"(.*)"];
511
512                                                         // Store the color value if we found one.
513                                                         regex = [AGRegex regexWithPattern:expression options:AGRegexCaseInsensitive];
514                                                         AGRegexMatch *vmatch = [regex findInString:value];
515                                                         if( [vmatch count] ) [info setObject:[vmatch groupAtIndex:1] forKey:@"value"];
516                                                 }
517                                         } else if( [[info objectForKey:@"type"] isEqualToString:@"file"] ) {
518                                                 if( value && [v rangeOfString:@"%@"].location != NSNotFound ) {
519                                                         // Strip the "!important" flag to compare correctly.
520                                                         regex = [AGRegex regexWithPattern:@"\\s*!\\s*important\\s*$" options:AGRegexCaseInsensitive];
521
522                                                         [info setObject:[NSNumber numberWithInt:0] forKey:@"value"];
523                                                         [info setObject:[NSNumber numberWithInt:0] forKey:@"default"];
524
525                                                         // Replace %@ with (.*) so we can pull the color value out.
526                                                         NSString *expression = [regex replaceWithString:@"" inString:v];
527                                                         expression = [expression stringByEscapingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"^[]{}()\\.$*+?|"]];
528                                                         expression = [NSString stringWithFormat:expression, @"(.*)"];
529
530                                                         // Store the color value if we found one.
531                                                         regex = [AGRegex regexWithPattern:expression options:AGRegexCaseInsensitive];
532                                                         AGRegexMatch *vmatch = [regex findInString:value];
533                                                         if( [vmatch count] ) {
534                                                                 if( ! [[vmatch groupAtIndex:1] isEqualToString:@"none"] )
535                                                                         [info setObject:[vmatch groupAtIndex:1] forKey:@"path"];
536                                                                 else [info removeObjectForKey:@"path"];
537                                                                 if( [info objectForKey:@"cell"] )
538                                                                         [self buildFileMenuForCell:[info objectForKey:@"cell"] andOptions:info];
539                                                         }
540                                                 }
541                                         }
542                                 }
543                         }
544
545                         count++;
546                 }
547
548                 [info setObject:styleLayouts forKey:@"layouts"];
549         }
550
551         [optionsTable reloadData];
552 }
553
554 // reads a value form a CSS file for the property and selector provided.
555 - (NSString *) valueOfProperty:(NSString *) property forSelector:(NSString *) selector inStyle:(NSString *) style {
556         NSCharacterSet *escapeSet = [NSCharacterSet characterSetWithCharactersInString:@"^[]{}()\\.$*+?|"];
557         selector = [selector stringByEscapingCharactersInSet:escapeSet];
558         property = [property stringByEscapingCharactersInSet:escapeSet];
559
560         AGRegex *regex = [AGRegex regexWithPattern:[NSString stringWithFormat:@"%@\\s*\\{[^\\}]*?\\s%@:\\s*(.*?)(?:\\s*!\\s*important\\s*)?;.*?\\}", selector, property] options:( AGRegexCaseInsensitive | AGRegexDotAll )];
561         AGRegexMatch *match = [regex findInString:style];
562         if( [match count] > 1 ) return [match groupAtIndex:1];
563
564         return nil;
565 }
566
567 // Saves a CSS value to the specified property and selector, creating it if one isn't already in the file.
568 - (void) setStyleProperty:(NSString *) property forSelector:(NSString *) selector toValue:(NSString *) value {
569         NSCharacterSet *escapeSet = [NSCharacterSet characterSetWithCharactersInString:@"^[]{}()\\.$*+?|"];
570         NSString *rselector = [selector stringByEscapingCharactersInSet:escapeSet];
571         NSString *rproperty = [property stringByEscapingCharactersInSet:escapeSet];
572
573         AGRegex *regex = [AGRegex regexWithPattern:[NSString stringWithFormat:@"(%@\\s*\\{[^\\}]*?\\s%@:\\s*)(?:.*?)(;.*?\\})", rselector, rproperty] options:( AGRegexCaseInsensitive | AGRegexDotAll )];
574         if( [[regex findInString:_userStyle] count] ) { // Change existing property in selector block
575                 [self setUserStyle:[regex replaceWithString:[NSString stringWithFormat:@"$1%@$2", value] inString:_userStyle]];
576         } else {
577                 regex = [AGRegex regexWithPattern:[NSString stringWithFormat:@"(\\s%@\\s*\\{)(\\s*)", rselector] options:AGRegexCaseInsensitive];
578                 if( [[regex findInString:_userStyle] count] ) { // Append to existing selector block
579                         [self setUserStyle:[regex replaceWithString:[NSString stringWithFormat:@"$1$2%@: %@;$2", rproperty, value] inString:_userStyle]];
580                 } else { // Create new selector block
581                         [self setUserStyle:[_userStyle stringByAppendingFormat:@"%@%@ {\n\t%@: %@;\n}", ( [_userStyle length] ? @"\n\n": @"" ), selector, property, value]];
582                 }
583         }
584 }
585
586 - (void) setUserStyle:(NSString *) style {
587         [_userStyle autorelease];
588         if( ! style ) _userStyle = [[NSString string] retain];
589         else _userStyle = [style retain];
590 }
591
592 // Saves the custom variant to the user's area.
593 - (void) saveStyleOptions {
594         if( _variantLocked ) return;
595         [_userStyle writeToURL:[_style variantStyleSheetLocationWithName:[_style defaultVariantName]] atomically:YES];
596
597         NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:[_style defaultVariantName], @"variant", nil];
598         NSNotification *notification = [NSNotification notificationWithName:JVStyleVariantChangedNotification object:_style userInfo:info];
599         [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP coalesceMask:( NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender ) forModes:nil];
600 }
601
602 // Shows the drawer, option clicking the button will open the custom variant CSS file.
603 - (IBAction) showOptions:(id) sender {
604         if( ! _variantLocked && [[[NSApplication sharedApplication] currentEvent] modifierFlags] & NSAlternateKeyMask ) {
605                 [[NSWorkspace sharedWorkspace] openURL:[_style variantStyleSheetLocationWithName:[_style defaultVariantName]]];
606                 return;
607         }
608
609         if( _variantLocked && [optionsDrawer state] == NSDrawerClosedState )
610                 [self showNewVariantSheet];
611
612         [optionsDrawer setParentWindow:[sender window]];
613         [optionsDrawer setPreferredEdge:NSMaxXEdge];
614         if( [optionsDrawer contentSize].width < [optionsDrawer minContentSize].width )
615                 [optionsDrawer setContentSize:[optionsDrawer minContentSize]];
616         [optionsDrawer toggle:sender];
617 }
618
619 #pragma mark -
620
621 - (int) numberOfRowsInTableView:(NSTableView *) view {
622         return [_styleOptions count];
623 }
624
625 - (id) tableView:(NSTableView *) view objectValueForTableColumn:(NSTableColumn *) column row:(int) row {
626         if( [[column identifier] isEqualToString:@"key"] ) {
627                 return [[_styleOptions objectAtIndex:row] objectForKey:@"description"];
628         } else if( [[column identifier] isEqualToString:@"value"] ) {
629                 NSDictionary *info = [_styleOptions objectAtIndex:row];
630                 id value = [info objectForKey:@"value"];
631                 if( value ) return value;
632                 return [info objectForKey:@"default"];
633         }
634         return nil;
635 }
636
637 - (void) tableView:(NSTableView *) view setObjectValue:(id) object forTableColumn:(NSTableColumn *) column row:(int) row {
638         if( _variantLocked ) return;
639
640         if( [[column identifier] isEqualToString:@"value"] ) {
641                 NSMutableDictionary *info = [_styleOptions objectAtIndex:row];
642                 if( [[info objectForKey:@"type"] isEqualToString:@"list"] ) {
643                         [info setObject:object forKey:@"value"];
644
645                         NSEnumerator *enumerator = [[[info objectForKey:@"layouts"] objectAtIndex:[object intValue]] objectEnumerator];
646                         NSDictionary *styleInfo = nil;
647                         while( ( styleInfo = [enumerator nextObject] ) ) {
648                                 [self setStyleProperty:[styleInfo objectForKey:@"property"] forSelector:[styleInfo objectForKey:@"selector"] toValue:[styleInfo objectForKey:@"value"]];
649                         }
650
651                         [self saveStyleOptions];
652                 } else if( [[info objectForKey:@"type"] isEqualToString:@"file"] ) {
653                         if( [object intValue] == -1 ) return;
654
655                         NSString *path = [[[info objectForKey:@"cell"] itemAtIndex:[object intValue]] representedObject];
656                         if( ! path ) return;
657
658                         [info setObject:object forKey:@"value"];
659
660                         NSEnumerator *enumerator = [[[info objectForKey:@"layouts"] objectAtIndex:0] objectEnumerator];
661                         NSDictionary *styleInfo = nil;
662                         while( ( styleInfo = [enumerator nextObject] ) ) {
663                                 NSString *setting = [NSString stringWithFormat:[styleInfo objectForKey:@"value"], path];
664                                 [self setStyleProperty:[styleInfo objectForKey:@"property"] forSelector:[styleInfo objectForKey:@"selector"] toValue:setting];
665                         }
666
667                         [self saveStyleOptions];
668                 } else return;
669         }
670 }
671
672 // Called when JVColorWell's color changes.
673 - (void) colorWellDidChangeColor:(NSNotification *) notification {
674         if( _variantLocked ) return;
675
676         JVColorWellCell *cell = [notification object];
677         if( ! [[cell representedObject] isKindOfClass:[NSNumber class]] ) return;
678         int row = [[cell representedObject] intValue];
679
680         NSMutableDictionary *info = [_styleOptions objectAtIndex:row];
681         [info setObject:[cell color] forKey:@"value"];
682
683         NSArray *style = [[info objectForKey:@"layouts"] objectAtIndex:0];
684         NSString *value = [[cell color] CSSAttributeValue];
685         NSEnumerator *enumerator = [style objectEnumerator];
686         NSDictionary *styleInfo = nil;
687         NSString *setting = nil;
688
689         while( ( styleInfo = [enumerator nextObject] ) ) {
690                 setting = [NSString stringWithFormat:[styleInfo objectForKey:@"value"], value];
691                 [self setStyleProperty:[styleInfo objectForKey:@"property"] forSelector:[styleInfo objectForKey:@"selector"] toValue:setting];
692         }
693
694         [self saveStyleOptions];
695 }
696
697 - (IBAction) selectImageFile:(id) sender {
698         NSOpenPanel *openPanel = [NSOpenPanel openPanel];
699         int index = [optionsTable selectedRow];
700         NSMutableDictionary *info = [_styleOptions objectAtIndex:index];
701
702         [openPanel setAllowsMultipleSelection:NO];
703         [openPanel setTreatsFilePackagesAsDirectories:NO];