root/tags/2C10/JVAppearancePreferences.m

Revision 1835, 33.6 kB (checked in by timothy, 4 years ago)

Switching styles in the prefs no-longer flashes the display as the font preferences are changing.

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