source: trunk/PrefsPane/src/FilterRuleEdit.m @ 1046

Revision 1033, 46.0 KB checked in by speck, 8 months ago (diff)

Add '%t' to keyword expansion which uses latin-1 encoding of the url argument as some sites uses latin-1 instead of utf-8. By report from Andreas Maks.

Line 
1/* Copyright (C) 2008-2009 Peter Speck
2 *
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, either version 3 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
15 */
16
17#import "FilterRuleEdit.h"
18#import "FilterRuleEditUtils.h"
19#import "GBDebug.h"
20#import "XmlDoc.h"
21#import "Xml.h"
22#import "ServerCommunication.h"
23#import "AlertHelper.h"
24#import "Prefs.h"
25#import "FilterTabController.h"
26#import "Rule.h"
27#import "Filter.h"
28#import "JavascriptSyntaxHighlighter.h"
29#import "ClickActionTextField.h"
30#import "NamedEnum.h"
31
32
33#ifdef DEBUG
34//#define DEBUG_TEXT_CHANGE
35//#define DEBUG_RESPONDS_TO_SELECTOR
36//#define DEBUG_DCBS
37#endif
38
39static NSFont* regexpCommentFont = NULL;
40static NSFont* regexpErrorFont = NULL;
41
42@interface FilterRuleEdit ()
43
44- (void)makeFilterPopMenu;
45- (void)disableControls:(NSView*)view;
46
47- (BOOL)isJavascriptTextStorage:(NSTextStorage*)storage;
48
49- (BOOL)validate:(id)sender;
50- (void)saveSettingsInRuleElement:(NSXMLElement*)ruleE;
51- (NSData*)previewDataForCurrentSettings;
52
53@end
54
55@implementation FilterRuleEdit
56
57const static int ACTION_BLOCK = 0;
58const static int ACTION_WHITELIST = 1;
59const static int ACTION_REQUEST = 2;
60const static int ACTION_KEYWORD = 3;
61
62static NamedEnum* hostTypeHelper;
63static NamedEnum* pathTypeHelper;
64static NamedEnum* queryTypeHelper;
65static NamedEnum* keywordTypeHelper;
66static NamedEnum* jsPlacementHelper;
67static NamedEnum* transformContentTypeHelper;
68
69- (id)initWithFilterTabController:(FilterTabController*)filterTabController
70                  withOwnerFilter:(Filter*)ownerFilter
71                  withRuleElement:(NSXMLElement*)ruleE
72               withSuspectElement:(NSXMLElement*)suspectE
73{
74    if (!(self = [super init]))
75        return nil;
76    if (!regexpCommentFont) {
77        regexpCommentFont = [[NSFont systemFontOfSize:9] retain];
78        regexpErrorFont = [[NSFont boldSystemFontOfSize:9] retain];
79        hostTypeHelper = [[NamedEnum alloc] initWithSemicolonList:@"+ignored;is;domain;regexp"];
80        pathTypeHelper = [[NamedEnum alloc] initWithSemicolonList:@"+ignored;starts-with;ends-with;contains;not-contains;is;not-is;regexp;not-regexp"];
81        queryTypeHelper = [[NamedEnum alloc] initWithSemicolonList:@"+ignored;contains;not-contains;regexp;not-regexp"];
82        keywordTypeHelper = [[NamedEnum alloc] initWithSemicolonList:@"+is;starts-with;regexp"];
83        jsPlacementHelper = [[NamedEnum alloc] initWithSemicolonList:@"head-start;head-end;body-start;+body-end"];
84        transformContentTypeHelper = [[NamedEnum alloc] initWithSemicolonList:@"+html;xml;js;css;*;*not-ok*"];
85    }
86    NSAssert([NSBundle loadNibNamed: @"FilterRuleEdit" owner: self], @"Can't load nib 'FilterRuleEdit'");
87    NSAssert(ruleSheet, @"ruleSheet is null");
88    [ruleSheet setDelegate:self];
89    [requestTabViewItem retain];
90    [cssTabViewItem retain];
91    [jsTabViewItem retain];
92    [transformTabViewItem retain];
93    [expansionTabViewItem retain];
94    _filterTabController = [filterTabController retain];
95    _comm = [[filterTabController comm] retain];
96    _prefs = [[_comm prefs] retain];
97    _alertHelper = [[_prefs alertHelper] retain];
98    _bundle = [[filterTabController bundle] retain];
99    _ownerFilter = [ownerFilter retain];
100    _codeEditHelper = [[CodeEditHelper alloc] initWithDelegate:self];
101    //
102    if (ruleE) {
103        NSAssert(_ownerFilter, @"_ownerFilter");
104        if ([_ownerFilter safetySimpleRulesOnly] && ![Rule ruleIsSafe:ruleE])
105            [enableCB setTitle:@"Rule enabled (but ignored)"];
106        _virgin = NO;
107        _ruleE = [ruleE retain];
108        [saveBtn setTitle:@"Save"];
109    } else {
110        _ruleE = [[Xml createElementWithName:@"rule"] retain];
111        _virgin = YES;
112    }
113    [enableCB setState:![Xml getBoolAttribute:ruleE withName:@"disabled"]];
114    [mobileSafariBtn setState:[Xml getBoolAttribute:ruleE withName:@"mobile"]];
115    int p = [Xml getIntAttribute:ruleE withName:@"priority"];
116    p = (p > 0 ? p : 2) - 1;
117    [priorityPopup selectItemAtIndex:MIN(p, [priorityPopup numberOfItems] - 1)];
118    NSString* type = [Xml getAttribute:ruleE withName:@"type"];
119    if ([type isEqual:@"keyword"])
120        [actionMatrix selectCellAtRow:ACTION_KEYWORD column:0];
121    else if ([type isEqual:@"request"])
122        [actionMatrix selectCellAtRow:ACTION_REQUEST column:0];
123    else if (!type || [type isEqual:@"block"])
124        [actionMatrix selectCellAtRow:ACTION_BLOCK column:0];
125    else
126        [actionMatrix selectCellAtRow:ACTION_WHITELIST column:0];
127    [commentsTextView setString:soft([Xml stringForXPath:@"comments" withRoot:_ruleE])];
128    [commentsTextView setAlignment:NSLeftTextAlignment range:NSMakeRange(0, [[commentsTextView string] length])];
129    [keywordTextField setStringValue:soft([Xml getAttribute:_ruleE withName:@"keyword"])];
130    NSArray* keywordsE = [_ruleE elementsForName:@"keyword"];
131    if ([keywordsE count]) {
132        NSXMLElement* keywordE = [keywordsE objectAtIndex:0];
133        [keywordUrlField setStringValue:soft([Xml getAttribute:keywordE withName:@"keyword-url"])];
134        [keywordTypeHelper updatePopButton:keywordTypePopup fromXPath:@"@keyword-type" withRoot:_ruleE];
135        [keywordTypeMatrix selectCellAtRow:([Xml getBoolAttribute:keywordE withName:@"keyword-uses-js"] ? 0 : 1) column:0];
136    } else {
137        [keywordUrlField setStringValue:@""];
138        [keywordTypeHelper selectDefaultItemInPop:keywordTypePopup];
139        [keywordTypeMatrix selectCellAtRow:1 column:0];
140    }
141    //-------------
142    NSString* host = [Xml getAttribute:_ruleE withName:@"host"];
143    if (host && [host hasPrefix:@"*."]) {
144        [hostTextField setStringValue:[host substringFromIndex:2]];
145        [hostTypeHelper updatePopButton:hostTypePopup toValue:@"domain"];
146    } else {
147        [hostTextField setStringValue:soft(host)];
148        if (_virgin || [host length])
149            [hostTypeHelper updatePopButton:hostTypePopup fromXPath:@"@host-type" withRoot:_ruleE withDefault:@"is"];
150        else
151            [hostTypeHelper selectDefaultItemInPop:hostTypePopup];
152    }
153    //-------------
154    NSString* path = [Xml getAttribute:_ruleE withName:@"path"];
155    [pathTextField setStringValue:soft(path)];
156    if ([path length])
157        [pathTypeHelper updatePopButton:pathTypePopup fromXPath:@"@path-type" withRoot:_ruleE withDefault:@"starts-with"];
158    else
159        [pathTypeHelper selectDefaultItemInPop:pathTypePopup];
160    //-------------
161    NSString* query = [Xml getAttribute:_ruleE withName:@"query"];
162    [queryTextField setStringValue:soft(query)];
163    if ([query length])
164        [queryTypeHelper updatePopButton:queryTypePopup fromXPath:@"@query-type" withRoot:_ruleE withDefault:@"contains"];
165    else
166        [queryTypeHelper selectDefaultItemInPop:queryTypePopup];
167    //-------------
168    [_codeEditHelper setCodeTextView:requestTextView withText:[Xml stringForXPath:@"request" withRoot:_ruleE]];
169    [_codeEditHelper setCodeTextView:cssTextView withText:[Xml stringForXPath:@"css" withRoot:_ruleE]];
170    [_codeEditHelper setCodeTextView:jsTextView withText:[Xml stringForXPath:@"js" withRoot:_ruleE]];
171    [_codeEditHelper setCodeTextView:transformTextView withText:[Xml stringForXPath:@"transform" withRoot:_ruleE]];
172    [_codeEditHelper setCodeTextView:expansionTextView withText:[Xml stringForXPath:@"keyword" withRoot:_ruleE]];
173    [self makeFilterPopMenu];
174    [jsPlacementHelper updatePopButton:jsPlacementPop
175                               fromXPath:@"js/@placement"
176                                withRoot:_ruleE];
177    [transformContentTypeHelper updatePopButton:transformContentTypePop
178                                        fromXPath:@"transform/@content-type"
179                                         withRoot:_ruleE];
180    if (suspectE) {
181        [hostTextField setStringValue:[Xml getAttribute:suspectE withName:@"host"]];
182        [pathTextField setStringValue:[Xml getAttribute:suspectE withName:@"path"]];
183        [queryTextField setStringValue:[Xml getAttribute:suspectE withName:@"query"]];
184    }
185    _editable = [_prefs hasGlimmerAuth] && ![_ownerFilter isSubscription];
186    DebugNSLog(@"_editable = %d", _editable);
187    if (!_editable) {
188        NSView* view = enableCB;
189        while ([view superview])
190            view = [view superview];
191        [self disableControls:view];
192    }
193    NSInteger tabIndex = 0;
194    if (!_virgin) {
195        tabIndex = [tabView indexOfTabViewItemWithIdentifier:[_prefs stringForXPath:@"iu/rule/@selected-tab"]];
196        if (tabIndex == NSNotFound)
197            tabIndex = 0;
198    }
199    [tabView setDelegate:self];
200    [tabView selectTabViewItemAtIndex:tabIndex];
201    //
202    [keywordTextField setRuleController:self];
203    [keywordTextField setDelegate:self];
204    [keywordUrlField setRuleController:self];
205    [keywordUrlField setDelegate:self];
206    //
207    [hostTextField setRuleController:self];
208    [hostTextField setDelegate:self];
209    [pathTextField setRuleController:self];
210    [pathTextField setDelegate:self];
211    [queryTextField setRuleController:self];
212    [queryTextField setDelegate:self];
213    [commentsTextView setDelegate:self];
214    //
215    [requestTextView setDelegate:self];
216    [[requestTextView layoutManager] setAllowsNonContiguousLayout:YES];
217    [[requestTextView textStorage] setDelegate:self];
218    [_codeEditHelper syntaxHighlight:[requestTextView textStorage] isJavascript:YES];
219    //
220    [cssTextView setDelegate:self];
221    [[cssTextView layoutManager] setAllowsNonContiguousLayout:YES];
222    [[cssTextView textStorage] setDelegate:self];
223    //
224    [jsTextView setDelegate:self];
225    [[jsTextView layoutManager] setAllowsNonContiguousLayout:YES];
226    [[jsTextView textStorage] setDelegate:self];
227    [_codeEditHelper syntaxHighlight:[jsTextView textStorage] isJavascript:YES];
228    //
229    [transformTextView setDelegate:self];
230    [[transformTextView layoutManager] setAllowsNonContiguousLayout:YES];
231    [[transformTextView textStorage] setDelegate:self];
232    [_codeEditHelper syntaxHighlight:[transformTextView textStorage] isJavascript:YES];
233    //
234    [expansionTextView setDelegate:self];
235    [[expansionTextView layoutManager] setAllowsNonContiguousLayout:YES];
236    [[expansionTextView textStorage] setDelegate:self];
237    [_codeEditHelper syntaxHighlight:[expansionTextView textStorage] isJavascript:YES];
238    //
239    [requestHelpTextLink setTarget:self selector:@selector(requestHelpAction:)];
240    [transformHelpTextLink setTarget:self selector:@selector(transformHelpAction:)];
241    [expansionHelpTextLink setTarget:self selector:@selector(expansionHelpAction:)];
242    [keywordHelpTextLink setTarget:self selector:@selector(expansionHelpAction:)];
243    //
244    //
245    [_prefs zapRulePreviewElement];
246    _previewedData = [[self previewDataForCurrentSettings] retain];
247    NSRect kwr = [keywordContainer frame];
248    [keywordContainer setFrame:[hostPathQueryContainer frame]];
249    NSRect cr = [commentsContainer frame];
250    [commentsContainer setFrameSize:NSMakeSize(cr.size.width, cr.size.height + kwr.size.height)];
251    if (![ruleSheet setFrameUsingName:@"FilterEditSheet"]) {
252        NSRect r = [ruleSheet frame];
253        r.size.height = 547;
254        [ruleSheet setFrame:r display:YES];
255    }
256    [self updateDependentStates:self];
257    [self retain]; // until sheet is dismissed.
258    [NSApp beginSheet: ruleSheet
259       modalForWindow: [_alertHelper window]
260        modalDelegate: nil
261       didEndSelector: nil
262          contextInfo: nil];
263    if ([tabView selectedTabViewItem] == setupTabViewItem) {
264        [self performSelectorOnMainThread:@selector(updateDependentStates:)
265                               withObject:actionMatrix
266                            waitUntilDone:NO];
267    }
268    return self;
269}
270
271- (void)dealloc
272{
273    //DebugNSLog(@"Dealloc RuleEdit");
274    [hostTextField setRuleController:NULL];
275    [pathTextField setRuleController:NULL];
276    [queryTextField setRuleController:NULL];
277    [_alertHelper release];
278    [_prefs release];
279    [_comm release];
280    [_bundle release];
281    [_ruleE release];
282    [_filterTabController release];
283    [_filterArray release];
284    [_ownerFilter release];
285    [_previewedData release];
286    [_cachedHostPathQueryValidationPathArgs release];
287    [_cachedHostPathQueryValidationXmlDoc release];
288    [requestTabViewItem release];
289    [cssTabViewItem release];
290    [jsTabViewItem release];
291    [transformTabViewItem release];
292    [expansionTabViewItem release];
293    [_fieldEditor release];
294    [_codeEditHelper release];
295    [ruleSheet release];
296    [super dealloc];
297}
298
299- (void)makeFilterPopMenu
300{
301    [filterPopupBtn removeAllItems];
302    /*
303     System font is not provided in italic (only bold) [WTF], so this doesn't work anyway.
304     NSFont* font = [NSFont menuFontOfSize:[NSFont systemFontSize]];
305     font = [[NSFontManager sharedFontManager] convertFont:font toHaveTrait:NSBoldFontMask];
306     NSMutableDictionary* styleDict = [NSMutableDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
307     NSMutableAttributedString* attributedTitle =
308        [[NSMutableAttributedString alloc] initWithString:@"New bbbbbb" attributes:styleDict];
309     [[blockerSetPopupBtn itemAtIndex:0] setAttributedTitle:attributedTitle];
310     */
311    NSInteger selectedFilterId = _ownerFilter ? [_ownerFilter filterId] : [_prefs intForXPath:@"iu/rule/@default-filter"];
312    NSMutableArray* a = [NSMutableArray arrayWithCapacity:10];
313    BOOL didSelectFilter = NO;
314    NSUInteger lastPrivateFilterIdx = NSNotFound;
315    for (Filter* filter in [_filterTabController filterArray]) {
316        [a addObject:filter];
317        [filterPopupBtn addItemWithTitle:[filter name]];
318        NSUInteger idx = [filterPopupBtn numberOfItems] - 1;
319        if ([filter isSubscription])
320            [[filterPopupBtn itemAtIndex:idx] setEnabled:NO];
321        else
322            lastPrivateFilterIdx = idx;
323        if (selectedFilterId && [filter filterId] == selectedFilterId) {
324            [filterPopupBtn selectItemAtIndex:idx];
325            didSelectFilter = YES;
326        }
327    }
328    [_filterArray release];
329    _filterArray = [a retain];
330    [filterPopupBtn addItemWithTitle:@"+ Create new filter"];
331    if (didSelectFilter)
332        return;
333    if (lastPrivateFilterIdx != NSNotFound)
334        [filterPopupBtn selectItemAtIndex:lastPrivateFilterIdx];
335    else
336        [filterPopupBtn selectItemAtIndex:[filterPopupBtn numberOfItems] - 1];
337}
338
339- (void)disableControls:(NSView*)view
340{
341    if ([view isKindOfClass:[NSButton class]]) {
342        NSButton* btn = (NSButton*)view;
343        if ([btn bezelStyle] == NSHelpButtonBezelStyle)
344            return; // don't disable help buttons
345    }
346    if ([view isKindOfClass:[ClickActionTextField class]])
347        return; // don't disable help links
348    if (view != cancelBtn && [view isKindOfClass:[NSControl class]])
349        [(NSControl*)view setEnabled:NO];
350    if ([view isKindOfClass:[NSText class]])
351        [(NSText*)view setEditable:NO];
352    if ([view isKindOfClass:[NSTabView class]]) {
353        // NSTabViewItem is not a subview, so special iterating is required.
354        for (NSTabViewItem* item in [(NSTabView*)view tabViewItems])
355            [self disableControls:[item view]];
356    }
357    for (NSView* sub in [view subviews])
358        [self disableControls:sub];
359}
360
361#pragma mark ------------------------------------- validation
362
363- (void)showErrorImage:(NSImageView*)imageView
364           atCharIndex:(NSUInteger)charIndex
365          errorMessage:(NSString*)errorMessage
366             forString:(NSAttributedString*)as
367          forTextField:(FilterRuleTextField*)textField
368         errorTextView:(NSTextField*)errorTextView
369{
370    [errorTextView setHidden:NO];
371    [errorTextView setTextColor:[NSColor redColor]];
372    [errorTextView setFont:regexpErrorFont];
373    [errorTextView setStringValue:errorMessage];
374    //
375    CGFloat w1 = 0, w2 = 0; // get middle of problematic char.
376    NSUInteger len = [as length];
377    if (charIndex < len)
378        as = [as attributedSubstringFromRange:NSMakeRange(0, charIndex + 1)];
379    w2 = [as size].width;
380    if (charIndex <= len)
381        as = [as attributedSubstringFromRange:NSMakeRange(0, charIndex)];
382    w1 = [as size].width;
383    NSRect frame = [imageView frame];
384    CGFloat x = [textField frame].origin.x + 4;
385    x -= frame.size.width / 2;
386    x += (w1 + w2) / 2;
387    x = (CGFloat)round(x);
388    [imageView setFrameOrigin:NSMakePoint(x, frame.origin.y)];
389    [imageView setHidden:NO];
390}
391
392- (BOOL)hasSelectedRegexpInPopMenu:(NSPopUpButton*)typePopup
393                         popHelper:(NamedEnum*)popHelper
394{
395    NSInteger idx = [typePopup indexOfSelectedItem];
396    if (idx < 0)
397        return NO;
398    return [[popHelper nameFromIndex:idx] rangeOfString:@"regexp"].location != NSNotFound;
399}
400
401- (void)showErrorImage:(NSImageView*)imageView
402           atCharIndex:(NSString*)charIndex
403          errorMessage:(NSString*)errorMessage
404             forString:(NSAttributedString*)as
405          forTextField:(FilterRuleTextField*)textField
406             typePopup:(NSPopUpButton*)typePopup
407             popHelper:(NamedEnum*)popHelper
408         errorTextView:(NSTextField*)errorTextView
409         regexpComment:(NSString*)regexpComment
410{
411    BOOL isRegExp = [self hasSelectedRegexpInPopMenu:typePopup popHelper:popHelper];
412    [textField setIsRegExp:isRegExp];
413    if (![charIndex length]) {
414        [imageView setHidden:YES];
415        [errorTextView setHidden:!isRegExp];
416        if (isRegExp) {
417            [errorTextView setTextColor:[NSColor grayColor]];
418            [errorTextView setFont:regexpCommentFont];
419            [errorTextView setStringValue:regexpComment];
420        }
421        return;
422    }
423    [self showErrorImage:imageView
424             atCharIndex:MAX(0, [charIndex intValue])
425            errorMessage:errorMessage
426               forString:as
427            forTextField:textField
428           errorTextView:errorTextView];
429}
430
431- (NSAttributedString*)addValidateItem:(NSPopUpButton*)popup
432                             popHelper:(NamedEnum*)popHelper
433                             textField:(FilterRuleTextField*)textField
434                                  name:(NSString*)name
435                              pathArgs:(NSMutableString*)pathArgs
436{
437    NSAttributedString* as = [textField attributedStringValue];
438    NSString* type = [popHelper selectedPopValue:popup];
439    if (!type) {
440        [textField setEnabled:NO];
441        return as;
442    }
443    [textField setEnabled:_editable];
444    if (![as length])
445        return as;
446    BOOL hasQ = [pathArgs rangeOfString:@"?"].location != NSNotFound;
447    [pathArgs appendString:(hasQ ? @"&" : @"?")];
448    [pathArgs appendString:name];
449    [pathArgs appendString:@"-type="];
450    [pathArgs appendString:type];
451    [pathArgs appendString:@"&"];
452    [pathArgs appendString:name];
453    [pathArgs appendString:@"="];
454    [pathArgs appendString:[GBDebug percentEscapeUrlParameterValue:[as string]]];
455    return as;
456}
457
458- (BOOL)validateHostPathQuery
459{
460    //NSDate* st0 = [NSDate date];
461    NSMutableString* pathArgs = [NSMutableString stringWithString:@"/filters/validate-edit"];
462    NSAttributedString* host = [self addValidateItem:hostTypePopup
463                                           popHelper:hostTypeHelper
464                                           textField:hostTextField
465                                                name:@"host"
466                                            pathArgs:pathArgs];
467    NSAttributedString* path = [self addValidateItem:pathTypePopup
468                                           popHelper:pathTypeHelper
469                                           textField:pathTextField
470                                                name:@"path"
471                                            pathArgs:pathArgs];
472    NSAttributedString* query = [self addValidateItem:queryTypePopup
473                                            popHelper:queryTypeHelper
474                                            textField:queryTextField
475                                                 name:@"query"
476                                             pathArgs:pathArgs];
477    NSString* actionDesc = @"Validating host/path/query";
478    NSError* error = NULL;
479    XmlDoc* doc;
480    if ([_cachedHostPathQueryValidationPathArgs isEqual:pathArgs]) {
481        doc = _cachedHostPathQueryValidationXmlDoc;
482    } else {
483        doc = [_comm curlToXml:pathArgs withActionDescription:actionDesc withErrorRef:&error];
484        if (!doc) {
485            NSLog(@"Can't validate regexp: %@", error);
486            return NO;
487        }
488        [_cachedHostPathQueryValidationPathArgs release];
489        _cachedHostPathQueryValidationPathArgs = [pathArgs retain];
490        [_cachedHostPathQueryValidationXmlDoc release];
491        _cachedHostPathQueryValidationXmlDoc = [doc retain];
492    }
493    [self showErrorImage:hostErrorLocationImageView
494             atCharIndex:[doc stringForXPath:@"@host-index"]
495            errorMessage:[doc stringForXPath:@"@host-error-message"]
496               forString:host
497            forTextField:hostTextField
498               typePopup:hostTypePopup
499               popHelper:hostTypeHelper
500           errorTextView:hostErrorTextField
501           regexpComment:@"must match the complete hostname"];
502    [self showErrorImage:pathErrorLocationImageView
503             atCharIndex:[doc stringForXPath:@"@path-index"]
504            errorMessage:[doc stringForXPath:@"@path-error-message"]
505               forString:path
506            forTextField:pathTextField
507               typePopup:pathTypePopup
508               popHelper:pathTypeHelper
509           errorTextView:pathErrorTextField
510           regexpComment:@"must match the complete path"];
511    [self showErrorImage:queryErrorLocationImageView
512             atCharIndex:[doc stringForXPath:@"@query-index"]
513            errorMessage:[doc stringForXPath:@"@query-error-message"]
514               forString:query
515            forTextField:queryTextField
516               typePopup:queryTypePopup
517               popHelper:queryTypeHelper
518           errorTextView:queryErrorTextField
519           regexpComment:@"must match the complete query string"];
520    return ![doc boolForXPath:@"@has-error"];
521}
522
523- (void)setTextFieldFocus:(FilterRuleTextField*)textField
524                 forPopup:(NSPopUpButton*)popup
525                forSender:(id)sender
526{
527    if (sender != popup || [_ownerFilter isSubscription])
528        return;
529    BOOL used = [popup indexOfSelectedItem] > 0;
530    BOOL active = !![textField currentEditor];
531    if (used && !active) {
532        // doesn't work unless post-poned.
533        [ruleSheet performSelectorOnMainThread:@selector(makeFirstResponder:)
534                                    withObject:textField
535                                 waitUntilDone:NO];
536        return;
537    }
538    if (!used && active) {
539        [ruleSheet performSelectorOnMainThread:@selector(makeFirstResponder:)
540                                    withObject:commentsTextView
541                                 waitUntilDone:NO];
542    }
543}
544
545- (IBAction)updateDependentStates:(id)sender // needed as validate returns BOOL and not void.
546{
547    [self validate:sender];
548}
549
550- (void)getPreviewData:(NSData**)data ruleElementRef:(NSXMLElement**)ruleRef
551{
552    NSXMLElement* ruleE = [Xml createElementWithName:@"rule"];
553    if (ruleRef)
554        *ruleRef = ruleE;
555    [self saveSettingsInRuleElement:ruleE];
556    if (!_virgin)
557        [Xml setAttribute:[Xml getAttribute:_ruleE withName:@"rule-id"] withName:@"rule-id" inElement:ruleE];
558    *data = [Xml subtreeToData:ruleE];
559}
560
561- (NSData*)previewDataForCurrentSettings
562{
563    NSData* data;
564    [self getPreviewData:&data ruleElementRef:NULL];
565    return data;
566}
567
568- (void)setTextViewEditable:(NSTextView*)textView
569{
570    [textView setEditable:_editable];
571    [textView setSelectable:YES];
572    if (textView == cssTextView) {
573        NSColor* color = _editable ? [NSColor blackColor] : [NSColor grayColor];
574        [textView setTextColor:color];
575    }
576}
577
578- (void)makeTabItem:(NSTabViewItem*)tabItem visible:(BOOL)wantedVisible atIndex:(int*)tabIndex
579{
580    BOOL isVisible = ([tabView indexOfTabViewItem:tabItem] != NSNotFound);
581    //DebugNSLog(@"makeTabItem (%d/%d = %@) visible %d -> %d", *tabIndex, [tabView numberOfTabViewItems], [tabItem label], isVisible, wantedVisible);
582    if (wantedVisible) {
583        if (!isVisible)
584            [tabView insertTabViewItem:tabItem atIndex:*tabIndex];
585        (*tabIndex)++;
586    } else if (isVisible) {
587        [tabView removeTabViewItem:tabItem];
588    }
589}
590
591-(void)setTabItem:(NSTabViewItem*)tabItem withLabelDot:(BOOL)wantsDot
592{
593    NSString* s = [tabItem label];
594    NSString* dot = @"•\u2009"; // 2009 is thin space
595    BOOL hasDot = [s hasPrefix:dot];
596    if (hasDot && !wantsDot)
597        [tabItem setLabel:[s substringFromIndex:[dot length]]];
598    else if (!hasDot && wantsDot)
599        [tabItem setLabel:[dot stringByAppendingString:s]];
600}
601
602- (BOOL)validateKeywordText
603{
604    NSString* s = [keywordTextField stringValue];
605    NSInteger len = [s length];
606    if (!len) {
607        [keywordErrorLocationImageView setHidden:YES];
608        [keywordErrorTextView setStringValue:@"Keyword must be specified."];
609        return NO;
610    }
611    NSInteger type = [keywordTypePopup indexOfSelectedItem];
612    if (type == 2) {
613        NSMutableString* pathArgs = [NSMutableString stringWithString:@"/filters/validate-edit"];
614        NSAttributedString* keyword = [self addValidateItem:keywordTypePopup
615                                                  popHelper:keywordTypeHelper
616                                                  textField:keywordTextField
617                                                       name:@"keyword"
618                                                   pathArgs:pathArgs];
619        NSString* actionDesc = @"Validating keyword";
620        NSError* error = NULL;
621        XmlDoc* doc = [_comm curlToXml:pathArgs withActionDescription:actionDesc withErrorRef:&error];
622        if (!doc) {
623            NSLog(@"Can't validate regexp: %@", error);
624            return NO;
625        }
626        [self showErrorImage:keywordErrorLocationImageView
627                 atCharIndex:[doc stringForXPath:@"@keyword-index"]
628                errorMessage:[doc stringForXPath:@"@keyword-error-message"]
629                   forString:keyword
630                forTextField:keywordTextField
631                   typePopup:keywordTypePopup
632                   popHelper:keywordTypeHelper
633               errorTextView:keywordErrorTextView
634               regexpComment:@"must match the complete keyword"];
635        return ![doc boolForXPath:@"@has-error"];
636    }
637    for (int i = 0; i < len; i++) {
638        unichar ch = [s characterAtIndex:i];
639        if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-')
640            continue;
641        NSString* msg;
642        if (ch == ' ')
643            msg = @"Keywords must not contain spaces.";
644        else
645            msg = @"Invalid character. Only a-z, A-Z, 0-9 and dash (-) are allowed.";
646        [self showErrorImage:keywordErrorLocationImageView
647                 atCharIndex:i
648                errorMessage:msg
649                   forString:[keywordTextField attributedStringValue]
650                forTextField:keywordTextField
651               errorTextView:keywordErrorTextView];
652        return NO;
653    }
654    [keywordErrorLocationImageView setHidden:YES];
655    BOOL useJS = ![keywordTypeMatrix selectedRow];
656    if (type == 1 && !useJS) {
657        [keywordErrorTextView setStringValue:@"Starts-with is useful only for Javascript based expansion."];
658        return NO;
659    }
660    [keywordErrorTextView setStringValue:@""];
661    return YES;
662}
663
664- (BOOL)validateKeyword:(id)sender  tabIndex:(int*)tabIndex
665{
666    [hostErrorLocationImageView setHidden:YES];
667    [pathErrorLocationImageView setHidden:YES];
668    [queryErrorLocationImageView setHidden:YES];
669    if (sender == actionMatrix) {
670        [ruleSheet performSelectorOnMainThread:@selector(makeFirstResponder:)
671                                    withObject:keywordTextField
672                                 waitUntilDone:NO];
673    }
674    BOOL useJS = ![keywordTypeMatrix selectedRow];
675    [keywordUrlField setHidden:useJS];
676    [self makeTabItem:expansionTabViewItem visible:useJS atIndex:tabIndex];
677    BOOL valid = [self validateKeywordText];
678    NSString* urlError = NULL;
679    if (useJS) {
680        BOOL hasExpansion = !![_codeEditHelper trimmedTextViewString:expansionTextView];
681        [self setTabItem:expansionTabViewItem withLabelDot:hasExpansion];
682        if (!hasExpansion)
683            urlError = @"Javascript missing in 'expansion' tab.";
684    } else {
685        NSString* u = [keywordUrlField stringValue];
686        if (![u length]) {
687            urlError = @"URL must be specified.";
688        } else {
689            NSString* u2 = [u stringByReplacingOccurrencesOfString:@"%s" withString:@"dummy"];
690            u2 = [u2 stringByReplacingOccurrencesOfString:@"%S" withString:@"dummy"];
691            u2 = [u2 stringByReplacingOccurrencesOfString:@"%t" withString:@"dummy"];
692            u2 = [u2 stringByReplacingOccurrencesOfString:@"|" withString:@"z"]; // NSUrl doesn't accept '|' in URLs.
693            @try {
694                NSURL* url = [NSURL URLWithString:u2];
695                NSString* scheme = [url scheme];
696                if (![scheme length])
697                    urlError = @"URL is missing scheme";
698                else if (([scheme isEqual:@"http"] || [scheme isEqual:@"https"]) && ![[url host] length])
699                    urlError = @"URL is missing hostname";
700                else if ([u2 isEqual:u])
701                    urlError = @"URL is missing %s or %S";
702            }
703            @catch (NSException *ex) {
704                urlError = @"URL is invalid.";
705            }
706        }
707    }
708    [globalErrorTextView setStringValue:soft(urlError)];
709    valid &= !urlError;
710    [saveBtn setEnabled:_editable && valid];
711    [previewBtn setEnabled:_editable && valid && ![_previewedData isEqualToData:[self previewDataForCurrentSettings]]];
712    //--------------------------------------------------------------------------------
713    return valid;
714}
715
716- (BOOL)validate:(id)sender
717{
718    NSInteger action = [actionMatrix selectedRow];
719    BOOL isModification = (action == ACTION_WHITELIST);
720    BOOL isRequest = (action == ACTION_REQUEST);
721    BOOL isKeyword = (action == ACTION_KEYWORD);
722    int tabIndex = 1;
723    [self makeTabItem:requestTabViewItem visible:isRequest atIndex:&tabIndex];
724    [self makeTabItem:cssTabViewItem visible:isModification atIndex:&tabIndex];
725    [self makeTabItem:jsTabViewItem visible:isModification atIndex:&tabIndex];
726    [self makeTabItem:transformTabViewItem visible:isModification atIndex:&tabIndex];
727    //
728    [keywordContainer setHidden:!isKeyword];
729    [hostPathQueryContainer setHidden:isKeyword];
730    //
731    [hostLabel setHidden:isKeyword];
732    [hostTypePopup setHidden:isKeyword];
733    [hostErrorTextField setHidden:isKeyword];
734    [hostTextField setHidden:isKeyword];
735    //
736    [pathLabel setHidden:isKeyword];
737    [pathTypePopup setHidden:isKeyword];
738    [pathErrorTextField setHidden:isKeyword];
739    [pathTextField setHidden:isKeyword];
740    //
741    [queryLabel setHidden:isKeyword];
742    [queryTypePopup setHidden:isKeyword];
743    [queryErrorTextField setHidden:isKeyword];
744    [queryTextField setHidden:isKeyword];
745    //
746    [self setTextViewEditable:requestTextView];
747    [self setTextViewEditable:cssTextView];
748    [self setTextViewEditable:jsTextView];
749    [self setTextViewEditable:transformTextView];
750    [self setTextViewEditable:expansionTextView];
751    //
752    if (isKeyword)
753        return [self validateKeyword:sender tabIndex:&tabIndex];
754    [self makeTabItem:expansionTabViewItem visible:NO atIndex:&tabIndex];
755    //
756    [keywordErrorLocationImageView setHidden:YES];
757    [self setTextFieldFocus:hostTextField forPopup:hostTypePopup forSender:sender];
758    [self setTextFieldFocus:pathTextField forPopup:pathTypePopup forSender:sender];
759    [self setTextFieldFocus:queryTextField forPopup:queryTypePopup forSender:sender];
760    if (sender == actionMatrix) {
761        NSView* dst;
762        if ([hostTextField isEnabled])
763            dst = hostTextField;
764        else if ([pathTextField isEnabled])
765            dst = pathTextField;
766        else if ([queryTextField isEnabled])
767            dst = queryTextField;
768        else
769            dst = commentsTextView;
770        [ruleSheet performSelectorOnMainThread:@selector(makeFirstResponder:)
771                                    withObject:dst
772                                 waitUntilDone:NO];
773    }
774    //--------------------------------------------------------------------------------
775    BOOL hostPathQueryAreValid = [self validateHostPathQuery];
776    BOOL valid = _editable && hostPathQueryAreValid;
777    //--------------------------------------------------------------------------------
778    BOOL hasRequestScript = !![_codeEditHelper trimmedTextViewString:requestTextView];
779    BOOL css = isModification && !![_codeEditHelper trimmedTextViewString:cssTextView];
780    BOOL js = isModification && !![_codeEditHelper trimmedTextViewString:jsTextView];
781    BOOL transform = isModification && !![_codeEditHelper trimmedTextViewString:transformTextView];
782    [self setTabItem:requestTabViewItem withLabelDot:isRequest && hasRequestScript];
783    [self setTabItem:cssTabViewItem withLabelDot:!!css];
784    [self setTabItem:jsTabViewItem withLabelDot:!!js];
785    [self setTabItem:transformTabViewItem withLabelDot:!!transform];
786    //--------------------------------------------------------------------------------
787    [globalErrorTextView setStringValue:(isRequest && !hasRequestScript) ? @"Missing script in 'request' tab" : @""];
788    valid &= !isRequest || hasRequestScript;
789    //--------------------------------------------------------------------------------
790    [saveBtn setEnabled:valid];
791    [previewBtn setEnabled:valid && ![_previewedData isEqualToData:[self previewDataForCurrentSettings]]];
792    //--------------------------------------------------------------------------------
793    return valid;
794}
795
796- (void)textDidChange:(NSNotification *)aNotification
797{
798#ifdef DEBUG_TEXT_CHANGE
799    DebugNSLog(@"textDidChange: %@", aNotification);
800#endif
801    [self updateDependentStates:self];
802}
803
804- (void)controlTextDidChange:(NSNotification *)aNotification
805{
806#ifdef DEBUG_TEXT_CHANGE
807    DebugNSLog(@"controlTextDidChange: %@", aNotification);
808#endif
809    [self updateDependentStates:self];
810}
811
812- (void)tabView:(NSTabView *)tabView didSelectTabViewItem:(NSTabViewItem *)tabViewItem
813{
814#ifdef DEBUG_TEXT_CHANGE
815    DebugNSLog(@"didSelectTabViewItem: %@", tabViewItem);
816#endif
817    [self updateDependentStates:self];
818    [_codeEditHelper delayedSyntaxHighlightJavascript:[requestTextView textStorage]];
819    [_codeEditHelper delayedSyntaxHighlightJavascript:[jsTextView textStorage]];
820    [_codeEditHelper delayedSyntaxHighlightJavascript:[transformTextView textStorage]];
821}
822
823#pragma mark --------------------------- URL paste
824
825- (BOOL)handlePasteUrl:(NSString*)txt withTextField:(NSTextField*)textField
826{
827    BOOL isHostField = (textField == hostTextField);
828    BOOL isPathField = (textField == pathTextField);
829    if (!isHostField && !isPathField)
830        return NO;
831    if ([Xml getHttpUrlSyntaxError:txt])
832        return NO;
833    NSURL* url;
834    @try {
835        url = [NSURL URLWithString:txt];
836    }
837    @catch (NSException* ex) {
838        return NO;
839    }
840    NSString* host = [url host];
841    if (![host length])
842        return NO;
843    if (isHostField)
844        [hostTextField setStringValue:host];
845    NSString* path = [url path];
846    BOOL needValidate = NO;
847    if ([path length] && ![path isEqual:@"/"] && (isPathField || ![[pathTextField stringValue] length])) {
848        if (![pathTypePopup indexOfSelectedItem])
849            [pathTypePopup selectItemAtIndex:1];
850        [pathTextField setEnabled:YES];
851        [pathTextField setStringValue:path];
852        needValidate = YES;
853        if (isHostField) {
854            [ruleSheet performSelectorOnMainThread:@selector(makeFirstResponder:)
855                                        withObject:pathTextField
856                                     waitUntilDone:NO];
857        }
858    } else {
859        [self updateDependentStates:self];
860    }
861    NSString* query = [url query];
862    if ([query length] && ![[queryTextField stringValue] length])
863        [queryTextField setStringValue:query];
864    if (needValidate)
865        [self updateDependentStates:self];
866    return YES;
867}
868
869#pragma mark --------------------------- prettyprænt
870
871- (void)syntaxHighlightRegExp:(NSMutableAttributedString*)attributedString
872{
873    [_codeEditHelper syntaxHighlight:attributedString isJavascript:NO];
874}
875
876- (BOOL)isJavascriptTextStorage:(NSTextStorage*)storage
877{
878    return storage != [cssTextView textStorage];
879}
880
881- (void)textStorageDidProcessEditing:(NSNotification *)noti
882{
883    [_codeEditHelper textStorageDidProcessEditing:noti];
884}
885
886- (IBAction)requestHelpAction:(id)sender
887{
888    NSURL* url = [NSURL URLWithString:@"http://glimmerblocker.org/wiki/ModifyRequest"];
889    [[NSWorkspace sharedWorkspace] openURL:url];
890}
891
892- (IBAction)transformHelpAction:(id)sender
893{
894    NSURL* url = [NSURL URLWithString:@"http://glimmerblocker.org/wiki/Transform2"];
895    [[NSWorkspace sharedWorkspace] openURL:url];
896}
897
898- (IBAction)expansionHelpAction:(id)sender
899{
900    NSURL* url = [NSURL URLWithString:@"http://glimmerblocker.org/wiki/KeywordExpansion"];
901    [[NSWorkspace sharedWorkspace] openURL:url];
902}
903
904#pragma mark --------------------------- committing etc.
905
906- (void)saveSelectedTabInPrefs
907{
908    NSTabViewItem* tab = [tabView selectedTabViewItem];
909    if (!tab)
910        return;
911    [_prefs setString:[tab identifier] forXPath:@"iu/rule/@selected-tab"];
912    [_prefs markAsDirty];
913}
914
915- (void)dismissSheet
916{
917    [ruleSheet saveFrameUsingName:@"FilterEditSheet"];
918    [NSApp endSheet:ruleSheet];
919    [ruleSheet orderOut:self];
920}
921
922- (IBAction)cancelAction:(id)sender
923{
924    if (!_virgin)
925        [self saveSelectedTabInPrefs];
926    [self dismissSheet];
927    if ([_prefs zapRulePreviewElement])
928        [_comm saveDirtyPrefsAndNotify];
929    [self autorelease];
930}
931
932- (NSXMLElement*)storeCDataText:(NSString*)txt withName:(NSString*)name inElement:(NSXMLElement*)parentE
933{
934    if (![txt length])
935        return NULL;
936    NSXMLElement* e = [Xml createElementWithName:name];
937    [parentE addChild:e];
938    NSXMLNode* cdata = [[NSXMLNode alloc] initWithKind:NSXMLTextKind options:NSXMLNodeIsCDATA];
939    [cdata setStringValue:txt];
940    [e addChild:cdata];
941    [cdata release];
942    return e;
943}
944
945- (void)saveTextField:(NSTextField*)textField
946             attrName:(NSString*)attrName
947             withType:(NSString*)type
948         typeAttrName:(NSString*)typeAttrName
949            inElement:(NSXMLElement*)e
950{
951    NSCharacterSet* ws = [NSCharacterSet whitespaceAndNewlineCharacterSet];
952    NSString* value = [[textField stringValue] stringByTrimmingCharactersInSet:ws];
953    if (!type || ![value length])
954        type = value = NULL;
955    [Xml setAttribute:value withName:attrName inElement:e];
956    [Xml setAttribute:type withName:typeAttrName inElement:e];
957}
958
959- (void)saveHostPathQueryInRuleElement:(NSXMLElement*)ruleE
960{
961    [self saveTextField:hostTextField
962               attrName:@"host"
963               withType:[hostTypeHelper selectedPopValue:hostTypePopup]
964           typeAttrName:@"host-type"
965              inElement:ruleE];
966    [self saveTextField:pathTextField
967               attrName:@"path"
968               withType:[pathTypeHelper selectedPopValue:pathTypePopup]
969           typeAttrName:@"path-type"
970              inElement:ruleE];
971    [self saveTextField:queryTextField
972               attrName:@"query"
973               withType:[queryTypeHelper selectedPopValue:queryTypePopup]
974           typeAttrName:@"query-type"
975              inElement:ruleE];
976}
977
978- (void)saveSettingsInRuleElement:(NSXMLElement*)ruleE
979{
980    [Xml zapChildsAndAttributes:ruleE];
981    //
982    [Xml setBool:![enableCB state] withName:@"disabled" inElement:ruleE];
983    [Xml setBool:NO withName:@"enabled" inElement:ruleE]; // old style.
984    [Xml setBool:[mobileSafariBtn state] withName:@"mobile" inElement:ruleE];
985    [Xml setAttribute:[NSString stringWithFormat:@"%d", [priorityPopup indexOfSelectedItem] + 1] withName:@"priority" inElement:ruleE];
986    NSInteger action = [actionMatrix selectedRow];
987    BOOL isWhitelist = (action == ACTION_WHITELIST);
988    BOOL isRequest = (action == ACTION_REQUEST);
989    BOOL isKeyword = (action == ACTION_KEYWORD);
990    //--------------------------------------------------------------------------------
991    [self storeCDataText:[_codeEditHelper trimmedTextViewString:commentsTextView]
992                withName:@"comments"
993               inElement:ruleE];
994    if (isKeyword) {
995        [Xml setAttribute:@"keyword" withName:@"type" inElement:ruleE];
996        [self saveTextField:keywordTextField
997                   attrName:@"keyword"
998                   withType:[keywordTypeHelper selectedPopValue:keywordTypePopup]
999               typeAttrName:@"keyword-type"
1000                  inElement:ruleE];
1001        NSXMLElement* keywordE = [self storeCDataText:[_codeEditHelper trimmedTextViewString:expansionTextView]
1002                                             withName:@"keyword"
1003                                            inElement:ruleE];
1004        if (keywordE) {
1005            [Xml setAttribute:@"js" withName:@"language" inElement:keywordE];
1006            [Xml setAttribute:@"1" withName:@"version" inElement:keywordE];
1007        } else {
1008            keywordE = [Xml createElementWithName:@"keyword"];
1009            [ruleE addChild:keywordE];
1010        }
1011        [Xml setBool:([keywordTypeMatrix selectedRow] ? NO : YES) withName:@"keyword-uses-js" inElement:keywordE];
1012        [Xml setAttribute:[keywordUrlField stringValue] withName:@"keyword-url" inElement:keywordE];
1013    } else if (isRequest) {
1014        [Xml setAttribute:@"request" withName:@"type" inElement:ruleE];
1015        [self saveHostPathQueryInRuleElement:ruleE];
1016        NSXMLElement* requestE = [self storeCDataText:[_codeEditHelper trimmedTextViewString:requestTextView]
1017                                             withName:@"request"
1018                                            inElement:ruleE];
1019        if (requestE) {
1020            [Xml setAttribute:@"js" withName:@"language" inElement:requestE];
1021            [Xml setAttribute:@"1" withName:@"version" inElement:requestE];
1022        }
1023    } else {
1024        [self saveHostPathQueryInRuleElement:ruleE];
1025        //--------------------------------------------------------------------------------
1026        //
1027        NSXMLElement* cssE = [self storeCDataText:[_codeEditHelper trimmedTextViewString:cssTextView]
1028                                         withName:@"css"
1029                                        inElement:ruleE];
1030        //
1031        NSXMLElement* jsE = [self storeCDataText:[_codeEditHelper trimmedTextViewString:jsTextView]
1032                                        withName:@"js"
1033                                       inElement:ruleE];
1034        if (jsE) {
1035            [Xml setAttribute:[jsPlacementHelper selectedPopValue:jsPlacementPop]
1036                     withName:@"placement"
1037                    inElement:jsE];
1038        }
1039        //
1040        NSXMLElement* transformE = [self storeCDataText:[_codeEditHelper trimmedTextViewString:transformTextView]
1041                                               withName:@"transform"
1042                                              inElement:ruleE];
1043        if (transformE) {
1044            [Xml setAttribute:@"js" withName:@"language" inElement:transformE];
1045            [Xml setAttribute:@"1" withName:@"version" inElement:transformE];
1046            NSString* ct = [transformContentTypeHelper selectedPopValue:transformContentTypePop];
1047            [Xml setAttribute:ct withName:@"content-type" inElement:transformE];
1048        }
1049        //
1050        NSString* type;
1051        if (!isWhitelist)
1052            type = @"block";
1053        else if (cssE || jsE || transformE)
1054            type = @"modify";
1055        else
1056            type = @"whitelist";
1057        [Xml setAttribute:type withName:@"type" inElement:ruleE];
1058        // @whitelist is obsolete attribute, but keep for backwards compability with 1.1
1059        [Xml setBool:isWhitelist withName:@"whitelist" inElement:ruleE];
1060    }
1061    //--------------------------------------------------------------------------------
1062    [Xml setAttribute:NULL withName:@"comments" inElement:ruleE]; // obsolete.
1063    [Xml setAttribute:NULL withName:@"path-starts-with" inElement:ruleE]; // obsolete.
1064    [Xml setAttribute:NULL withName:@"path-ends-with" inElement:ruleE]; // obsolete.
1065    [Xml setAttribute:NULL withName:@"path-regexp" inElement:ruleE]; // obsolete.
1066}
1067
1068- (IBAction)previewAction:(id)sender
1069{
1070    if (![self validate:sender])
1071        return;
1072    NSXMLElement* rulePreviewE = [_prefs rulePreviewE];
1073    [Xml zapChildsAndAttributes:rulePreviewE];
1074    NSXMLElement* ruleE;
1075    NSData* data;
1076    [self getPreviewData:&data ruleElementRef:&ruleE];
1077    [_previewedData release];
1078    _previewedData = [data retain];
1079    [rulePreviewE addChild:ruleE];
1080    if (_ownerFilter)
1081        [Xml setInt:[_ownerFilter filterId] withName:@"for-filter-id" inElement:rulePreviewE];
1082    [_prefs markAsDirty];
1083    [_comm saveDirtyPrefsAndNotify];
1084    [self validate:self];
1085}
1086
1087- (void)commitRuleToFilter:(NSInteger)idx wantsNewFilter:(BOOL)wantsNewFilter
1088{
1089    Filter* newOwnerFilter;
1090    if (wantsNewFilter)
1091        newOwnerFilter = [_filterTabController createNewUntitledFilter];
1092    else
1093        newOwnerFilter = [_filterArray objectAtIndex:idx];
1094    //DebugNSLog(@"newOwnerFilter = %@", [newOwnerFilter name]);
1095    if (newOwnerFilter != _ownerFilter) {
1096        [_ruleE detach];
1097        [_ownerFilter notifyDirty];
1098        [_ownerFilter release];
1099        [Xml setAttribute:NULL withName:@"rule-id" inElement:_ruleE]; // current rule-id might not be unique in new filter.
1100        _ownerFilter = NULL;
1101        _ownerFilter = [newOwnerFilter retain];
1102        [_prefs setInt:[_ownerFilter filterId] forXPath:@"iu/rule/@default-filter"];
1103    }
1104    if (![_ruleE parent])
1105        [[_ownerFilter filterDataE] addChild:_ruleE];
1106    [FilterRuleEdit getRuleId:_ruleE]; // ensure it has a rule-id
1107}
1108
1109- (IBAction)commitAction:(id)sender
1110{
1111    NSAssert(!(_virgin && [_ruleE parent]), @"virgin with parent");
1112    if (![self validate:sender])
1113        return;
1114    //
1115    NSInteger idx = [filterPopupBtn indexOfSelectedItem];
1116    NSAssert(idx >= 0, @"Missing filter in pop");
1117    BOOL wantsNewFilter = idx >= (int)[_filterArray count];
1118    BOOL filterUpdated = wantsNewFilter || idx != (int)[_filterArray indexOfObject:_ownerFilter];
1119    //
1120    [_prefs zapRulePreviewElement];
1121    NSData* oldContents = _virgin || filterUpdated ? NULL : [Xml subtreeToData:_ruleE];
1122    [self saveSettingsInRuleElement:_ruleE];
1123    BOOL changed = ![oldContents isEqualToData:[Xml subtreeToData:_ruleE]];
1124    DebugNSLog(changed ? @"Rule changed." : @"Rule NOT changed.");
1125    if (changed)
1126        [self commitRuleToFilter:idx wantsNewFilter:wantsNewFilter];
1127    [self saveSelectedTabInPrefs];
1128    [self dismissSheet];
1129    if (changed)
1130        [_filterTabController ruleEditCommit:_ruleE withFilter:_ownerFilter isVirgin:_virgin];
1131    else
1132        [_prefs saveIfDirty:self];
1133    [self autorelease];
1134}
1135
1136- (void)selectTextView:(NSView*)view
1137{
1138    if ([view isKindOfClass:[NSTextField class]])
1139        [view setFocusRingType:NSFocusRingTypeExterior]; // NSTextView/NSScrollView can't display focus ring.
1140    [ruleSheet makeFirstResponder:view];
1141}
1142
1143- (BOOL)handleTab:(NSView *)control delta:(int)delta
1144{
1145    NSMutableArray* a = [NSMutableArray array];
1146    if ([actionMatrix selectedRow] == ACTION_KEYWORD) {
1147        [a addObject:keywordTextField];
1148        if ([keywordTypeMatrix selectedRow])
1149            [a addObject:keywordUrlField];
1150        DebugNSLog(@"handleTab.kw: %@", a);
1151    } else {
1152        if ([hostTextField isEnabled])
1153            [a addObject:hostTextField];
1154        if ([pathTextField isEnabled])
1155            [a addObject:pathTextField];
1156        if ([queryTextField isEnabled])
1157            [a addObject:queryTextField];
1158    }
1159    [a addObject:commentsTextView];
1160    if ([a count] <= 1)
1161        return YES;
1162    NSInteger current = [a indexOfObject:control];
1163    NSInteger idx;
1164    if (current == NSNotFound) {
1165        DebugNSLog(@"Did not find current tab-view in available views.");
1166        idx = 0;
1167    } else {
1168        idx = (current + [a count] + delta) % [a count];
1169    }
1170    [control setFocusRingType:NSFocusRingTypeNone];
1171    if ([control isKindOfClass:[NSTextField class]]) {
1172        NSText* t = [(NSTextField*)control currentEditor];
1173        [t setSelectedRange:NSMakeRange(0, 0)];
1174    }
1175    [self performSelectorOnMainThread:@selector(selectTextView:)
1176                           withObject:[a objectAtIndex:idx]
1177                        waitUntilDone:NO];
1178    return YES;
1179}
1180
1181- (BOOL)textView:(NSTextView*)aTextView doCommandBySelector:(SEL)aSelector
1182{
1183#ifdef DEBUG_DCBS
1184    DebugNSLog(@"doCommandBySelector: %s", aSelector);
1185#endif
1186    if (aTextView == commentsTextView) {
1187        if (aSelector == @selector(insertNewline:)) {
1188            [commentsTextView insertNewlineIgnoringFieldEditor:self];
1189            return YES; // Yes, I did handle it.
1190        }
1191        if (aSelector == @selector(insertTab:))
1192            return [self handleTab:commentsTextView delta:1];
1193        if (aSelector == @selector(insertBacktab:))
1194            return [self handleTab:commentsTextView delta:-1];
1195    }
1196    BOOL newlineSelector = (aSelector == @selector(insertNewline:));
1197    // I can't find noop: anywhere, so I don't know what the expected return type is.
1198    BOOL noopSelector = [NSStringFromSelector(aSelector) isEqual:@"noop:"];
1199    if (!newlineSelector && !noopSelector)
1200        return NO; // No, I did not handle it.
1201    NSEvent* event = [NSApp currentEvent];
1202    if ([event type] != NSKeyDown || [event isARepeat])
1203        return NO; // No, I did not handle it.
1204#ifdef DEBUG_DCBS
1205    DebugNSLog(@" current keydown event: %@", [NSApp currentEvent]);
1206#endif
1207    NSUInteger modifiers = [event modifierFlags] & NSDeviceIndependentModifierFlagsMask;
1208    NSString* s = [event charactersIgnoringModifiers];
1209    BOOL enter = newlineSelector && !modifiers && [s length] == 1 && [s characterAtIndex:0] == 3;
1210    BOOL appleReturn = noopSelector && (modifiers == NSCommandKeyMask) && [s length] == 1 && [s characterAtIndex:0] == 13;
1211#ifdef DEBUG_DCBS
1212    DebugNSLog(@"KeyDown(%d = %x) for %d, enter = %d, appleReturn = %d", modifiers, modifiers, [s characterAtIndex:0], enter, appleReturn);
1213#endif
1214    if (!enter && !appleReturn)
1215        return NO; // No, I did not handle it.
1216#ifdef DEBUG_DCBS
1217    DebugNSLog(@"KeyDown executes self.commitAction:");
1218#endif
1219    [saveBtn highlight:YES];
1220    [self performSelectorOnMainThread:@selector(commitAction:)
1221                           withObject:self
1222                        waitUntilDone:NO];
1223    return YES; // Yes, I did handle it.
1224}
1225
1226- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
1227{
1228#ifdef DEBUG_DCBS
1229    DebugNSLog(@"control.textView.dCBS: %@  for %@", NSStringFromSelector(command), (control == hostTextField ? @"host" : @"?"));
1230#endif
1231    // dunno why tab doesn't work. Hack so it does.
1232    if (command == @selector(insertTab:))
1233        return [self handleTab:control delta:1];
1234    if (command == @selector(insertBacktab:))
1235        return [self handleTab:control delta:-1];
1236    if (command == @selector(insertNewline:) && [saveBtn isEnabled]) {
1237        // dunno why this doesn't work the first time return is hit while being in the host field.
1238        [saveBtn highlight:YES];
1239        [self performSelectorOnMainThread:@selector(commitAction:)
1240                               withObject:self
1241                            waitUntilDone:NO];
1242        return YES;
1243    }
1244    return NO;
1245}
1246
1247- (id)windowWillReturnFieldEditor:(NSWindow *)window toObject:(id)anObject
1248{
1249    if (![anObject isKindOfClass:[FilterRuleTextField class]])
1250        return NULL;
1251    //DebugNSLog(@"windowWillReturnFieldEditor:  %s", object_getClassName(anObject));
1252    if (!_fieldEditor)
1253        _fieldEditor = [[FilterRuleFieldEditor alloc] initWithFrame:NSMakeRect(0, 0, 1, 1)];
1254    return _fieldEditor;
1255}
1256
1257+ (int)getRuleId:(NSXMLElement*)ruleE
1258{
1259    int ruleId = [Xml getIntAttribute:ruleE withName:@"rule-id"];
1260    if (!ruleId) {
1261        ruleId = [Xml makeUniqueIdWithRoot:(NSXMLElement*)[ruleE parent] withAttributeName:@"rule-id"];
1262        [Xml setInt:ruleId withName:@"rule-id" inElement:ruleE];
1263    }
1264    return ruleId;
1265}
1266
1267#ifdef DEBUG_RESPONDS_TO_SELECTOR
1268- (BOOL)respondsToSelector:(SEL)aSelector
1269{
1270    BOOL rts = [super respondsToSelector:aSelector];
1271    DebugNSLog(@"FilterRuleEdit, %@ respondsToSelector  %s", (rts ? @"does   " : @"does not"), aSelector);
1272    return rts;
1273}
1274#endif
1275
1276@end
1277
Note: See TracBrowser for help on using the repository browser.