Skip to content

Commit f0987c5

Browse files
committed
Add option to enable/disable compound rows in rule filter (#3303)
1 parent 8a37c10 commit f0987c5

File tree

1 file changed

+242
-26
lines changed

1 file changed

+242
-26
lines changed

Source/SPRuleFilterController.m

Lines changed: 242 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,10 @@ @interface ConnectorNode : RuleNode {
181181

182182
@interface EnableNode : RuleNode {
183183
BOOL initialState;
184+
BOOL allowsMixedState;
184185
}
185186
@property (assign, nonatomic) BOOL initialState;
187+
@property (assign, nonatomic) BOOL allowsMixedState;
186188
@end
187189

188190
#pragma mark -
@@ -256,6 +258,12 @@ - (void)_ensureValidOperatorCache:(ColumnNode *)col;
256258
- (IBAction)addFilter:(id)sender;
257259
- (void)_updateButtonStates;
258260
- (void)_doChangeToRuleEditorData:(void (^)(void))duringBlock;
261+
- (IBAction)_checkboxClicked:(id)sender;
262+
- (void)_updateCheckedStateUpwardsFromCompoundRow:(NSInteger)row;
263+
- (void)_updateCheckedStateForRow:(NSInteger)row to:(NSCellStateValue)newState;
264+
- (void)_updateCheckedStateDownwardsFromCompoundRow:(NSInteger)row to:(NSCellStateValue)newState;
265+
- (NSCellStateValue)_recalculateCheckboxStatesFromRow:(NSInteger)row;
266+
- (NSCellStateValue)_checkboxStateForRow:(NSInteger)row;
259267
@end
260268

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

397405
- (NSInteger)ruleEditor:(NSRuleEditor *)editor numberOfChildrenForCriterion:(nullable id)criterion withRowType:(NSRuleEditorRowType)rowType
398406
{
399-
// nil criterion is always the first element in a row, compound rows are only for "AND"/"OR" groups
400-
if(!criterion && rowType == NSRuleEditorRowTypeCompound) {
401-
return 2;
402-
}
403-
else if(!criterion && rowType == NSRuleEditorRowTypeSimple) {
404-
return 1; // enable checkbox
407+
// nil criterion is always the first element in a row
408+
if(rowType == NSRuleEditorRowTypeCompound) {
409+
if(!criterion) {
410+
return 1; //enable checkbox
411+
}
412+
RuleNodeType type = [(RuleNode *)criterion type];
413+
// compound rows are only for "AND"/"OR" groups
414+
if(type == RuleNodeTypeEnable) {
415+
return 2;
416+
}
405417
}
406418
else if(rowType == NSRuleEditorRowTypeSimple) {
419+
if(!criterion) {
420+
return 1; // enable checkbox
421+
}
407422
RuleNodeType type = [(RuleNode *)criterion type];
408423
// the children of the enable checkbox are the columns
409424
if(type == RuleNodeTypeEnable) {
@@ -439,20 +454,29 @@ - (NSInteger)ruleEditor:(NSRuleEditor *)editor numberOfChildrenForCriterion:(nul
439454

440455
- (id)ruleEditor:(NSRuleEditor *)editor child:(NSInteger)index forCriterion:(nullable id)criterion withRowType:(NSRuleEditorRowType)rowType
441456
{
442-
// nil criterion is always the first element in a row, compound rows are only for "AND"/"OR" groups
443-
if(!criterion && rowType == NSRuleEditorRowTypeCompound) {
444-
StringNode *node = [[StringNode alloc] init];
445-
switch(index) {
446-
case 0: [node setValue:@"AND"]; break;
447-
case 1: [node setValue:@"OR"]; break;
457+
// nil criterion is always the first element in a row
458+
if(rowType == NSRuleEditorRowTypeCompound) {
459+
if(!criterion) {
460+
EnableNode *node = [[EnableNode alloc] init];
461+
[node setAllowsMixedState:YES];
462+
return [node autorelease];
463+
}
464+
RuleNodeType type = [(RuleNode *) criterion type];
465+
// compound rows are only for "AND"/"OR" groups
466+
if(type == RuleNodeTypeEnable) {
467+
StringNode *node = [[StringNode alloc] init];
468+
switch(index) {
469+
case 0: [node setValue:@"AND"]; break;
470+
case 1: [node setValue:@"OR"]; break;
471+
}
472+
return [node autorelease];
448473
}
449-
return [node autorelease];
450-
}
451-
// this is the enable checkbox
452-
else if(!criterion && rowType == NSRuleEditorRowTypeSimple) {
453-
return [[[EnableNode alloc] init] autorelease];
454474
}
455475
else if(rowType == NSRuleEditorRowTypeSimple) {
476+
// this is the enable checkbox
477+
if(!criterion) {
478+
return [[[EnableNode alloc] init] autorelease];
479+
}
456480
RuleNodeType type = [(RuleNode *) criterion type];
457481
// this is the column field
458482
if (type == RuleNodeTypeEnable) {
@@ -513,7 +537,10 @@ - (id)ruleEditor:(NSRuleEditor *)editor displayValueForCriterion:(id)criterion i
513537
[check setBordered:NO];
514538
[check setImagePosition:NSImageOnly];
515539
[check sizeToFit];
540+
[check setAllowsMixedState:[node allowsMixedState]];
516541
[check setState:([node initialState] ? NSOnState : NSOffState)];
542+
[check setTarget:self];
543+
[check setAction:@selector(_checkboxClicked:)];
517544
return [check autorelease];
518545
}
519546
case RuleNodeTypeColumn: {
@@ -626,6 +653,181 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBy
626653
return NO;
627654
}
628655

656+
- (IBAction)_checkboxClicked:(id)sender
657+
{
658+
NSInteger row = [filterRuleEditor rowForDisplayValue:sender];
659+
NSCellStateValue newState = [(NSButton *)sender state];
660+
661+
// to have -setState: accept mixed state we have to -setAllowsMixedState:YES in which case the user, too, can cycle all three states m(
662+
if(newState == NSMixedState) {
663+
[sender setNextState];
664+
newState = [sender state];
665+
}
666+
667+
if(row >= 0 && (newState == NSOnState || newState == NSOffState)) {
668+
// if we are a compound row, go downwards to update our children
669+
if([filterRuleEditor rowTypeForRow:row] == NSRuleEditorRowTypeCompound) {
670+
[self _updateCheckedStateDownwardsFromCompoundRow:row to:newState];
671+
}
672+
// then go upwards to update the checkbox state of our parent
673+
[self _updateCheckedStateUpwardsFromCompoundRow:[filterRuleEditor parentRowForRow:row]];
674+
}
675+
}
676+
677+
/**
678+
* This method will recursively update the checkbox state of all children rows of the given row index with `newState`.
679+
* `row` itself will not be changed!
680+
*
681+
* @param row
682+
* The row index of a compound row for which to update the children states (can be -1)
683+
* @param newState
684+
* The new state to set for all children rows
685+
*/
686+
- (void)_updateCheckedStateDownwardsFromCompoundRow:(NSInteger)row to:(NSCellStateValue)newState
687+
{
688+
NSIndexSet *subrows = [filterRuleEditor subrowIndexesForRow:row];
689+
[subrows enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
690+
[self _updateCheckedStateForRow:idx to:newState];
691+
// go deeper for compound rows
692+
if([filterRuleEditor rowTypeForRow:idx] == NSRuleEditorRowTypeCompound) {
693+
[self _updateCheckedStateDownwardsFromCompoundRow:idx to:newState];
694+
}
695+
}];
696+
}
697+
698+
/**
699+
* This method will update the checkbox state of the row specified by the given index.
700+
* No other rows will be affected by this call.
701+
*
702+
* @param row
703+
* The row index to update (must be >= 0)
704+
* @param newState
705+
* The new checkbox state to set
706+
*/
707+
- (void)_updateCheckedStateForRow:(NSInteger)row to:(NSCellStateValue)newState
708+
{
709+
NSArray *displayValues = [filterRuleEditor displayValuesForRow:row];
710+
RuleNode *firstCriterion = [[filterRuleEditor criteriaForRow:row] objectAtIndex:0];
711+
if([firstCriterion type] == RuleNodeTypeEnable) {
712+
NSButton *button = [displayValues objectAtIndex:0];
713+
[button setState:newState];
714+
}
715+
}
716+
717+
/**
718+
* This method will recursively update the checkbox state of the given row and all of its parent rows
719+
* to match the aggregate state of all simple rows in the affected subtree.
720+
*
721+
* The rule editor row tree will be walked upwards using `-parentRowForRow:` until a top-level row is encountered
722+
* (parent=-1).
723+
*
724+
* NOTE: This method assumes that all compound child rows of `row` already have a consistent checkbox state!
725+
*
726+
* @param row
727+
* The row index of a compound row (can be `-1`)
728+
*/
729+
- (void)_updateCheckedStateUpwardsFromCompoundRow:(NSInteger)row
730+
{
731+
// stop condition for recursion
732+
if(row < 0) return;
733+
734+
__block NSCellStateValue newState = NSOnState;
735+
__block NSUInteger countOff = 0;
736+
NSIndexSet *subrows = [filterRuleEditor subrowIndexesForRow:row];
737+
[subrows enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
738+
NSCellStateValue subState = [self _checkboxStateForRow:idx];
739+
// mixed is easy: if at least one child is mixed, the parent is mixed, too
740+
if(subState == NSMixedState) {
741+
newState = NSMixedState;
742+
*stop = YES;
743+
return;
744+
}
745+
else if(subState == NSOffState) {
746+
countOff++;
747+
}
748+
}];
749+
if(countOff) {
750+
// off only happens if all children are off
751+
newState = (countOff == [subrows count]) ? NSOffState : NSMixedState;
752+
}
753+
754+
//update ourselves
755+
[self _updateCheckedStateForRow:row to:newState];
756+
757+
// notify our own parent of the change
758+
[self _updateCheckedStateUpwardsFromCompoundRow:[filterRuleEditor parentRowForRow:row]];
759+
}
760+
761+
/**
762+
* This method recursively walks the rule editor tree starting with the children of `row`,
763+
* updates the state of any compound row along the way (except `row` itself!) to be consistent with its child rows and
764+
* returns the compound checkbox state of all children.
765+
*
766+
* @param row
767+
* The row index of a compound row (can be `-1` to start walking with all top-level rows)
768+
* @return
769+
* The resulting state for the given row according to all children (recursive).
770+
* This will be:
771+
* - `NSOnState` if the row either has no children or all children are also checked
772+
* - `NSMixedState` if at least one child row is also in mixed state or some (but not all) of the child rows are unchecked
773+
* - `NSOffState` if there are child rows and all of them are unchecked
774+
*/
775+
- (NSCellStateValue)_recalculateCheckboxStatesFromRow:(NSInteger)row
776+
{
777+
NSIndexSet *subrows = [filterRuleEditor subrowIndexesForRow:row];
778+
779+
__block NSCellStateValue newState = NSOnState;
780+
__block NSUInteger countOff = 0;
781+
[subrows enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
782+
NSCellStateValue subState;
783+
if([filterRuleEditor rowTypeForRow:idx] == NSRuleEditorRowTypeCompound) {
784+
subState = [self _recalculateCheckboxStatesFromRow:idx];
785+
// if the current row is a compound row, update its state from its children
786+
[self _updateCheckedStateForRow:idx to:subState];
787+
}
788+
else {
789+
subState = [self _checkboxStateForRow:idx];
790+
}
791+
if(subState == NSMixedState) {
792+
newState = NSMixedState;
793+
}
794+
else if(subState == NSOffState) {
795+
countOff++;
796+
}
797+
}];
798+
if(countOff) {
799+
// off only happens if all children are off
800+
newState = (countOff == [subrows count]) ? NSOffState : NSMixedState;
801+
}
802+
803+
return newState;
804+
}
805+
806+
/**
807+
* Get the current checkbox state for a row
808+
*
809+
* NOTE: This method can be used on simple and compound rows, but it will not check that the compound state is actually
810+
* consistent with its children before returning it.
811+
*
812+
* @param row
813+
* The row index to return the checkbox state of (must be >= 0)
814+
* @return
815+
* The checkbox state.
816+
* Defaults to `NSOnState` if no checkbox is found in the row.
817+
*/
818+
- (NSCellStateValue)_checkboxStateForRow:(NSInteger)row
819+
{
820+
NSArray *displayValues = [filterRuleEditor displayValuesForRow:row];
821+
RuleNode *firstCriterion = [[filterRuleEditor criteriaForRow:row] objectAtIndex:0];
822+
if([firstCriterion type] == RuleNodeTypeEnable) {
823+
NSButton *subButton = [displayValues objectAtIndex:0];
824+
return [subButton state];
825+
}
826+
827+
SPLog(@"row=%ld: row does not have enable node as first child!? (type=%ld)", row, (NSInteger)[firstCriterion type]);
828+
return NSOnState;
829+
}
830+
629831
- (IBAction)_textFieldAction:(id)sender
630832
{
631833
// if the action was caused by pressing return or enter, trigger filtering
@@ -852,6 +1054,9 @@ - (void)ruleEditorRowsDidChange:(NSNotification *)notification
8521054
//[self _resize];
8531055
[self _updateButtonStates];
8541056

1057+
// if a row has been added, we need to update the checkboxes to match again
1058+
[self _recalculateCheckboxStatesFromRow:-1];
1059+
8551060
// if the user removed the last row in the editor by pressing "-" (and only then) we immediately want to trigger a filter reset.
8561061
// There are two problems with that:
8571062
// - 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
12091414
NSDictionary *out = nil;
12101415
// if we are empty return nil instead (can happen if all children are disabled)
12111416
if([children count]) {
1212-
StringNode *node = [[item objectForKey:@"criteria"] objectAtIndex:0];
1417+
StringNode *node = [[item objectForKey:@"criteria"] objectAtIndex:1]; //enable state is the result of the children's enable state - not serialized
12131418
BOOL isConjunction = [@"AND" isEqualToString:[node value]];
12141419
out = @{
12151420
SerFilterClass: SerFilterClassGroup,
@@ -1285,6 +1490,9 @@ - (void)restoreSerializedFilters:(NSDictionary *)serialized
12851490
[proxy setArray:newModel];
12861491
}];
12871492

1493+
//finally update all checkboxes
1494+
[self _recalculateCheckboxStatesFromRow:-1];
1495+
12881496
[newModel release];
12891497
}
12901498

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

1298-
StringNode *sn = [[StringNode alloc] init];
1299-
[sn setValue:([[serialized objectForKey:SerFilterGroupIsConjunction] boolValue] ? @"AND" : @"OR")];
1506+
//the checkbox state is not important here. that will be updated at the end
1507+
EnableNode *checkbox = [[EnableNode alloc] init];
1508+
[checkbox setAllowsMixedState:YES];
1509+
1510+
StringNode *criterion = [[StringNode alloc] init];
1511+
[criterion setValue:([[serialized objectForKey:SerFilterGroupIsConjunction] boolValue] ? @"AND" : @"OR")];
13001512
// those have to be mutable arrays for the rule editor to work
1301-
NSMutableArray *criteria = [NSMutableArray arrayWithObject:sn];
1513+
NSMutableArray *criteria = [NSMutableArray arrayWithArray:@[checkbox,criterion]];
13021514
[obj setObject:criteria forKey:@"criteria"];
1515+
[checkbox release];
13031516

1304-
id displayValue = [self ruleEditor:filterRuleEditor displayValueForCriterion:sn inRow:-1];
1305-
NSMutableArray *displayValues = [NSMutableArray arrayWithObject:displayValue];
1517+
id checkDisplayValue = [self ruleEditor:filterRuleEditor displayValueForCriterion:checkbox inRow:-1];
1518+
id displayValue = [self ruleEditor:filterRuleEditor displayValueForCriterion:criterion inRow:-1];
1519+
NSMutableArray *displayValues = [NSMutableArray arrayWithArray:@[checkDisplayValue,displayValue]];
13061520
[obj setObject:displayValues forKey:@"displayValues"];
1307-
[sn release];
1521+
[criterion release];
13081522

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

17962010
@synthesize initialState = initialState;
2011+
@synthesize allowsMixedState = allowsMixedState;
17972012

17982013
- (instancetype)init {
17992014
self = [super init];
18002015
if (self) {
18012016
type = RuleNodeTypeEnable;
18022017
initialState = YES;
2018+
allowsMixedState = NO;
18032019
}
18042020
return self;
18052021
}
18062022

18072023
- (NSUInteger)hash {
1808-
return (([super hash] << 1) | initialState);
2024+
return (([super hash] << 2) | (initialState << 1) | allowsMixedState);
18092025
}
18102026

18112027
- (BOOL)isEqual:(id)other {
18122028
if (other == self) return YES;
1813-
if (other && [[other class] isEqual:[self class]] && [self initialState] == [(EnableNode *)other initialState]) return YES;
2029+
if (other && [[other class] isEqual:[self class]] && [self initialState] == [(EnableNode *)other initialState] && [self allowsMixedState] == [(EnableNode *)other allowsMixedState]) return YES;
18142030

18152031
return NO;
18162032
}

0 commit comments

Comments
 (0)