Skip to content

Commit c42e19a

Browse files
committed
#63: * Fix filter rule editor not working on 10.6 (caused by Apple’s abysmally vague documentation on NSRuleEditor)
* Also fix filter dropdown not updating after filter definitions have been changed
1 parent 38b9659 commit c42e19a

File tree

2 files changed

+165
-18
lines changed

2 files changed

+165
-18
lines changed

Source/SPRuleFilterController.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ NSString * const SPRuleFilterHeightChangedNotification;
5757
SEL action;
5858

5959
BOOL enabled;
60+
61+
NSUInteger opNodeCacheVersion;
6062
}
6163

6264
/**

Source/SPRuleFilterController.m

Lines changed: 163 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,12 @@ @interface ColumnNode : RuleNode {
120120
NSString *name;
121121
NSString *typegrouping;
122122
NSArray *operatorCache;
123+
NSUInteger opCacheVersion;
123124
}
124125
@property(copy, nonatomic) NSString *name;
125126
@property(copy, nonatomic) NSString *typegrouping;
126127
@property(retain, nonatomic) NSArray *operatorCache;
128+
@property(assign, nonatomic) NSUInteger opCacheVersion;
127129
@end
128130

129131
@interface StringNode : RuleNode {
@@ -221,6 +223,10 @@ - (BOOL)_focusOnFieldInSubtree:(NSDictionary *)dict;
221223
- (void)_resize;
222224
- (void)openContentFilterManagerForFilterType:(NSString *)filterType;
223225
- (IBAction)filterTable:(id)sender;
226+
- (IBAction)_menuItemInRuleEditorClicked:(id)sender;
227+
- (void)_pretendPlayRuleEditorForCriteria:(NSMutableArray *)criteria displayValues:(NSMutableArray *)displayValues inRow:(NSInteger)row;
228+
- (void)_ensureValidOperatorCache:(ColumnNode *)col;
229+
static BOOL _arrayContainsInViewHierarchy(NSArray *haystack, id needle);
224230

225231
@end
226232

@@ -238,6 +244,7 @@ - (instancetype)init
238244
preferredHeight = 0.0;
239245
target = nil;
240246
action = NULL;
247+
opNodeCacheVersion = 1;
241248

242249
// Init default filters for Content Browser
243250
contentFilters = [[NSMutableDictionary alloc] init];
@@ -357,10 +364,7 @@ - (NSInteger)ruleEditor:(NSRuleEditor *)editor numberOfChildrenForCriterion:(nul
357364
RuleNodeType type = [(RuleNode *)criterion type];
358365
if(type == RuleNodeTypeColumn) {
359366
ColumnNode *node = (ColumnNode *)criterion;
360-
if(![node operatorCache]) {
361-
NSArray *ops = [self _compareTypesForColumn:node];
362-
[node setOperatorCache:ops];
363-
}
367+
[self _ensureValidOperatorCache:node];
364368
return [[node operatorCache] count];
365369
}
366370
// the first child of an operator is the first argument (if it has one)
@@ -454,23 +458,29 @@ - (id)ruleEditor:(NSRuleEditor *)editor displayValueForCriterion:(id)criterion i
454458
item = [NSMenuItem separatorItem];
455459
}
456460
else {
461+
/* NOTE:
462+
* Apple's doc on NSRuleEditor says that returning NSMenuItems is supported.
463+
* However there seems to be a major discrepancy between what Apple considers "supported" and what any
464+
* sane person would consider supported.
465+
*
466+
* Basically one would expect NSMenuItems to be handled in the same way a number of NSString children of a
467+
* row's element will be handled, but that was not Apples intention. By supported they actually mean
468+
* "Your app won't crash immediately if you return an NSMenuItem here" - but that's about it.
469+
* Even selecting such an NSMenuItem will already cause an exception on 10.6 and be treated as a NOOP on
470+
* later OSes.
471+
* So if we return NSMenuItems we have to implement the full logic of the NSRuleEditor for updating and
472+
* displaying the row ourselves, starting with handling the target/action of the NSMenuItems!
473+
*/
457474
item = [[NSMenuItem alloc] initWithTitle:[[node settings] objectForKey:@"title"] action:NULL keyEquivalent:@""];
458475
[item setToolTip:[[node settings] objectForKey:@"tooltip"]];
459476
[item setTag:[[[node settings] objectForKey:@"tag"] integerValue]];
460-
//TODO the following seems to be mentioned exactly nowhere on the internet/in documentation, but without it NSMenuItems won't work properly, even though Apple says they are supported
461477
[item setRepresentedObject:@{
462-
@"item": node,
463-
@"value": [item title],
478+
@"node": node,
464479
// this one is needed by the "Edit filters…" item for context
465480
@"filterType": SPBoxNil([[node settings] objectForKey:@"filterType"]),
466481
}];
467-
// override the default action from the rule editor if given (used to open the edit content filters sheet)
468-
id _target = [[node settings] objectForKey:@"target"];
469-
SEL _action = (SEL)[(NSValue *)[[node settings] objectForKey:@"action"] pointerValue];
470-
if(_target && _action) {
471-
[item setTarget:_target];
472-
[item setAction:_action];
473-
}
482+
[item setTarget:self];
483+
[item setAction:@selector(_menuItemInRuleEditorClicked:)];
474484
[item autorelease];
475485
}
476486
return item;
@@ -512,6 +522,131 @@ - (IBAction)_textFieldAction:(id)sender
512522
}
513523
}
514524

