@@ -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