From 66e8dc21b8dfe25f83872036a1268d4e014f16eb Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 7 May 2018 02:40:59 +0200 Subject: #63: Add basic support for multiple filters --- Source/SPContentFilterManager.h | 4 +- Source/SPContentFilterManager.m | 38 +- Source/SPDatabaseDocument.m | 4 +- Source/SPHistoryController.m | 70 +- Source/SPTableContent.h | 51 +- Source/SPTableContent.m | 739 ++++++----------- Source/SPTableContentFilterController.h | 94 ++- Source/SPTableContentFilterController.m | 1342 +++++++++++++++++++++++++++++++ Source/SPTableFilterParser.m | 3 +- 9 files changed, 1737 insertions(+), 608 deletions(-) (limited to 'Source') diff --git a/Source/SPContentFilterManager.h b/Source/SPContentFilterManager.h index 3c170bfc..cfc5bc76 100644 --- a/Source/SPContentFilterManager.h +++ b/Source/SPContentFilterManager.h @@ -39,7 +39,7 @@ SPDatabaseDocument *tableDocumentInstance; #ifndef SP_CODA /* ivars */ - NSURL *delegatesFileURL; + NSURL *documentFileURL; #endif IBOutlet id encodingPopUp; @@ -65,7 +65,7 @@ NSString *filterType; } -- (id)initWithDelegate:(id)managerDelegate forFilterType:(NSString *)compareType; +- (id)initWithDatabaseDocument:(SPDatabaseDocument *)document forFilterType:(NSString *)compareType; // Accessors - (NSMutableArray *)contentFilterForFileURL:(NSURL *)fileURL; diff --git a/Source/SPContentFilterManager.m b/Source/SPContentFilterManager.m index 8d8a1bcf..71013dc7 100644 --- a/Source/SPContentFilterManager.m +++ b/Source/SPContentFilterManager.m @@ -48,31 +48,29 @@ static NSString *SPExportFilterAction = @"SPExportFilter"; @implementation SPContentFilterManager /** - * Initialize the manager with the supplied delegate + * Initialize the manager with the supplied document */ -- (id)initWithDelegate:(id)managerDelegate forFilterType:(NSString *)compareType +- (id)initWithDatabaseDocument:(SPDatabaseDocument *)document forFilterType:(NSString *)compareType { - if ((self = [super initWithWindowNibName:@"ContentFilterManager"])) { + if (document == nil) { + NSBeep(); + NSLog(@"ContentFilterManager was called without a document."); + return nil; + } + + if ((self = [super initWithWindowNibName:@"ContentFilterManager"])) { #ifndef SP_CODA prefs = [NSUserDefaults standardUserDefaults]; #endif contentFilters = [[NSMutableArray alloc] init]; - - if (managerDelegate == nil) { - NSBeep(); - NSLog(@"ContentFilterManager was called without a delegate."); - - return nil; - } - - tableDocumentInstance = [managerDelegate valueForKeyPath:@"tableDocumentInstance"]; + tableDocumentInstance = document; #ifndef SP_CODA - delegatesFileURL = [tableDocumentInstance fileURL]; + documentFileURL = [[tableDocumentInstance fileURL] copy]; #endif - filterType = [NSString stringWithString:compareType]; + filterType = [compareType copy]; } return self; @@ -113,13 +111,13 @@ static NSString *SPExportFilterAction = @"SPExportFilter"; // Build doc-based filters [contentFilters addObject:[NSDictionary dictionaryWithObjectsAndKeys: - [[[delegatesFileURL absoluteString] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] lastPathComponent], @"MenuLabel", - [delegatesFileURL absoluteString], @"headerOfFileURL", + [[[documentFileURL absoluteString] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] lastPathComponent], @"MenuLabel", + [documentFileURL absoluteString], @"headerOfFileURL", @"", @"Clause", nil]]; - if ([[SPQueryController sharedQueryController] contentFilterForFileURL:delegatesFileURL]) { - id filters = [[SPQueryController sharedQueryController] contentFilterForFileURL:delegatesFileURL]; + if ([[SPQueryController sharedQueryController] contentFilterForFileURL:documentFileURL]) { + id filters = [[SPQueryController sharedQueryController] contentFilterForFileURL:documentFileURL]; if([filters objectForKey:filterType]) for(id fav in [filters objectForKey:filterType]) [contentFilters addObject:[[fav mutableCopy] autorelease]]; @@ -395,7 +393,7 @@ static NSString *SPExportFilterAction = @"SPExportFilter"; #ifndef SP_CODA // Update current document's content filters in the SPQueryController [[SPQueryController sharedQueryController] replaceContentFilterByArray: - [self contentFilterForFileURL:delegatesFileURL] ofType:filterType forFileURL:delegatesFileURL]; + [self contentFilterForFileURL:documentFileURL] ofType:filterType forFileURL:documentFileURL]; // Update global preferences' list id cf = [[prefs objectForKey:SPContentFilters] mutableCopy]; @@ -968,6 +966,8 @@ static NSString *SPExportFilterAction = @"SPExportFilter"; - (void)dealloc { SPClear(contentFilters); + SPClear(filterType); + SPClear(documentFileURL); [super dealloc]; } diff --git a/Source/SPDatabaseDocument.m b/Source/SPDatabaseDocument.m index 0ed51ce0..4bdd0aad 100644 --- a/Source/SPDatabaseDocument.m +++ b/Source/SPDatabaseDocument.m @@ -4711,7 +4711,7 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0; [sessionState setObject:[NSNumber numberWithInteger:[tableContentInstance pageNumber]] forKey:@"contentPageNumber"]; [sessionState setObject:NSStringFromRect([tableContentInstance viewport]) forKey:@"contentViewport"]; NSDictionary *filterSettings = [tableContentInstance filterSettings]; - if (filterSettings) [sessionState setObject:filterSettings forKey:@"contentFilter"]; + if (filterSettings) [sessionState setObject:filterSettings forKey:@"contentFilterV2"]; NSDictionary *contentSelectedRows = [tableContentInstance selectionDetailsAllowingIndexSelection:YES]; if (contentSelectedRows) { @@ -5110,7 +5110,7 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0; if([spfSession objectForKey:@"contentSortCol"]) [tableContentInstance setSortColumnNameToRestore:[spfSession objectForKey:@"contentSortCol"] isAscending:[[spfSession objectForKey:@"contentSortColIsAsc"] boolValue]]; if([spfSession objectForKey:@"contentPageNumber"]) [tableContentInstance setPageToRestore:[[spfSession objectForKey:@"pageNumber"] integerValue]]; if([spfSession objectForKey:@"contentViewport"]) [tableContentInstance setViewportToRestore:NSRectFromString([spfSession objectForKey:@"contentViewport"])]; - if([spfSession objectForKey:@"contentFilter"]) [tableContentInstance setFiltersToRestore:[spfSession objectForKey:@"contentFilter"]]; + if([spfSession objectForKey:@"contentFilterV2"]) [tableContentInstance setFiltersToRestore:[spfSession objectForKey:@"contentFilterV2"]]; // Select table [tablesListInstance selectTableAtIndex:[NSNumber numberWithInteger:[tables indexOfObject:[spfSession objectForKey:@"table"]]]]; diff --git a/Source/SPHistoryController.m b/Source/SPHistoryController.m index aab54d2b..c39421a9 100644 --- a/Source/SPHistoryController.m +++ b/Source/SPHistoryController.m @@ -264,7 +264,7 @@ nil]; if (contentSortCol) [contentState setObject:contentSortCol forKey:@"sortCol"]; if (contentSelectedRows) [contentState setObject:contentSelectedRows forKey:@"selection"]; - if (contentFilter) [contentState setObject:contentFilter forKey:@"filter"]; + if (contentFilter) [contentState setObject:contentFilter forKey:@"filterV2"]; if (filterTableData) [contentState setObject:filterTableData forKey:@"filterTable"]; // Update the table content states with this information - used when switching tables to restore last used view. @@ -278,38 +278,45 @@ } else if (historyPosition != NSNotFound && historyPosition == [history count] - 1) { NSMutableDictionary *currentHistoryEntry = [history objectAtIndex:historyPosition]; + BOOL databaseIsTheSame = [[currentHistoryEntry objectForKey:@"database"] isEqualToString:theDatabase]; + BOOL tableIsTheSame = [[currentHistoryEntry objectForKey:@"table"] isEqualToString:theTable]; + BOOL viewIsTheSame = ([[currentHistoryEntry objectForKey:@"view"] unsignedIntegerValue] == theView); // If the table is the same, and the filter settings haven't changed, delete the // last entry so it can be replaced. This updates navigation within a table, rather than // creating a new entry every time detail is changed. - if ([[currentHistoryEntry objectForKey:@"database"] isEqualToString:theDatabase] - && [[currentHistoryEntry objectForKey:@"table"] isEqualToString:theTable] - && ([[currentHistoryEntry objectForKey:@"view"] unsignedIntegerValue] != theView - || ((![currentHistoryEntry objectForKey:@"contentFilter"] && !contentFilter) - || (![currentHistoryEntry objectForKey:@"contentFilter"] - && ![(NSString *)[contentFilter objectForKey:@"filterValue"] length] - && ![[contentFilter objectForKey:@"filterComparison"] isEqualToString:@"IS NULL"] - && ![[contentFilter objectForKey:@"filterComparison"] isEqualToString:@"IS NOT NULL"]) - || [[currentHistoryEntry objectForKey:@"contentFilter"] isEqualToDictionary:contentFilter]))) - { + if ( + databaseIsTheSame && + tableIsTheSame && + ( + !viewIsTheSame || + ( + (![currentHistoryEntry objectForKey:@"contentFilterV2"] && !contentFilter) || + [[currentHistoryEntry objectForKey:@"contentFilterV2"] isEqualToDictionary:contentFilter] + ) + ) + ) { [history removeLastObject]; - + } // If the only db/table/view are the same, but the filter settings have changed, also store the // position details on the *previous* history item - } else if ([[currentHistoryEntry objectForKey:@"database"] isEqualToString:theDatabase] - && [[currentHistoryEntry objectForKey:@"table"] isEqualToString:theTable] - && ([[currentHistoryEntry objectForKey:@"view"] unsignedIntegerValue] == theView - || ((![currentHistoryEntry objectForKey:@"contentFilter"] && contentFilter) - || ![[currentHistoryEntry objectForKey:@"contentFilter"] isEqualToDictionary:contentFilter]))) - { + else if ( + databaseIsTheSame && + tableIsTheSame && + ( + viewIsTheSame || + ( + (![currentHistoryEntry objectForKey:@"contentFilterV2"] && contentFilter) || + ![[currentHistoryEntry objectForKey:@"contentFilterV2"] isEqualToDictionary:contentFilter] + ) + ) + ) { [currentHistoryEntry setObject:[NSValue valueWithRect:contentViewport] forKey:@"contentViewport"]; if (contentSelectedRows) [currentHistoryEntry setObject:contentSelectedRows forKey:@"contentSelection"]; - + } // Special case: if the last history item is currently active, and has no table, // but the new selection does - delete the last entry, in order to replace it. // This improves history flow. - } else if ([[currentHistoryEntry objectForKey:@"database"] isEqualToString:theDatabase] - && ![currentHistoryEntry objectForKey:@"table"]) - { + else if (databaseIsTheSame && ![currentHistoryEntry objectForKey:@"table"]) { [history removeLastObject]; } } @@ -325,7 +332,7 @@ nil]; if (contentSortCol) [newEntry setObject:contentSortCol forKey:@"contentSortCol"]; if (contentSelectedRows) [newEntry setObject:contentSelectedRows forKey:@"contentSelection"]; - if (contentFilter) [newEntry setObject:contentFilter forKey:@"contentFilter"]; + if (contentFilter) [newEntry setObject:contentFilter forKey:@"contentFilterV2"]; [history addObject:newEntry]; @@ -379,7 +386,7 @@ [tableContentInstance setPageToRestore:[[historyEntry objectForKey:@"contentPageNumber"] integerValue]]; [tableContentInstance setSelectionToRestore:[historyEntry objectForKey:@"contentSelection"]]; [tableContentInstance setViewportToRestore:[[historyEntry objectForKey:@"contentViewport"] rectValue]]; - [tableContentInstance setFiltersToRestore:[historyEntry objectForKey:@"contentFilter"]]; + [tableContentInstance setFiltersToRestore:[historyEntry objectForKey:@"contentFilterV2"]]; // If the database, table, and view are the same and content - just trigger a table reload (filters) if ( @@ -495,7 +502,7 @@ abort_entry_load: [tableContentInstance setPageToRestore:[[contentState objectForKey:@"page"] unsignedIntegerValue]]; [tableContentInstance setSelectionToRestore:[contentState objectForKey:@"selection"]]; [tableContentInstance setViewportToRestore:[[contentState objectForKey:@"viewport"] rectValue]]; - [tableContentInstance setFiltersToRestore:[contentState objectForKey:@"filter"]]; + [tableContentInstance setFiltersToRestore:[contentState objectForKey:@"filterV2"]]; } #pragma mark - @@ -529,21 +536,14 @@ abort_entry_load: [theName appendFormat:@"/%@", [theEntry objectForKey:@"table"]]; - if ([theEntry objectForKey:@"contentFilter"]) { - NSDictionary *filterSettings = [theEntry objectForKey:@"contentFilter"]; - if ([filterSettings objectForKey:@"filterField"]) { - if([filterSettings objectForKey:@"menuLabel"]) { - theName = [NSMutableString stringWithFormat:NSLocalizedString(@"%@ (Filtered by %@)", @"History item filtered by values label"), - theName, [filterSettings objectForKey:@"menuLabel"]]; - } - } + if ([theEntry objectForKey:@"contentFilterV2"]) { + theName = [NSMutableString stringWithFormat:NSLocalizedString(@"%@ (Filtered)", @"History item filtered by values label"), theName]; } if ([theEntry objectForKey:@"contentPageNumber"]) { NSUInteger pageNumber = [[theEntry objectForKey:@"contentPageNumber"] unsignedIntegerValue]; if (pageNumber > 1) { - theName = [NSMutableString stringWithFormat:NSLocalizedString(@"%@ (Page %lu)", @"History item with page number label"), - theName, (unsigned long)pageNumber]; + theName = [NSMutableString stringWithFormat:NSLocalizedString(@"%@ (Page %lu)", @"History item with page number label"), theName, (unsigned long)pageNumber]; } } diff --git a/Source/SPTableContent.h b/Source/SPTableContent.h index 486df122..be7fd58d 100644 --- a/Source/SPTableContent.h +++ b/Source/SPTableContent.h @@ -43,13 +43,18 @@ @class SPDatabaseDocument; @class SPTablesList; @class SPTableStructure; -@class SPTableList; -@class SPContentFilterManager; #ifndef SP_CODA @class SPSplitView; #endif @class SPTableContentFilterController; +typedef NS_ENUM(NSInteger, SPTableContentFilterSource) { + SPTableContentFilterSourceNone = -1, + SPTableContentFilterSourceRuleFilter = 0, + SPTableContentFilterSourceTableFilter = 1, + SPTableContentFilterSourceURLScheme = 2, +}; + #import "SPDatabaseContentViewDelegate.h" @interface SPTableContent : NSObject @@ -65,10 +70,9 @@ #endif IBOutlet SPCopyTable *tableContentView; - IBOutlet NSPopUpButton *fieldField; - IBOutlet id compareField; - IBOutlet id argumentField; - IBOutlet id filterButton; + + IBOutlet NSButton *filterButton; + IBOutlet NSButton *toggleRuleFilterButton; IBOutlet id addButton; IBOutlet id duplicateButton; IBOutlet id removeButton; @@ -80,9 +84,6 @@ IBOutlet id limitRowsButton; IBOutlet id limitRowsStepper; #endif - IBOutlet id firstBetweenField; - IBOutlet id secondBetweenField; - IBOutlet id betweenTextField; IBOutlet NSButton *paginationPreviousButton; #ifndef SP_CODA @@ -115,9 +116,6 @@ IBOutlet NSPanel *filterTableSetDefaultOperatorSheet; IBOutlet NSComboBox* filterTableSetDefaultOperatorValue; - // Temporary to avoid nib conflicts during WIP - IBOutlet SPSplitView *contentSplitView; - IBOutlet SPTableContentFilterController *filterControllerInstance; #endif SPMySQLConnection *mySQLConnection; @@ -133,7 +131,6 @@ SPDataStorage *tableValues; NSMutableArray *dataColumns, *keys, *oldRow; NSUInteger tableRowsCount, previousTableRowsCount; - NSString *compareType; NSNumber *sortCol; BOOL isEditingRow, isEditingNewRow, isSavingRow, isDesc, setLimit; BOOL isFiltered, isLimited, isInterruptedLoad, maxNumRowsIsEstimate; @@ -142,8 +139,6 @@ NSMutableDictionary *contentFilters; NSMutableDictionary *numberOfDefaultFilters; - NSUInteger lastSelectedContentFilterIndex; - SPContentFilterManager *contentFilterManager; NSUInteger contentPage; #ifndef SP_CODA @@ -153,7 +148,7 @@ BOOL filterTableIsSwapped; NSString *filterTableDefaultOperator; NSString *lastEditedFilterTableValue; - NSInteger activeFilter; // 0 = default filter; 1 = filter table; 2 = sequelpro url scheme + SPTableContentFilterSource activeFilter; NSString *schemeFilter; #endif @@ -163,7 +158,6 @@ NSUInteger pageToRestore; NSDictionary *selectionToRestore; NSRect selectionViewportToRestore; - NSString *filterFieldToRestore, *filterComparisonToRestore, *filterValueToRestore, *firstBetweenValueToRestore, *secondBetweenValueToRestore; #ifndef SP_CODA NSInteger paginationViewHeight; @@ -173,7 +167,6 @@ NSUInteger tableLoadInterfaceUpdateInterval, tableLoadTimerTicksSinceLastUpdate, tableLoadLastRowCount, tableLoadTargetRowCount; NSArray *cqColumnDefinition; - NSString *fieldIDQueryString; BOOL isFirstChangeInView; NSString *kCellEditorErrorNoMatch; @@ -186,17 +179,20 @@ NSColor *whiteColor; SPFieldEditorController *fieldEditor; - NSRange fieldEditorSelectedRange; + + // this represents the visible area of the whole content view at runtime. + // we use it as a positioning aide for the other two views below + IBOutlet NSView *contentAreaContainer; + IBOutlet NSView *filterRuleEditorContainer; + IBOutlet NSView *tableContentContainer; + + BOOL showFilterRuleEditor; + + NSDictionary *filtersToRestore; } #ifdef SP_CODA /* glue */ @property (assign) id filterButton; -@property (assign) id fieldField; -@property (assign) id compareField; -@property (assign) id betweenTextField; -@property (assign) id firstBetweenField; -@property (assign) id secondBetweenField; -@property (assign) id argumentField; @property (assign) NSButton* addButton; @property (assign) NSButton* duplicateButton; @property (assign) NSButton* removeButton; @@ -229,9 +225,10 @@ - (IBAction)reloadTable:(id)sender; - (void)reloadTableTask; - (IBAction)filterTable:(id)sender; +- (IBAction)toggleRuleEditorVisible:(id)sender; - (void)filterTableTask; -- (IBAction)toggleFilterField:(id)sender; - (void)setUsedQuery:(NSString *)query; +- (NSString *)selectedTable; // Pagination - (IBAction)navigatePaginationFromButton:(id)sender; @@ -269,7 +266,6 @@ - (void)setConnection:(SPMySQLConnection *)theConnection; - (void)clickLinkArrow:(SPTextAndLinkCell *)theArrowCell; - (void)clickLinkArrowTask:(SPTextAndLinkCell *)theArrowCell; -- (IBAction)setCompareTypes:(id)sender; - (void)updateResultStore:(SPMySQLStreamingResultStore *)theResultStore approximateRowCount:(NSUInteger)targetRowCount; - (BOOL)saveRowToTable; - (void) addRowErrorSheetDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo; @@ -305,7 +301,6 @@ - (NSData *)filterTableData; //- (NSString *)escapeFilterArgument:(NSString *)argument againstClause:(NSString *)clause; -- (void)openContentFilterManager; - (NSArray *)fieldEditStatusForRow:(NSInteger)rowIndex andColumn:(NSInteger)columnIndex; diff --git a/Source/SPTableContent.m b/Source/SPTableContent.m index 5353b653..74af1dba 100644 --- a/Source/SPTableContent.m +++ b/Source/SPTableContent.m @@ -46,7 +46,6 @@ #import "SPFieldEditorController.h" #import "SPTooltip.h" #import "RegexKitLite.h" -#import "SPContentFilterManager.h" #import "SPDataStorage.h" #import "SPAlertSheets.h" #import "SPHistoryController.h" @@ -60,6 +59,7 @@ #import "SPThreadAdditions.h" #import "SPTableFilterParser.h" #import "SPFunctions.h" +#import "SPTableContentFilterController.h" #import #import @@ -79,7 +79,13 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper - (BOOL)cancelRowEditing; - (void)documentWillClose:(NSNotification *)notification; -- (void)contentFiltersHaveBeenUpdated:(NSNotification *)notification; + +- (void)updateFilterRuleEditorSize:(CGFloat)requestedHeight animate:(BOOL)animate; +- (void)filterRuleEditorPreferredSizeChanged:(NSNotification *)notification; +- (void)contentViewSizeChanged:(NSNotification *)notification; +- (void)setRuleEditorVisible:(BOOL)show animate:(BOOL)animate; + +- (void)_setViewBlankState; #pragma mark - SPTableContentDataSource_Private_API @@ -91,19 +97,13 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper #ifdef SP_CODA @synthesize addButton; -@synthesize argumentField; -@synthesize betweenTextField; -@synthesize compareField; @synthesize duplicateButton; -@synthesize fieldField; @synthesize filterButton; -@synthesize firstBetweenField; @synthesize paginationNextButton; @synthesize paginationPageField; @synthesize paginationPreviousButton; @synthesize reloadButton; @synthesize removeButton; -@synthesize secondBetweenField; @synthesize tableContentView; @synthesize tableDataInstance; @synthesize tableDocumentInstance; @@ -139,7 +139,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper filterTableDistinct = NO; filterTableIsSwapped = NO; lastEditedFilterTableValue = nil; - activeFilter = 0; + activeFilter = SPTableContentFilterSourceNone; schemeFilter = nil; paginationPopover = nil; #endif @@ -157,15 +157,12 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper pageToRestore = 1; selectionToRestore = nil; selectionViewportToRestore = NSZeroRect; - filterFieldToRestore = nil; - filterComparisonToRestore = nil; - filterValueToRestore = nil; - firstBetweenValueToRestore = nil; - secondBetweenValueToRestore = nil; + filtersToRestore = nil; tableRowsSelectable = YES; - contentFilterManager = nil; isFirstChangeInView = YES; + showFilterRuleEditor = NO; + isFiltered = NO; isLimited = NO; isInterruptedLoad = NO; @@ -227,10 +224,9 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper _mainNibLoaded = YES; #ifndef SP_CODA /* ui manipulation */ - // Temporary to avoid nib conflicts during WIP - [contentSplitView setCollapsibleSubviewIndex:0]; - [contentSplitView setCollapsibleSubviewCollapsed:YES animate:NO]; - [contentSplitView setMaxSize:0.f ofSubviewAtIndex:0]; + + // initially hide the filter rule editor + [self updateFilterRuleEditorSize:0.0 animate:NO]; // Set the table content view's vertical gridlines if required [tableContentView setGridStyleMask:([prefs boolForKey:SPDisplayTableViewVerticalGridlines]) ? NSTableViewSolidVerticalGridLineMask : NSTableViewGridNone]; @@ -297,6 +293,19 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper [prefs addObserver:self forKeyPath:SPGlobalResultTableFont options:NSKeyValueObservingOptionNew context:TableContentKVOContext]; [prefs addObserver:self forKeyPath:SPDisplayBinaryDataAsHex options:NSKeyValueObservingOptionNew context:TableContentKVOContext]; + // Add observer to change view sizes with filter rule editor + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(filterRuleEditorPreferredSizeChanged:) + name:SPTableContentFilterHeightChangedNotification + object:filterControllerInstance]; + [contentAreaContainer setPostsFrameChangedNotifications:YES]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(contentViewSizeChanged:) + name:NSViewFrameDidChangeNotification + object:contentAreaContainer]; + [filterControllerInstance setTarget:self]; + [filterControllerInstance setAction:@selector(filterTable:)]; + // Add observers for document task activity [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startDocumentTaskForTab:) @@ -310,10 +319,6 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper selector:@selector(documentWillClose:) name:SPDocumentWillCloseNotification object:tableDocumentInstance]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(contentFiltersHaveBeenUpdated:) - name:SPContentFiltersHaveBeenUpdatedNotification - object:nil]; } #pragma mark - @@ -383,6 +388,68 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper [self clearDetailsToRestore]; } +/** + * This configures the table content view in the way it should look like when no valid table is selected + */ +- (void)_setViewBlankState +{ + // Remove existing columns from the table + while ([[tableContentView tableColumns] count]) { + [NSArrayObjectAtIndex([tableContentView tableColumns], 0) setHeaderToolTip:nil]; // prevent crash #2414 + [tableContentView removeTableColumn:NSArrayObjectAtIndex([tableContentView tableColumns], 0)]; + } + + // Empty the stored data arrays, including emptying the tableValues array + // by ressignment for thread safety. + previousTableRowsCount = 0; + [self clearTableValues]; + [tableContentView reloadData]; + isFiltered = NO; + isLimited = NO; +#ifndef SP_CODA + [countText setStringValue:@""]; +#endif + + // Reset sort column + if (sortCol) SPClear(sortCol); + isDesc = NO; + + // Empty and disable filter options + [self setRuleEditorVisible:NO animate:NO]; + [filterButton setEnabled:NO]; + [toggleRuleFilterButton setEnabled:NO]; + [toggleRuleFilterButton setState:NSOffState]; + [filterControllerInstance updateFiltersFrom:self]; + + // Disable pagination + [paginationPreviousButton setEnabled:NO]; +#ifndef SP_CODA + [paginationButton setEnabled:NO]; + [paginationButton setTitle:@""]; +#endif + [paginationNextButton setEnabled:NO]; + + // Disable table action buttons + [addButton setEnabled:NO]; + [duplicateButton setEnabled:NO]; + [removeButton setEnabled:NO]; + + // Clear restoration settings + [self clearDetailsToRestore]; + +#ifndef SP_CODA + // Clear filter table + while ([[filterTableView tableColumns] count]) { + [NSArrayObjectAtIndex([filterTableView tableColumns], 0) setHeaderToolTip:nil]; // prevent crash #2414 + [filterTableView removeTableColumn:NSArrayObjectAtIndex([filterTableView tableColumns], 0)]; + } + // Clear filter table data + [filterTableData removeAllObjects]; + [filterTableWhereClause setString:@""]; + activeFilter = SPTableContentFilterSourceNone; +#endif +} + /** * Update stored table details and update the interface to match the supplied * table details. @@ -463,75 +530,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper // If no table has been supplied, reset the view to a blank table and disabled elements. if (!newTableName) { - // Remove existing columns from the table - while ([[tableContentView tableColumns] count]) { - [NSArrayObjectAtIndex([tableContentView tableColumns], 0) setHeaderToolTip:nil]; // prevent crash #2414 - [tableContentView removeTableColumn:NSArrayObjectAtIndex([tableContentView tableColumns], 0)]; - } - - // Empty the stored data arrays, including emptying the tableValues array - // by ressignment for thread safety. - previousTableRowsCount = 0; - [self clearTableValues]; - [tableContentView reloadData]; - isFiltered = NO; - isLimited = NO; -#ifndef SP_CODA - [countText setStringValue:@""]; -#endif - - // Reset sort column - if (sortCol) SPClear(sortCol); - isDesc = NO; - - // Empty and disable filter options - [fieldField setEnabled:NO]; - [fieldField removeAllItems]; - [fieldField addItemWithTitle:NSLocalizedString(@"field", @"popup menuitem for field (showing only if disabled)")]; - [compareField setEnabled:NO]; - [compareField removeAllItems]; - [compareField addItemWithTitle:@"="]; - [argumentField setHidden:NO]; - [argumentField setEnabled:NO]; - [firstBetweenField setEnabled:NO]; - [secondBetweenField setEnabled:NO]; - [firstBetweenField setStringValue:@""]; - [secondBetweenField setStringValue:@""]; - [argumentField setStringValue:@""]; - [filterButton setEnabled:NO]; - - // Hide BETWEEN operator controls - [firstBetweenField setHidden:YES]; - [secondBetweenField setHidden:YES]; - [betweenTextField setHidden:YES]; - - // Disable pagination - [paginationPreviousButton setEnabled:NO]; -#ifndef SP_CODA - [paginationButton setEnabled:NO]; - [paginationButton setTitle:@""]; -#endif - [paginationNextButton setEnabled:NO]; - - // Disable table action buttons - [addButton setEnabled:NO]; - [duplicateButton setEnabled:NO]; - [removeButton setEnabled:NO]; - - // Clear restoration settings - [self clearDetailsToRestore]; - -#ifndef SP_CODA - // Clear filter table - while ([[filterTableView tableColumns] count]) { - [NSArrayObjectAtIndex([filterTableView tableColumns], 0) setHeaderToolTip:nil]; // prevent crash #2414 - [filterTableView removeTableColumn:NSArrayObjectAtIndex([filterTableView tableColumns], 0)]; - } - // Clear filter table data - [filterTableData removeAllObjects]; - [filterTableWhereClause setString:@""]; - activeFilter = 0; -#endif + [self _setViewBlankState]; return; } @@ -552,7 +551,9 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper // Clear filter table data [filterTableData removeAllObjects]; [filterTableWhereClause setString:@""]; - activeFilter = 0; + // TODO code smell + //...but keep it, if the rule filter is the active one + if(activeFilter != SPTableContentFilterSourceRuleFilter) activeFilter = SPTableContentFilterSourceNone; #endif // Retrieve the field names and types for this table from the data cache. This is used when requesting all data as part @@ -732,41 +733,23 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper } // Enable and initialize filter fields (with tags for position of menu item and field position) - [fieldField setEnabled:YES]; - [fieldField removeAllItems]; - NSArray *columnTitles = ([prefs boolForKey:SPAlphabeticalTableSorting])? [columnNames sortedArrayUsingSelector:@selector(compare:)] : columnNames; - [fieldField addItemsWithTitles:columnTitles]; - [compareField setEnabled:YES]; - [self setCompareTypes:self]; - [argumentField setEnabled:YES]; - [argumentField setStringValue:@""]; - [filterButton setEnabled:enableInteraction]; - + [filterControllerInstance updateFiltersFrom:self]; // Restore preserved filter settings if appropriate and valid - if (filterFieldToRestore) { - [fieldField selectItemWithTitle:filterFieldToRestore]; - [self setCompareTypes:self]; - - if ([fieldField itemWithTitle:filterFieldToRestore] - && ((!filterComparisonToRestore && filterValueToRestore) - || (filterComparisonToRestore && [compareField itemWithTitle:filterComparisonToRestore]))) - { - if (filterComparisonToRestore) [compareField selectItemWithTitle:filterComparisonToRestore]; - if([filterComparisonToRestore isEqualToString:@"BETWEEN"]) { - [argumentField setHidden:YES]; - if (firstBetweenValueToRestore) [firstBetweenField setStringValue:firstBetweenValueToRestore]; - if (secondBetweenValueToRestore) [secondBetweenField setStringValue:secondBetweenValueToRestore]; - } else { - if (filterValueToRestore) [argumentField setStringValue:filterValueToRestore]; - } - [self toggleFilterField:self]; - - } + [filterControllerInstance restoreSerializedFilters:filtersToRestore]; + //if we did restore some filters, set filtering enabled + if(![filterControllerInstance isEmpty]) { + [self setRuleEditorVisible:YES animate:NO]; + [toggleRuleFilterButton setState:NSOnState]; + } + else { + [self setRuleEditorVisible:NO animate:NO]; //immediately hide the filter editor when switching tables + [toggleRuleFilterButton setState:NSOffState]; } + [filterButton setEnabled:enableInteraction]; + [toggleRuleFilterButton setEnabled:enableInteraction]; // Restore page number if limiting is set - if ([prefs boolForKey:SPLimitResults]) - contentPage = pageToRestore; + if ([prefs boolForKey:SPLimitResults]) contentPage = pageToRestore; // Restore first responder [[tableDocumentInstance parentWindow] makeFirstResponder:currentFirstResponder]; @@ -787,6 +770,11 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper } +- (NSString *)selectedTable +{ + return selectedTable; +} + /** * Remove all items from the current table value store. Do this by * reassigning the tableValues store and releasing the old location, @@ -832,7 +820,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper // Start construction of the query string queryString = [NSMutableString stringWithFormat:@"SELECT %@%@ FROM %@", #ifndef SP_CODA - (activeFilter == 1 && [self tableFilterString] && filterTableDistinct) ? @"DISTINCT " : + (activeFilter == SPTableContentFilterSourceTableFilter && [self tableFilterString] && filterTableDistinct) ? @"DISTINCT " : #endif @"", [self fieldListForQuery], [selectedTable backtickQuotedString]]; @@ -1014,7 +1002,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper if ([mySQLConnection queryErrored] && ![mySQLConnection lastQueryWasCancelled]) { #ifndef SP_CODA - if(activeFilter == 0) { + if(activeFilter == SPTableContentFilterSourceRuleFilter || activeFilter == SPTableContentFilterSourceNone) { #endif NSString *errorDetail; if([filterString length]) @@ -1026,7 +1014,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper } #ifndef SP_CODA // Filter task came from filter table - else if(activeFilter == 1){ + else if(activeFilter == SPTableContentFilterSourceTableFilter) { [filterTableWindow setTitle:[NSString stringWithFormat:@"%@ – %@", NSLocalizedString(@"Filter", @"filter label"), NSLocalizedString(@"WHERE clause not valid", @"WHERE clause not valid")]]; } } @@ -1113,64 +1101,40 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper } // Call did come from filter table and is filter table window still open? - if(activeFilter == 1 && [filterTableWindow isVisible]) { - - if([[[filterTableWhereClause textStorage] string] length]) - if([filterTableNegateCheckbox state] == NSOnState) + if(activeFilter == SPTableContentFilterSourceTableFilter && [filterTableWindow isVisible]) { + if([[[filterTableWhereClause textStorage] string] length]) { + if ([filterTableNegateCheckbox state] == NSOnState) { return [NSString stringWithFormat:@"NOT (%@)", [[filterTableWhereClause textStorage] string]]; - else + } + else { return [[filterTableWhereClause textStorage] string]; - else + } + } + else { return nil; - + } } #endif - - // If the clause has the placeholder $BINARY that placeholder will be replaced - // by BINARY if the user pressed ⇧ while invoking 'Filter' otherwise it will - // replaced by @"". - BOOL caseSensitive = (([[[NSApp onMainThread] currentEvent] modifierFlags] & NSShiftKeyMask) > 0); - - if(contentFilters == nil) { - NSLog(@"Fatal error while retrieving content filters. No filters found."); - NSBeep(); - return nil; - } - - // Current selected filter type - if(![contentFilters objectForKey:compareType]) { - NSLog(@"Error while retrieving filters. Filter type “%@” unknown.", compareType); - NSBeep(); - return nil; - } - NSDictionary *filter = [[contentFilters objectForKey:compareType] objectAtIndex:[[compareField selectedItem] tag]]; - - if(![filter objectForKey:@"NumberOfArguments"]) { - NSLog(@"Error while retrieving filter clause. No “NumberOfArguments” key found."); - NSBeep(); - return nil; + if(activeFilter == SPTableContentFilterSourceRuleFilter && showFilterRuleEditor) { + // If the clause has the placeholder $BINARY that placeholder will be replaced + // by BINARY if the user pressed ⇧ while invoking 'Filter' otherwise it will + // replaced by @"". + BOOL caseSensitive = (([[NSApp currentEvent] modifierFlags] & NSShiftKeyMask) > 0); + + NSError *err = nil; + NSString *filter = [filterControllerInstance sqlWhereExpressionWithBinary:caseSensitive error:&err]; + if(err) { + SPOnewayAlertSheet( + NSLocalizedString(@"Invalid Filter", @"table content : apply filter : invalid filter message title"), + [tableDocumentInstance parentWindow], + [err localizedDescription] + ); + return nil; + } + return filter; } - if(![filter objectForKey:@"Clause"] || ![(NSString *)[filter objectForKey:@"Clause"] length]) { - - SPOnewayAlertSheet( - NSLocalizedString(@"Warning", @"warning"), - [tableDocumentInstance parentWindow], - NSLocalizedString(@"Content Filter clause is empty.", @"content filter clause is empty tooltip.") - ); - - return nil; - } - - SPTableFilterParser *parser = [[[SPTableFilterParser alloc] initWithFilterClause:[filter objectForKey:@"Clause"] numberOfArguments:[[filter objectForKey:@"NumberOfArguments"] integerValue]] autorelease]; - [parser setArgument:[argumentField stringValue]]; - [parser setFirstBetweenArgument:[firstBetweenField stringValue]]; - [parser setSecondBetweenArgument:[secondBetweenField stringValue]]; - [parser setSuppressLeadingTablePlaceholder:[[filter objectForKey:@"SuppressLeadingFieldPlaceholder"] boolValue]]; - [parser setCaseSensitive:caseSensitive]; - [parser setCurrentField:[fieldField titleOfSelectedItem]]; - - return [parser filterString]; + return nil; } /** @@ -1386,20 +1350,20 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper // If the filter table is being used - the advanced filter - switch type if(sender == filterTableFilterButton) { - activeFilter = 1; + activeFilter = SPTableContentFilterSourceTableFilter; } // If a string was supplied, use a custom query from that URL scheme else if([sender isKindOfClass:[NSString class]] && [(NSString *)sender length]) { if(schemeFilter) SPClear(schemeFilter); schemeFilter = [sender retain]; - activeFilter = 2; + activeFilter = SPTableContentFilterSourceURLScheme; } // If a button other than the pagination buttons was used, set the active filter type to // the standard filter field. else if (!senderIsPaginationButton) { - activeFilter = 0; + activeFilter = SPTableContentFilterSourceRuleFilter; } #endif @@ -1461,7 +1425,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper #endif // Reset and reload data using the new filter settings - [self setSelectionToRestore:[self selectionDetailsAllowingIndexSelection:NO]]; + [self setSelectionToRestore:[[self onMainThread] selectionDetailsAllowingIndexSelection:NO]]; previousTableRowsCount = 0; [self clearTableValues]; [self loadTableValues]; @@ -1471,61 +1435,28 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper } } -/** - * Enables or disables the filter input field based on the selected filter type. - */ -- (IBAction)toggleFilterField:(id)sender +- (IBAction)toggleRuleEditorVisible:(id)sender { + [self setRuleEditorVisible:!showFilterRuleEditor animate:YES]; +} - // Check if user called "Edit Filter…" - if([[compareField selectedItem] tag] == (NSInteger)[[contentFilters objectForKey:compareType] count]) { - [self openContentFilterManager]; - return; - } - - // Remember last selection for "Edit filter…" - lastSelectedContentFilterIndex = [[compareField selectedItem] tag]; - - NSDictionary *filter = [[contentFilters objectForKey:compareType] objectAtIndex:lastSelectedContentFilterIndex]; - NSUInteger numOfArgs = [[filter objectForKey:@"NumberOfArguments"] integerValue]; - if (numOfArgs == 2) { - [argumentField setHidden:YES]; - - if([filter objectForKey:@"ConjunctionLabels"] && [[filter objectForKey:@"ConjunctionLabels"] count] == 1) - [betweenTextField setStringValue:[[filter objectForKey:@"ConjunctionLabels"] objectAtIndex:0]]; - else - [betweenTextField setStringValue:@""]; - - [betweenTextField setHidden:NO]; - [firstBetweenField setHidden:NO]; - [secondBetweenField setHidden:NO]; - - [firstBetweenField setEnabled:YES]; - [secondBetweenField setEnabled:YES]; - [firstBetweenField selectText:self]; - } - else if (numOfArgs == 1){ - [argumentField setHidden:NO]; - [argumentField setEnabled:YES]; - [argumentField selectText:self]; - - [betweenTextField setHidden:YES]; - [firstBetweenField setHidden:YES]; - [secondBetweenField setHidden:YES]; +- (void)setRuleEditorVisible:(BOOL)show animate:(BOOL)animate +{ + // we can't change the state of the button here, because the mouse click already changed it + if(show) { + if([filterControllerInstance isEmpty]) { + [filterControllerInstance addFilterExpression]; + // the sizing will be updated automatically by adding a row + } + else { + [self updateFilterRuleEditorSize:[filterControllerInstance preferredHeight] animate:animate]; + } } else { - [argumentField setHidden:NO]; - [argumentField setEnabled:NO]; - - [betweenTextField setHidden:YES]; - [firstBetweenField setHidden:YES]; - [secondBetweenField setHidden:YES]; - - // Start search if no argument is required - if(numOfArgs == 0) - [self filterTable:self]; + [self updateFilterRuleEditorSize:0.0 animate:animate]; } - + showFilterRuleEditor = show; + [filterButton setHidden:!show]; } - (void)setUsedQuery:(NSString *)query @@ -2542,38 +2473,30 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper [spHistoryControllerInstance setModifyingState:YES]; #endif - NSString *targetFilterValue = [tableValues cellDataAtRow:[theArrowCell getClickedRow] column:dataColumnIndex]; + id targetFilterValue = [tableValues cellDataAtRow:[theArrowCell getClickedRow] column:dataColumnIndex]; //when navigating binary relations (eg. raw UUID) do so via a hex-encoded value for charset safety BOOL navigateAsHex = ([targetFilterValue isKindOfClass:[NSData class]] && [[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"binary"]); if(navigateAsHex) targetFilterValue = [mySQLConnection escapeData:(NSData *)targetFilterValue includingQuotes:NO]; + NSString *filterComparison = @"="; + if([targetFilterValue isNSNull]) filterComparison = @"IS NULL"; + else if(navigateAsHex) filterComparison = @"= (Hex String)"; + + // Store the filter details to use when loading the target table + NSDictionary *filterSettings = [filterControllerInstance 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(^{ - [fieldField selectItemWithTitle:[refDictionary objectForKey:@"column"]]; - [self setCompareTypes:self]; - if ([targetFilterValue isNSNull]) { - [compareField selectItemWithTitle:@"IS NULL"]; - } - else { - if(navigateAsHex) [compareField selectItemWithTitle:@"= (Hex String)"]; - [argumentField setStringValue:targetFilterValue]; - } + [filterControllerInstance restoreSerializedFilters:filterSettings]; + [self setRuleEditorVisible:YES animate:YES]; }); tableFilterRequired = YES; } else { - NSString *filterComparison = nil; - if([targetFilterValue isNSNull]) filterComparison = @"IS NULL"; - else if(navigateAsHex) filterComparison = @"= (Hex String)"; - - // Store the filter details to use when loading the target table - NSDictionary *filterSettings = @{ - @"filterField": [refDictionary objectForKey:@"column"], - @"filterValue": targetFilterValue, - @"filterComparison": SPBoxNil(filterComparison) - }; SPMainQSync(^{ [self setFiltersToRestore:filterSettings]; @@ -2601,188 +2524,6 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper } } -- (void)contentFiltersHaveBeenUpdated:(NSNotification *)notification -{ - [self setCompareTypes:nil]; -} - -/** - * Sets the compare types for the filter and the appropriate formatter for the textField - */ -- (IBAction)setCompareTypes:(id)sender -{ - - if(contentFilters == nil - || ![contentFilters objectForKey:@"number"] - || ![contentFilters objectForKey:@"string"] - || ![contentFilters objectForKey:@"date"]) { - NSLog(@"Error while setting filter types."); - NSBeep(); - return; - } - - // Retrieve the current field comparison setting for later restoration if possible - NSString *titleToRestore = [[compareField selectedItem] title]; - - // Reset the menu before building it back up - [compareField removeAllItems]; - - NSString *fieldTypeGrouping; - if([[tableDataInstance columnWithName:[fieldField titleOfSelectedItem]] objectForKey:@"typegrouping"]) - fieldTypeGrouping = [NSString stringWithString:[[tableDataInstance columnWithName:[fieldField titleOfSelectedItem]] objectForKey:@"typegrouping"]]; - else - return; - - 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 %@", [[tableDataInstance columnWithName:[fieldField titleOfSelectedItem]] objectForKey:@"type"], 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--; - } - } - -#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 - - // Rebuild operator popup menu - NSUInteger i = 0; - NSMenu *menu = [compareField menu]; - if([contentFilters objectForKey:compareType]) - for(id filter in [contentFilters objectForKey:compareType]) { - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:([filter objectForKey:@"MenuLabel"])?[filter objectForKey:@"MenuLabel"]:@"not specified" action:NULL keyEquivalent:@""]; - // Create the tooltip - if([filter objectForKey:@"Tooltip"]) - [item setToolTip:[filter objectForKey:@"Tooltip"]]; - else { - NSMutableString *tip = [[NSMutableString alloc] init]; - if([filter objectForKey:@"Clause"] && [(NSString *)[filter objectForKey:@"Clause"] length]) { - [tip setString:[[filter objectForKey:@"Clause"] stringByReplacingOccurrencesOfRegex:@"(? #import @@ -9,13 +34,64 @@ @class SPTableData; @class SPDatabaseDocument; @class SPTablesList; +@class SPTableContent; +@class SPContentFilterManager; + +NSString * const SPTableContentFilterHeightChangedNotification; @interface SPTableContentFilterController : NSObject { - IBOutlet SPSplitView *contentSplitView; - IBOutlet NSRuleEditor *filterRuleEditor; - IBOutlet SPTableData *tableDataInstance; - IBOutlet SPDatabaseDocument *tableDocumentInstance; - IBOutlet SPTablesList *tablesListInstance; + IBOutlet NSRuleEditor *filterRuleEditor; + IBOutlet SPTableData *tableDataInstance; + IBOutlet SPDatabaseDocument *tableDocumentInstance; + IBOutlet SPTablesList *tablesListInstance; + IBOutlet NSView *tableContentViewBelow; + + NSMutableArray *columns; + NSMutableDictionary *contentFilters; + NSMutableDictionary *numberOfDefaultFilters; + + NSMutableArray *model; + + SPContentFilterManager *contentFilterManager; + + CGFloat preferredHeight; + + id target; + SEL action; } +/** + * Makes the first NSTextField found in the rule editor the first responder + */ +- (void)focusFirstInputField; + +- (void)updateFiltersFrom:(SPTableContent *)tableContent; + +- (void)openContentFilterManagerForFilterType:(NSString *)filterType; + +- (NSString *)sqlWhereExpressionWithBinary:(BOOL)isBINARY error:(NSError **)err; + +- (NSDictionary *)serializedFilter; +- (void)restoreSerializedFilters:(NSDictionary *)serialized; + +- (NSDictionary *)makeSerializedFilterForColumn:(NSString *)colName operator:(NSString *)opName values:(NSArray *)values; + +@property (readonly, assign, nonatomic) CGFloat preferredHeight; + +/** + * Indicates whether the rule editor has no expressions + */ +- (BOOL)isEmpty; + +/** + * Adds a new row to the rule editor + */ +- (void)addFilterExpression; + +/** + * Used when the rule editor wants to trigger filtering + */ +@property (assign, nonatomic) id target; +@property (assign, nonatomic) SEL action; + @end diff --git a/Source/SPTableContentFilterController.m b/Source/SPTableContentFilterController.m index 39c0f722..6c8d16bc 100644 --- a/Source/SPTableContentFilterController.m +++ b/Source/SPTableContentFilterController.m @@ -1,5 +1,1347 @@ +// +// 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 "SPTableContent.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"; + +const NSString * const SerFilterClass = @"filterClass"; +const NSString * const SerFilterClassGroup = @"groupNode"; +const NSString * const SerFilterClassExpression = @"expressionNode"; +const NSString * const SerFilterGroupIsConjunction = @"isConjunction"; +const NSString * const SerFilterGroupChildren = @"children"; +/** + * The name of the column to filter in (left side expression) + * + * Legacy names: + * @"filterField", fieldField + */ +const NSString * const SerFilterExprColumn = @"column"; +/** + * The data type grouping of the column for applicable filters + */ +const NSString * const SerFilterExprType = @"filterType"; +/** + * The title of the filter operator to apply + * + * Legacy names: + * @"filterComparison", compareField + */ +const NSString * const SerFilterExprComparison = @"filterComparison"; +/** + * The values to apply the filter with + * + * Legacy names: + * @"filterValue", argumentField + * @"firstBetweenField", @"secondBetweenField", firstBetweenField, secondBetweenField + */ +const NSString * const SerFilterExprValues = @"filterValues"; +/** + * the filter definition dictionary (as in ContentFilters.plist) + * + * This item is not designed to be serialized to disk + */ +const NSString * const SerFilterExprDefinition = @"filterDefinition"; + +@interface RuleNode : NSObject { + RuleNodeType type; +} +@property(assign, nonatomic) RuleNodeType type; +@end + +@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 + +#pragma mark - + +@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 + +@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 + +#pragma mark - + +@interface StringNode : RuleNode { + NSString *value; +} +@property(copy, nonatomic) NSString *value; +@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 + +#pragma mark - + +@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 + +@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 + +#pragma mark - + +@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 + +@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 + +#pragma mark - + +@interface ConnectorNode : RuleNode { + NSDictionary *filter; + NSUInteger labelIndex; +} +@property (retain, nonatomic) NSDictionary *filter; +@property (assign, nonatomic) NSUInteger labelIndex; +@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 + +#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; + +@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; + + contentFilters = [[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)updateFiltersFrom:(SPTableContent *)tableContent +{ + [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(![tableContent selectedTable]) return; + + //sort column names if enabled + NSArray *columnDefinitions = [tableContent dataColumnDefinitions]; + 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]; +} + +- (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)) { + if(target && action) [target performSelector:action withObject:self]; + } +} + +- (void)_resize +{ + // The situation with the sizing is a bit f'ed up: + // - When this method 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 +{ + [self performSelector:@selector(_resize) withObject:nil afterDelay:0.2]; //TODO find a better way to trigger resize + //[self _resize]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + SPClear(model); + SPClear(columns); + [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 + +//TODO move +@interface SPFillView : NSView +{ + NSColor *currentColor; +} + +/** + * This method is invoked when unarchiving the View from the xib. + * The value is configured in IB under "User Defined Runtime Attributes" + */ +- (void)setSystemColorOfName:(NSString *)name; + +@end + +@implementation SPFillView + +- (void)setSystemColorOfName:(NSString *)name +{ + //TODO: xibs after 10.6 support storing colors as user defined attributes + NSColorList *scl = [NSColorList colorListNamed:@"System"]; + NSColor *color = [scl colorWithKey:name]; + if(color) { + [color retain]; + [currentColor release]; + currentColor = color; + [self setNeedsDisplay:YES]; + } +} + +- (void)drawRect:(NSRect)dirtyRect { + if(currentColor) { + [currentColor set]; + NSRectFill(dirtyRect); + } +} + +- (void)dealloc +{ + [currentColor release]; + [super dealloc]; +} + @end diff --git a/Source/SPTableFilterParser.m b/Source/SPTableFilterParser.m index 93b976f8..ac408baf 100644 --- a/Source/SPTableFilterParser.m +++ b/Source/SPTableFilterParser.m @@ -109,8 +109,7 @@ [clause flushCachedRegexData]; // Escape % sign for format insertion ie if number of arguments is greater than 0 - if(numberOfArguments > 0) - [clause replaceOccurrencesOfRegex:@"%" withString:@"%%"]; + if(numberOfArguments > 0) [clause replaceOccurrencesOfRegex:@"%" withString:@"%%"]; [clause flushCachedRegexData]; // Replace placeholder ${} by %@ -- cgit v1.2.3