Skip to content
Permalink
Browse files

Add option to enable/disable compound rows in rule filter (#3303)

  • Loading branch information...
dmoagx committed Sep 30, 2019
1 parent 8a37c10 commit f0987c5354dcadf4a13aa5595504977b157158a0
Showing with 242 additions and 26 deletions.
  1. +242 −26 Source/SPRuleFilterController.m
@@ -181,8 +181,10 @@ @interface ConnectorNode : RuleNode {

@interface EnableNode : RuleNode {
BOOL initialState;
BOOL allowsMixedState;
}
@property (assign, nonatomic) BOOL initialState;
@property (assign, nonatomic) BOOL allowsMixedState;
@end

#pragma mark -
@@ -256,6 +258,12 @@ - (void)_ensureValidOperatorCache:(ColumnNode *)col;
- (IBAction)addFilter:(id)sender;
- (void)_updateButtonStates;
- (void)_doChangeToRuleEditorData:(void (^)(void))duringBlock;
- (IBAction)_checkboxClicked:(id)sender;
- (void)_updateCheckedStateUpwardsFromCompoundRow:(NSInteger)row;
- (void)_updateCheckedStateForRow:(NSInteger)row to:(NSCellStateValue)newState;
- (void)_updateCheckedStateDownwardsFromCompoundRow:(NSInteger)row to:(NSCellStateValue)newState;
- (NSCellStateValue)_recalculateCheckboxStatesFromRow:(NSInteger)row;
- (NSCellStateValue)_checkboxStateForRow:(NSInteger)row;
@end

@implementation SPRuleFilterController
@@ -396,14 +404,21 @@ - (void)setColumns:(NSArray *)dataColumns;

- (NSInteger)ruleEditor:(NSRuleEditor *)editor numberOfChildrenForCriterion:(nullable id)criterion withRowType:(NSRuleEditorRowType)rowType
{
// nil criterion is always the first element in a row, compound rows are only for "AND"/"OR" groups
if(!criterion && rowType == NSRuleEditorRowTypeCompound) {
return 2;
}
else if(!criterion && rowType == NSRuleEditorRowTypeSimple) {
return 1; // enable checkbox
// nil criterion is always the first element in a row
if(rowType == NSRuleEditorRowTypeCompound) {
if(!criterion) {
return 1; //enable checkbox
}
RuleNodeType type = [(RuleNode *)criterion type];
// compound rows are only for "AND"/"OR" groups
if(type == RuleNodeTypeEnable) {
return 2;
}
}
else if(rowType == NSRuleEditorRowTypeSimple) {
if(!criterion) {
return 1; // enable checkbox
}
RuleNodeType type = [(RuleNode *)criterion type];
// the children of the enable checkbox are the columns
if(type == RuleNodeTypeEnable) {
@@ -439,20 +454,29 @@ - (NSInteger)ruleEditor:(NSRuleEditor *)editor numberOfChildrenForCriterion:(nul

- (id)ruleEditor:(NSRuleEditor *)editor child:(NSInteger)index forCriterion:(nullable id)criterion withRowType:(NSRuleEditorRowType)rowType
{
// nil criterion is always the first element in a row, compound rows are only for "AND"/"OR" groups
if(!criterion && rowType == NSRuleEditorRowTypeCompound) {
StringNode *node = [[StringNode alloc] init];
switch(index) {
case 0: [node setValue:@"AND"]; break;
case 1: [node setValue:@"OR"]; break;
// nil criterion is always the first element in a row
if(rowType == NSRuleEditorRowTypeCompound) {
if(!criterion) {
EnableNode *node = [[EnableNode alloc] init];
[node setAllowsMixedState:YES];
return [node autorelease];
}
RuleNodeType type = [(RuleNode *) criterion type];
// compound rows are only for "AND"/"OR" groups
if(type == RuleNodeTypeEnable) {
StringNode *node = [[StringNode alloc] init];
switch(index) {
case 0: [node setValue:@"AND"]; break;
case 1: [node setValue:@"OR"]; break;
}
return [node autorelease];
}
return [node autorelease];
}
// this is the enable checkbox
else if(!criterion && rowType == NSRuleEditorRowTypeSimple) {
return [[[EnableNode alloc] init] autorelease];
}
else if(rowType == NSRuleEditorRowTypeSimple) {
// this is the enable checkbox
if(!criterion) {
return [[[EnableNode alloc] init] autorelease];
}
RuleNodeType type = [(RuleNode *) criterion type];
// this is the column field
if (type == RuleNodeTypeEnable) {
@@ -513,7 +537,10 @@ - (id)ruleEditor:(NSRuleEditor *)editor displayValueForCriterion:(id)criterion i
[check setBordered:NO];
[check setImagePosition:NSImageOnly];
[check sizeToFit];
[check setAllowsMixedState:[node allowsMixedState]];
[check setState:([node initialState] ? NSOnState : NSOffState)];
[check setTarget:self];
[check setAction:@selector(_checkboxClicked:)];
return [check autorelease];
}
case RuleNodeTypeColumn: {
@@ -626,6 +653,181 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBy
return NO;
}

- (IBAction)_checkboxClicked:(id)sender
{
NSInteger row = [filterRuleEditor rowForDisplayValue:sender];
NSCellStateValue newState = [(NSButton *)sender state];

// to have -setState: accept mixed state we have to -setAllowsMixedState:YES in which case the user, too, can cycle all three states m(
if(newState == NSMixedState) {
[sender setNextState];
newState = [sender state];
}

if(row >= 0 && (newState == NSOnState || newState == NSOffState)) {
// if we are a compound row, go downwards to update our children
if([filterRuleEditor rowTypeForRow:row] == NSRuleEditorRowTypeCompound) {
[self _updateCheckedStateDownwardsFromCompoundRow:row to:newState];
}
// then go upwards to update the checkbox state of our parent
[self _updateCheckedStateUpwardsFromCompoundRow:[filterRuleEditor parentRowForRow:row]];
}
}

/**
* This method will recursively update the checkbox state of all children rows of the given row index with `newState`.
* `row` itself will not be changed!
*
* @param row
* The row index of a compound row for which to update the children states (can be -1)
* @param newState
* The new state to set for all children rows
*/
- (void)_updateCheckedStateDownwardsFromCompoundRow:(NSInteger)row to:(NSCellStateValue)newState
{
NSIndexSet *subrows = [filterRuleEditor subrowIndexesForRow:row];
[subrows enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
[self _updateCheckedStateForRow:idx to:newState];
// go deeper for compound rows
if([filterRuleEditor rowTypeForRow:idx] == NSRuleEditorRowTypeCompound) {
[self _updateCheckedStateDownwardsFromCompoundRow:idx to:newState];
}
}];
}

/**
* This method will update the checkbox state of the row specified by the given index.
* No other rows will be affected by this call.
*
* @param row
* The row index to update (must be >= 0)
* @param newState
* The new checkbox state to set
*/
- (void)_updateCheckedStateForRow:(NSInteger)row to:(NSCellStateValue)newState
{
NSArray *displayValues = [filterRuleEditor displayValuesForRow:row];
RuleNode *firstCriterion = [[filterRuleEditor criteriaForRow:row] objectAtIndex:0];
if([firstCriterion type] == RuleNodeTypeEnable) {
NSButton *button = [displayValues objectAtIndex:0];
[button setState:newState];
}
}

/**
* This method will recursively update the checkbox state of the given row and all of its parent rows
* to match the aggregate state of all simple rows in the affected subtree.
*
* The rule editor row tree will be walked upwards using `-parentRowForRow:` until a top-level row is encountered
* (parent=-1).
*
* NOTE: This method assumes that all compound child rows of `row` already have a consistent checkbox state!
*
* @param row
* The row index of a compound row (can be `-1`)
*/
- (void)_updateCheckedStateUpwardsFromCompoundRow:(NSInteger)row
{
// stop condition for recursion
if(row < 0) return;

__block NSCellStateValue newState = NSOnState;
__block NSUInteger countOff = 0;
NSIndexSet *subrows = [filterRuleEditor subrowIndexesForRow:row];
[subrows enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
NSCellStateValue subState = [self _checkboxStateForRow:idx];
// mixed is easy: if at least one child is mixed, the parent is mixed, too
if(subState == NSMixedState) {
newState = NSMixedState;
*stop = YES;
return;
}
else if(subState == NSOffState) {
countOff++;
}
}];
if(countOff) {
// off only happens if all children are off
newState = (countOff == [subrows count]) ? NSOffState : NSMixedState;
}

//update ourselves
[self _updateCheckedStateForRow:row to:newState];

// notify our own parent of the change
[self _updateCheckedStateUpwardsFromCompoundRow:[filterRuleEditor parentRowForRow:row]];
}

/**
* This method recursively walks the rule editor tree starting with the children of `row`,
* updates the state of any compound row along the way (except `row` itself!) to be consistent with its child rows and
* returns the compound checkbox state of all children.
*
* @param row
* The row index of a compound row (can be `-1` to start walking with all top-level rows)
* @return
* The resulting state for the given row according to all children (recursive).
* This will be:
* - `NSOnState` if the row either has no children or all children are also checked
* - `NSMixedState` if at least one child row is also in mixed state or some (but not all) of the child rows are unchecked
* - `NSOffState` if there are child rows and all of them are unchecked
*/
- (NSCellStateValue)_recalculateCheckboxStatesFromRow:(NSInteger)row
{
NSIndexSet *subrows = [filterRuleEditor subrowIndexesForRow:row];

__block NSCellStateValue newState = NSOnState;
__block NSUInteger countOff = 0;
[subrows enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
NSCellStateValue subState;
if([filterRuleEditor rowTypeForRow:idx] == NSRuleEditorRowTypeCompound) {
subState = [self _recalculateCheckboxStatesFromRow:idx];
// if the current row is a compound row, update its state from its children
[self _updateCheckedStateForRow:idx to:subState];
}
else {
subState = [self _checkboxStateForRow:idx];
}
if(subState == NSMixedState) {
newState = NSMixedState;
}
else if(subState == NSOffState) {
countOff++;
}
}];
if(countOff) {
// off only happens if all children are off
newState = (countOff == [subrows count]) ? NSOffState : NSMixedState;
}

return newState;
}

/**
* Get the current checkbox state for a row
*
* NOTE: This method can be used on simple and compound rows, but it will not check that the compound state is actually
* consistent with its children before returning it.
*
* @param row
* The row index to return the checkbox state of (must be >= 0)
* @return
* The checkbox state.
* Defaults to `NSOnState` if no checkbox is found in the row.
*/
- (NSCellStateValue)_checkboxStateForRow:(NSInteger)row
{
NSArray *displayValues = [filterRuleEditor displayValuesForRow:row];
RuleNode *firstCriterion = [[filterRuleEditor criteriaForRow:row] objectAtIndex:0];
if([firstCriterion type] == RuleNodeTypeEnable) {
NSButton *subButton = [displayValues objectAtIndex:0];
return [subButton state];
}

SPLog(@"row=%ld: row does not have enable node as first child!? (type=%ld)", row, (NSInteger)[firstCriterion type]);
return NSOnState;
}

- (IBAction)_textFieldAction:(id)sender
{
// if the action was caused by pressing return or enter, trigger filtering
@@ -852,6 +1054,9 @@ - (void)ruleEditorRowsDidChange:(NSNotification *)notification
//[self _resize];
[self _updateButtonStates];

// if a row has been added, we need to update the checkboxes to match again
[self _recalculateCheckboxStatesFromRow:-1];

// if the user removed the last row in the editor by pressing "-" (and only then) we immediately want to trigger a filter reset.
// There are two problems with that:
// - The rule editor is very liberal in the use of this notification. Receiving it does not mean the number of rows actually did change
@@ -1209,7 +1414,7 @@ - (NSDictionary *)_serializeSubtree:(NSDictionary *)item includingDefinition:(BO
NSDictionary *out = nil;
// if we are empty return nil instead (can happen if all children are disabled)
if([children count]) {
StringNode *node = [[item objectForKey:@"criteria"] objectAtIndex:0];
StringNode *node = [[item objectForKey:@"criteria"] objectAtIndex:1]; //enable state is the result of the children's enable state - not serialized
BOOL isConjunction = [@"AND" isEqualToString:[node value]];
out = @{
SerFilterClass: SerFilterClassGroup,
@@ -1285,6 +1490,9 @@ - (void)restoreSerializedFilters:(NSDictionary *)serialized
[proxy setArray:newModel];
}];

//finally update all checkboxes
[self _recalculateCheckboxStatesFromRow:-1];

[newModel release];
}

@@ -1295,16 +1503,22 @@ - (NSMutableDictionary *)_restoreSerializedFilter:(NSDictionary *)serialized
if(SerIsGroup(serialized)) {
[obj setObject:@(NSRuleEditorRowTypeCompound) forKey:@"rowType"];

StringNode *sn = [[StringNode alloc] init];
[sn setValue:([[serialized objectForKey:SerFilterGroupIsConjunction] boolValue] ? @"AND" : @"OR")];
//the checkbox state is not important here. that will be updated at the end
EnableNode *checkbox = [[EnableNode alloc] init];
[checkbox setAllowsMixedState:YES];

StringNode *criterion = [[StringNode alloc] init];
[criterion setValue:([[serialized objectForKey:SerFilterGroupIsConjunction] boolValue] ? @"AND" : @"OR")];
// those have to be mutable arrays for the rule editor to work
NSMutableArray *criteria = [NSMutableArray arrayWithObject:sn];
NSMutableArray *criteria = [NSMutableArray arrayWithArray:@[checkbox,criterion]];
[obj setObject:criteria forKey:@"criteria"];
[checkbox release];

id displayValue = [self ruleEditor:filterRuleEditor displayValueForCriterion:sn inRow:-1];
NSMutableArray *displayValues = [NSMutableArray arrayWithObject:displayValue];
id checkDisplayValue = [self ruleEditor:filterRuleEditor displayValueForCriterion:checkbox inRow:-1];
id displayValue = [self ruleEditor:filterRuleEditor displayValueForCriterion:criterion inRow:-1];
NSMutableArray *displayValues = [NSMutableArray arrayWithArray:@[checkDisplayValue,displayValue]];
[obj setObject:displayValues forKey:@"displayValues"];
[sn release];
[criterion release];

NSArray *children = [serialized objectForKey:SerFilterGroupChildren];
NSMutableArray *subrows = [[NSMutableArray alloc] initWithCapacity:[children count]];
@@ -1794,23 +2008,25 @@ - (BOOL)isEqual:(id)other {
@implementation EnableNode

@synthesize initialState = initialState;
@synthesize allowsMixedState = allowsMixedState;

- (instancetype)init {
self = [super init];
if (self) {
type = RuleNodeTypeEnable;
initialState = YES;
allowsMixedState = NO;
}
return self;
}

- (NSUInteger)hash {
return (([super hash] << 1) | initialState);
return (([super hash] << 2) | (initialState << 1) | allowsMixedState);
}

- (BOOL)isEqual:(id)other {
if (other == self) return YES;
if (other && [[other class] isEqual:[self class]] && [self initialState] == [(EnableNode *)other initialState]) return YES;
if (other && [[other class] isEqual:[self class]] && [self initialState] == [(EnableNode *)other initialState] && [self allowsMixedState] == [(EnableNode *)other allowsMixedState]) return YES;

return NO;
}

0 comments on commit f0987c5

Please sign in to comment.
You can’t perform that action at this time.