root/tags/2D14/JVAppearancePreferences.m

Revision 2558, 31.5 kB (checked in by eridius, 3 years ago)

Remove all trailing whitespace from lines
Remove indentation on blank lines

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