Files
cappuccino/Resources/SmartFoldersDemo/SmartFolderEditorController.j
T

391 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* SmartFolderEditorController.j
* SmartFoldersDemo
*
* Document-modal sheet for creating / editing a smart folder. Exercises:
* - CPWindowController owning a CPDocModalWindowMask window, run as a sheet
* with -beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo:
* - CPPredicateEditor (CPRuleEditor subclass) built entirely from
* CPPredicateEditorRowTemplate instances: compound (AND/OR/NOT),
* key path + string attribute type with [cd] options, key path + integer
* attribute type, key path + constant right expressions (pop-up)
* - round trip predicate -> editor (setObjectValue:) -> predicate
* - CPRuleEditorRowsDidChangeNotification + target/action for live preview
* - resizing a sheet while it is attached
* - default button / escape key equivalent
*/
@import <AppKit/AppKit.j>
@import "Library.j"
var EditorWidth = 680.0,
EditorChromeHeight = 200.0,
EditorMinHeight = 300.0,
EditorMaxHeight = 600.0,
EditorRowHeight = 28.0,
EditorTitles = {
"firstName" : "First Name",
"lastName" : "Last Name",
"email" : "Email",
"city" : "City",
"department" : "Department",
"age" : "Age",
"salary" : "Salary"
};
// --------------------------------------------------------------------------------
// LabeledRowTemplate – shows human readable titles for key path expressions
// --------------------------------------------------------------------------------
@implementation LabeledRowTemplate : CPPredicateEditorRowTemplate
{
}
- (CPArray)templateViews
{
var views = [super templateViews],
first = [views firstObject];
if ([first isKindOfClass:[CPPopUpButton class]])
{
var items = [first itemArray];
for (var i = 0; i < items.length; i++)
{
var expression = [items[i] representedObject];
if ([expression respondsToSelector:@selector(expressionType)] && [expression expressionType] === CPKeyPathExpressionType)
{
var title = EditorTitles[[expression keyPath]];
if (title && [items[i] title] !== title)
[items[i] setTitle:title];
}
}
}
return views;
}
@end
// --------------------------------------------------------------------------------
// SmartFolderEditorController
// --------------------------------------------------------------------------------
@implementation SmartFolderEditorController : CPWindowController
{
Library _library;
CPTextField _nameField;
CPScrollView _editorScrollView;
CPPredicateEditor _predicateEditor;
CPTextField _formatField;
CPTextField _matchField;
CPButton _okButton;
SmartFolder _folder;
Function _completion;
}
- (id)initWithLibrary:(Library)aLibrary
{
var window = [[CPWindow alloc] initWithContentRect:CGRectMake(0, 0, EditorWidth, EditorMinHeight) styleMask:CPDocModalWindowMask];
if (self = [super initWithWindow:window])
{
_library = aLibrary;
[self _buildInterface];
[[CPNotificationCenter defaultCenter] addObserver:self
selector:@selector(ruleEditorRowsDidChange:)
name:CPRuleEditorRowsDidChangeNotification
object:_predicateEditor];
}
return self;
}
- (void)_buildInterface
{
var window = [self window],
content = [window contentView],
bounds = [content bounds],
width = CGRectGetWidth(bounds),
height = CGRectGetHeight(bounds);
var title = [CPTextField labelWithTitle:@"Smart Folder"];
[title setFont:[CPFont boldSystemFontOfSize:13.0]];
[title setFrameOrigin:CGPointMake(20, 14)];
[content addSubview:title];
var nameLabel = [CPTextField labelWithTitle:@"Name:"];
[nameLabel setAlignment:CPRightTextAlignment];
[nameLabel setFrame:CGRectMake(20, 46, 60, 20)];
[content addSubview:nameLabel];
_nameField = [[CPTextField alloc] initWithFrame:CGRectMake(85, 40, width - 105, 29)];
[_nameField setEditable:YES];
[_nameField setBezeled:YES];
[_nameField setAutoresizingMask:CPViewWidthSizable];
[_nameField setPlaceholderString:@"Smart folder name"];
[content addSubview:_nameField];
var matchLabel = [CPTextField labelWithTitle:@"Match people where:"];
[matchLabel setFrameOrigin:CGPointMake(20, 80)];
[content addSubview:matchLabel];
// --- Predicate editor inside a scroll view
var top = 102.0,
bottomChrome = EditorChromeHeight - top;
_editorScrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(20, top, width - 40, height - top - bottomChrome)];
[_editorScrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[_editorScrollView setAutohidesScrollers:YES];
[_editorScrollView setHasHorizontalScroller:NO];
[_editorScrollView setBorderType:CPBezelBorder];
_predicateEditor = [[CPPredicateEditor alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([[_editorScrollView contentView] bounds]), EditorRowHeight)];
[_predicateEditor setAutoresizingMask:CPViewWidthSizable];
[_predicateEditor setRowHeight:EditorRowHeight];
[_predicateEditor setNestingMode:CPRuleEditorNestingModeCompound];
[_predicateEditor setCanRemoveAllRows:NO];
[_predicateEditor setRowTemplates:[self _rowTemplates]];
[_predicateEditor setTarget:self];
[_predicateEditor setAction:@selector(predicateEditorChanged:)];
[_editorScrollView setDocumentView:_predicateEditor];
[content addSubview:_editorScrollView];
// --- Footer
_formatField = [CPTextField labelWithTitle:@""];
[_formatField setFrame:CGRectMake(20, height - 88, width - 40, 18)];
[_formatField setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin];
[_formatField setFont:[CPFont fontWithName:@"Menlo, Courier" size:11.0]];
[_formatField setTextColor:[CPColor grayColor]];
[_formatField setLineBreakMode:CPLineBreakByTruncatingTail];
[_formatField setSelectable:YES];
[content addSubview:_formatField];
_matchField = [CPTextField labelWithTitle:@""];
[_matchField setFrame:CGRectMake(20, height - 40, 300, 20)];
[_matchField setAutoresizingMask:CPViewMaxXMargin | CPViewMinYMargin];
[content addSubview:_matchField];
var cancelButton = [CPButton buttonWithTitle:@"Cancel"];
[cancelButton setFrame:CGRectMake(width - 200, height - 44, 85, 25)];
[cancelButton setAutoresizingMask:CPViewMinXMargin | CPViewMinYMargin];
[cancelButton setTag:CPCancelButton];
[cancelButton setTarget:self];
[cancelButton setAction:@selector(closeSheet:)];
[cancelButton setKeyEquivalent:CPEscapeFunctionKey];
[content addSubview:cancelButton];
_okButton = [CPButton buttonWithTitle:@"OK"];
[_okButton setFrame:CGRectMake(width - 105, height - 44, 85, 25)];
[_okButton setAutoresizingMask:CPViewMinXMargin | CPViewMinYMargin];
[_okButton setTag:CPOKButton];
[_okButton setTarget:self];
[_okButton setAction:@selector(closeSheet:)];
[content addSubview:_okButton];
[window setDefaultButton:_okButton];
}
- (CPArray)_rowTemplates
{
var keyPaths = function(keys)
{
var result = [];
for (var i = 0; i < keys.length; i++)
result.push([CPExpression expressionForKeyPath:keys[i]]);
return result;
};
var compound = [[CPPredicateEditorRowTemplate alloc] initWithCompoundTypes:[CPAndPredicateType, CPOrPredicateType, CPNotPredicateType]];
var strings = [[LabeledRowTemplate alloc] initWithLeftExpressions:keyPaths([@"lastName", @"firstName", @"email", @"city"])
rightExpressionAttributeType:CPStringAttributeType
modifier:CPDirectPredicateModifier
operators:[CPContainsPredicateOperatorType,
CPBeginsWithPredicateOperatorType,
CPEndsWithPredicateOperatorType,
CPEqualToPredicateOperatorType,
CPNotEqualToPredicateOperatorType,
CPMatchesPredicateOperatorType]
options:CPCaseInsensitivePredicateOption | CPDiacriticInsensitivePredicateOption];
var numbers = [[LabeledRowTemplate alloc] initWithLeftExpressions:keyPaths([@"age", @"salary"])
rightExpressionAttributeType:CPInteger32AttributeType
modifier:CPDirectPredicateModifier
operators:[CPEqualToPredicateOperatorType,
CPNotEqualToPredicateOperatorType,
CPLessThanPredicateOperatorType,
CPLessThanOrEqualToPredicateOperatorType,
CPGreaterThanPredicateOperatorType,
CPGreaterThanOrEqualToPredicateOperatorType]
options:0];
var departmentValues = [];
for (var i = 0; i < LibraryDepartments.length; i++)
departmentValues.push([CPExpression expressionForConstantValue:LibraryDepartments[i]]);
var departments = [[LabeledRowTemplate alloc] initWithLeftExpressions:keyPaths([@"department"])
rightExpressions:departmentValues
modifier:CPDirectPredicateModifier
operators:[CPEqualToPredicateOperatorType, CPNotEqualToPredicateOperatorType]
options:0];
return [compound, strings, numbers, departments];
}
// ---------------------------------------------------------------- Running the sheet
// aFolder == nil creates a new folder. aCallback(folderOrNil) is invoked after OK.
- (void)beginEditingFolder:(SmartFolder)aFolder modalForWindow:(CPWindow)aWindow completion:(Function)aCallback
{
_folder = aFolder;
_completion = aCallback;
[_nameField setStringValue:aFolder ? [aFolder name] : @"Untitled Smart Folder"];
var predicate = aFolder ? [aFolder predicate] : [CPPredicate predicateWithFormat:@"lastName CONTAINS[cd] %@", @""];
[self _setEditorPredicate:predicate];
[CPApp beginSheet:[self window]
modalForWindow:aWindow
modalDelegate:self
didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
contextInfo:nil];
[[self window] makeFirstResponder:_nameField];
[_nameField selectText:self];
[self _updatePreview];
[self _fitWindowToRows];
}
- (void)_setEditorPredicate:(CPPredicate)aPredicate
{
// In compound nesting mode the root row must be a compound predicate.
if (![aPredicate isKindOfClass:[CPCompoundPredicate class]])
aPredicate = [CPCompoundPredicate andPredicateWithSubpredicates:[aPredicate]];
try
{
[_predicateEditor setObjectValue:aPredicate];
}
catch (e)
{
CPLog.warn("SmartFoldersDemo: CPPredicateEditor could not represent " + [aPredicate predicateFormat] + ": " + e);
[_predicateEditor setObjectValue:[CPCompoundPredicate andPredicateWithSubpredicates:[[CPPredicate predicateWithFormat:@"lastName CONTAINS[cd] %@", @""]]]];
}
}
- (CPPredicate)_currentPredicate
{
try
{
[_predicateEditor reloadPredicate];
return [_predicateEditor predicate];
}
catch (e)
{
CPLog.warn("SmartFoldersDemo: could not build predicate: " + e);
return nil;
}
}
- (void)closeSheet:(id)sender
{
if ([sender tag] === CPOKButton)
{
if (![[[_nameField stringValue] stringByTrimmingWhitespace] length])
{
[[self window] makeFirstResponder:_nameField];
return;
}
if (![self _currentPredicate])
return;
}
[CPApp endSheet:[self window] returnCode:[sender tag]];
}
- (void)sheetDidEnd:(CPWindow)aSheet returnCode:(int)aReturnCode contextInfo:(id)aContext
{
[aSheet orderOut:self];
var callback = _completion,
folder = _folder;
_completion = nil;
_folder = nil;
if (aReturnCode !== CPOKButton)
return;
var name = [[_nameField stringValue] stringByTrimmingWhitespace],
predicate = [self _currentPredicate];
if (folder)
[_library updateSmartFolder:folder name:name predicate:predicate];
else
folder = [_library addSmartFolderWithName:name predicate:predicate];
if (callback)
callback(folder);
}
// ---------------------------------------------------------------- Live preview
- (void)predicateEditorChanged:(id)sender
{
[self _updatePreview];
}
- (void)ruleEditorRowsDidChange:(CPNotification)aNotification
{
[self _updatePreview];
[self _fitWindowToRows];
}
- (void)_updatePreview
{
var predicate = [self _currentPredicate],
matches = predicate ? [_library peopleMatchingPredicate:predicate] : nil;
[_formatField setStringValue:predicate ? [predicate predicateFormat] : @"(incomplete predicate)"];
if (matches)
[_matchField setStringValue:[CPString stringWithFormat:@"%d of %d people match", [matches count], [[_library people] count]]];
else
[_matchField setStringValue:@"Predicate cannot be evaluated"];
[_okButton setEnabled:!!matches];
}
- (void)_fitWindowToRows
{
var window = [self window],
rows = MAX([_predicateEditor numberOfRows], 1),
desired = EditorChromeHeight + rows * EditorRowHeight + 6,
height = MIN(MAX(desired, EditorMinHeight), EditorMaxHeight),
frame = [window frame];
// Make sure the editor's own frame tracks its rows so the scroll view can scroll.
[_predicateEditor setFrameSize:CGSizeMake(CGRectGetWidth([[_editorScrollView contentView] bounds]), rows * EditorRowHeight)];
if (ABS(CGRectGetHeight(frame) - height) < 1)
return;
[window setFrame:CGRectMake(CGRectGetMinX(frame), CGRectGetMinY(frame), CGRectGetWidth(frame), height) display:YES animate:NO];
}
@end