Skip to content

Commit

Permalink
Preserve input values when switching the content filter criterion (#3306
Browse files Browse the repository at this point in the history
)
  • Loading branch information
dmoagx committed Jun 15, 2019
1 parent 1ae3a5e commit 7966e25
Showing 1 changed file with 117 additions and 34 deletions.
151 changes: 117 additions & 34 deletions Source/SPRuleFilterController.m
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -564,64 +589,76 @@ - (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!
*/
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];
Expand Down Expand Up @@ -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];
}
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1420,6 +1485,11 @@ - (BOOL)isEqual:(id)other {
return NO;
}

- (BOOL)isViableReplacementFor:(RuleNode *)other
{
return [other type] == type;
}

@end

@implementation ColumnNode
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 7966e25

Please sign in to comment.