525+
- (IBAction)_menuItemInRuleEditorClicked:(id)sender
526+
{
527+
if(!sender) return; // NSRuleEditor will throw on nil
528+
529+
NSInteger row = [filterRuleEditor rowForDisplayValue:sender];
530+
531+
if(row == NSNotFound) return; // unknown display values
532+
533+
OpNode *node = [[(NSMenuItem *)sender representedObject] objectForKey:@"node"];
534+
535+
// if the row has an explicit handler, pass on the action and do nothing
536+
id _target = [[node settings] objectForKey:@"target"];
537+
SEL _action = (SEL)[(NSValue *)[[node settings] objectForKey:@"action"] pointerValue];
538+
if(_target && _action) {
539+
[_target performSelector:_action withObject:sender];
540+
return;
541+
}
542+
543+
/* now comes the painful part, where we'd have to find out where exactly in the row this
544+
* displayValue should appear.
545+
*
546+
* Luckily we know that this method will only be invoked by the displayValues of OpNode
547+
* and currently OpNode can only appear as the second node in a row (after the column).
548+
*
549+
* Annoyingly we can't tell the rule editor to just replace a single element. We actually
550+
* have to recalculate the whole row starting with the element we replaced - a task the
551+
* rule editor would normally do for us when using NSStrings!
552+
*/
553+
NSMutableArray *criteria = [[filterRuleEditor criteriaForRow:row] mutableCopy];
554+
NSMutableArray *displayValues = [[filterRuleEditor displayValuesForRow:row] mutableCopy];
555+
556+
// find the position of the previous opnode (just for safety)
557+
NSUInteger opIndex = NSNotFound;
558+
NSUInteger i = 0;
559+
for(RuleNode *obj in criteria) {
560+
if([obj type] == RuleNodeTypeOperator) {
561+
opIndex = i;
562+
break;
563+
}
564+
i++;
565+
}
566+
567+
if(opIndex < [criteria count]) {
568+
// yet another uglyness: if one of the displayValues is an input and currently the first responder
569+
// we have to manually restore that for the new input we create for UX reasons.
570+
// However an NSTextField is seldom a first responder, usually it's an invisible subview of the text field...
571+
id firstResponder = [[filterRuleEditor window] firstResponder];
572+
BOOL hasFirstResponderInRow = _arrayContainsInViewHierarchy(displayValues, firstResponder);
573+
574+
//remove previous opnode and everything that follows and append new opnode
575+
NSRange stripRange = NSMakeRange(opIndex, ([criteria count] - opIndex));
576+
[criteria removeObjectsInRange:stripRange];
577+
[criteria addObject:node];
578+
579+
//remove the display value for the old op node and everything that followed
580+
[displayValues removeObjectsInRange:stripRange];
581+
582+
//now we'll fill in everything again
583+
[self _pretendPlayRuleEditorForCriteria:criteria displayValues:displayValues inRow:row];
584+
585+
//and update the row to its new state
586+
[filterRuleEditor setCriteria:criteria andDisplayValues:displayValues forRowAtIndex:row];
587+
588+
if(hasFirstResponderInRow) {
589+
// make the next possible object after the opnode the new next responder (since the previous one is gone now)
590+
for (NSUInteger j = stripRange.location + 1; j < [displayValues count]; ++j) {
591+
id obj = [displayValues objectAtIndex:j];
592+
if([obj respondsToSelector:@selector(acceptsFirstResponder)] && [obj acceptsFirstResponder]) {
593+
[[filterRuleEditor window] makeFirstResponder:obj];
594+
break;
595+
}
596+
}
597+
}
598+
}
599+
600+
[criteria release];
601+
[displayValues release];
602+
}
603+
604+
BOOL _arrayContainsInViewHierarchy(NSArray *haystack, id needle)
605+
{
606+
//first, try it the easy way
607+
if([haystack indexOfObjectIdenticalTo:needle] != NSNotFound) return YES;
608+
609+
// otherwise, if needle is a view, check if it appears as a desencdant of some other view in haystack
610+
Class NSViewClass = [NSView class];
611+
if([needle isKindOfClass:NSViewClass]) {
612+
for(id obj in haystack) {
613+
if([obj isKindOfClass:NSViewClass] && [needle isDescendantOf:obj]) return YES;
614+
}
615+
}
616+
617+
return NO;
618+
}
619+
620+
/**
621+
* This method recursively fills up the passed-in criteria and displayValues arrays with objects in the way the
622+
* NSRuleEditor would, so they can be used with the -setCriteria:andDisplayValues:forRowAtIndex: call.
623+
*
624+
* Assumptions made:
625+
* - row is a valid row within the bounds of the rule editor
626+
* - criteria contains at least one object
627+
* - displayValues contains exactly one less object than criteria
628+
*/
629+
- (void)_pretendPlayRuleEditorForCriteria:(NSMutableArray *)criteria displayValues:(NSMutableArray *)displayValues inRow:(NSInteger)row
630+
{
631+
id curCriterion = [criteria lastObject];
632+
633+
//first fill in the display value for the current criterion
634+
id display = [self ruleEditor:filterRuleEditor displayValueForCriterion:curCriterion inRow:row];
635+
if(!display) return; // abort if unset
636+
[displayValues addObject:display];
637+
638+
// now let's check if we have to go deeper
639+
NSRuleEditorRowType rowType = [filterRuleEditor rowTypeForRow:row];
640+
if([self ruleEditor:filterRuleEditor numberOfChildrenForCriterion:curCriterion withRowType:rowType]) {
641+
// we only care for the first child, though
642+
id nextCriterion = [self ruleEditor:filterRuleEditor child:0 forCriterion:curCriterion withRowType:rowType];
643+
if(nextCriterion) {
644+
[criteria addObject:nextCriterion];
645+
[self _pretendPlayRuleEditorForCriteria:criteria displayValues:displayValues inRow:row];
646+
}
647+
}
648+
}
649+
515650
- (IBAction)filterTable:(id)sender
516651
{
517652
if(target && action) [target performSelector:action withObject:self];
@@ -764,10 +899,21 @@ - (void)openContentFilterManagerForFilterType:(NSString *)filterType
764899

765900
- (void)_contentFiltersHaveBeenUpdated:(NSNotification *)notification
766901
{
902+
// invalidate our OpNode caches
903+
opNodeCacheVersion++;
767904
//tell the rule editor to reload its criteria
768905
[filterRuleEditor reloadCriteria];
769906
}
770907

908+
- (void)_ensureValidOperatorCache:(ColumnNode *)col
909+
{
910+
if(![col operatorCache] || [col opCacheVersion] != opNodeCacheVersion) {
911+
NSArray *ops = [self _compareTypesForColumn:col];
912+
[col setOperatorCache:ops];
913+
[col setOpCacheVersion:opNodeCacheVersion];
914+
}
915+
}
916+
771917
- (BOOL)isEmpty
772918
{
773919
return ([[_modelContainer model] count] == 0);
@@ -1059,10 +1205,7 @@ - (OpNode *)_operatorNamed:(NSString *)title forColumn:(ColumnNode *)col
10591205
{
10601206
if([title length]) {
10611207
// check if we have the operator cache, otherwise build it
1062-
if(![col operatorCache]) {
1063-
NSArray *ops = [self _compareTypesForColumn:col];
1064-
[col setOperatorCache:ops];
1065-
}
1208+
[self _ensureValidOperatorCache:col];
10661209
// try to find it in the operator cache
10671210
for(OpNode *node in [col operatorCache]) {
10681211
if([[[node filter] objectForKey:@"MenuLabel"] isEqualToString:title]) return node;
@@ -1234,11 +1377,13 @@ @implementation ColumnNode
12341377
@synthesize name = name;
12351378
@synthesize typegrouping = typegrouping;
12361379
@synthesize operatorCache = operatorCache;
1380+
@synthesize opCacheVersion = opCacheVersion;
12371381

12381382
- (instancetype)init
12391383
{
12401384
if((self = [super init])) {
12411385
type = RuleNodeTypeColumn;
1386+
opCacheVersion = 0;
12421387
}
12431388
return self;
12441389
}

0 commit comments

Comments
 (0)