diff --git a/Source/SPRuleFilterController.m b/Source/SPRuleFilterController.m index 0e97bb31f..7c100b0fe 100644 --- a/Source/SPRuleFilterController.m +++ b/Source/SPRuleFilterController.m @@ -114,6 +114,11 @@ @interface RuleNode : NSObject { RuleNodeType type; } @property(assign, nonatomic) RuleNodeType type; +/** + * This method checks if a node other can take the place of self in a filter row. + * The RuleNode implementation checks only that both nodes are of the same type. + */ +- (BOOL)isViableReplacementFor:(RuleNode *)other; @end @interface ColumnNode : RuleNode { @@ -144,6 +149,10 @@ @interface OpNode : RuleNode { @property (assign, nonatomic) ColumnNode *parentColumn; @property (retain, nonatomic) NSDictionary *settings; @property (retain, nonatomic) NSDictionary *filter; +/** + * This method is only a shortcut to `-[[node filter] objectForKey:@"MenuLabel"]` + */ +- (NSString *)name; @end @interface ArgNode : RuleNode { @@ -225,7 +234,11 @@ - (void)openContentFilterManagerForFilterType:(NSString *)filterType; - (IBAction)filterTable:(id)sender; - (IBAction)resetFilter:(id)sender; - (IBAction)_menuItemInRuleEditorClicked:(id)sender; -- (void)_pretendPlayRuleEditorForCriteria:(NSMutableArray *)criteria displayValues:(NSMutableArray *)displayValues inRow:(NSInteger)row; +- (void)_pretendPlayRuleEditorForCriteria:(NSMutableArray *)criteria + displayValues:(NSMutableArray *)displayValues + inRow:(NSInteger)row + tryingToPreserveOldCriteria:(NSArray *)oldCriteria + displayValues:(NSArray *)oldDisplayValues; - (void)_ensureValidOperatorCache:(ColumnNode *)col; static BOOL _arrayContainsInViewHierarchy(NSArray *haystack, id needle); @@ -451,7 +464,19 @@ - (id)ruleEditor:(NSRuleEditor *)editor displayValueForCriterion:(id)criterion i { switch([(RuleNode *)criterion type]) { case RuleNodeTypeString: return [(StringNode *)criterion value]; - case RuleNodeTypeColumn: return [(ColumnNode *)criterion name]; + case RuleNodeTypeColumn: { + /* + * We could also return a string here, but we want a hook into the selection process so we can preserve + * the other values in a row when a user changes the column (also see comment below) + */ + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:[(ColumnNode *)criterion name] action:NULL keyEquivalent:@""]; + [item setRepresentedObject:@{ + @"node": criterion, + }]; + [item setTarget:self]; + [item setAction:@selector(_menuItemInRuleEditorClicked:)]; + return [item autorelease]; + } case RuleNodeTypeOperator: { OpNode *node = (OpNode *)criterion; NSMenuItem *item; @@ -564,22 +589,22 @@ - (IBAction)_menuItemInRuleEditorClicked:(id)sender if(row == NSNotFound) return; // unknown display values - OpNode *node = [[(NSMenuItem *)sender representedObject] objectForKey:@"node"]; + RuleNode *criterion = [[(NSMenuItem *)sender representedObject] objectForKey:@"node"]; - // if the row has an explicit handler, pass on the action and do nothing - id _target = [[node settings] objectForKey:@"target"]; - SEL _action = (SEL)[(NSValue *)[[node settings] objectForKey:@"action"] pointerValue]; - if(_target && _action) { - [_target performSelector:_action withObject:sender]; - return; - } + if([criterion type] == RuleNodeTypeOperator) { + OpNode *node = (OpNode *)criterion; + // if the row has an explicit handler, pass on the action and do nothing + id _target = [[node settings] objectForKey:@"target"]; + SEL _action = (SEL)[(NSValue *)[[node settings] objectForKey:@"action"] pointerValue]; + if(_target && _action) { + [_target performSelector:_action withObject:sender]; + return; + } + } /* now comes the painful part, where we'd have to find out where exactly in the row this * displayValue should appear. - * - * Luckily we know that this method will only be invoked by the displayValues of OpNode - * and currently OpNode can only appear as the second node in a row (after the column). - * + * * Annoyingly we can't tell the rule editor to just replace a single element. We actually * have to recalculate the whole row starting with the element we replaced - a task the * rule editor would normally do for us when using NSStrings! @@ -587,41 +612,53 @@ - (IBAction)_menuItemInRuleEditorClicked:(id)sender NSMutableArray *criteria = [[filterRuleEditor criteriaForRow:row] mutableCopy]; NSMutableArray *displayValues = [[filterRuleEditor displayValuesForRow:row] mutableCopy]; - // find the position of the previous opnode (just for safety) - NSUInteger opIndex = NSNotFound; + // find the position of the previous node (just for safety) + NSUInteger nodeIndex = NSNotFound; NSUInteger i = 0; for(RuleNode *obj in criteria) { - if([obj type] == RuleNodeTypeOperator) { - opIndex = i; + if([obj isViableReplacementFor:criterion]) { + nodeIndex = i; break; } i++; } - if(opIndex < [criteria count]) { + if(nodeIndex < [criteria count]) { // yet another uglyness: if one of the displayValues is an input and currently the first responder // we have to manually restore that for the new input we create for UX reasons. // However an NSTextField is seldom a first responder, usually it's an invisible subview of the text field... id firstResponder = [[filterRuleEditor window] firstResponder]; BOOL hasFirstResponderInRow = _arrayContainsInViewHierarchy(displayValues, firstResponder); - //remove previous opnode and everything that follows and append new opnode - NSRange stripRange = NSMakeRange(opIndex, ([criteria count] - opIndex)); + //remove previous node and everything that follows and append new node + NSRange stripRange = NSMakeRange(nodeIndex, ([criteria count] - nodeIndex)); + + //preserve the old criteria and displayValues, so we can restore the values of input fields if appropriate + NSArray *oldCriteria = [criteria copy]; + NSArray *oldDisplayValues = [displayValues copy]; + [criteria removeObjectsInRange:stripRange]; - [criteria addObject:node]; + [criteria addObject:criterion]; //remove the display value for the old op node and everything that followed [displayValues removeObjectsInRange:stripRange]; //now we'll fill in everything again - [self _pretendPlayRuleEditorForCriteria:criteria displayValues:displayValues inRow:row]; + [self _pretendPlayRuleEditorForCriteria:criteria + displayValues:displayValues + inRow:row + tryingToPreserveOldCriteria:[oldCriteria subarrayWithRange:stripRange] + displayValues:[oldDisplayValues subarrayWithRange:stripRange]]; + + [oldCriteria release]; + [oldDisplayValues release]; //and update the row to its new state [filterRuleEditor setCriteria:criteria andDisplayValues:displayValues forRowAtIndex:row]; if(hasFirstResponderInRow) { // make the next possible object after the opnode the new next responder (since the previous one is gone now) - for (NSUInteger j = stripRange.location + 1; j < [displayValues count]; ++j) { + for (NSUInteger j = nodeIndex + 1; j < [displayValues count]; ++j) { id obj = [displayValues objectAtIndex:j]; if([obj respondsToSelector:@selector(acceptsFirstResponder)] && [obj acceptsFirstResponder]) { [[filterRuleEditor window] makeFirstResponder:obj]; @@ -659,25 +696,53 @@ BOOL _arrayContainsInViewHierarchy(NSArray *haystack, id needle) * - row is a valid row within the bounds of the rule editor * - criteria contains at least one object * - displayValues contains exactly one less object than criteria + * - the first object in oldCriteria is what the last object in criteria replaced + * - all objects in oldDisplayValues correspond to the objects at the same index in oldCriteria */ -- (void)_pretendPlayRuleEditorForCriteria:(NSMutableArray *)criteria displayValues:(NSMutableArray *)displayValues inRow:(NSInteger)row +- (void)_pretendPlayRuleEditorForCriteria:(NSMutableArray *)criteria + displayValues:(NSMutableArray *)displayValues + inRow:(NSInteger)row + tryingToPreserveOldCriteria:(NSArray *)oldCriteria + displayValues:(NSArray *)oldDisplayValues { - id curCriterion = [criteria lastObject]; + RuleNode *curCriterion = [criteria lastObject]; //first fill in the display value for the current criterion id display = [self ruleEditor:filterRuleEditor displayValueForCriterion:curCriterion inRow:row]; if(!display) return; // abort if unset + + // try to restore the value from the previous displayValue for input fields + RuleNode *oldCriterion = [oldCriteria objectOrNilAtIndex:0]; + if([curCriterion type] == RuleNodeTypeArgument && oldCriterion && [curCriterion type] == [oldCriterion type]) { + NSTextField *oldField = [oldDisplayValues objectOrNilAtIndex:0]; + if(oldField) [display setStringValue:[oldField stringValue]]; + } [displayValues addObject:display]; // now let's check if we have to go deeper NSRuleEditorRowType rowType = [filterRuleEditor rowTypeForRow:row]; - if([self ruleEditor:filterRuleEditor numberOfChildrenForCriterion:curCriterion withRowType:rowType]) { - // we only care for the first child, though - id nextCriterion = [self ruleEditor:filterRuleEditor child:0 forCriterion:curCriterion withRowType:rowType]; - if(nextCriterion) { - [criteria addObject:nextCriterion]; - [self _pretendPlayRuleEditorForCriteria:criteria displayValues:displayValues inRow:row]; + if(![self ruleEditor:filterRuleEditor numberOfChildrenForCriterion:curCriterion withRowType:rowType]) return; + + // we only care for the first child, though + id nextCriterion = [self ruleEditor:filterRuleEditor child:0 forCriterion:curCriterion withRowType:rowType]; + if(nextCriterion) { + NSArray *nextOldCriteria = ([oldCriteria count] > 1 ? [oldCriteria subarrayWithRange:NSMakeRange(1, [oldCriteria count] - 1)] : [NSArray array]); + NSArray *nextOldDisplayValues = ([oldDisplayValues count] > 1 ? [oldDisplayValues subarrayWithRange:NSMakeRange(1, [oldDisplayValues count] - 1)] : [NSArray array]); + + // if the user changed the column, try to retain the previously selected operation + RuleNode *nextOldCriterion = [nextOldCriteria objectOrNilAtIndex:0]; + if(nextOldCriterion && [nextOldCriterion type] == RuleNodeTypeOperator && [curCriterion type] == RuleNodeTypeColumn) { + NSString *opName = [(OpNode *)nextOldCriterion name]; + OpNode *op = [self _operatorNamed:opName forColumn:(ColumnNode *)curCriterion]; + if(op) nextCriterion = op; } + [criteria addObject:nextCriterion]; + + [self _pretendPlayRuleEditorForCriteria:criteria + displayValues:displayValues + inRow:row + tryingToPreserveOldCriteria:nextOldCriteria + displayValues:nextOldDisplayValues]; } } @@ -1081,7 +1146,7 @@ - (NSDictionary *)_serializeSubtree:(NSDictionary *)item includingDefinition:(BO SerFilterClass: SerFilterClassExpression, SerFilterExprColumn: [col name], SerFilterExprType: [[op settings] objectForKey:@"filterType"], - SerFilterExprComparison: [[op filter] objectForKey:@"MenuLabel"], + SerFilterExprComparison: [op name], SerFilterExprValues: filterValues, }; if(includeDefinition) { @@ -1258,7 +1323,7 @@ - (OpNode *)_operatorNamed:(NSString *)title forColumn:(ColumnNode *)col [self _ensureValidOperatorCache:col]; // try to find it in the operator cache for(OpNode *node in [col operatorCache]) { - if([[[node filter] objectForKey:@"MenuLabel"] isEqualToString:title]) return node; + if([[node name] isEqualToString:title]) return node; } } return nil; @@ -1420,6 +1485,11 @@ - (BOOL)isEqual:(id)other { return NO; } +- (BOOL)isViableReplacementFor:(RuleNode *)other +{ + return [other type] == type; +} + @end @implementation ColumnNode @@ -1514,6 +1584,15 @@ - (BOOL)isEqual:(id)other { return NO; } +- (BOOL)isViableReplacementFor:(RuleNode *)other { + return [super isViableReplacementFor:other] && [parentColumn isEqual:[(OpNode *)other parentColumn]]; +} + +- (NSString *)name +{ + return [filter objectForKey:@"MenuLabel"]; +} + @end @implementation ArgNode @@ -1550,6 +1629,10 @@ - (BOOL)isEqual:(id)other { return NO; } +- (BOOL)isViableReplacementFor:(RuleNode *)other { + return [super isViableReplacementFor:other] && [(ArgNode *)other argIndex] == argIndex; +} + @end @implementation ConnectorNode