root/trunk/Preferences/JVAppearancePreferences.m

Revision 3775, 31.6 kB (checked in by timothy, 10 months ago)

Changes to emoticons not reflected in appearance preview. Patch by tjohns. #1184

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