From c2fe67ba840a5f4eaa2aed786da3e559b3a8b150 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 13 May 2018 20:01:14 +0200 Subject: #63: Rename class SPTableContentFilterController to SPRuleFilterController to reduce confusion --- Source/SPRuleFilterController.h | 163 ++++ Source/SPRuleFilterController.m | 1355 +++++++++++++++++++++++++++++++ Source/SPTableContent.h | 4 +- Source/SPTableContent.m | 54 +- Source/SPTableContentFilterController.h | 163 ---- Source/SPTableContentFilterController.m | 1355 ------------------------------- 6 files changed, 1547 insertions(+), 1547 deletions(-) create mode 100644 Source/SPRuleFilterController.h create mode 100644 Source/SPRuleFilterController.m delete mode 100644 Source/SPTableContentFilterController.h delete mode 100644 Source/SPTableContentFilterController.m (limited to 'Source') diff --git a/Source/SPRuleFilterController.h b/Source/SPRuleFilterController.h new file mode 100644 index 00000000..45c28fce --- /dev/null +++ b/Source/SPRuleFilterController.h @@ -0,0 +1,163 @@ +// +// SPRuleFilterController.h +// sequel-pro +// +// Created by Max Lohrmann on 04.05.18. +// Copyright (c) 2018 Max Lohrmann. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at + +@class SPTableData; +@class SPDatabaseDocument; +@class SPTablesList; +@class SPContentFilterManager; + +NSString * const SPRuleFilterHeightChangedNotification; + +@interface SPRuleFilterController : NSObject { + IBOutlet NSRuleEditor *filterRuleEditor; + IBOutlet SPTableData *tableDataInstance; + IBOutlet SPDatabaseDocument *tableDocumentInstance; + IBOutlet SPTablesList *tablesListInstance; + IBOutlet NSView *tableContentViewBelow; + IBOutlet NSButton *filterButton; + + NSMutableArray *columns; + NSMutableDictionary *contentFilters; + NSMutableDictionary *numberOfDefaultFilters; + + NSMutableArray *model; + + SPContentFilterManager *contentFilterManager; + + CGFloat preferredHeight; + + id target; + SEL action; + + BOOL enabled; +} + +/** + * Returns the rule editor view that is managed by this object + * + * SHOULD be called on the UI thread, or results may be inconsistent! + */ +- (NSRuleEditor *)view; + +/** + * Makes the first NSTextField found in the rule editor the first responder + * + * MUST BE CALLED ON THE UI THREAD! + */ +- (void)focusFirstInputField; + +/** + * Will reconfigure the columns of the rule editor from the given array. + * Call with nil to reset the editor to its initial empty state. + * Existing rows will be removed in any case! + * + * MUST BE CALLED ON THE UI THREAD! + */ +- (void)setColumns:(NSArray *)dataColumns; + +/** + * Converts the current filter expression displayed in the UI into an + * SQL string suitable for use in a WHERE clause. + * + * @param isBINARY Indicates that the filter should use the BINARY qualifier for ignoring + * collations during search. + * @param err Upon return contains and object describing why the SQL conversion failed, + * if it failed or nil, if no errors occured. + * + * MUST BE CALLED ON THE UI THREAD! + */ +- (NSString *)sqlWhereExpressionWithBinary:(BOOL)isBINARY error:(NSError **)err; + +/** + * Returns the current filter configuration in a serialized form that can be exported and + * reapplied later. + * + * MUST BE CALLED ON THE UI THREAD! + */ +- (NSDictionary *)serializedFilter; + +/** + * Restores the filter rule configuration from a given dictionary. + * The current column configuration must match the schema that was used when generating + * the serialized data, otherwise the invalid rules will be ignored. + * + * @param serialized A dictionary previously generated by calling -serializedFilter. + * @return A serialized filter + * + * MUST BE CALLED ON THE UI THREAD! + */ +- (void)restoreSerializedFilters:(NSDictionary *)serialized; + +/** + * Create a serialized filter from a given column, operator and operand. + * This is used when navigating foreign key links between tables to create the filter for the target table. + * + * @param colName Name of the column to filter (left side operand) + * @param opName Name of the filter (operator) + * @param values The values to filter with (right side operand) + * @return A serialized filter + * + * This method is thread-safe. + */ ++ (NSDictionary *)makeSerializedFilterForColumn:(NSString *)colName operator:(NSString *)opName values:(NSArray *)values; + +/** + * The view height the rule editor needs in order to not have to resort to scrollbars + * + * SHOULD be called on the UI thread, or results may be inconsistent! + */ +@property (readonly, assign, nonatomic) CGFloat preferredHeight; + +/** + * Indicates whether the rule editor has no filter expressions + * + * SHOULD be called on the UI thread, or results may be inconsistent! + */ +- (BOOL)isEmpty; + +/** + * Adds a new row to the rule editor + * + * MUST BE CALLED ON THE UI THREAD! + */ +- (void)addFilterExpression; + +/** + * Used when the rule editor wants to trigger filtering + * + * SHOULD be called on the UI thread, or results may be inconsistent! + */ +@property (assign, nonatomic) id target; +@property (assign, nonatomic) SEL action; + +- (BOOL)isEnabled; +- (void)setEnabled:(BOOL)enabled; + +@end diff --git a/Source/SPRuleFilterController.m b/Source/SPRuleFilterController.m new file mode 100644 index 00000000..baba3eb7 --- /dev/null +++ b/Source/SPRuleFilterController.m @@ -0,0 +1,1355 @@ +// +// SPRuleFilterController.m +// sequel-pro +// +// Created by Max Lohrmann on 04.05.18. +// Copyright (c) 2018 Max Lohrmann. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at + +#import "SPRuleFilterController.h" +#import "SPQueryController.h" +#import "SPDatabaseDocument.h" +#import "RegexKitLite.h" +#import "SPContentFilterManager.h" +#import "SPFunctions.h" +#import "SPTableFilterParser.h" + +typedef NS_ENUM(NSInteger, RuleNodeType) { + RuleNodeTypeColumn, + RuleNodeTypeString, + RuleNodeTypeOperator, + RuleNodeTypeArgument, + RuleNodeTypeConnector, +}; + +NSString * const SPRuleFilterHeightChangedNotification = @"SPRuleFilterHeightChanged"; + +/** + * The type of filter rule that the current item represents. + */ +const NSString * const SerFilterClass = @"filterClass"; +/** + * The current rule is a group row (an "AND" or "OR" expression with children) + */ +const NSString * const SerFilterClassGroup = @"groupNode"; +/** + * The current rule is a filter expression + */ +const NSString * const SerFilterClassExpression = @"expressionNode"; +/** + * Group Nodes only: + * Indicates whether the group is a conjunction. + * If YES, the children will be combined using "AND", otherwise using "OR". + */ +const NSString * const SerFilterGroupIsConjunction = @"isConjunction"; +/** + * Group Nodes only: + * An array of child filter rules (which again can be group or expression rules) + */ +const NSString * const SerFilterGroupChildren = @"children"; +/** + * Expression Nodes only: + * The name of the column to filter in (left side expression) + * + * Legacy names: + * @"filterField", fieldField + */ +const NSString * const SerFilterExprColumn = @"column"; +/** + * Expression Nodes only: + * The data type grouping of the column for applicable filters + */ +const NSString * const SerFilterExprType = @"filterType"; +/** + * Expression Nodes only: + * The title of the filter operator to apply + * + * Legacy names: + * @"filterComparison", compareField + */ +const NSString * const SerFilterExprComparison = @"filterComparison"; +/** + * Expression Nodes only: + * The values to apply the filter with (an array of 0 or more elements) + * + * Legacy names: + * @"filterValue", argumentField + * @"firstBetweenField", @"secondBetweenField", firstBetweenField, secondBetweenField + */ +const NSString * const SerFilterExprValues = @"filterValues"; +/** + * Expression Nodes only: + * the filter definition dictionary (as in ContentFilters.plist) + * for the filter represented by SerFilterExprComparison. + * + * This item is not designed to be serialized to disk + */ +const NSString * const SerFilterExprDefinition = @"_filterDefinition"; + +#pragma mark - + +@interface RuleNode : NSObject { + RuleNodeType type; +} +@property(assign, nonatomic) RuleNodeType type; +@end + +@interface ColumnNode : RuleNode { + NSString *name; + NSString *typegrouping; + NSArray *operatorCache; +} +@property(copy, nonatomic) NSString *name; +@property(copy, nonatomic) NSString *typegrouping; +@property(retain, nonatomic) NSArray *operatorCache; +@end + +@interface StringNode : RuleNode { + NSString *value; +} +@property(copy, nonatomic) NSString *value; +@end + +@interface OpNode : RuleNode { + // Note: The main purpose of this field is to have @"=" for column A and @"=" for column B to return NO in -isEqual: + // because otherwise NSRuleEditor will get confused and blow up. + ColumnNode *parentColumn; + NSDictionary *settings; + NSDictionary *filter; +} +@property (assign, nonatomic) ColumnNode *parentColumn; +@property (retain, nonatomic) NSDictionary *settings; +@property (retain, nonatomic) NSDictionary *filter; +@end + +@interface ArgNode : RuleNode { + NSDictionary *filter; + NSUInteger argIndex; + NSString *initialValue; +} +@property (copy, nonatomic) NSString *initialValue; +@property (retain, nonatomic) NSDictionary *filter; +@property (assign, nonatomic) NSUInteger argIndex; +@end + +@interface ConnectorNode : RuleNode { + NSDictionary *filter; + NSUInteger labelIndex; +} +@property (retain, nonatomic) NSDictionary *filter; +@property (assign, nonatomic) NSUInteger labelIndex; +@end + +#pragma mark - + +@interface SPRuleFilterController () + +@property (readwrite, assign, nonatomic) CGFloat preferredHeight; + +// This is the binding used by NSRuleEditor for the current state +@property (retain, nonatomic) NSMutableArray *model; + +- (NSArray *)_compareTypesForColumn:(ColumnNode *)colNode; +- (IBAction)_textFieldAction:(id)sender; +- (IBAction)_editFiltersAction:(id)sender; +- (void)_contentFiltersHaveBeenUpdated:(NSNotification *)notification; ++ (NSDictionary *)_flattenSerializedFilter:(NSDictionary *)in; +static BOOL SerIsGroup(NSDictionary *dict); +- (NSDictionary *)_serializedFilterIncludingFilterDefinition:(BOOL)includeDefinition; ++ (void)_writeFilterTree:(NSDictionary *)in toString:(NSMutableString *)out wrapInParenthesis:(BOOL)wrap binary:(BOOL)isBINARY error:(NSError **)err; +- (NSMutableDictionary *)_restoreSerializedFilter:(NSDictionary *)serialized; +static void _addIfNotNil(NSMutableArray *array, id toAdd); +- (ColumnNode *)_columnForName:(NSString *)name; +- (OpNode *)_operatorNamed:(NSString *)title forColumn:(ColumnNode *)col; +- (BOOL)_focusOnFieldInSubtree:(NSDictionary *)dict; +- (void)_resize; +- (void)openContentFilterManagerForFilterType:(NSString *)filterType; +- (IBAction)filterTable:(id)sender; + +@end + +@implementation SPRuleFilterController + +@synthesize model = model; +@synthesize preferredHeight = preferredHeight; +@synthesize target = target; +@synthesize action = action; + +- (instancetype)init +{ + if((self = [super init])) { + columns = [[NSMutableArray alloc] init]; + model = [[NSMutableArray alloc] init]; + preferredHeight = 0.0; + target = nil; + action = NULL; + + // Init default filters for Content Browser + contentFilters = [[NSMutableDictionary alloc] init]; + numberOfDefaultFilters = [[NSMutableDictionary alloc] init]; + + NSError *readError = nil; + NSString *filePath = [NSBundle pathForResource:@"ContentFilters.plist" ofType:nil inDirectory:[[NSBundle mainBundle] bundlePath]]; + NSData *defaultFilterData = [NSData dataWithContentsOfFile:filePath + options:NSMappedRead + error:&readError]; + + if(defaultFilterData && !readError) { + NSDictionary *defaultFilterDict = [NSPropertyListSerialization propertyListWithData:defaultFilterData + options:NSPropertyListMutableContainersAndLeaves + format:NULL + error:&readError]; + + if(defaultFilterDict && !readError) { + [contentFilters setDictionary:defaultFilterDict]; + } + } + + if (readError) { + NSLog(@"Error while reading 'ContentFilters.plist':\n%@", readError); + NSBeep(); + } + else { + [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"number"] count]] forKey:@"number"]; + [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"date"] count]] forKey:@"date"]; + [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"string"] count]] forKey:@"string"]; + [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"spatial"] count]] forKey:@"spatial"]; + } + } + return self; +} + +- (void)awakeFromNib +{ + [filterRuleEditor bind:@"rows" toObject:self withKeyPath:@"model" options:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_contentFiltersHaveBeenUpdated:) + name:SPContentFiltersHaveBeenUpdatedNotification + object:nil]; +} + +- (void)focusFirstInputField +{ + for(NSDictionary *rootItem in model) { + if([self _focusOnFieldInSubtree:rootItem]) return; + } +} + +- (BOOL)_focusOnFieldInSubtree:(NSDictionary *)dict +{ + //if we are a simple row we might have an input field ourself, otherwise search among our children + if([[dict objectForKey:@"rowType"] unsignedIntegerValue] == NSRuleEditorRowTypeSimple) { + for(id obj in [dict objectForKey:@"displayValues"]) { + if([obj isKindOfClass:[NSTextField class]]) { + [[(NSTextField *)obj window] makeFirstResponder:obj]; + return YES; + } + } + } + else { + for(NSDictionary *child in [dict objectForKey:@"subrows"]) { + if([self _focusOnFieldInSubtree:child]) return YES; + } + } + return NO; +} + +- (void)setColumns:(NSArray *)dataColumns; +{ + [self willChangeValueForKey:@"model"]; // manual KVO is needed for filter rule editor to notice change + [model removeAllObjects]; + [self didChangeValueForKey:@"model"]; + + [columns removeAllObjects]; + + //without a table there is nothing to filter + if(dataColumns) { + //sort column names if enabled + NSArray *columnDefinitions = dataColumns; + if ([[NSUserDefaults standardUserDefaults] boolForKey:SPAlphabeticalTableSorting]) { + NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]; + columnDefinitions = [columnDefinitions sortedArrayUsingDescriptors:@[sortDescriptor]]; + } + + // get the columns + for (NSDictionary *colDef in columnDefinitions) { + ColumnNode *node = [[ColumnNode alloc] init]; + [node setName:[colDef objectForKey:@"name"]]; + [node setTypegrouping:[colDef objectForKey:@"typegrouping"]]; + [columns addObject:node]; + [node release]; + } + } + + // make the rule editor reload the criteria + [filterRuleEditor reloadCriteria]; + + // disable UI if no criteria exist + [self setEnabled:([columns count] != 0)]; +} + +- (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 [columns count]; + } + else if(rowType == NSRuleEditorRowTypeSimple) { + // the children of the columns are their operators + RuleNodeType type = [(RuleNode *)criterion type]; + if(type == RuleNodeTypeColumn) { + ColumnNode *node = (ColumnNode *)criterion; + if(![node operatorCache]) { + NSArray *ops = [self _compareTypesForColumn:node]; + [node setOperatorCache:ops]; + } + return [[node operatorCache] count]; + } + // the first child of an operator is the first argument (if it has one) + else if(type == RuleNodeTypeOperator) { + OpNode *node = (OpNode *)criterion; + NSInteger numOfArgs = [[[node filter] objectForKey:@"NumberOfArguments"] integerValue]; + return (numOfArgs > 0) ? 1 : 0; + } + // the child of an argument can only be the conjunction label if more arguments follow + else if(type == RuleNodeTypeArgument) { + ArgNode *node = (ArgNode *)criterion; + NSInteger numOfArgs = [[[node filter] objectForKey:@"NumberOfArguments"] integerValue]; + return (numOfArgs > [node argIndex]+1) ? 1 : 0; + } + // the child of a conjunction is the next argument, if we have one + else if(type == RuleNodeTypeConnector) { + ConnectorNode *node = (ConnectorNode *)criterion; + NSInteger numOfArgs = [[[node filter] objectForKey:@"NumberOfArguments"] integerValue]; + return (numOfArgs > [node labelIndex]+1) ? 1 : 0; + } + } + return 0; +} + +- (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; + } + return [node autorelease]; + } + // this is the column field + else if(!criterion && rowType == NSRuleEditorRowTypeSimple) { + return [columns objectAtIndex:index]; + } + else if(rowType == NSRuleEditorRowTypeSimple) { + // the children of the columns are their operators + RuleNodeType type = [(RuleNode *) criterion type]; + if (type == RuleNodeTypeColumn) { + return [[criterion operatorCache] objectAtIndex:index]; + } + // the first child of an operator is the first argument + else if(type == RuleNodeTypeOperator) { + NSDictionary *filter = [(OpNode *)criterion filter]; + if([[filter objectForKey:@"NumberOfArguments"] integerValue]) { + ArgNode *arg = [[ArgNode alloc] init]; + [arg setFilter:filter]; + [arg setArgIndex:0]; + return [arg autorelease]; + } + } + // the child of an argument can only be the conjunction label if more arguments follow + else if(type == RuleNodeTypeArgument) { + NSDictionary *filter = [(ArgNode *)criterion filter]; + NSUInteger argIndex = [(ArgNode *)criterion argIndex]; + if([[filter objectForKey:@"NumberOfArguments"] integerValue] > argIndex +1) { + ConnectorNode *node = [[ConnectorNode alloc] init]; + [node setFilter:filter]; + [node setLabelIndex:argIndex]; // label 0 follows argument 0 + return [node autorelease]; + } + } + // the child of a conjunction is the next argument, if we have one + else if(type == RuleNodeTypeConnector) { + ConnectorNode *node = (ConnectorNode *)criterion; + NSInteger numOfArgs = [[[node filter] objectForKey:@"NumberOfArguments"] integerValue]; + if(numOfArgs > [node labelIndex]+1) { + ArgNode *arg = [[ArgNode alloc] init]; + [arg setFilter:[node filter]]; + [arg setArgIndex:([node labelIndex]+1)]; + return [arg autorelease]; + } + } + } + return nil; +} + +- (id)ruleEditor:(NSRuleEditor *)editor displayValueForCriterion:(id)criterion inRow:(NSInteger)row +{ + switch([(RuleNode *)criterion type]) { + case RuleNodeTypeString: return [(StringNode *)criterion value]; + case RuleNodeTypeColumn: return [(ColumnNode *)criterion name]; + case RuleNodeTypeOperator: { + OpNode *node = (OpNode *)criterion; + NSMenuItem *item; + if ([[[node settings] objectForKey:@"isSeparator"] boolValue]) { + item = [NSMenuItem separatorItem]; + } + else { + item = [[NSMenuItem alloc] initWithTitle:[[node settings] objectForKey:@"title"] action:NULL keyEquivalent:@""]; + [item setToolTip:[[node settings] objectForKey:@"tooltip"]]; + [item setTag:[[[node settings] objectForKey:@"tag"] integerValue]]; + //TODO the following seems to be mentioned exactly nowhere on the internet/in documentation, but without it NSMenuItems won't work properly, even though Apple says they are supported + [item setRepresentedObject:@{ + @"item": node, + @"value": [item title], + // this one is needed by the "Edit filters…" item for context + @"filterType": SPBoxNil([[node settings] objectForKey:@"filterType"]), + }]; + // override the default action from the rule editor if given (used to open the edit content filters sheet) + id _target = [[node settings] objectForKey:@"target"]; + SEL _action = (SEL)[(NSValue *)[[node settings] objectForKey:@"action"] pointerValue]; + if(_target && _action) { + [item setTarget:_target]; + [item setAction:_action]; + } + [item autorelease]; + } + return item; + } + case RuleNodeTypeArgument: { + //an argument is a textfield + ArgNode *node = (ArgNode *)criterion; + NSTextField *textField = [[NSTextField alloc] init]; + [[textField cell] setSendsActionOnEndEditing:YES]; + [[textField cell] setUsesSingleLineMode:YES]; + [textField setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + [textField sizeToFit]; + [textField setTarget:self]; + [textField setAction:@selector(_textFieldAction:)]; + if([node initialValue]) [textField setStringValue:[node initialValue]]; + NSRect frame = [textField frame]; + //adjust width, to make the field wider + frame.size.width = 500; //TODO determine a good width (possibly from the field type size) - how to access the rule editors bounds? + [textField setFrame:frame]; + return [textField autorelease]; + } + case RuleNodeTypeConnector: { + // a simple string for once + ConnectorNode *node = (ConnectorNode *)criterion; + NSArray* labels = [[node filter] objectForKey:@"ConjunctionLabels"]; + return (labels && [labels count] == 1)? [labels objectAtIndex:0] : @""; + } + } + + return nil; +} + +- (IBAction)_textFieldAction:(id)sender +{ + // if the action was caused by pressing return or enter, trigger filtering + NSEvent *event = [NSApp currentEvent]; + if(event && [event type] == NSKeyDown && ([event keyCode] == 36 || [event keyCode] == 76)) { + [self filterTable:nil]; + } +} + +- (IBAction)filterTable:(id)sender +{ + if(target && action) [target performSelector:action withObject:self]; +} + +- (void)_resize +{ + // The situation with the sizing is a bit f'ed up: + // - When -ruleEditorRowsDidChange: is invoked the NSRuleEditor has not yet updated its required frame size + // - We can't use KVO on -frame either, because SPTableContent will update the container size which + // ultimately also updates the NSRuleEditor's frame, causing a loop + // - Calling -sizeToFit works, but only when the NSRuleEditor is growing. It won't shrink + // after removing rows. + // - -intrinsicContentSize is what we want, but that method is 10.7+, so on 10.6 let's do the + // easiest workaround (note that both -intrinsicContentSize and -sizeToFit internally use -[NSRuleEditor _minimumFrameHeight]) + CGFloat wantsHeight; + if([filterRuleEditor respondsToSelector:@selector(intrinsicContentSize)]) { + NSSize sz = [filterRuleEditor intrinsicContentSize]; + wantsHeight = sz.height; + } + else { + wantsHeight = [filterRuleEditor rowHeight] * [filterRuleEditor numberOfRows]; + } + if(wantsHeight != preferredHeight) { + [self setPreferredHeight:wantsHeight]; + [[NSNotificationCenter defaultCenter] postNotificationName:SPRuleFilterHeightChangedNotification object:self]; + } +} + +- (void)ruleEditorRowsDidChange:(NSNotification *)notification +{ + //TODO find a better way to trigger resize + // We can't do this here, because it will cause rows to jump around when removing them (the add case works fine, though) + [self performSelector:@selector(_resize) withObject:nil afterDelay:0.2]; + //[self _resize]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + SPClear(model); + SPClear(columns); + SPClear(contentFilters); + SPClear(numberOfDefaultFilters); + [super dealloc]; +} + +/** + * Sets the compare types for the filter and the appropriate formatter for the textField + */ +- (NSArray *)_compareTypesForColumn:(ColumnNode *)colNode +{ + if(contentFilters == nil + || ![contentFilters objectForKey:@"number"] + || ![contentFilters objectForKey:@"string"] + || ![contentFilters objectForKey:@"date"]) { + NSLog(@"Error while setting filter types."); + NSBeep(); + return @[]; + } + + NSString *fieldTypeGrouping; + if([colNode typegrouping]) { + fieldTypeGrouping = [NSString stringWithString:[colNode typegrouping]]; + } + else { + return @[]; + } + + NSMutableArray *compareItems = [NSMutableArray array]; + + NSString *compareType; + + if ( [fieldTypeGrouping isEqualToString:@"date"] ) { + compareType = @"date"; + + /* + if ([fieldType isEqualToString:@"timestamp"]) { + [argumentField setFormatter:[[NSDateFormatter alloc] + initWithDateFormat:@"%Y-%m-%d %H:%M:%S" allowNaturalLanguage:YES]]; + } + if ([fieldType isEqualToString:@"datetime"]) { + [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%Y-%m-%d %H:%M:%S" allowNaturalLanguage:YES]]; + } + if ([fieldType isEqualToString:@"date"]) { + [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%Y-%m-%d" allowNaturalLanguage:YES]]; + } + if ([fieldType isEqualToString:@"time"]) { + [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%H:%M:%S" allowNaturalLanguage:YES]]; + } + if ([fieldType isEqualToString:@"year"]) { + [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%Y" allowNaturalLanguage:YES]]; + } + */ + + // TODO: A bug in the framework previously meant enum fields had to be treated as string fields for the purposes + // of comparison - this can now be split out to support additional comparison fucntionality if desired. + } + else if ([fieldTypeGrouping isEqualToString:@"string"] || [fieldTypeGrouping isEqualToString:@"binary"] + || [fieldTypeGrouping isEqualToString:@"textdata"] || [fieldTypeGrouping isEqualToString:@"blobdata"] + || [fieldTypeGrouping isEqualToString:@"enum"]) { + + compareType = @"string"; + // [argumentField setFormatter:nil]; + + } + else if ([fieldTypeGrouping isEqualToString:@"bit"] || [fieldTypeGrouping isEqualToString:@"integer"] + || [fieldTypeGrouping isEqualToString:@"float"]) { + compareType = @"number"; + // [argumentField setFormatter:numberFormatter]; + + } + else if ([fieldTypeGrouping isEqualToString:@"geometry"]) { + compareType = @"spatial"; + + } + else { + compareType = @""; + NSBeep(); + NSLog(@"ERROR: unknown type for comparision: in %@", fieldTypeGrouping); + } + + // Add IS NULL and IS NOT NULL as they should always be available + // [compareField addItemWithTitle:@"IS NULL"]; + // [compareField addItemWithTitle:@"IS NOT NULL"]; + + // Remove user-defined filters first + if([numberOfDefaultFilters objectForKey:compareType]) { + NSUInteger cycles = [[contentFilters objectForKey:compareType] count] - [[numberOfDefaultFilters objectForKey:compareType] integerValue]; + while(cycles > 0) { + [[contentFilters objectForKey:compareType] removeLastObject]; + cycles--; + } + } + + NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + +#ifndef SP_CODA /* content filters */ + // Load global user-defined content filters + if([prefs objectForKey:SPContentFilters] + && [contentFilters objectForKey:compareType] + && [[prefs objectForKey:SPContentFilters] objectForKey:compareType]) + { + [[contentFilters objectForKey:compareType] addObjectsFromArray:[[prefs objectForKey:SPContentFilters] objectForKey:compareType]]; + } + + // Load doc-based user-defined content filters + if([[SPQueryController sharedQueryController] contentFilterForFileURL:[tableDocumentInstance fileURL]]) { + id filters = [[SPQueryController sharedQueryController] contentFilterForFileURL:[tableDocumentInstance fileURL]]; + if([filters objectForKey:compareType]) + [[contentFilters objectForKey:compareType] addObjectsFromArray:[filters objectForKey:compareType]]; + } +#endif + + NSUInteger i = 0; + if([contentFilters objectForKey:compareType]) { + for (id filter in [contentFilters objectForKey:compareType]) { + // Create the tooltip + NSString *tooltip; + if ([filter objectForKey:@"Tooltip"]) + tooltip = [filter objectForKey:@"Tooltip"]; + else { + NSMutableString *tip = [[NSMutableString alloc] init]; + if ([filter objectForKey:@"Clause"] && [(NSString *) [filter objectForKey:@"Clause"] length]) { + [tip setString:[[filter objectForKey:@"Clause"] stringByReplacingOccurrencesOfRegex:@"(? [values count]) { + SPLog(@"filter operator %@ requires %ld arguments, but only have %ld stored values!",op,numOfArgs,[values count]); + goto fail; + } + + // otherwise add them + for (NSUInteger i = 0; i < numOfArgs; ++i) { + // insert connector node between args? + if(i > 0) { + ConnectorNode *node = [[ConnectorNode alloc] init]; + [node setFilter:[op filter]]; + [node setLabelIndex:(i-1)]; // label 0 follows argument 0 + [criteria addObject:node]; + [node release]; + } + ArgNode *arg = [[ArgNode alloc] init]; + [arg setArgIndex:i]; + [arg setFilter:[op filter]]; + [arg setInitialValue:[values objectAtIndex:i]]; + [criteria addObject:arg]; + [arg release]; + } + + [obj setObject:criteria forKey:@"criteria"]; + + //the last thing that remains is creating the displayValues for all criteria + NSMutableArray *displayValues = [NSMutableArray arrayWithCapacity:[criteria count]]; + for(id criterion in criteria) { + id dispValue = [self ruleEditor:filterRuleEditor displayValueForCriterion:criterion inRow:-1]; + if(!dispValue) { + SPLog(@"got nil displayValue for criterion %@ on deserialization!",criterion); + goto fail; + } + [displayValues addObject:dispValue]; + } + [obj setObject:displayValues forKey:@"displayValues"]; + } + + return [obj autorelease]; + +fail: + [obj release]; + return nil; +} + ++ (NSDictionary *)makeSerializedFilterForColumn:(NSString *)colName operator:(NSString *)opName values:(NSArray *)values +{ + return @{ + SerFilterClass: SerFilterClassExpression, + SerFilterExprColumn: colName, + SerFilterExprComparison: opName, + SerFilterExprValues: values, + }; +} + +- (ColumnNode *)_columnForName:(NSString *)name +{ + if([name length]) { + for (ColumnNode *col in columns) { + if ([name isEqualToString:[col name]]) return col; + } + } + return nil; +} + +- (OpNode *)_operatorNamed:(NSString *)title forColumn:(ColumnNode *)col +{ + if([title length]) { + // check if we have the operator cache, otherwise build it + if(![col operatorCache]) { + NSArray *ops = [self _compareTypesForColumn:col]; + [col setOperatorCache:ops]; + } + // try to find it in the operator cache + for(OpNode *node in [col operatorCache]) { + if([[[node filter] objectForKey:@"MenuLabel"] isEqualToString:title]) return node; + } + } + return nil; +} + +BOOL SerIsGroup(NSDictionary *dict) +{ + return [SerFilterClassGroup isEqual:[dict objectForKey:SerFilterClass]]; +} + +/** + * This method looks at the given serialized filter in a recursive manner and + * when it encounters + * - a group node with only a single child or + * - a child that is a group node of the same kind as the parent one + * it will pull the child(ren) up + * + * So for example: + * AND(expr1) => expr1 + * AND(expr1,AND(expr2,expr3)) => AND(expr1,expr2,expr3) + * + * The input dict is not modified, the returned dict will be equal to the input + * dict or have parts of it removed or replaced with new dicts. + */ ++ (NSDictionary *)_flattenSerializedFilter:(NSDictionary *)in +{ + // return non-group-nodes as is + if(!SerIsGroup(in)) return in; + + NSNumber *inIsConjunction = [in objectForKey:SerFilterGroupIsConjunction]; + + // first give all children the chance to flatten (depth first) + NSArray *children = [in objectForKey:SerFilterGroupChildren]; + NSMutableArray *flatChildren = [NSMutableArray arrayWithCapacity:[children count]]; + NSUInteger changed = 0; + for(NSDictionary *child in children) { + NSDictionary *flattened = [self _flattenSerializedFilter:child]; + //take a closer look at the (possibly changed) child - is it a group node of the same kind as us? + if(SerIsGroup(flattened) && [inIsConjunction isEqual:[flattened objectForKey:SerFilterGroupIsConjunction]]) { + [flatChildren addObjectsFromArray:[flattened objectForKey:SerFilterGroupChildren]]; + changed++; + } + else if(flattened != child) { + changed++; + } + [flatChildren addObject:flattened]; + } + // if there is only a single child, return it (flattening) + if([flatChildren count] == 1) return [flatChildren objectAtIndex:0]; + // if none of the children changed return the original input + if(!changed) return in; + // last variant: some of our children changed, but we remain + return @{ + SerFilterClass: SerFilterClassGroup, + SerFilterGroupIsConjunction: inIsConjunction, + SerFilterGroupChildren: flatChildren + }; +} + ++ (void)_writeFilterTree:(NSDictionary *)in toString:(NSMutableString *)out wrapInParenthesis:(BOOL)wrap binary:(BOOL)isBINARY error:(NSError **)err +{ + NSError *myErr = nil; + + if(wrap) [out appendString:@"("]; + + if(SerIsGroup(in)) { + BOOL isConjunction = [[in objectForKey:SerFilterGroupIsConjunction] boolValue]; + NSString *connector = isConjunction ? @"AND" : @"OR"; + BOOL first = YES; + NSArray *children = [in objectForKey:SerFilterGroupChildren]; + for(NSDictionary *child in children) { + if(!first) [out appendFormat:@" %@ ",connector]; + else first = NO; + // if the child is a group node but of a different kind we want to wrap it in order to prevent operator precedence confusion + // expression children will always be wrapped for clarity, except if there is only a single one and we are already wrapped + BOOL wrapChild = YES; + if(SerIsGroup(child)) { + BOOL childIsConjunction = [[child objectForKey:SerFilterGroupIsConjunction] boolValue]; + if(isConjunction == childIsConjunction) wrapChild = NO; + } + else { + if(wrap && [children count] == 1) wrapChild = NO; + } + [self _writeFilterTree:child toString:out wrapInParenthesis:wrapChild binary:isBINARY error:&myErr]; + if(myErr) { + if(err) *err = myErr; + return; + } + } + } + else { + // finally - build a SQL filter expression + NSDictionary *filter = [in objectForKey:SerFilterExprDefinition]; + if(!filter) { + if(err) *err = [NSError errorWithDomain:SPErrorDomain code:0 userInfo:@{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Fatal error while retrieving content filter. No filter definition found.", @"filter to sql conversion : internal error : 0"), + }]; + return; + } + + if(![filter objectForKey:@"NumberOfArguments"]) { + if(err) *err = [NSError errorWithDomain:SPErrorDomain code:1 userInfo:@{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Error while retrieving filter clause. No “NumberOfArguments” key found.", @"filter to sql conversion : internal error : invalid filter definition (1)"), + }]; + return; + } + + if(![filter objectForKey:@"Clause"] || ![(NSString *)[filter objectForKey:@"Clause"] length]) { + if(err) *err = [NSError errorWithDomain:SPErrorDomain code:2 userInfo:@{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Content Filter clause is empty.", @"filter to sql conversion : internal error : invalid filter definition (2)"), + }]; + return; + } + + NSArray *values = [in objectForKey:SerFilterExprValues]; + + SPTableFilterParser *parser = [[SPTableFilterParser alloc] initWithFilterClause:[filter objectForKey:@"Clause"] + numberOfArguments:[[filter objectForKey:@"NumberOfArguments"] integerValue]]; + [parser setArgument:[values objectOrNilAtIndex:0]]; + [parser setFirstBetweenArgument:[values objectOrNilAtIndex:0]]; + [parser setSecondBetweenArgument:[values objectOrNilAtIndex:1]]; + [parser setSuppressLeadingTablePlaceholder:[[filter objectForKey:@"SuppressLeadingFieldPlaceholder"] boolValue]]; + [parser setCaseSensitive:isBINARY]; + [parser setCurrentField:[in objectForKey:SerFilterExprColumn]]; + + NSString *sql = [parser filterString]; + // SPTableFilterParser will return nil if it doesn't like the arguments and NSMutableString doesn't like nil + if(!sql) { + if(err) *err = [NSError errorWithDomain:SPErrorDomain code:3 userInfo:@{ + NSLocalizedDescriptionKey: NSLocalizedString(@"No valid SQL expression could be generated. Make sure that you have filled in all required fields.", @"filter to sql conversion : internal error : SPTableFilterParser failed"), + }]; + [parser release]; + return; + } + [out appendString:sql]; + + [parser release]; + } + + if(wrap) [out appendString:@")"]; +} + +@end + +#pragma mark - + +@implementation RuleNode + +@synthesize type = type; + +- (NSUInteger)hash { + return type; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (other && [[other class] isEqual:[self class]] && [(RuleNode *)other type] == type) return YES; + + return NO; +} + +@end + +@implementation ColumnNode + +@synthesize name = name; +@synthesize typegrouping = typegrouping; +@synthesize operatorCache = operatorCache; + +- (instancetype)init +{ + if((self = [super init])) { + type = RuleNodeTypeColumn; + } + return self; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"ColumnNode<%@@%p>",[self name],self]; +} + +- (NSUInteger)hash { + return ([name hash] ^ [typegrouping hash] ^ [super hash]); +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (other && [[other class] isEqual:[self class]] && [name isEqualToString:[other name]] && [typegrouping isEqualToString:[other typegrouping]]) return YES; + + return NO; +} + +@end + + +@implementation StringNode + +@synthesize value = value; + +- (instancetype)init +{ + if((self = [super init])) { + type = RuleNodeTypeString; + } + return self; +} + +- (NSUInteger)hash { + return ([value hash] ^ [super hash]); +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (other && [[other class] isEqual:[self class]] && [value isEqualToString:[(StringNode *)other value]]) return YES; + + return NO; +} + +@end + +@implementation OpNode + +@synthesize parentColumn = parentColumn; +@synthesize settings = settings; +@synthesize filter = filter; + +- (instancetype)init +{ + if((self = [super init])) { + type = RuleNodeTypeOperator; + } + return self; +} + +- (void)dealloc +{ + [self setFilter:nil]; + [self setSettings:nil]; + [super dealloc]; +} + +- (NSUInteger)hash { + return (([parentColumn hash] << 16) ^ [settings hash] ^ [super hash]); +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (other && [[other class] isEqual:[self class]] && [settings isEqualToDictionary:[(OpNode *)other settings]] && [parentColumn isEqual:[other parentColumn]]) return YES; + + return NO; +} + +@end + +@implementation ArgNode + +@synthesize filter = filter; +@synthesize argIndex = argIndex; +@synthesize initialValue = initialValue; + +- (instancetype)init +{ + if((self = [super init])) { + type = RuleNodeTypeArgument; + } + return self; +} + +- (void)dealloc +{ + [self setInitialValue:nil]; + [self setFilter:nil]; + [super dealloc]; +} + +- (NSUInteger)hash { + // initialValue does not count towards hash because two Args are not different if only the initialValue differs + return ((argIndex << 16) ^ [filter hash] ^ [super hash]); +} + +- (BOOL)isEqual:(id)other { + // initialValue does not count towards isEqual: because two Args are not different if only the initialValue differs + if (other == self) return YES; + if (other && [[other class] isEqual:[self class]] && [filter isEqualToDictionary:[(ArgNode *)other filter]] && argIndex == [(ArgNode *)other argIndex]) return YES; + + return NO; +} + +@end + +@implementation ConnectorNode + +@synthesize filter = filter; +@synthesize labelIndex = labelIndex; + +- (instancetype)init +{ + if((self = [super init])) { + type = RuleNodeTypeConnector; + } + return self; +} + +- (void)dealloc +{ + [self setFilter:nil]; + [super dealloc]; +} + +- (NSUInteger)hash { + return ((labelIndex << 16) ^ [filter hash] ^ [super hash]); +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (other && [[other class] isEqual:[self class]] && [filter isEqualToDictionary:[(ConnectorNode *)other filter]] && labelIndex == [(ConnectorNode *)other labelIndex]) return YES; + + return NO; +} + +@end diff --git a/Source/SPTableContent.h b/Source/SPTableContent.h index 1d5390bb..30b4c587 100644 --- a/Source/SPTableContent.h +++ b/Source/SPTableContent.h @@ -43,7 +43,7 @@ @class SPDatabaseDocument; @class SPTablesList; @class SPTableStructure; -@class SPTableContentFilterController; +@class SPRuleFilterController; @class SPFilterTableController; typedef NS_ENUM(NSInteger, SPTableContentFilterSource) { @@ -96,7 +96,7 @@ typedef NS_ENUM(NSInteger, SPTableContentFilterSource) { #ifndef SP_CODA IBOutlet NSStepper *paginationPageStepper; - IBOutlet SPTableContentFilterController *filterControllerInstance; + IBOutlet SPRuleFilterController *ruleFilterController; IBOutlet SPFilterTableController *filterTableController; BOOL scrollViewHasRubberbandScrolling; #endif diff --git a/Source/SPTableContent.m b/Source/SPTableContent.m index b27200c0..750a4a52 100644 --- a/Source/SPTableContent.m +++ b/Source/SPTableContent.m @@ -56,7 +56,7 @@ #import "SPThreadAdditions.h" #import "SPTableFilterParser.h" #import "SPFunctions.h" -#import "SPTableContentFilterController.h" +#import "SPRuleFilterController.h" #import "SPFilterTableController.h" #import @@ -235,20 +235,20 @@ static void *TableContentKVOContext = &TableContentKVOContext; // Add observer to change view sizes with filter rule editor [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(filterRuleEditorPreferredSizeChanged:) - name:SPTableContentFilterHeightChangedNotification - object:filterControllerInstance]; + name:SPRuleFilterHeightChangedNotification + object:ruleFilterController]; [contentAreaContainer setPostsFrameChangedNotifications:YES]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentViewSizeChanged:) name:NSViewFrameDidChangeNotification object:contentAreaContainer]; - [filterControllerInstance setTarget:self]; - [filterControllerInstance setAction:@selector(filterTable:)]; + [ruleFilterController setTarget:self]; + [ruleFilterController setAction:@selector(filterTable:)]; [filterTableController setTarget:self]; [filterTableController setAction:@selector(filterTable:)]; //TODO This is only needed for 10.6 compatibility - scrollViewHasRubberbandScrolling = [[[filterControllerInstance view] enclosingScrollView] respondsToSelector:@selector(setVerticalScrollElasticity:)]; + scrollViewHasRubberbandScrolling = [[[ruleFilterController view] enclosingScrollView] respondsToSelector:@selector(setVerticalScrollElasticity:)]; // Add observers for document task activity [[NSNotificationCenter defaultCenter] addObserver:self @@ -362,7 +362,7 @@ static void *TableContentKVOContext = &TableContentKVOContext; [self setRuleEditorVisible:NO animate:NO]; [toggleRuleFilterButton setEnabled:NO]; [toggleRuleFilterButton setState:NSOffState]; - [filterControllerInstance setColumns:nil]; + [ruleFilterController setColumns:nil]; // Disable pagination [paginationPreviousButton setEnabled:NO]; @@ -634,9 +634,9 @@ static void *TableContentKVOContext = &TableContentKVOContext; [filterTableController setColumns:dataColumns]; // Enable and initialize filter fields (with tags for position of menu item and field position) - [filterControllerInstance setColumns:dataColumns]; + [ruleFilterController setColumns:dataColumns]; // Restore preserved filter settings if appropriate and valid - [filterControllerInstance restoreSerializedFilters:filtersToRestore]; + [ruleFilterController restoreSerializedFilters:filtersToRestore]; // hide/show the rule filter editor, based on its previous state (so that it says visible when switching tables, if someone has enabled it and vice versa) if(showFilterRuleEditor) { [self setRuleEditorVisible:YES animate:NO]; @@ -646,7 +646,7 @@ static void *TableContentKVOContext = &TableContentKVOContext; [self setRuleEditorVisible:NO animate:NO]; [toggleRuleFilterButton setState:NSOffState]; } - [filterControllerInstance setEnabled:enableInteraction]; + [ruleFilterController setEnabled:enableInteraction]; [toggleRuleFilterButton setEnabled:enableInteraction]; // restore the filter to the previously choosen one for the table activeFilter = activeFilterToRestore; @@ -1014,7 +1014,7 @@ static void *TableContentKVOContext = &TableContentKVOContext; BOOL caseSensitive = (([[NSApp currentEvent] modifierFlags] & NSShiftKeyMask) > 0); NSError *err = nil; - NSString *filter = [filterControllerInstance sqlWhereExpressionWithBinary:caseSensitive error:&err]; + NSString *filter = [ruleFilterController sqlWhereExpressionWithBinary:caseSensitive error:&err]; if(err) { SPOnewayAlertSheet( NSLocalizedString(@"Invalid Filter", @"table content : apply filter : invalid filter message title"), @@ -1256,7 +1256,7 @@ static void *TableContentKVOContext = &TableContentKVOContext; } // If a button other than the pagination buttons was used, set the active filter type to // the standard filter field. - else if (sender == filterControllerInstance) { + else if (sender == ruleFilterController) { activeFilter = SPTableContentFilterSourceRuleFilter; resetPaging = YES; } @@ -1349,12 +1349,12 @@ static void *TableContentKVOContext = &TableContentKVOContext; { // we can't change the state of the button here, because the mouse click already changed it if(show) { - if([filterControllerInstance isEmpty]) { - [filterControllerInstance addFilterExpression]; + if([ruleFilterController isEmpty]) { + [ruleFilterController addFilterExpression]; // the sizing will be updated automatically by adding a row } else { - [self updateFilterRuleEditorSize:[filterControllerInstance preferredHeight] animate:animate]; + [self updateFilterRuleEditorSize:[ruleFilterController preferredHeight] animate:animate]; } } else { @@ -2388,14 +2388,14 @@ static void *TableContentKVOContext = &TableContentKVOContext; else if(navigateAsHex) filterComparison = @"= (Hex String)"; // Store the filter details to use when loading the target table - NSDictionary *filterSettings = [SPTableContentFilterController makeSerializedFilterForColumn:[refDictionary objectForKey:@"column"] + NSDictionary *filterSettings = [SPRuleFilterController makeSerializedFilterForColumn:[refDictionary objectForKey:@"column"] operator:filterComparison values:@[targetFilterValue]]; // If the link is within the current table, apply filter settings manually if ([[refDictionary objectForKey:@"table"] isEqualToString:selectedTable]) { SPMainQSync(^{ - [filterControllerInstance restoreSerializedFilters:filterSettings]; + [ruleFilterController restoreSerializedFilters:filterSettings]; [self setRuleEditorVisible:YES animate:YES]; activeFilter = SPTableContentFilterSourceRuleFilter; }); @@ -3337,7 +3337,7 @@ static void *TableContentKVOContext = &TableContentKVOContext; */ - (NSDictionary *) filterSettings { - return [filterControllerInstance serializedFilter]; + return [ruleFilterController serializedFilter]; } /** @@ -3445,7 +3445,7 @@ static void *TableContentKVOContext = &TableContentKVOContext; NSRect contentAreaRect = [contentAreaContainer frame]; CGFloat availableHeight = contentAreaRect.size.height; - NSRect ruleEditorRect = [[[filterControllerInstance view] enclosingScrollView] frame]; + NSRect ruleEditorRect = [[[ruleFilterController view] enclosingScrollView] frame]; //adjust for the UI elements below the rule editor, but only if the view height should not be 0 (ie. hidden) CGFloat containerRequestedHeight = requestedHeight ? requestedHeight + ruleEditorRect.origin.y : 0; @@ -3472,18 +3472,18 @@ static void *TableContentKVOContext = &TableContentKVOContext; [NSAnimationContext beginGrouping]; [[tableContentContainer animator] setFrame:bottomContainerRect]; [[filterRuleEditorContainer animator] setFrame:topContainerRect]; - [[[[filterControllerInstance view] enclosingScrollView] animator] setFrame:ruleEditorRect]; + [[[[ruleFilterController view] enclosingScrollView] animator] setFrame:ruleEditorRect]; [NSAnimationContext endGrouping]; } else { [tableContentContainer setFrameSize:bottomContainerRect.size]; [filterRuleEditorContainer setFrame:topContainerRect]; - [[[filterControllerInstance view] enclosingScrollView] setFrame:ruleEditorRect]; + [[[ruleFilterController view] enclosingScrollView] setFrame:ruleEditorRect]; } //disable rubberband scrolling as long as there is nothing to scroll if(scrollViewHasRubberbandScrolling) { - NSScrollView *filterControllerScroller = [[filterControllerInstance view] enclosingScrollView]; + NSScrollView *filterControllerScroller = [[ruleFilterController view] enclosingScrollView]; if (ruleEditorRect.size.height >= requestedHeight) { [filterControllerScroller setVerticalScrollElasticity:NSScrollElasticityNone]; } else { @@ -3495,14 +3495,14 @@ static void *TableContentKVOContext = &TableContentKVOContext; - (void)filterRuleEditorPreferredSizeChanged:(NSNotification *)notification { if(showFilterRuleEditor) { - [self updateFilterRuleEditorSize:[filterControllerInstance preferredHeight] animate:YES]; + [self updateFilterRuleEditorSize:[ruleFilterController preferredHeight] animate:YES]; } } - (void)contentViewSizeChanged:(NSNotification *)notification { if(showFilterRuleEditor) { - [self updateFilterRuleEditorSize:[filterControllerInstance preferredHeight] animate:NO]; + [self updateFilterRuleEditorSize:[ruleFilterController preferredHeight] animate:NO]; } } @@ -3627,7 +3627,7 @@ static void *TableContentKVOContext = &TableContentKVOContext; [removeButton setEnabled:NO]; [duplicateButton setEnabled:NO]; [reloadButton setEnabled:NO]; - [filterControllerInstance setEnabled:NO]; + [ruleFilterController setEnabled:NO]; [toggleRuleFilterButton setEnabled:NO]; tableRowsSelectable = NO; [paginationPreviousButton setEnabled:NO]; @@ -3663,7 +3663,7 @@ static void *TableContentKVOContext = &TableContentKVOContext; } } - [filterControllerInstance setEnabled:(!![selectedTable length])]; + [ruleFilterController setEnabled:(!![selectedTable length])]; [toggleRuleFilterButton setEnabled:(!![selectedTable length])]; tableRowsSelectable = YES; } @@ -3931,7 +3931,7 @@ static void *TableContentKVOContext = &TableContentKVOContext; { [self setRuleEditorVisible:YES animate:YES]; [toggleRuleFilterButton setState:NSOnState]; - [filterControllerInstance focusFirstInputField]; + [ruleFilterController focusFirstInputField]; } #endif diff --git a/Source/SPTableContentFilterController.h b/Source/SPTableContentFilterController.h deleted file mode 100644 index 3fe5e8ea..00000000 --- a/Source/SPTableContentFilterController.h +++ /dev/null @@ -1,163 +0,0 @@ -// -// SPTableContentFilterController.h -// sequel-pro -// -// Created by Max Lohrmann on 04.05.18. -// Copyright (c) 2018 Max Lohrmann. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// -// More info at - -@class SPTableData; -@class SPDatabaseDocument; -@class SPTablesList; -@class SPContentFilterManager; - -NSString * const SPTableContentFilterHeightChangedNotification; - -@interface SPTableContentFilterController : NSObject { - IBOutlet NSRuleEditor *filterRuleEditor; - IBOutlet SPTableData *tableDataInstance; - IBOutlet SPDatabaseDocument *tableDocumentInstance; - IBOutlet SPTablesList *tablesListInstance; - IBOutlet NSView *tableContentViewBelow; - IBOutlet NSButton *filterButton; - - NSMutableArray *columns; - NSMutableDictionary *contentFilters; - NSMutableDictionary *numberOfDefaultFilters; - - NSMutableArray *model; - - SPContentFilterManager *contentFilterManager; - - CGFloat preferredHeight; - - id target; - SEL action; - - BOOL enabled; -} - -/** - * Returns the rule editor view that is managed by this object - * - * SHOULD be called on the UI thread, or results may be inconsistent! - */ -- (NSRuleEditor *)view; - -/** - * Makes the first NSTextField found in the rule editor the first responder - * - * MUST BE CALLED ON THE UI THREAD! - */ -- (void)focusFirstInputField; - -/** - * Will reconfigure the columns of the rule editor from the given array. - * Call with nil to reset the editor to its initial empty state. - * Existing rows will be removed in any case! - * - * MUST BE CALLED ON THE UI THREAD! - */ -- (void)setColumns:(NSArray *)dataColumns; - -/** - * Converts the current filter expression displayed in the UI into an - * SQL string suitable for use in a WHERE clause. - * - * @param isBINARY Indicates that the filter should use the BINARY qualifier for ignoring - * collations during search. - * @param err Upon return contains and object describing why the SQL conversion failed, - * if it failed or nil, if no errors occured. - * - * MUST BE CALLED ON THE UI THREAD! - */ -- (NSString *)sqlWhereExpressionWithBinary:(BOOL)isBINARY error:(NSError **)err; - -/** - * Returns the current filter configuration in a serialized form that can be exported and - * reapplied later. - * - * MUST BE CALLED ON THE UI THREAD! - */ -- (NSDictionary *)serializedFilter; - -/** - * Restores the filter rule configuration from a given dictionary. - * The current column configuration must match the schema that was used when generating - * the serialized data, otherwise the invalid rules will be ignored. - * - * @param serialized A dictionary previously generated by calling -serializedFilter. - * @return A serialized filter - * - * MUST BE CALLED ON THE UI THREAD! - */ -- (void)restoreSerializedFilters:(NSDictionary *)serialized; - -/** - * Create a serialized filter from a given column, operator and operand. - * This is used when navigating foreign key links between tables to create the filter for the target table. - * - * @param colName Name of the column to filter (left side operand) - * @param opName Name of the filter (operator) - * @param values The values to filter with (right side operand) - * @return A serialized filter - * - * This method is thread-safe. - */ -+ (NSDictionary *)makeSerializedFilterForColumn:(NSString *)colName operator:(NSString *)opName values:(NSArray *)values; - -/** - * The view height the rule editor needs in order to not have to resort to scrollbars - * - * SHOULD be called on the UI thread, or results may be inconsistent! - */ -@property (readonly, assign, nonatomic) CGFloat preferredHeight; - -/** - * Indicates whether the rule editor has no filter expressions - * - * SHOULD be called on the UI thread, or results may be inconsistent! - */ -- (BOOL)isEmpty; - -/** - * Adds a new row to the rule editor - * - * MUST BE CALLED ON THE UI THREAD! - */ -- (void)addFilterExpression; - -/** - * Used when the rule editor wants to trigger filtering - * - * SHOULD be called on the UI thread, or results may be inconsistent! - */ -@property (assign, nonatomic) id target; -@property (assign, nonatomic) SEL action; - -- (BOOL)isEnabled; -- (void)setEnabled:(BOOL)enabled; - -@end diff --git a/Source/SPTableContentFilterController.m b/Source/SPTableContentFilterController.m deleted file mode 100644 index a2d6b2d2..00000000 --- a/Source/SPTableContentFilterController.m +++ /dev/null @@ -1,1355 +0,0 @@ -// -// SPTableContentFilterController.m -// sequel-pro -// -// Created by Max Lohrmann on 04.05.18. -// Copyright (c) 2018 Max Lohrmann. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// -// More info at - -#import "SPTableContentFilterController.h" -#import "SPQueryController.h" -#import "SPDatabaseDocument.h" -#import "RegexKitLite.h" -#import "SPContentFilterManager.h" -#import "SPFunctions.h" -#import "SPTableFilterParser.h" - -typedef NS_ENUM(NSInteger, RuleNodeType) { - RuleNodeTypeColumn, - RuleNodeTypeString, - RuleNodeTypeOperator, - RuleNodeTypeArgument, - RuleNodeTypeConnector, -}; - -NSString * const SPTableContentFilterHeightChangedNotification = @"SPTableContentFilterHeightChanged"; - -/** - * The type of filter rule that the current item represents. - */ -const NSString * const SerFilterClass = @"filterClass"; -/** - * The current rule is a group row (an "AND" or "OR" expression with children) - */ -const NSString * const SerFilterClassGroup = @"groupNode"; -/** - * The current rule is a filter expression - */ -const NSString * const SerFilterClassExpression = @"expressionNode"; -/** - * Group Nodes only: - * Indicates whether the group is a conjunction. - * If YES, the children will be combined using "AND", otherwise using "OR". - */ -const NSString * const SerFilterGroupIsConjunction = @"isConjunction"; -/** - * Group Nodes only: - * An array of child filter rules (which again can be group or expression rules) - */ -const NSString * const SerFilterGroupChildren = @"children"; -/** - * Expression Nodes only: - * The name of the column to filter in (left side expression) - * - * Legacy names: - * @"filterField", fieldField - */ -const NSString * const SerFilterExprColumn = @"column"; -/** - * Expression Nodes only: - * The data type grouping of the column for applicable filters - */ -const NSString * const SerFilterExprType = @"filterType"; -/** - * Expression Nodes only: - * The title of the filter operator to apply - * - * Legacy names: - * @"filterComparison", compareField - */ -const NSString * const SerFilterExprComparison = @"filterComparison"; -/** - * Expression Nodes only: - * The values to apply the filter with (an array of 0 or more elements) - * - * Legacy names: - * @"filterValue", argumentField - * @"firstBetweenField", @"secondBetweenField", firstBetweenField, secondBetweenField - */ -const NSString * const SerFilterExprValues = @"filterValues"; -/** - * Expression Nodes only: - * the filter definition dictionary (as in ContentFilters.plist) - * for the filter represented by SerFilterExprComparison. - * - * This item is not designed to be serialized to disk - */ -const NSString * const SerFilterExprDefinition = @"_filterDefinition"; - -#pragma mark - - -@interface RuleNode : NSObject { - RuleNodeType type; -} -@property(assign, nonatomic) RuleNodeType type; -@end - -@interface ColumnNode : RuleNode { - NSString *name; - NSString *typegrouping; - NSArray *operatorCache; -} -@property(copy, nonatomic) NSString *name; -@property(copy, nonatomic) NSString *typegrouping; -@property(retain, nonatomic) NSArray *operatorCache; -@end - -@interface StringNode : RuleNode { - NSString *value; -} -@property(copy, nonatomic) NSString *value; -@end - -@interface OpNode : RuleNode { - // Note: The main purpose of this field is to have @"=" for column A and @"=" for column B to return NO in -isEqual: - // because otherwise NSRuleEditor will get confused and blow up. - ColumnNode *parentColumn; - NSDictionary *settings; - NSDictionary *filter; -} -@property (assign, nonatomic) ColumnNode *parentColumn; -@property (retain, nonatomic) NSDictionary *settings; -@property (retain, nonatomic) NSDictionary *filter; -@end - -@interface ArgNode : RuleNode { - NSDictionary *filter; - NSUInteger argIndex; - NSString *initialValue; -} -@property (copy, nonatomic) NSString *initialValue; -@property (retain, nonatomic) NSDictionary *filter; -@property (assign, nonatomic) NSUInteger argIndex; -@end - -@interface ConnectorNode : RuleNode { - NSDictionary *filter; - NSUInteger labelIndex; -} -@property (retain, nonatomic) NSDictionary *filter; -@property (assign, nonatomic) NSUInteger labelIndex; -@end - -#pragma mark - - -@interface SPTableContentFilterController () - -@property (readwrite, assign, nonatomic) CGFloat preferredHeight; - -// This is the binding used by NSRuleEditor for the current state -@property (retain, nonatomic) NSMutableArray *model; - -- (NSArray *)_compareTypesForColumn:(ColumnNode *)colNode; -- (IBAction)_textFieldAction:(id)sender; -- (IBAction)_editFiltersAction:(id)sender; -- (void)_contentFiltersHaveBeenUpdated:(NSNotification *)notification; -+ (NSDictionary *)_flattenSerializedFilter:(NSDictionary *)in; -static BOOL SerIsGroup(NSDictionary *dict); -- (NSDictionary *)_serializedFilterIncludingFilterDefinition:(BOOL)includeDefinition; -+ (void)_writeFilterTree:(NSDictionary *)in toString:(NSMutableString *)out wrapInParenthesis:(BOOL)wrap binary:(BOOL)isBINARY error:(NSError **)err; -- (NSMutableDictionary *)_restoreSerializedFilter:(NSDictionary *)serialized; -static void _addIfNotNil(NSMutableArray *array, id toAdd); -- (ColumnNode *)_columnForName:(NSString *)name; -- (OpNode *)_operatorNamed:(NSString *)title forColumn:(ColumnNode *)col; -- (BOOL)_focusOnFieldInSubtree:(NSDictionary *)dict; -- (void)_resize; -- (void)openContentFilterManagerForFilterType:(NSString *)filterType; -- (IBAction)filterTable:(id)sender; - -@end - -@implementation SPTableContentFilterController - -@synthesize model = model; -@synthesize preferredHeight = preferredHeight; -@synthesize target = target; -@synthesize action = action; - -- (instancetype)init -{ - if((self = [super init])) { - columns = [[NSMutableArray alloc] init]; - model = [[NSMutableArray alloc] init]; - preferredHeight = 0.0; - target = nil; - action = NULL; - - // Init default filters for Content Browser - contentFilters = [[NSMutableDictionary alloc] init]; - numberOfDefaultFilters = [[NSMutableDictionary alloc] init]; - - NSError *readError = nil; - NSString *filePath = [NSBundle pathForResource:@"ContentFilters.plist" ofType:nil inDirectory:[[NSBundle mainBundle] bundlePath]]; - NSData *defaultFilterData = [NSData dataWithContentsOfFile:filePath - options:NSMappedRead - error:&readError]; - - if(defaultFilterData && !readError) { - NSDictionary *defaultFilterDict = [NSPropertyListSerialization propertyListWithData:defaultFilterData - options:NSPropertyListMutableContainersAndLeaves - format:NULL - error:&readError]; - - if(defaultFilterDict && !readError) { - [contentFilters setDictionary:defaultFilterDict]; - } - } - - if (readError) { - NSLog(@"Error while reading 'ContentFilters.plist':\n%@", readError); - NSBeep(); - } - else { - [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"number"] count]] forKey:@"number"]; - [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"date"] count]] forKey:@"date"]; - [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"string"] count]] forKey:@"string"]; - [numberOfDefaultFilters setObject:[NSNumber numberWithInteger:[[contentFilters objectForKey:@"spatial"] count]] forKey:@"spatial"]; - } - } - return self; -} - -- (void)awakeFromNib -{ - [filterRuleEditor bind:@"rows" toObject:self withKeyPath:@"model" options:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(_contentFiltersHaveBeenUpdated:) - name:SPContentFiltersHaveBeenUpdatedNotification - object:nil]; -} - -- (void)focusFirstInputField -{ - for(NSDictionary *rootItem in model) { - if([self _focusOnFieldInSubtree:rootItem]) return; - } -} - -- (BOOL)_focusOnFieldInSubtree:(NSDictionary *)dict -{ - //if we are a simple row we might have an input field ourself, otherwise search among our children - if([[dict objectForKey:@"rowType"] unsignedIntegerValue] == NSRuleEditorRowTypeSimple) { - for(id obj in [dict objectForKey:@"displayValues"]) { - if([obj isKindOfClass:[NSTextField class]]) { - [[(NSTextField *)obj window] makeFirstResponder:obj]; - return YES; - } - } - } - else { - for(NSDictionary *child in [dict objectForKey:@"subrows"]) { - if([self _focusOnFieldInSubtree:child]) return YES; - } - } - return NO; -} - -- (void)setColumns:(NSArray *)dataColumns; -{ - [self willChangeValueForKey:@"model"]; // manual KVO is needed for filter rule editor to notice change - [model removeAllObjects]; - [self didChangeValueForKey:@"model"]; - - [columns removeAllObjects]; - - //without a table there is nothing to filter - if(dataColumns) { - //sort column names if enabled - NSArray *columnDefinitions = dataColumns; - if ([[NSUserDefaults standardUserDefaults] boolForKey:SPAlphabeticalTableSorting]) { - NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]; - columnDefinitions = [columnDefinitions sortedArrayUsingDescriptors:@[sortDescriptor]]; - } - - // get the columns - for (NSDictionary *colDef in columnDefinitions) { - ColumnNode *node = [[ColumnNode alloc] init]; - [node setName:[colDef objectForKey:@"name"]]; - [node setTypegrouping:[colDef objectForKey:@"typegrouping"]]; - [columns addObject:node]; - [node release]; - } - } - - // make the rule editor reload the criteria - [filterRuleEditor reloadCriteria]; - - // disable UI if no criteria exist - [self setEnabled:([columns count] != 0)]; -} - -- (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 [columns count]; - } - else if(rowType == NSRuleEditorRowTypeSimple) { - // the children of the columns are their operators - RuleNodeType type = [(RuleNode *)criterion type]; - if(type == RuleNodeTypeColumn) { - ColumnNode *node = (ColumnNode *)criterion; - if(![node operatorCache]) { - NSArray *ops = [self _compareTypesForColumn:node]; - [node setOperatorCache:ops]; - } - return [[node operatorCache] count]; - } - // the first child of an operator is the first argument (if it has one) - else if(type == RuleNodeTypeOperator) { - OpNode *node = (OpNode *)criterion; - NSInteger numOfArgs = [[[node filter] objectForKey:@"NumberOfArguments"] integerValue]; - return (numOfArgs > 0) ? 1 : 0; - } - // the child of an argument can only be the conjunction label if more arguments follow - else if(type == RuleNodeTypeArgument) { - ArgNode *node = (ArgNode *)criterion; - NSInteger numOfArgs = [[[node filter] objectForKey:@"NumberOfArguments"] integerValue]; - return (numOfArgs > [node argIndex]+1) ? 1 : 0; - } - // the child of a conjunction is the next argument, if we have one - else if(type == RuleNodeTypeConnector) { - ConnectorNode *node = (ConnectorNode *)criterion; - NSInteger numOfArgs = [[[node filter] objectForKey:@"NumberOfArguments"] integerValue]; - return (numOfArgs > [node labelIndex]+1) ? 1 : 0; - } - } - return 0; -} - -- (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; - } - return [node autorelease]; - } - // this is the column field - else if(!criterion && rowType == NSRuleEditorRowTypeSimple) { - return [columns objectAtIndex:index]; - } - else if(rowType == NSRuleEditorRowTypeSimple) { - // the children of the columns are their operators - RuleNodeType type = [(RuleNode *) criterion type]; - if (type == RuleNodeTypeColumn) { - return [[criterion operatorCache] objectAtIndex:index]; - } - // the first child of an operator is the first argument - else if(type == RuleNodeTypeOperator) { - NSDictionary *filter = [(OpNode *)criterion filter]; - if([[filter objectForKey:@"NumberOfArguments"] integerValue]) { - ArgNode *arg = [[ArgNode alloc] init]; - [arg setFilter:filter]; - [arg setArgIndex:0]; - return [arg autorelease]; - } - } - // the child of an argument can only be the conjunction label if more arguments follow - else if(type == RuleNodeTypeArgument) { - NSDictionary *filter = [(ArgNode *)criterion filter]; - NSUInteger argIndex = [(ArgNode *)criterion argIndex]; - if([[filter objectForKey:@"NumberOfArguments"] integerValue] > argIndex +1) { - ConnectorNode *node = [[ConnectorNode alloc] init]; - [node setFilter:filter]; - [node setLabelIndex:argIndex]; // label 0 follows argument 0 - return [node autorelease]; - } - } - // the child of a conjunction is the next argument, if we have one - else if(type == RuleNodeTypeConnector) { - ConnectorNode *node = (ConnectorNode *)criterion; - NSInteger numOfArgs = [[[node filter] objectForKey:@"NumberOfArguments"] integerValue]; - if(numOfArgs > [node labelIndex]+1) { - ArgNode *arg = [[ArgNode alloc] init]; - [arg setFilter:[node filter]]; - [arg setArgIndex:([node labelIndex]+1)]; - return [arg autorelease]; - } - } - } - return nil; -} - -- (id)ruleEditor:(NSRuleEditor *)editor displayValueForCriterion:(id)criterion inRow:(NSInteger)row -{ - switch([(RuleNode *)criterion type]) { - case RuleNodeTypeString: return [(StringNode *)criterion value]; - case RuleNodeTypeColumn: return [(ColumnNode *)criterion name]; - case RuleNodeTypeOperator: { - OpNode *node = (OpNode *)criterion; - NSMenuItem *item; - if ([[[node settings] objectForKey:@"isSeparator"] boolValue]) { - item = [NSMenuItem separatorItem]; - } - else { - item = [[NSMenuItem alloc] initWithTitle:[[node settings] objectForKey:@"title"] action:NULL keyEquivalent:@""]; - [item setToolTip:[[node settings] objectForKey:@"tooltip"]]; - [item setTag:[[[node settings] objectForKey:@"tag"] integerValue]]; - //TODO the following seems to be mentioned exactly nowhere on the internet/in documentation, but without it NSMenuItems won't work properly, even though Apple says they are supported - [item setRepresentedObject:@{ - @"item": node, - @"value": [item title], - // this one is needed by the "Edit filters…" item for context - @"filterType": SPBoxNil([[node settings] objectForKey:@"filterType"]), - }]; - // override the default action from the rule editor if given (used to open the edit content filters sheet) - id _target = [[node settings] objectForKey:@"target"]; - SEL _action = (SEL)[(NSValue *)[[node settings] objectForKey:@"action"] pointerValue]; - if(_target && _action) { - [item setTarget:_target]; - [item setAction:_action]; - } - [item autorelease]; - } - return item; - } - case RuleNodeTypeArgument: { - //an argument is a textfield - ArgNode *node = (ArgNode *)criterion; - NSTextField *textField = [[NSTextField alloc] init]; - [[textField cell] setSendsActionOnEndEditing:YES]; - [[textField cell] setUsesSingleLineMode:YES]; - [textField setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; - [textField sizeToFit]; - [textField setTarget:self]; - [textField setAction:@selector(_textFieldAction:)]; - if([node initialValue]) [textField setStringValue:[node initialValue]]; - NSRect frame = [textField frame]; - //adjust width, to make the field wider - frame.size.width = 500; //TODO determine a good width (possibly from the field type size) - how to access the rule editors bounds? - [textField setFrame:frame]; - return [textField autorelease]; - } - case RuleNodeTypeConnector: { - // a simple string for once - ConnectorNode *node = (ConnectorNode *)criterion; - NSArray* labels = [[node filter] objectForKey:@"ConjunctionLabels"]; - return (labels && [labels count] == 1)? [labels objectAtIndex:0] : @""; - } - } - - return nil; -} - -- (IBAction)_textFieldAction:(id)sender -{ - // if the action was caused by pressing return or enter, trigger filtering - NSEvent *event = [NSApp currentEvent]; - if(event && [event type] == NSKeyDown && ([event keyCode] == 36 || [event keyCode] == 76)) { - [self filterTable:nil]; - } -} - -- (IBAction)filterTable:(id)sender -{ - if(target && action) [target performSelector:action withObject:self]; -} - -- (void)_resize -{ - // The situation with the sizing is a bit f'ed up: - // - When -ruleEditorRowsDidChange: is invoked the NSRuleEditor has not yet updated its required frame size - // - We can't use KVO on -frame either, because SPTableContent will update the container size which - // ultimately also updates the NSRuleEditor's frame, causing a loop - // - Calling -sizeToFit works, but only when the NSRuleEditor is growing. It won't shrink - // after removing rows. - // - -intrinsicContentSize is what we want, but that method is 10.7+, so on 10.6 let's do the - // easiest workaround (note that both -intrinsicContentSize and -sizeToFit internally use -[NSRuleEditor _minimumFrameHeight]) - CGFloat wantsHeight; - if([filterRuleEditor respondsToSelector:@selector(intrinsicContentSize)]) { - NSSize sz = [filterRuleEditor intrinsicContentSize]; - wantsHeight = sz.height; - } - else { - wantsHeight = [filterRuleEditor rowHeight] * [filterRuleEditor numberOfRows]; - } - if(wantsHeight != preferredHeight) { - [self setPreferredHeight:wantsHeight]; - [[NSNotificationCenter defaultCenter] postNotificationName:SPTableContentFilterHeightChangedNotification object:self]; - } -} - -- (void)ruleEditorRowsDidChange:(NSNotification *)notification -{ - //TODO find a better way to trigger resize - // We can't do this here, because it will cause rows to jump around when removing them (the add case works fine, though) - [self performSelector:@selector(_resize) withObject:nil afterDelay:0.2]; - //[self _resize]; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; - SPClear(model); - SPClear(columns); - SPClear(contentFilters); - SPClear(numberOfDefaultFilters); - [super dealloc]; -} - -/** - * Sets the compare types for the filter and the appropriate formatter for the textField - */ -- (NSArray *)_compareTypesForColumn:(ColumnNode *)colNode -{ - if(contentFilters == nil - || ![contentFilters objectForKey:@"number"] - || ![contentFilters objectForKey:@"string"] - || ![contentFilters objectForKey:@"date"]) { - NSLog(@"Error while setting filter types."); - NSBeep(); - return @[]; - } - - NSString *fieldTypeGrouping; - if([colNode typegrouping]) { - fieldTypeGrouping = [NSString stringWithString:[colNode typegrouping]]; - } - else { - return @[]; - } - - NSMutableArray *compareItems = [NSMutableArray array]; - - NSString *compareType; - - if ( [fieldTypeGrouping isEqualToString:@"date"] ) { - compareType = @"date"; - - /* - if ([fieldType isEqualToString:@"timestamp"]) { - [argumentField setFormatter:[[NSDateFormatter alloc] - initWithDateFormat:@"%Y-%m-%d %H:%M:%S" allowNaturalLanguage:YES]]; - } - if ([fieldType isEqualToString:@"datetime"]) { - [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%Y-%m-%d %H:%M:%S" allowNaturalLanguage:YES]]; - } - if ([fieldType isEqualToString:@"date"]) { - [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%Y-%m-%d" allowNaturalLanguage:YES]]; - } - if ([fieldType isEqualToString:@"time"]) { - [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%H:%M:%S" allowNaturalLanguage:YES]]; - } - if ([fieldType isEqualToString:@"year"]) { - [argumentField setFormatter:[[NSDateFormatter alloc] initWithDateFormat:@"%Y" allowNaturalLanguage:YES]]; - } - */ - - // TODO: A bug in the framework previously meant enum fields had to be treated as string fields for the purposes - // of comparison - this can now be split out to support additional comparison fucntionality if desired. - } - else if ([fieldTypeGrouping isEqualToString:@"string"] || [fieldTypeGrouping isEqualToString:@"binary"] - || [fieldTypeGrouping isEqualToString:@"textdata"] || [fieldTypeGrouping isEqualToString:@"blobdata"] - || [fieldTypeGrouping isEqualToString:@"enum"]) { - - compareType = @"string"; - // [argumentField setFormatter:nil]; - - } - else if ([fieldTypeGrouping isEqualToString:@"bit"] || [fieldTypeGrouping isEqualToString:@"integer"] - || [fieldTypeGrouping isEqualToString:@"float"]) { - compareType = @"number"; - // [argumentField setFormatter:numberFormatter]; - - } - else if ([fieldTypeGrouping isEqualToString:@"geometry"]) { - compareType = @"spatial"; - - } - else { - compareType = @""; - NSBeep(); - NSLog(@"ERROR: unknown type for comparision: in %@", fieldTypeGrouping); - } - - // Add IS NULL and IS NOT NULL as they should always be available - // [compareField addItemWithTitle:@"IS NULL"]; - // [compareField addItemWithTitle:@"IS NOT NULL"]; - - // Remove user-defined filters first - if([numberOfDefaultFilters objectForKey:compareType]) { - NSUInteger cycles = [[contentFilters objectForKey:compareType] count] - [[numberOfDefaultFilters objectForKey:compareType] integerValue]; - while(cycles > 0) { - [[contentFilters objectForKey:compareType] removeLastObject]; - cycles--; - } - } - - NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; - -#ifndef SP_CODA /* content filters */ - // Load global user-defined content filters - if([prefs objectForKey:SPContentFilters] - && [contentFilters objectForKey:compareType] - && [[prefs objectForKey:SPContentFilters] objectForKey:compareType]) - { - [[contentFilters objectForKey:compareType] addObjectsFromArray:[[prefs objectForKey:SPContentFilters] objectForKey:compareType]]; - } - - // Load doc-based user-defined content filters - if([[SPQueryController sharedQueryController] contentFilterForFileURL:[tableDocumentInstance fileURL]]) { - id filters = [[SPQueryController sharedQueryController] contentFilterForFileURL:[tableDocumentInstance fileURL]]; - if([filters objectForKey:compareType]) - [[contentFilters objectForKey:compareType] addObjectsFromArray:[filters objectForKey:compareType]]; - } -#endif - - NSUInteger i = 0; - if([contentFilters objectForKey:compareType]) { - for (id filter in [contentFilters objectForKey:compareType]) { - // Create the tooltip - NSString *tooltip; - if ([filter objectForKey:@"Tooltip"]) - tooltip = [filter objectForKey:@"Tooltip"]; - else { - NSMutableString *tip = [[NSMutableString alloc] init]; - if ([filter objectForKey:@"Clause"] && [(NSString *) [filter objectForKey:@"Clause"] length]) { - [tip setString:[[filter objectForKey:@"Clause"] stringByReplacingOccurrencesOfRegex:@"(? [values count]) { - SPLog(@"filter operator %@ requires %ld arguments, but only have %ld stored values!",op,numOfArgs,[values count]); - goto fail; - } - - // otherwise add them - for (NSUInteger i = 0; i < numOfArgs; ++i) { - // insert connector node between args? - if(i > 0) { - ConnectorNode *node = [[ConnectorNode alloc] init]; - [node setFilter:[op filter]]; - [node setLabelIndex:(i-1)]; // label 0 follows argument 0 - [criteria addObject:node]; - [node release]; - } - ArgNode *arg = [[ArgNode alloc] init]; - [arg setArgIndex:i]; - [arg setFilter:[op filter]]; - [arg setInitialValue:[values objectAtIndex:i]]; - [criteria addObject:arg]; - [arg release]; - } - - [obj setObject:criteria forKey:@"criteria"]; - - //the last thing that remains is creating the displayValues for all criteria - NSMutableArray *displayValues = [NSMutableArray arrayWithCapacity:[criteria count]]; - for(id criterion in criteria) { - id dispValue = [self ruleEditor:filterRuleEditor displayValueForCriterion:criterion inRow:-1]; - if(!dispValue) { - SPLog(@"got nil displayValue for criterion %@ on deserialization!",criterion); - goto fail; - } - [displayValues addObject:dispValue]; - } - [obj setObject:displayValues forKey:@"displayValues"]; - } - - return [obj autorelease]; - -fail: - [obj release]; - return nil; -} - -+ (NSDictionary *)makeSerializedFilterForColumn:(NSString *)colName operator:(NSString *)opName values:(NSArray *)values -{ - return @{ - SerFilterClass: SerFilterClassExpression, - SerFilterExprColumn: colName, - SerFilterExprComparison: opName, - SerFilterExprValues: values, - }; -} - -- (ColumnNode *)_columnForName:(NSString *)name -{ - if([name length]) { - for (ColumnNode *col in columns) { - if ([name isEqualToString:[col name]]) return col; - } - } - return nil; -} - -- (OpNode *)_operatorNamed:(NSString *)title forColumn:(ColumnNode *)col -{ - if([title length]) { - // check if we have the operator cache, otherwise build it - if(![col operatorCache]) { - NSArray *ops = [self _compareTypesForColumn:col]; - [col setOperatorCache:ops]; - } - // try to find it in the operator cache - for(OpNode *node in [col operatorCache]) { - if([[[node filter] objectForKey:@"MenuLabel"] isEqualToString:title]) return node; - } - } - return nil; -} - -BOOL SerIsGroup(NSDictionary *dict) -{ - return [SerFilterClassGroup isEqual:[dict objectForKey:SerFilterClass]]; -} - -/** - * This method looks at the given serialized filter in a recursive manner and - * when it encounters - * - a group node with only a single child or - * - a child that is a group node of the same kind as the parent one - * it will pull the child(ren) up - * - * So for example: - * AND(expr1) => expr1 - * AND(expr1,AND(expr2,expr3)) => AND(expr1,expr2,expr3) - * - * The input dict is not modified, the returned dict will be equal to the input - * dict or have parts of it removed or replaced with new dicts. - */ -+ (NSDictionary *)_flattenSerializedFilter:(NSDictionary *)in -{ - // return non-group-nodes as is - if(!SerIsGroup(in)) return in; - - NSNumber *inIsConjunction = [in objectForKey:SerFilterGroupIsConjunction]; - - // first give all children the chance to flatten (depth first) - NSArray *children = [in objectForKey:SerFilterGroupChildren]; - NSMutableArray *flatChildren = [NSMutableArray arrayWithCapacity:[children count]]; - NSUInteger changed = 0; - for(NSDictionary *child in children) { - NSDictionary *flattened = [self _flattenSerializedFilter:child]; - //take a closer look at the (possibly changed) child - is it a group node of the same kind as us? - if(SerIsGroup(flattened) && [inIsConjunction isEqual:[flattened objectForKey:SerFilterGroupIsConjunction]]) { - [flatChildren addObjectsFromArray:[flattened objectForKey:SerFilterGroupChildren]]; - changed++; - } - else if(flattened != child) { - changed++; - } - [flatChildren addObject:flattened]; - } - // if there is only a single child, return it (flattening) - if([flatChildren count] == 1) return [flatChildren objectAtIndex:0]; - // if none of the children changed return the original input - if(!changed) return in; - // last variant: some of our children changed, but we remain - return @{ - SerFilterClass: SerFilterClassGroup, - SerFilterGroupIsConjunction: inIsConjunction, - SerFilterGroupChildren: flatChildren - }; -} - -+ (void)_writeFilterTree:(NSDictionary *)in toString:(NSMutableString *)out wrapInParenthesis:(BOOL)wrap binary:(BOOL)isBINARY error:(NSError **)err -{ - NSError *myErr = nil; - - if(wrap) [out appendString:@"("]; - - if(SerIsGroup(in)) { - BOOL isConjunction = [[in objectForKey:SerFilterGroupIsConjunction] boolValue]; - NSString *connector = isConjunction ? @"AND" : @"OR"; - BOOL first = YES; - NSArray *children = [in objectForKey:SerFilterGroupChildren]; - for(NSDictionary *child in children) { - if(!first) [out appendFormat:@" %@ ",connector]; - else first = NO; - // if the child is a group node but of a different kind we want to wrap it in order to prevent operator precedence confusion - // expression children will always be wrapped for clarity, except if there is only a single one and we are already wrapped - BOOL wrapChild = YES; - if(SerIsGroup(child)) { - BOOL childIsConjunction = [[child objectForKey:SerFilterGroupIsConjunction] boolValue]; - if(isConjunction == childIsConjunction) wrapChild = NO; - } - else { - if(wrap && [children count] == 1) wrapChild = NO; - } - [self _writeFilterTree:child toString:out wrapInParenthesis:wrapChild binary:isBINARY error:&myErr]; - if(myErr) { - if(err) *err = myErr; - return; - } - } - } - else { - // finally - build a SQL filter expression - NSDictionary *filter = [in objectForKey:SerFilterExprDefinition]; - if(!filter) { - if(err) *err = [NSError errorWithDomain:SPErrorDomain code:0 userInfo:@{ - NSLocalizedDescriptionKey: NSLocalizedString(@"Fatal error while retrieving content filter. No filter definition found.", @"filter to sql conversion : internal error : 0"), - }]; - return; - } - - if(![filter objectForKey:@"NumberOfArguments"]) { - if(err) *err = [NSError errorWithDomain:SPErrorDomain code:1 userInfo:@{ - NSLocalizedDescriptionKey: NSLocalizedString(@"Error while retrieving filter clause. No “NumberOfArguments” key found.", @"filter to sql conversion : internal error : invalid filter definition (1)"), - }]; - return; - } - - if(![filter objectForKey:@"Clause"] || ![(NSString *)[filter objectForKey:@"Clause"] length]) { - if(err) *err = [NSError errorWithDomain:SPErrorDomain code:2 userInfo:@{ - NSLocalizedDescriptionKey: NSLocalizedString(@"Content Filter clause is empty.", @"filter to sql conversion : internal error : invalid filter definition (2)"), - }]; - return; - } - - NSArray *values = [in objectForKey:SerFilterExprValues]; - - SPTableFilterParser *parser = [[SPTableFilterParser alloc] initWithFilterClause:[filter objectForKey:@"Clause"] - numberOfArguments:[[filter objectForKey:@"NumberOfArguments"] integerValue]]; - [parser setArgument:[values objectOrNilAtIndex:0]]; - [parser setFirstBetweenArgument:[values objectOrNilAtIndex:0]]; - [parser setSecondBetweenArgument:[values objectOrNilAtIndex:1]]; - [parser setSuppressLeadingTablePlaceholder:[[filter objectForKey:@"SuppressLeadingFieldPlaceholder"] boolValue]]; - [parser setCaseSensitive:isBINARY]; - [parser setCurrentField:[in objectForKey:SerFilterExprColumn]]; - - NSString *sql = [parser filterString]; - // SPTableFilterParser will return nil if it doesn't like the arguments and NSMutableString doesn't like nil - if(!sql) { - if(err) *err = [NSError errorWithDomain:SPErrorDomain code:3 userInfo:@{ - NSLocalizedDescriptionKey: NSLocalizedString(@"No valid SQL expression could be generated. Make sure that you have filled in all required fields.", @"filter to sql conversion : internal error : SPTableFilterParser failed"), - }]; - [parser release]; - return; - } - [out appendString:sql]; - - [parser release]; - } - - if(wrap) [out appendString:@")"]; -} - -@end - -#pragma mark - - -@implementation RuleNode - -@synthesize type = type; - -- (NSUInteger)hash { - return type; -} - -- (BOOL)isEqual:(id)other { - if (other == self) return YES; - if (other && [[other class] isEqual:[self class]] && [(RuleNode *)other type] == type) return YES; - - return NO; -} - -@end - -@implementation ColumnNode - -@synthesize name = name; -@synthesize typegrouping = typegrouping; -@synthesize operatorCache = operatorCache; - -- (instancetype)init -{ - if((self = [super init])) { - type = RuleNodeTypeColumn; - } - return self; -} - -- (NSString *)description -{ - return [NSString stringWithFormat:@"ColumnNode<%@@%p>",[self name],self]; -} - -- (NSUInteger)hash { - return ([name hash] ^ [typegrouping hash] ^ [super hash]); -} - -- (BOOL)isEqual:(id)other { - if (other == self) return YES; - if (other && [[other class] isEqual:[self class]] && [name isEqualToString:[other name]] && [typegrouping isEqualToString:[other typegrouping]]) return YES; - - return NO; -} - -@end - - -@implementation StringNode - -@synthesize value = value; - -- (instancetype)init -{ - if((self = [super init])) { - type = RuleNodeTypeString; - } - return self; -} - -- (NSUInteger)hash { - return ([value hash] ^ [super hash]); -} - -- (BOOL)isEqual:(id)other { - if (other == self) return YES; - if (other && [[other class] isEqual:[self class]] && [value isEqualToString:[(StringNode *)other value]]) return YES; - - return NO; -} - -@end - -@implementation OpNode - -@synthesize parentColumn = parentColumn; -@synthesize settings = settings; -@synthesize filter = filter; - -- (instancetype)init -{ - if((self = [super init])) { - type = RuleNodeTypeOperator; - } - return self; -} - -- (void)dealloc -{ - [self setFilter:nil]; - [self setSettings:nil]; - [super dealloc]; -} - -- (NSUInteger)hash { - return (([parentColumn hash] << 16) ^ [settings hash] ^ [super hash]); -} - -- (BOOL)isEqual:(id)other { - if (other == self) return YES; - if (other && [[other class] isEqual:[self class]] && [settings isEqualToDictionary:[(OpNode *)other settings]] && [parentColumn isEqual:[other parentColumn]]) return YES; - - return NO; -} - -@end - -@implementation ArgNode - -@synthesize filter = filter; -@synthesize argIndex = argIndex; -@synthesize initialValue = initialValue; - -- (instancetype)init -{ - if((self = [super init])) { - type = RuleNodeTypeArgument; - } - return self; -} - -- (void)dealloc -{ - [self setInitialValue:nil]; - [self setFilter:nil]; - [super dealloc]; -} - -- (NSUInteger)hash { - // initialValue does not count towards hash because two Args are not different if only the initialValue differs - return ((argIndex << 16) ^ [filter hash] ^ [super hash]); -} - -- (BOOL)isEqual:(id)other { - // initialValue does not count towards isEqual: because two Args are not different if only the initialValue differs - if (other == self) return YES; - if (other && [[other class] isEqual:[self class]] && [filter isEqualToDictionary:[(ArgNode *)other filter]] && argIndex == [(ArgNode *)other argIndex]) return YES; - - return NO; -} - -@end - -@implementation ConnectorNode - -@synthesize filter = filter; -@synthesize labelIndex = labelIndex; - -- (instancetype)init -{ - if((self = [super init])) { - type = RuleNodeTypeConnector; - } - return self; -} - -- (void)dealloc -{ - [self setFilter:nil]; - [super dealloc]; -} - -- (NSUInteger)hash { - return ((labelIndex << 16) ^ [filter hash] ^ [super hash]); -} - -- (BOOL)isEqual:(id)other { - if (other == self) return YES; - if (other && [[other class] isEqual:[self class]] && [filter isEqualToDictionary:[(ConnectorNode *)other filter]] && labelIndex == [(ConnectorNode *)other labelIndex]) return YES; - - return NO; -} - -@end -- cgit v1.2.3