aboutsummaryrefslogtreecommitdiffstats
path: root/Source
diff options
context:
space:
mode:
Diffstat (limited to 'Source')
-rw-r--r--Source/SPContentFilterManager.h4
-rw-r--r--Source/SPContentFilterManager.m38
-rw-r--r--Source/SPDatabaseDocument.m4
-rw-r--r--Source/SPHistoryController.m70
-rw-r--r--Source/SPTableContent.h51
-rw-r--r--Source/SPTableContent.m739
-rw-r--r--Source/SPTableContentFilterController.h94
-rw-r--r--Source/SPTableContentFilterController.m1342
-rw-r--r--Source/SPTableFilterParser.m3
9 files changed, 1737 insertions, 608 deletions
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 <NSTableViewDelegate, NSTableViewDataSource, NSComboBoxDataSource, NSComboBoxDelegate, SPDatabaseContentViewDelegate>
@@ -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 <pthread.h>
#import <SPMySQL/SPMySQL.h>
@@ -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 -
@@ -384,6 +389,68 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
}
/**
+ * 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.
* Should be called on the main thread.
@@ -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:@"(?<!\\\\)(\\$\\{.*?\\})" withString:@"[arg]"]];
- if([tip isMatchedByRegex:@"(?<!\\\\)\\$BINARY"]) {
- [tip replaceOccurrencesOfRegex:@"(?<!\\\\)\\$BINARY" withString:@""];
- [tip appendString:NSLocalizedString(@"\n\nPress ⇧ for binary search (case-sensitive).", @"\n\npress shift for binary search tooltip message")];
- }
- [tip flushCachedRegexData];
- [tip replaceOccurrencesOfRegex:@"(?<!\\\\)\\$CURRENT_FIELD" withString:[[fieldField titleOfSelectedItem] backtickQuotedString]];
- [tip flushCachedRegexData];
- [item setToolTip:tip];
- } else {
- [item setToolTip:@""];
- }
- [tip release];
- }
- [item setTag:i];
- [menu addItem:item];
- [item release];
- i++;
- }
-
-#ifndef SP_CODA
- [menu addItem:[NSMenuItem separatorItem]];
- NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Edit Filters…", @"edit filter") action:NULL keyEquivalent:@""];
- [item setToolTip:NSLocalizedString(@"Edit user-defined Filters…", @"edit user-defined filter")];
- [item setTag:i];
- [menu addItem:item];
- [item release];
-#endif
-
- // Attempt to reselect the previously selected title, falling back to the first
- // item on failure, as long as there is no filter selection to be restored.
- if (!filterFieldToRestore) {
- [compareField selectItemWithTitle:titleToRestore];
- if (![compareField selectedItem]) [compareField selectItemAtIndex:0];
- }
-
- // Update the argumentField enabled state
- [self performSelectorOnMainThread:@selector(toggleFilterField:) withObject:self waitUntilDone:YES];
-
- // set focus on argumentField
- [argumentField performSelectorOnMainThread:@selector(selectText:) withObject:self waitUntilDone:YES];
-
-}
-
-- (void)openContentFilterManager
-{
- [compareField selectItemWithTag:lastSelectedContentFilterIndex];
-
- // init query favorites controller
-#ifndef SP_CODA
- [prefs synchronize];
-#endif
- if(contentFilterManager) [contentFilterManager release];
- contentFilterManager = [[SPContentFilterManager alloc] initWithDelegate:self forFilterType:compareType];
-
- // Open query favorite manager
- [NSApp beginSheet:[contentFilterManager window]
- modalForWindow:[tableDocumentInstance parentWindow]
- modalDelegate:contentFilterManager
- didEndSelector:nil
- contextInfo:nil];
-}
-
/**
* Tries to write a new row to the table.
* Returns YES if row is written to table, otherwise NO; also returns YES if no row
@@ -3877,21 +3618,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
*/
- (NSDictionary *) filterSettings
{
- NSDictionary *theDictionary;
-
- if (![fieldField isEnabled]) return nil;
-
- theDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
- [self tableFilterString], @"menuLabel",
- [fieldField titleOfSelectedItem], @"filterField",
- [[compareField selectedItem] title], @"filterComparison",
- [NSNumber numberWithInteger:[[compareField selectedItem] tag]], @"filterComparisonTag",
- [argumentField stringValue], @"filterValue",
- [firstBetweenField stringValue], @"firstBetweenField",
- [secondBetweenField stringValue], @"secondBetweenField",
- nil];
-
- return theDictionary;
+ return [filterControllerInstance serializedFilter];
}
/**
@@ -3938,44 +3665,9 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
*/
- (void) setFiltersToRestore:(NSDictionary *)filterSettings
{
- if (filterFieldToRestore) SPClear(filterFieldToRestore);
- if (filterComparisonToRestore) SPClear(filterComparisonToRestore);
- if (filterValueToRestore) SPClear(filterValueToRestore);
- if (firstBetweenValueToRestore) SPClear(firstBetweenValueToRestore);
- if (secondBetweenValueToRestore) SPClear(secondBetweenValueToRestore);
-
- if ([filterSettings count]) {
- if ([filterSettings objectForKey:@"filterField"])
- filterFieldToRestore = [[NSString alloc] initWithString:[filterSettings objectForKey:@"filterField"]];
- if ([[filterSettings objectForKey:@"filterComparison"] unboxNull]) {
- // Check if operator is BETWEEN, if so set up input fields
- if([[filterSettings objectForKey:@"filterComparison"] isEqualToString:@"BETWEEN"]) {
- [argumentField setHidden:YES];
- [betweenTextField setHidden:NO];
- [firstBetweenField setHidden:NO];
- [secondBetweenField setHidden:NO];
- [firstBetweenField setEnabled:YES];
- [secondBetweenField setEnabled:YES];
- }
-
- filterComparisonToRestore = [[NSString alloc] initWithString:[filterSettings objectForKey:@"filterComparison"]];
- }
- if([filterComparisonToRestore isEqualToString:@"BETWEEN"]) {
- if ([filterSettings objectForKey:@"firstBetweenField"])
- firstBetweenValueToRestore = [[NSString alloc] initWithString:[filterSettings objectForKey:@"firstBetweenField"]];
- if ([filterSettings objectForKey:@"secondBetweenField"])
- secondBetweenValueToRestore = [[NSString alloc] initWithString:[filterSettings objectForKey:@"secondBetweenField"]];
- } else {
- id filterValue = [filterSettings objectForKey:@"filterValue"];
- if ([filterValue unboxNull]) {
- if ([filterValue isKindOfClass:[NSData class]]) {
- filterValueToRestore = [[NSString alloc] initWithData:(NSData *)filterValue encoding:[mySQLConnection stringEncoding]];
- } else {
- filterValueToRestore = [[NSString alloc] initWithString:(NSString *)filterValue];
- }
- }
- }
- }
+ [filterSettings retain];
+ SPClear(filtersToRestore);
+ filtersToRestore = filterSettings;
}
/**
@@ -4032,6 +3724,48 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
#pragma mark -
#pragma mark Table drawing and editing
+- (void)updateFilterRuleEditorSize:(CGFloat)requestedHeight animate:(BOOL)animate
+{
+ NSRect contentViewRect = [contentAreaContainer frame];
+ CGFloat availableHeight = contentViewRect.size.height;
+ //the rule editor can ask for about one-third of the available space before we have it use it's scrollbar
+ CGFloat givenHeight = MIN(requestedHeight + 1,(availableHeight / 3));
+
+ // abort if the size didn't really change
+ NSRect ruleEditorRect = [filterRuleEditorContainer frame];
+ if(givenHeight == ruleEditorRect.size.height) return;
+
+ CGFloat newTableContentHeight = availableHeight - givenHeight;
+
+ NSRect tableContentRect = [tableContentContainer frame];
+ tableContentRect.size.height = newTableContentHeight;
+
+
+ ruleEditorRect.origin.y = newTableContentHeight;
+ ruleEditorRect.size.height = givenHeight;
+
+ if(animate) {
+ [NSAnimationContext beginGrouping];
+ [[tableContentContainer animator] setFrame:tableContentRect];
+ [[filterRuleEditorContainer animator] setFrame:ruleEditorRect];
+ [NSAnimationContext endGrouping];
+ }
+ else {
+ [tableContentContainer setFrameSize:tableContentRect.size];
+ [filterRuleEditorContainer setFrame:ruleEditorRect];
+ }
+}
+
+- (void)filterRuleEditorPreferredSizeChanged:(NSNotification *)notification
+{
+ [self updateFilterRuleEditorSize:[filterControllerInstance preferredHeight] animate:YES];
+}
+
+- (void)contentViewSizeChanged:(NSNotification *)notification
+{
+ [self updateFilterRuleEditorSize:[filterControllerInstance preferredHeight] animate:NO];
+}
+
/**
* Updates the number of rows in the selected table.
* Attempts to use the fullResult count if available, also updating the
@@ -4153,6 +3887,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
[duplicateButton setEnabled:NO];
[reloadButton setEnabled:NO];
[filterButton setEnabled:NO];
+ [toggleRuleFilterButton setEnabled:NO];
tableRowsSelectable = NO;
[paginationPreviousButton setEnabled:NO];
[paginationNextButton setEnabled:NO];
@@ -4187,7 +3922,8 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
}
}
- [filterButton setEnabled:[fieldField isEnabled]];
+ [filterButton setEnabled:(!![selectedTable length])];
+ [toggleRuleFilterButton setEnabled:(!![selectedTable length])];
tableRowsSelectable = YES;
}
@@ -4667,24 +4403,9 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
*/
- (void)makeContentFilterHaveFocus
{
- NSDictionary *filter = [[contentFilters objectForKey:compareType] objectAtIndex:[[compareField selectedItem] tag]];
-
- if ([filter objectForKey:@"NumberOfArguments"]) {
-
- NSUInteger numOfArgs = [[filter objectForKey:@"NumberOfArguments"] integerValue];
-
- switch (numOfArgs)
- {
- case 2:
- [[firstBetweenField window] makeFirstResponder:firstBetweenField];
- break;
- case 1:
- [[argumentField window] makeFirstResponder:argumentField];
- break;
- default:
- [[compareField window] makeFirstResponder:compareField];
- }
- }
+ [self setRuleEditorVisible:YES animate:YES];
+ [toggleRuleFilterButton setState:NSOnState];
+ [filterControllerInstance focusFirstInputField];
}
#endif
@@ -5515,11 +5236,7 @@ static NSString *SPTableFilterSetDefaultOperator = @"SPTableFilterSetDefaultOper
if (selectionToRestore) SPClear(selectionToRestore);
if (cqColumnDefinition) SPClear(cqColumnDefinition);
- if (filterFieldToRestore) filterFieldToRestore = nil;
- if (filterComparisonToRestore) filterComparisonToRestore = nil;
- if (filterValueToRestore) filterValueToRestore = nil;
- if (firstBetweenValueToRestore) firstBetweenValueToRestore = nil;
- if (secondBetweenValueToRestore) secondBetweenValueToRestore = nil;
+ SPClear(filtersToRestore);
[super dealloc];
}
diff --git a/Source/SPTableContentFilterController.h b/Source/SPTableContentFilterController.h
index 542f4bad..6c7f3fbb 100644
--- a/Source/SPTableContentFilterController.h
+++ b/Source/SPTableContentFilterController.h
@@ -1,7 +1,32 @@
-// This class is a dummy.
-// It is only present because DBView.xib already references it, but the
-// code itself is still in another branch. This stub is used to avoid a warning
-// from the Nib loader, saying 'this class was not found and replaced with a NSObject'.
+//
+// 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 <https://github.com/sequelpro/sequelpro>
#import <Foundation/Foundation.h>
@@ -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 <https://github.com/sequelpro/sequelpro>
+
#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 () <NSRuleEditorDelegate>
+
+@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:@"(?<!\\\\)(\\$\\{.*?\\})" withString:@"[arg]"]];
+ if ([tip isMatchedByRegex:@"(?<!\\\\)\\$BINARY"]) {
+ [tip replaceOccurrencesOfRegex:@"(?<!\\\\)\\$BINARY" withString:@""];
+ [tip appendString:NSLocalizedString(@"\n\nPress ⇧ for binary search (case-sensitive).", @"\n\npress shift for binary search tooltip message")];
+ }
+ [tip flushCachedRegexData];
+ [tip replaceOccurrencesOfRegex:@"(?<!\\\\)\\$CURRENT_FIELD" withString:[[colNode name] backtickQuotedString]];
+ [tip flushCachedRegexData];
+ tooltip = [NSString stringWithString:tip];
+ } else {
+ tooltip = @"";
+ }
+ [tip release];
+ }
+
+ OpNode *node = [[OpNode alloc] init];
+ [node setParentColumn:colNode];
+ [node setSettings:@{
+ @"title": ([filter objectForKey:@"MenuLabel"] ? [filter objectForKey:@"MenuLabel"] : @"not specified"),
+ @"tooltip": tooltip,
+ @"tag": @(i),
+ @"filterType": compareType,
+ }];
+ [node setFilter:filter];
+ [compareItems addObject:node];
+ [node release];
+ i++;
+ }
+ }
+
+ {
+ OpNode *node = [[OpNode alloc] init];
+ [node setParentColumn:colNode];
+ [node setSettings:@{
+ @"isSeparator": @YES,
+ }];
+ [compareItems addObject:node];
+ [node release];
+ }
+
+ {
+ OpNode *node = [[OpNode alloc] init];
+ [node setParentColumn:colNode];
+ [node setSettings:@{
+ @"title": NSLocalizedString(@"Edit Filters…", @"edit filter"),
+ @"tooltip": NSLocalizedString(@"Edit user-defined Filters…", @"edit user-defined filter"),
+ @"tag": @(i),
+ @"target": self,
+ @"action": [NSValue valueWithPointer:@selector(_editFiltersAction:)],
+ @"filterType": compareType,
+ }];
+ [compareItems addObject:node];
+ [node release];
+ }
+
+ return compareItems;
+}
+
+- (IBAction)_editFiltersAction:(id)sender
+{
+ if([sender isKindOfClass:[NSMenuItem class]]) {
+ NSMenuItem *menuItem = (NSMenuItem *)sender;
+ NSString *filterType = [(NSDictionary *)[menuItem representedObject] objectForKey:@"filterType"];
+ if([filterType unboxNull]) [self openContentFilterManagerForFilterType:filterType];
+ }
+}
+
+- (void)openContentFilterManagerForFilterType:(NSString *)filterType
+{
+ // init query favorites controller
+#ifndef SP_CODA
+ [[NSUserDefaults standardUserDefaults] synchronize];
+#endif
+ if(contentFilterManager) [contentFilterManager release];
+ contentFilterManager = [[SPContentFilterManager alloc] initWithDatabaseDocument:tableDocumentInstance forFilterType:filterType];
+
+ // Open query favorite manager
+ [NSApp beginSheet:[contentFilterManager window]
+ modalForWindow:[tableDocumentInstance parentWindow]
+ modalDelegate:contentFilterManager
+ didEndSelector:nil
+ contextInfo:nil];
+}
+
+- (void)_contentFiltersHaveBeenUpdated:(NSNotification *)notification
+{
+ //tell the rule editor to reload its criteria
+ [filterRuleEditor reloadCriteria];
+}
+
+- (BOOL)isEmpty
+{
+ return ([[self model] count] == 0);
+}
+
+- (void)addFilterExpression
+{
+ [filterRuleEditor insertRowAtIndex:0 withType:NSRuleEditorRowTypeSimple asSubrowOfRow:-1 animate:NO];
+}
+
+- (NSString *)sqlWhereExpressionWithBinary:(BOOL)isBINARY error:(NSError **)err
+{
+ NSMutableString *filterString = [[NSMutableString alloc] init];
+ NSError *innerError = nil;
+
+ @autoreleasepool {
+ //get the serialized filter and try to optimise it
+ NSDictionary *filterTree = [[self class] _flattenSerializedFilter:[self _serializedFilterIncludingFilterDefinition:YES]];
+
+ // build it recursively
+ [[self class] _writeFilterTree:filterTree toString:filterString wrapInParenthesis:NO binary:isBINARY error:&innerError];
+
+ [innerError retain]; // carry the error (if any) outside of the scope of the autoreleasepool
+ }
+
+ if(innerError) {
+ [filterString release];
+ if(err) *err = [innerError autorelease];
+ return nil;
+ }
+
+ NSString *out = [filterString copy];
+ [filterString release];
+
+ return [out autorelease];
+}
+
+- (NSDictionary *)serializedFilter
+{
+ return [self _serializedFilterIncludingFilterDefinition:NO];
+}
+
+- (NSDictionary *)_serializedFilterIncludingFilterDefinition:(BOOL)includeDefinition
+{
+ NSMutableArray *rootItems = [NSMutableArray arrayWithCapacity:[model count]];
+ for(NSDictionary *item in model) {
+ [rootItems addObject:[self _serializeSubtree:item includingDefinition:includeDefinition]];
+ }
+ //the root serialized filter can either be an AND of multiple root items or a single root item
+ if([rootItems count] == 1) {
+ return [rootItems objectAtIndex:0];
+ }
+ else {
+ return @{
+ SerFilterClass: SerFilterClassGroup,
+ SerFilterGroupIsConjunction: @YES,
+ SerFilterGroupChildren: rootItems,
+ };
+ }
+}
+
+- (NSDictionary *)_serializeSubtree:(NSDictionary *)item includingDefinition:(BOOL)includeDefinition
+{
+ NSRuleEditorRowType rowType = (NSRuleEditorRowType)[[item objectForKey:@"rowType"] unsignedIntegerValue];
+ // check if we have an AND or OR compound row
+ if(rowType == NSRuleEditorRowTypeCompound) {
+ // process all children
+ NSArray *subrows = [item objectForKey:@"subrows"];
+ NSMutableArray *children = [[NSMutableArray alloc] initWithCapacity:[subrows count]];
+ for(NSDictionary *subitem in subrows) {
+ [children addObject:[self _serializeSubtree:subitem includingDefinition:includeDefinition]];
+ }
+ StringNode *node = [[item objectForKey:@"criteria"] objectAtIndex:0];
+ BOOL isConjunction = [@"AND" isEqualToString:[node value]];
+ NSDictionary *out = @{
+ SerFilterClass: SerFilterClassGroup,
+ SerFilterGroupIsConjunction: @(isConjunction),
+ SerFilterGroupChildren: children,
+ };
+ [children release];
+ return out;
+ }
+ else {
+ NSArray *criteria = [item objectForKey:@"criteria"];
+ NSArray *displayValues = [item objectForKey:@"displayValues"];
+ ColumnNode *col = [criteria objectAtIndex:0];
+ OpNode *op = [criteria objectAtIndex:1];
+ NSMutableArray *filterValues = [[NSMutableArray alloc] initWithCapacity:2];
+ for (NSUInteger i = 2; i < [criteria count]; ++i) { // the first two must always be column and operator
+ if([(RuleNode *)[criteria objectAtIndex:i] type] != RuleNodeTypeArgument) continue;
+ // if we found an argument, the displayValue will be an NSTextField we can ask for the value
+ NSString *value = [(NSTextField *)[displayValues objectAtIndex:i] stringValue];
+ [filterValues addObject:value];
+ }
+ NSDictionary *out = @{
+ SerFilterClass: SerFilterClassExpression,
+ SerFilterExprColumn: [col name],
+ SerFilterExprType: [[op settings] objectForKey:@"filterType"],
+ SerFilterExprComparison: [[op filter] objectForKey:@"MenuLabel"],
+ SerFilterExprValues: filterValues,
+ };
+ if(includeDefinition) {
+ out = [NSMutableDictionary dictionaryWithDictionary:out];
+ [(NSMutableDictionary *)out setObject:[op filter] forKey:SerFilterExprDefinition];
+ }
+ [filterValues release];
+ return out;
+ }
+}
+
+void _addIfNotNil(NSMutableArray *array, id toAdd)
+{
+ if(toAdd != nil) [array addObject:toAdd];
+}
+
+- (void)restoreSerializedFilters:(NSDictionary *)serialized
+{
+ if(!serialized) return;
+
+ // we have to exchange the whole model object or NSRuleEditor will get confused
+ NSMutableArray *newModel = [[NSMutableArray alloc] init];
+
+ @autoreleasepool {
+ // if the root object is an AND group directly restore its contents, otherwise restore the object
+ if(SerIsGroup(serialized) && [[serialized objectForKey:SerFilterGroupIsConjunction] boolValue]) {
+ for(NSDictionary *child in [serialized objectForKey:SerFilterGroupChildren]) {
+ _addIfNotNil(newModel, [self _restoreSerializedFilter:child]);
+ }
+ }
+ else {
+ _addIfNotNil(newModel, [self _restoreSerializedFilter:serialized]);
+ }
+ }
+
+ [self setModel:newModel];
+ [newModel release];
+}
+
+- (NSMutableDictionary *)_restoreSerializedFilter:(NSDictionary *)serialized
+{
+ NSMutableDictionary *obj = [[NSMutableDictionary alloc] initWithCapacity:4];
+
+ if(SerIsGroup(serialized)) {
+ [obj setObject:@(NSRuleEditorRowTypeCompound) forKey:@"rowType"];
+
+ StringNode *sn = [[StringNode alloc] init];
+ [sn setValue:([[serialized objectForKey:SerFilterGroupIsConjunction] boolValue] ? @"AND" : @"OR")];
+ // those have to be mutable arrays for the rule editor to work
+ NSMutableArray *criteria = [NSMutableArray arrayWithObject:sn];
+ [obj setObject:criteria forKey:@"criteria"];
+
+ id displayValue = [self ruleEditor:filterRuleEditor displayValueForCriterion:sn inRow:-1];
+ NSMutableArray *displayValues = [NSMutableArray arrayWithObject:displayValue];
+ [obj setObject:displayValues forKey:@"displayValues"];
+ [sn release];
+
+ NSArray *children = [serialized objectForKey:SerFilterGroupChildren];
+ NSMutableArray *subrows = [[NSMutableArray alloc] initWithCapacity:[children count]];
+ for(NSDictionary *child in children) {
+ _addIfNotNil(subrows, [self _restoreSerializedFilter:child]);
+ }
+ [obj setObject:subrows forKey:@"subrows"];
+ [subrows release];
+ }
+ else {
+ [obj setObject:@(NSRuleEditorRowTypeSimple) forKey:@"rowType"];
+ //simple rows can't have child rows
+ [obj setObject:[NSMutableArray array] forKey:@"subrows"];
+
+ NSMutableArray *criteria = [NSMutableArray arrayWithCapacity:5];
+
+ //first look up the column, bail if it doesn't exist anymore or types changed
+ NSString *columnName = [serialized objectForKey:SerFilterExprColumn];
+ ColumnNode *col = [self _columnForName:columnName];
+ if(!col) {
+ SPLog(@"cannot deserialize unknown column: %@", columnName);
+ goto fail;
+ }
+ [criteria addObject:col];
+
+ //next try to find the given operator
+ NSString *operatorName = [serialized objectForKey:SerFilterExprComparison];
+ OpNode *op = [self _operatorNamed:operatorName forColumn:col];
+ if(!op) {
+ SPLog(@"cannot deserialize unknown operator: %@",operatorName);
+ goto fail;
+ }
+ [criteria addObject:op];
+
+ // we still have to check if the current column type is the same as when we serialized because an operator
+ // with the same name can still act differently for different types
+ NSString *curFilterType = [[op settings] objectForKey:@"filterType"];
+ NSString *serFilterType = [serialized objectForKey:SerFilterExprType]; // this is optional
+ if(serFilterType && ![curFilterType isEqualToString:serFilterType]) {
+ SPLog(@"mistmatch in filter types for operator %@: current=%@, serialized=%@",op,curFilterType,serFilterType);
+ goto fail;
+ }
+
+ //now we have to create the argument node(s)
+ NSInteger numOfArgs = [[[op filter] objectForKey:@"NumberOfArguments"] integerValue];
+ //fail if the current op requires more arguments than we have stored values for
+ NSArray *values = [serialized objectForKey:SerFilterExprValues];
+ if(numOfArgs > [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 %@