//
// $Id$
//
// SPContentFilterManager.m
// sequel-pro
//
// Created by Hans-Jörg Bibiko on Sep 29, 2009.
// Copyright (c) 2009 Hans-Jörg Bibiko. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// More info at
#import "SPContentFilterManager.h"
#import "ImageAndTextCell.h"
#import "RegexKitLite.h"
#import "SPQueryController.h"
#import "SPQueryDocumentsController.h"
#import "SPDatabaseDocument.h"
#import "SPTableContent.h"
#import "SPConnectionController.h"
#import "SPSplitView.h"
#define SP_MULTIPLE_SELECTION_PLACEHOLDER_STRING NSLocalizedString(@"[multiple selection]", @"[multiple selection]")
#define SP_NO_SELECTION_PLACEHOLDER_STRING NSLocalizedString(@"[no selection]", @"[no selection]")
#define SP_NAME_REQUIRED_PLACEHOLDER_STRING NSLocalizedString(@"[name required]", @"displayed when new content filter has empty Name field (ContentFilterManager)")
#define SP_FILE_PARSER_ERROR_TITLE_STRING NSLocalizedString(@"Error while reading data file", @"error while reading data file")
@implementation SPContentFilterManager
/**
* Initialize the manager with the supplied delegate
*/
- (id)initWithDelegate:(id)managerDelegate forFilterType:(NSString *)compareType
{
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"];
#ifndef SP_CODA
delegatesFileURL = [tableDocumentInstance fileURL];
#endif
filterType = [NSString stringWithString:compareType];
}
return self;
}
/**
* Upon awakening bind the query text view's background colour.
*/
- (void)awakeFromNib
{
// Set up the split view
[contentFilterSplitView setMinSize:120.f ofSubviewAtIndex:0];
[contentFilterSplitView setMaxSize:245.f ofSubviewAtIndex:0];
// Add global group row to contentFilters
[contentFilters addObject:[NSDictionary dictionaryWithObjectsAndKeys:
NSLocalizedString(@"Global",@"Content Filter Manager : Filter Entry List: 'Global' Header"), @"MenuLabel",
@"", @"headerOfFileURL",
@"", @"Clause",
@"", @"ConjunctionLabel",
nil]];
#ifndef SP_CODA /* prefs access */
// Build data source for global content filter (as mutable copy! otherwise each
// change will be stored in the prefs at once)
if([[prefs objectForKey:SPContentFilters] objectForKey:filterType]) {
for(id fav in [[prefs objectForKey:SPContentFilters] objectForKey:filterType]) {
id f = [[fav mutableCopy] autorelease];
if([f objectForKey:@"ConjunctionLabels"])
[f setObject:[[f objectForKey:@"ConjunctionLabels"] objectAtIndex:0] forKey:@"ConjunctionLabel"];
[contentFilters addObject:f];
}
}
// Build doc-based filters
[contentFilters addObject:[NSDictionary dictionaryWithObjectsAndKeys:
[[[delegatesFileURL absoluteString] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] lastPathComponent], @"MenuLabel",
[delegatesFileURL absoluteString], @"headerOfFileURL",
@"", @"Clause",
nil]];
if([[SPQueryController sharedQueryController] contentFilterForFileURL:delegatesFileURL]) {
id filters = [[SPQueryController sharedQueryController] contentFilterForFileURL:delegatesFileURL];
if([filters objectForKey:filterType])
for(id fav in [filters objectForKey:filterType])
[contentFilters addObject:[[fav mutableCopy] autorelease]];
}
#endif
// Select the first query if any
NSUInteger i = 0;
for(i=0; i < [contentFilters count]; i++ )
if(![[contentFilters objectAtIndex:i] objectForKey:@"headerOfFileURL"])
break;
[[self window] makeFirstResponder:contentFilterTableView];
// Init GUI elements
[contentFilterTableView selectRowIndexes:[NSIndexSet indexSet] byExtendingSelection:NO];
[[contentFilterNameTextField cell] setPlaceholderString:SP_NO_SELECTION_PLACEHOLDER_STRING];
[contentFilterNameTextField setStringValue:@""];
[contentFilterTextView setString:@""];
// Register drag types
[contentFilterTableView registerForDraggedTypes:[NSArray arrayWithObject:SPContentFilterPasteboardDragType]];
[contentFilterArrayController setContent:contentFilters];
[contentFilterTableView reloadData];
// Set Remove button state
[removeButton setEnabled:([contentFilterTableView numberOfSelectedRows] > 0)];
// Set column header
[[[contentFilterTableView tableColumnWithIdentifier:@"MenuLabel"] headerCell] setStringValue:[NSString stringWithFormat:NSLocalizedString(@"‘%@’ Fields Content Filters", @"table column header. Read: 'Showing all content filters for fields of type %@' (ContentFilterManager)"), filterType]];
}
#pragma mark -
#pragma mark Accessor methods
/**
* Returns the content filters array for fileURL.
*
* @param fileURL == The SPDatabaseDocument file URL; if fileURL == nil return the global content filters
*/
- (NSMutableArray *)contentFilterForFileURL:(NSURL *)fileURL
{
NSMutableArray *filters = [NSMutableArray array];
NSString *fileURLstring;
if(fileURL == nil)
fileURLstring = @"";
else
fileURLstring = [fileURL absoluteString];
NSUInteger i = 0;
// Look for the header specified by fileURL
while(i<[contentFilters count]) {
if ([[contentFilters objectAtIndex:i] objectForKey:@"headerOfFileURL"]
&& [[[contentFilters objectAtIndex:i] objectForKey:@"headerOfFileURL"] isEqualToString:fileURLstring]) {
i++;
break;
}
i++;
}
// Take all content filters until the next header or end of all content filters
NSUInteger numOfArgs;
for ( ; i<[contentFilters count]; i++) {
if(![[contentFilters objectAtIndex:i] objectForKey:@"headerOfFileURL"]) {
NSMutableDictionary *d = [[NSMutableDictionary alloc] init];
[d setDictionary:[contentFilters objectAtIndex:i]];
NSMutableArray *conjLabel = [[NSMutableArray alloc] init];
numOfArgs = [[[d objectForKey:@"Clause"] componentsMatchedByRegex:@"(? 1) {
if([d objectForKey:@"ConjunctionLabel"]) {
[conjLabel addObject:[d objectForKey:@"ConjunctionLabel"]];
[d setObject:conjLabel forKey:@"ConjunctionLabels"];
}
} else {
[d removeObjectForKey:@"ConjunctionLabels"];
}
[d removeObjectForKey:@"ConjunctionLabel"];
[conjLabel release];
[d setObject:[NSNumber numberWithInteger:numOfArgs] forKey:@"NumberOfArguments"];
[filters addObject:d];
[d release];
} else
break;
}
return filters;
}
/**
* This method is only implemented to be compatible with SPTextView.
*/
- (id)customQueryInstance
{
return [tableDocumentInstance valueForKey:@"customQueryInstance"];
}
#pragma mark -
#pragma mark IBAction methods
/**
* Adds/Inserts a content filter
*/
- (IBAction)addContentFilter:(id)sender
{
NSMutableDictionary *filter;
NSUInteger insertIndex;
// Store pending changes in Clause
[[self window] makeFirstResponder:contentFilterNameTextField];
// Duplicate a selected filter if sender == self
if(sender == self)
filter = [NSMutableDictionary dictionaryWithObjects:[NSArray arrayWithObjects:[NSString stringWithFormat:NSLocalizedString(@"%@ Copy",@"Content Filter Manager : Initial name of copied filter"),[contentFilterNameTextField stringValue]], [contentFilterTextView string], nil] forKeys:[NSArray arrayWithObjects:@"MenuLabel", @"Clause", nil]];
// Add a new filter
else
filter = [NSMutableDictionary dictionaryWithObjects:[NSArray arrayWithObjects:NSLocalizedString(@"New Filter",@"Content Filter Manager : Initial name for new filter"), @"", @"", nil] forKeys:[NSArray arrayWithObjects:@"MenuLabel", @"Clause", @"ConjunctionLabel", nil]];
// If a favourite is currently selected, add the new favourite next to it
if([contentFilterTableView numberOfSelectedRows] > 0) {
insertIndex = [[contentFilterTableView selectedRowIndexes] lastIndex]+1;
[contentFilters insertObject:filter atIndex:insertIndex];
}
#ifndef SP_CODA
// If the DatabaseDocument is an on-disk document, add the favourite to the bottom of that document's favourites
else if (![tableDocumentInstance isUntitled]) {
insertIndex = [contentFilters count] - 1;
[contentFilters addObject:filter];
}
#endif
// Otherwise, add to the bottom of the Global list by default
else {
insertIndex = 1;
while (![[contentFilters objectAtIndex:insertIndex] objectForKey:@"headerOfFileURL"]) {
insertIndex++;
}
[contentFilters insertObject:filter atIndex:insertIndex];
}
[contentFilterArrayController rearrangeObjects];
[contentFilterTableView reloadData];
[contentFilterTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:insertIndex] byExtendingSelection:NO];
[contentFilterTableView scrollRowToVisible:[contentFilterTableView selectedRow]];
[removeButton setEnabled:([contentFilterTableView numberOfSelectedRows] > 0)];
[[self window] makeFirstResponder:contentFilterNameTextField];
}
/**
* Duplicates a filter
*/
- (IBAction)duplicateContentFilter:(id)sender
{
if ([contentFilterTableView numberOfSelectedRows] == 1)
[self addContentFilter:self];
else
NSBeep();
}
/**
* Removes a filter
*/
- (IBAction)removeContentFilter:(id)sender
{
// Complete editing in the window
[[sender window] makeFirstResponder:[sender window]];
NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Remove selected content filters?", @"remove selected content filters message")
defaultButton:NSLocalizedString(@"Remove", @"remove button")
alternateButton:NSLocalizedString(@"Cancel", @"cancel button")
otherButton:nil
informativeTextWithFormat:NSLocalizedString(@"Are you sure you want to remove all selected content filters? This action cannot be undone.", @"remove all selected content filters informative message")];
[alert setAlertStyle:NSCriticalAlertStyle];
NSArray *buttons = [alert buttons];
// Change the alert's cancel button to have the key equivalent of return
[[buttons objectAtIndex:0] setKeyEquivalent:@"r"];
[[buttons objectAtIndex:0] setKeyEquivalentModifierMask:NSCommandKeyMask];
[[buttons objectAtIndex:1] setKeyEquivalent:@"\r"];
[alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:@"removeSelectedFilters"];
}
/**
* Insert placeholder - the to be inserted placeholder string is stored in sender's tooltip
*/
- (IBAction)insertPlaceholder:(id)sender
{
[contentFilterTextView insertText:[[[sender selectedItem] toolTip] substringToIndex:[[[sender selectedItem] toolTip] rangeOfString:@" – "].location]];
}
/**
* Show save panel sheet for exporting content filters to disk
*/
- (IBAction)exportContentFilter:(id)sender
{
#ifndef SP_CODA
NSSavePanel *panel = [NSSavePanel savePanel];
[panel setAllowedFileTypes:[NSArray arrayWithObject:SPFileExtensionDefault]];
[panel setExtensionHidden:NO];
[panel setAllowsOtherFileTypes:NO];
[panel setCanSelectHiddenExtension:YES];
[panel setCanCreateDirectories:YES];
[panel beginSheetForDirectory:nil file:nil modalForWindow:[self window] modalDelegate:self didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:@"exportFilter"];
#endif
}
/**
* Show open panel sheet for importing content filters by adding them to current ones
*/
- (IBAction)importContentFilterByAdding:(id)sender
{
#ifndef SP_CODA
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setCanSelectHiddenExtension:YES];
[panel setDelegate:self];
[panel setCanChooseDirectories:NO];
[panel setAllowsMultipleSelection:NO];
// [panel setResolvesAliases:YES];
[panel beginSheetForDirectory:nil
file:@""
types:[NSArray arrayWithObjects:SPFileExtensionDefault, nil]
modalForWindow:[self window]
modalDelegate:self
didEndSelector:@selector(importPanelDidEnd:returnCode:contextInfo:)
contextInfo:NULL];
#endif
}
/**
* Show open panel sheet for importing content filters by replacing the current ones. Not yet implemented
*/
- (IBAction)importFavoritesByReplacing:(id)sender
{
}
/**
* Closes the content filter manager
*/
- (IBAction)closeContentFilterManagerSheet:(id)sender
{
[NSApp endSheet:[self window] returnCode:0];
[[self window] orderOut:self];
// "Apply Changes" button was pressed
if([sender tag]) {
// Ensure that last changes will be written back
// if only one filter is selected; otherwise unstable state
if ([contentFilterTableView numberOfSelectedRows] == 1)
[[self window] makeFirstResponder:contentFilterTableView];
#ifndef SP_CODA
// Update current document's content filters in the SPQueryController
[[SPQueryController sharedQueryController] replaceContentFilterByArray:
[self contentFilterForFileURL:delegatesFileURL] ofType:filterType forFileURL:delegatesFileURL];
// Update global preferences' list
id cf = [[prefs objectForKey:SPContentFilters] mutableCopy];
[cf setObject:[self contentFilterForFileURL:nil] forKey:filterType];
[prefs setObject:cf forKey:SPContentFilters];
[cf release];
// Inform all opened documents to update the query favorites list
for(id doc in [[NSApp delegate] orderedDocuments])
if([[doc valueForKeyPath:@"tableContentInstance"] respondsToSelector:@selector(setCompareTypes:)])
[[doc valueForKeyPath:@"tableContentInstance"] setCompareTypes:nil];
#endif
}
}
/**
* It triggers an update of contentFilterTextView and
* resultingClauseContentLabel by inserting @"" into contentFilterTextView
*/
- (IBAction)suppressLeadingFiledPlaceholderWasChanged:(id)sender
{
[contentFilterTextView insertText:@""];
}
#pragma mark -
#pragma mark TableView delegate methods
/**
* Update contentFilterNameTextField if selection of contentFilterTableView was changed.
*/
- (void)tableViewSelectionDidChange:(NSNotification *)aNotification
{
NSInteger row = [contentFilterTableView selectedRow];
if ((row > -1) && (row < (NSInteger)[contentFilters count])) {
NSString *newName = [[contentFilters objectAtIndex:[contentFilterTableView selectedRow]] objectForKey:@"MenuLabel"];
[contentFilterNameTextField setStringValue:(newName) ? newName : @""];
}
}
/**
* Returns the number of all content filters.
*/
- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView
{
return [contentFilters count];
}
/**
* Returns the value for the requested table column and row index.
*/
- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
if (![[contentFilters objectAtIndex:rowIndex] objectForKey:[aTableColumn identifier]]) return @"";
return [[contentFilters objectAtIndex:rowIndex] objectForKey:[aTableColumn identifier]];
}
/**
* Save content filter name (MenuLabel) if inline edited (suppress empty names)
*/
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
if([[aTableColumn identifier] isEqualToString:@"MenuLabel"]) {
if([anObject isKindOfClass:[NSString class]] && [(NSString *)anObject length]) {
[[contentFilters objectAtIndex:rowIndex] setObject:anObject forKey:@"MenuLabel"];
[contentFilterNameTextField setStringValue:anObject];
}
}
[contentFilterTableView reloadData];
}
/**
* Before selecting an other filter save pending query string changes
* and make sure that no group table item can be selected
*/
- (BOOL)tableView:(NSTableView *)aTableView shouldSelectRow:(NSInteger)rowIndex
{
BOOL enable = ([contentFilterTableView numberOfSelectedRows] > 0);
[removeButton setEnabled:enable];
[numberOfArgsLabel setHidden:!enable];
[resultingClauseLabel setHidden:!enable];
[resultingClauseContentLabel setHidden:!enable];
[insertPlaceholderButton setEnabled:enable];
return (rowIndex >= 0 && [[contentFilters objectAtIndex:rowIndex] objectForKey:@"headerOfFileURL"]) ? NO : YES;
}
/**
* Set indention levels for headers and filters
* (maybe in the future use an image for headers for expanding and collapsing)
*/
- (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
if([[contentFilters objectAtIndex:rowIndex] objectForKey:@"headerOfFileURL"] && [[aTableColumn identifier] isEqualToString:@"MenuLabel"]) {
// if([[[favoriteProperties objectAtIndex:rowIndex] objectForKey:@"isGroup"] isEqualToString:@"1"])
// [(ImageAndTextCell*)aCell setImage:[NSImage imageNamed:@"NSRightFacingTriangleTemplate"]];
// else
// [(ImageAndTextCell*)aCell setImage:[NSImage imageNamed:@"NSLeftFacingTriangleTemplate"]];
[(ImageAndTextCell*)aCell setIndentationLevel:0];
}
else if(![[contentFilters objectAtIndex:rowIndex] objectForKey:@"headerOfFileURL"] && [[aTableColumn identifier] isEqualToString:@"MenuLabel"]) {
// [(ImageAndTextCell*)aCell setImage:[NSImage imageNamed:@"dummy-small"]];
[(ImageAndTextCell*)aCell setIndentationLevel:1];
}
}
/**
* A row of an header return is slighlty larger
*/
- (CGFloat)tableView:(NSTableView *)aTableView heightOfRow:(NSInteger)rowIndex
{
return ([[contentFilters objectAtIndex:rowIndex] objectForKey:@"headerOfFileURL"]) ? 20 : 18;
}
/**
* Only filter name can be edited inline
*/
- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
if([[contentFilters objectAtIndex:rowIndex] objectForKey:@"headerOfFileURL"]) {
return NO;
} else {
isTableCellEditing = YES;
return YES;
}
}
/**
* Sorting by clicking at a column header inside groups. Not yet implemented
*/
- (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn
{
// TODO: Not yet implemented
return;
}
/**
* If current row's contentFilters object has a key "headerOfFileURL" then row is grouped ie it's an header
*/
- (BOOL)tableView:(NSTableView *)aTableView isGroupRow:(NSInteger)rowIndex
{
return ([[contentFilters objectAtIndex:rowIndex] objectForKey:@"headerOfFileURL"]) ? YES : NO;
}
#pragma mark -
#pragma mark TableView drag & drop delegate methods
/**
* Return whether or not the supplied rows can be written.
*/
- (BOOL)tableView:(NSTableView *)aTableView writeRowsWithIndexes:(NSIndexSet *)rows toPasteboard:(NSPasteboard*)pboard
{
NSArray *pboardTypes = [NSArray arrayWithObject:SPContentFilterPasteboardDragType];
NSUInteger originalRow = [rows firstIndex];
if(originalRow < 1) return NO;
// Do not drag headers
if([[contentFilters objectAtIndex:originalRow] objectForKey:@"headerOfFileURL"]) return NO;
[pboard declareTypes:pboardTypes owner:nil];
NSMutableData *indexdata = [[[NSMutableData alloc] init] autorelease];
NSKeyedArchiver *archiver = [[[NSKeyedArchiver alloc] initForWritingWithMutableData:indexdata] autorelease];
[archiver encodeObject:rows forKey:@"indexdata"];
[archiver finishEncoding];
[pboard setData:indexdata forType:SPContentFilterPasteboardDragType];
return YES;
}
/**
* Validate the proposed drop of the supplied rows.
*/
- (NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id )info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)operation
{
NSArray *pboardTypes = [[info draggingPasteboard] types];
if (([pboardTypes count] > 1) && (row != -1)) {
if (([pboardTypes containsObject:SPContentFilterPasteboardDragType]) && (operation == NSTableViewDropAbove)) {
if (row > 0) {
return NSDragOperationMove;
}
}
}
return NSDragOperationNone;
}
/**
* Return whether or not to accept the drop of the supplied rows.
*/
- (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id )info row:(NSInteger)row dropOperation:(NSTableViewDropOperation)operation
{
if(row < 1) return NO;
NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:[[info draggingPasteboard] dataForType:SPContentFilterPasteboardDragType]] autorelease];
NSIndexSet *draggedIndexes = [[NSIndexSet alloc] initWithIndexSet:(NSIndexSet *)[unarchiver decodeObjectForKey:@"indexdata"]];
[unarchiver finishDecoding];
// TODO: still rely on a NSArray but in the future rewrite it to use the NSIndexSet directly
NSMutableArray *draggedRows = [[NSMutableArray alloc] initWithCapacity:1];
NSUInteger rowIndex = [draggedIndexes firstIndex];
while ( rowIndex != NSNotFound ) {
[draggedRows addObject:[NSNumber numberWithInteger:rowIndex]];
rowIndex = [draggedIndexes indexGreaterThanIndex: rowIndex];
}
NSInteger destinationRow = row;
NSInteger offset = 0;
NSUInteger i;
for(i=0; i<[draggedRows count]; i++) {
NSInteger originalRow = [[draggedRows objectAtIndex:i] integerValue];
if(originalRow < destinationRow) destinationRow--;
originalRow += offset;
// For safety reasons
if(originalRow > (NSInteger)[contentFilters count]-1) originalRow = [contentFilters count] - 1;
NSMutableDictionary *draggedRow = [NSMutableDictionary dictionaryWithDictionary:[contentFilters objectAtIndex:originalRow]];
[contentFilters removeObjectAtIndex:originalRow];
[contentFilterTableView reloadData];
if(destinationRow+i >= [contentFilters count])
[contentFilters addObject:draggedRow];
else
[contentFilters insertObject:draggedRow atIndex:destinationRow+i];
if(originalRow < row) offset--;
}
[contentFilterTableView reloadData];
[contentFilterArrayController rearrangeObjects];
[draggedIndexes release];
[draggedRows release];
return YES;
}
#pragma mark -
#pragma mark Various Control delegate methods
/**
* Detect if inline editing was done
*/
- (void)controlTextDidEndEditing:(NSNotification *)aNotification
{
isTableCellEditing = NO;
}
/**
* Trap the escape overriding default behaviour and ending editing,
* only within the current row.
*/
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
{
// Trap the escape key
if ( [[control window] methodForSelector:command] == [[control window] methodForSelector:@selector(cancelOperation:)] )
{
// Abort editing
[control abortEditing];
isTableCellEditing = NO;
// Reset name input text field
if([contentFilterTableView selectedRow] > -1)
[contentFilterNameTextField setStringValue:
[[contentFilters objectAtIndex:[contentFilterTableView selectedRow]] objectForKey:@"MenuLabel"]];
return TRUE;
}
return FALSE;
}
/**
* Changes in the name text field will be saved in data source directly
* to update the table view accordingly. If filter name is changed via inline editing
* in the tableView update name text field accordingly and check for empty names
*/
- (void)controlTextDidChange:(NSNotification *)notification
{
// Do nothing if no filter is selected
if([contentFilterTableView numberOfSelectedRows] < 1) return;
id object = [notification object];
if(object == contentFilterNameTextField) {
if([[contentFilterNameTextField stringValue] length]) {
[[contentFilters objectAtIndex:[contentFilterTableView selectedRow]] setObject:[contentFilterNameTextField stringValue] forKey:@"MenuLabel"];
[contentFilterTableView reloadData];
} else {
NSBeep();
[[contentFilters objectAtIndex:[contentFilterTableView selectedRow]] setObject:SP_NAME_REQUIRED_PLACEHOLDER_STRING forKey:@"MenuLabel"];
[contentFilterNameTextField setStringValue:SP_NAME_REQUIRED_PLACEHOLDER_STRING];
[contentFilterNameTextField selectText:nil];
}
}
else if (object == contentFilterTableView) {
NSTextView *editor = [[notification userInfo] objectForKey:@"NSFieldEditor"];
NSString *newName = [[editor textStorage] string];
if([newName length]) {
[contentFilterNameTextField setStringValue:newName];
} else {
NSBeep();
[editor insertText:SP_NAME_REQUIRED_PLACEHOLDER_STRING];
[editor setSelectedRange:NSMakeRange(0,[SP_NAME_REQUIRED_PLACEHOLDER_STRING length])];
[contentFilterNameTextField setStringValue:SP_NAME_REQUIRED_PLACEHOLDER_STRING];
}
}
}
/**
* Parse clause and update labels accordingly
*/
- (void)textViewDidChangeSelection:(NSNotification *)notification
{
// Do nothing if no filter is selected
if([contentFilterTableView numberOfSelectedRows] < 1) return;
id object = [notification object];
if(object == contentFilterTextView) {
[insertPlaceholderButton setEnabled:([[contentFilterTextView string] length])];
[resultingClauseLabel setHidden:(![[contentFilterTextView string] length])];
[resultingClauseContentLabel setHidden:(![[contentFilterTextView string] length])];
[numberOfArgsLabel setHidden:(![[contentFilterTextView string] length])];
NSUInteger numOfArgs = [[[contentFilterTextView string] componentsMatchedByRegex:@"(? 2) {
[resultingClauseLabel setStringValue:NSLocalizedString(@"Error", @"error")];
[resultingClauseContentLabel setStringValue:NSLocalizedString(@"Maximum number of arguments is 2!", @"Shown when user inserts too many arguments (ContentFilterManager)")];
} else {
[resultingClauseLabel setStringValue:@"SELECT * FROM WHERE"];
NSMutableString *c = [[NSMutableString alloc] init];
[c setString:[contentFilterTextView string]];
[c replaceOccurrencesOfRegex:@"(?"];
[c flushCachedRegexData];
[resultingClauseContentLabel setStringValue:[NSString stringWithFormat:@"%@%@", ([suppressLeadingFiledPlaceholderCheckbox state] == NSOnState) ? @"" : @" ", c]];
[c release];
}
}
}
#pragma mark -
#pragma mark Menu validation
/**
* Menu item validation.
*/
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
{
// Disable all if only GLOBAL is in the table
if([contentFilters count] < 2) return NO;
SEL action = [menuItem action];
if (action == @selector(duplicateContentFilter:))
{
return ([contentFilterTableView numberOfSelectedRows] == 1);
}
else if ( (action == @selector(removeContentFilter:)) || (action == @selector(exportFavorites:)) )
{
return ([contentFilterTableView numberOfSelectedRows] > 0);
}
return YES;
}
#pragma mark -
#pragma mark Other
/**
* Sheet did end method for removing content filters
*/
- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo
{
if ([contextInfo isEqualToString:@"removeSelectedFilters"]) {
if (returnCode == NSAlertDefaultReturn) {
NSIndexSet *indexes = [contentFilterTableView selectedRowIndexes];
// Get last index
NSUInteger currentIndex = [indexes lastIndex];
while (currentIndex != NSNotFound)
{
[contentFilters removeObjectAtIndex:currentIndex];
// Get next index (beginning from the end)
currentIndex = [indexes indexLessThanIndex:currentIndex];
}
if ([contentFilters count] == 2) {
[contentFilterNameTextField setStringValue:@""];
}
[contentFilterArrayController rearrangeObjects];
[contentFilterTableView reloadData];
// Set focus to filter list to avoid an unstable state
[[self window] makeFirstResponder:contentFilterTableView];
[removeButton setEnabled:([contentFilterTableView numberOfSelectedRows] > 0)];
}
}
}
/**
* Import panel did end method.
*/
- (void)importPanelDidEnd:(NSOpenPanel *)panel returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo
{
#ifndef SP_CODA
if (returnCode == NSOKButton) {
NSString *filename = [[panel filenames] objectAtIndex:0];
NSError *readError = nil;
NSString *convError = nil;
NSPropertyListFormat format;
NSInteger insertionIndexStart, insertionIndexEnd;
NSDictionary *spf = nil;
if([[[filename pathExtension] lowercaseString] isEqualToString:SPFileExtensionDefault]) {
NSData *pData = [NSData dataWithContentsOfFile:filename options:NSUncachedRead error:&readError];
spf = [[NSPropertyListSerialization propertyListFromData:pData
mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&convError] retain];
if(!spf || readError != nil || [convError length] || !(format == NSPropertyListXMLFormat_v1_0 || format == NSPropertyListBinaryFormat_v1_0)) {
NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithString:SP_FILE_PARSER_ERROR_TITLE_STRING]
defaultButton:NSLocalizedString(@"OK", @"OK button")
alternateButton:nil
otherButton:nil
informativeTextWithFormat:NSLocalizedString(@"File couldn't be read.", @"error while reading data file")];
[alert setAlertStyle:NSCriticalAlertStyle];
[alert runModal];
if (spf) [spf release];
return;
}
if([[spf objectForKey:SPContentFilters] objectForKey:filterType] && [[[spf objectForKey:SPContentFilters] objectForKey:filterType] count]) {
#ifndef SP_CODA
// If the DatabaseDocument is an on-disk document, add the favourites to the bottom of it
if (![tableDocumentInstance isUntitled]) {
insertionIndexStart = [contentFilters count];
[contentFilters addObjectsFromArray:[[spf objectForKey:SPContentFilters] objectForKey:filterType]];
insertionIndexEnd = [contentFilters count] - 1;
}
// Otherwise, add to the bottom of the Global array
else {
#endif
NSUInteger i, l;
insertionIndexStart = 1;
while (![[contentFilters objectAtIndex:insertionIndexStart] objectForKey:@"headerOfFileURL"]) {
insertionIndexStart++;
}
for (i = 0, l = [[[spf objectForKey:SPContentFilters] objectForKey:filterType] count]; i < l; i++) {
[contentFilters insertObject:[[[spf objectForKey:SPContentFilters] objectForKey:filterType] objectAtIndex:i] atIndex:insertionIndexStart + i];
}
insertionIndexEnd = insertionIndexStart + i;
#ifndef SP_CODA
}
#endif
[contentFilterArrayController rearrangeObjects];
[contentFilterTableView reloadData];
[contentFilterTableView selectRowIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(insertionIndexStart, insertionIndexEnd - insertionIndexStart)] byExtendingSelection:NO];
[contentFilterTableView scrollRowToVisible:insertionIndexEnd];
[spf release];
} else {
NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithString:SP_FILE_PARSER_ERROR_TITLE_STRING]
defaultButton:NSLocalizedString(@"OK", @"OK button")
alternateButton:nil
otherButton:nil
informativeTextWithFormat:NSLocalizedString(@"No content filters found.", @"No content filters were found in file to import (ContentFilterManager)")];
[alert setAlertStyle:NSInformationalAlertStyle];
[alert runModal];
[spf release];
return;
}
}
}
#endif
}
/**
* Save panel did end method.
*/
- (void)savePanelDidEnd:(NSSavePanel *)panel returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo
{
#ifndef SP_CODA
if([contextInfo isEqualToString:@"exportFilter"]) {
if (returnCode == NSOKButton) {
// Build a SPF with format = "content filters"
NSMutableDictionary *spfdata = [NSMutableDictionary dictionary];
NSMutableDictionary *cfdata = [NSMutableDictionary dictionary];
NSMutableArray *filterData = [NSMutableArray array];
[spfdata setObject:[NSNumber numberWithInteger:1] forKey:@"version"];
[spfdata setObject:@"content filters" forKey:@"format"];
[spfdata setObject:[NSNumber numberWithBool:NO] forKey:@"encrypted"];
NSIndexSet *indexes = [contentFilterTableView selectedRowIndexes];
// Get selected items and preserve the order
NSUInteger i;
for (i=1; i<[contentFilters count]; i++)
if([indexes containsIndex:i])
[filterData addObject:[contentFilters objectAtIndex:i]];
[cfdata setObject:filterData forKey:filterType];
[spfdata setObject:cfdata forKey:SPContentFilters];
NSString *err = nil;
NSData *plist = [NSPropertyListSerialization dataFromPropertyList:spfdata
format:NSPropertyListXMLFormat_v1_0
errorDescription:&err];
if(err != nil) {
NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithString:NSLocalizedString(@"Error while converting content filter data", @"Content filters could not be converted to plist upon export - message title (ContentFilterManager)")]
defaultButton:NSLocalizedString(@"OK", @"OK button")
alternateButton:nil
otherButton:nil
informativeTextWithFormat:@"%@", err];
[alert setAlertStyle:NSCriticalAlertStyle];
[alert runModal];
return;
}
NSError *error = nil;
[plist writeToURL:[panel URL] options:NSAtomicWrite error:&error];
if (error) [[NSAlert alertWithError:error] runModal];
}
}
#endif
}
#pragma mark -
- (void)dealloc
{
[contentFilters release];
[super dealloc];
}
@end