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