Deploying to gh-pages from @ cappuccino/cappuccino@1585123040 🚀

This commit is contained in:
enquora committed 2026-10-20 20:40:13 +00:00
1 parent 6e043d4529
commit 69afb8bb75
17 files changed
+3970 -154

No files matched your search

@@ -30945,7 +30945,7 @@ var meta_class = the_class.isa;class_addMethods(the_class, [new objj_method(sel_
,["void","BOOL"])]);
}
p;13;CPSplitView.jt;96518;@STATIC;1.0;i;13;CPButtonBar.ji;10;CPCursor.ji;9;CPImage.ji;16;CPTrackingArea.ji;8;CPView.jt;96419;objj_executeFile("CPButtonBar.j", YES);objj_executeFile("CPCursor.j", YES);objj_executeFile("CPImage.j", YES);objj_executeFile("CPTrackingArea.j", YES);objj_executeFile("CPView.j", YES);{var the_protocol = objj_allocateProtocol("CPSplitViewDelegate");
p;13;CPSplitView.jt;96732;@STATIC;1.0;i;13;CPButtonBar.ji;10;CPCursor.ji;9;CPImage.ji;16;CPTrackingArea.ji;8;CPView.jt;96633;objj_executeFile("CPButtonBar.j", YES);objj_executeFile("CPCursor.j", YES);objj_executeFile("CPImage.j", YES);objj_executeFile("CPTrackingArea.j", YES);objj_executeFile("CPView.j", YES);{var the_protocol = objj_allocateProtocol("CPSplitViewDelegate");
var aProtocol = objj_getProtocol("CPObject");
if (!aProtocol) throw new SyntaxError("*** Could not find definition for protocol \"CPSplitViewDelegate\"");
protocol_addProtocol(the_protocol, aProtocol);
@@ -31761,8 +31761,11 @@ class_addMethods(the_class, [new objj_method(sel_getUid("initWithFrame:"), funct
,["void","float","int"]), new objj_method(sel_getUid("setButtonBar:forDividerAtIndex:"), function $CPSplitView__setButtonBar_forDividerAtIndex_(self, _cmd, aButtonBar, dividerIndex)
{
(aButtonBar == null ? aButtonBar : (aButtonBar.isa.method_msgSend["setAutomaticResizeControl:"] || _objj_forward)(aButtonBar, "setAutomaticResizeControl:", YES));
(self.isa.method_msgSend["attachButtonBar:"] || _objj_forward)(self, "attachButtonBar:", aButtonBar);
if ((aButtonBar == null ? aButtonBar : (aButtonBar.isa.method_msgSend["respondsToSelector:"] || _objj_forward)(aButtonBar, "respondsToSelector:", sel_getUid("setAutomaticResizeControl:"))))
{
(aButtonBar == null ? aButtonBar : (aButtonBar.isa.method_msgSend["setAutomaticResizeControl:"] || _objj_forward)(aButtonBar, "setAutomaticResizeControl:", YES));
(self.isa.method_msgSend["attachButtonBar:"] || _objj_forward)(self, "attachButtonBar:", aButtonBar);
}
}
,["void","CPButtonBar","CPUInteger"]), new objj_method(sel_getUid("attachButtonBar:"), function $CPSplitView__attachButtonBar_(self, _cmd, aButtonBar)
@@ -30104,7 +30104,7 @@ var meta_class = the_class.isa;class_addMethods(the_class, [new objj_method(sel_
,["void","BOOL"])]);
}
p;13;CPSplitView.jt;96518;@STATIC;1.0;i;13;CPButtonBar.ji;10;CPCursor.ji;9;CPImage.ji;16;CPTrackingArea.ji;8;CPView.jt;96419;objj_executeFile("CPButtonBar.j", YES);objj_executeFile("CPCursor.j", YES);objj_executeFile("CPImage.j", YES);objj_executeFile("CPTrackingArea.j", YES);objj_executeFile("CPView.j", YES);{var the_protocol = objj_allocateProtocol("CPSplitViewDelegate");
p;13;CPSplitView.jt;96732;@STATIC;1.0;i;13;CPButtonBar.ji;10;CPCursor.ji;9;CPImage.ji;16;CPTrackingArea.ji;8;CPView.jt;96633;objj_executeFile("CPButtonBar.j", YES);objj_executeFile("CPCursor.j", YES);objj_executeFile("CPImage.j", YES);objj_executeFile("CPTrackingArea.j", YES);objj_executeFile("CPView.j", YES);{var the_protocol = objj_allocateProtocol("CPSplitViewDelegate");
var aProtocol = objj_getProtocol("CPObject");
if (!aProtocol) throw new SyntaxError("*** Could not find definition for protocol \"CPSplitViewDelegate\"");
protocol_addProtocol(the_protocol, aProtocol);
@@ -30920,8 +30920,11 @@ class_addMethods(the_class, [new objj_method(sel_getUid("initWithFrame:"), funct
,["void","float","int"]), new objj_method(sel_getUid("setButtonBar:forDividerAtIndex:"), function $CPSplitView__setButtonBar_forDividerAtIndex_(self, _cmd, aButtonBar, dividerIndex)
{
(aButtonBar == null ? aButtonBar : (aButtonBar.isa.method_msgSend["setAutomaticResizeControl:"] || _objj_forward)(aButtonBar, "setAutomaticResizeControl:", YES));
(self.isa.method_msgSend["attachButtonBar:"] || _objj_forward)(self, "attachButtonBar:", aButtonBar);
if ((aButtonBar == null ? aButtonBar : (aButtonBar.isa.method_msgSend["respondsToSelector:"] || _objj_forward)(aButtonBar, "respondsToSelector:", sel_getUid("setAutomaticResizeControl:"))))
{
(aButtonBar == null ? aButtonBar : (aButtonBar.isa.method_msgSend["setAutomaticResizeControl:"] || _objj_forward)(aButtonBar, "setAutomaticResizeControl:", YES));
(self.isa.method_msgSend["attachButtonBar:"] || _objj_forward)(self, "attachButtonBar:", aButtonBar);
}
}
,["void","CPButtonBar","CPUInteger"]), new objj_method(sel_getUid("attachButtonBar:"), function $CPSplitView__attachButtonBar_(self, _cmd, aButtonBar)
+109
View File
@@ -0,0 +1,109 @@
/*
* AppController.j
* SmartFoldersDemo
*
* Non-document-based Cappuccino integration test / demo, built entirely in
* code (no cib/xib). The application delegate only creates the model, the
* main menu and the main window controller; everything else lives in the
* view controllers.
*
* AppController
* └─ MainWindowController (CPWindowController, bridge window, toolbar)
* ├─ SourceListViewController (view-based CPOutlineView, badges, DnD target)
* ├─ PeopleViewController (bound CPTableView, CPArrayController, DnD source)
* │ └─ PersonDetailViewController (selection bindings)
* ├─ SmartFolderEditorController (sheet, CPPredicateEditor)
* └─ NameSheetController (sheet)
*
* Data (people, groups, smart folders) is persisted in CPUserDefaults.
* Use File ▸ Reset Library… to return to the deterministic sample data.
*/
@import <Foundation/Foundation.j>
@import <AppKit/AppKit.j>
@import "Library.j"
@import "MainWindowController.j"
@implementation AppController : CPObject
{
Library _library;
MainWindowController _mainWindowController;
}
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
_library = [[Library alloc] init];
[self _buildMainMenu];
[CPMenu setMenuBarVisible:YES];
_mainWindowController = [[MainWindowController alloc] initWithLibrary:_library];
[_mainWindowController showWindow:self];
}
// ---------------------------------------------------------------- Menu
- (CPMenuItem)_addItem:(CPString)aTitle action:(SEL)anAction key:(CPString)aKey modifiers:(unsigned)aMask to:(CPMenu)aMenu
{
var item = [aMenu addItemWithTitle:aTitle action:anAction keyEquivalent:aKey || @""];
if (aMask)
[item setKeyEquivalentModifierMask:aMask];
return item;
}
- (CPMenu)_addSubmenu:(CPString)aTitle to:(CPMenu)aMainMenu
{
var item = [aMainMenu addItemWithTitle:aTitle action:nil keyEquivalent:@""],
menu = [[CPMenu alloc] initWithTitle:aTitle];
[aMainMenu setSubmenu:menu forItem:item];
return menu;
}
- (void)_buildMainMenu
{
var mainMenu = [CPApp mainMenu];
while ([mainMenu numberOfItems] > 0)
[mainMenu removeItemAtIndex:0];
var commandOption = CPCommandKeyMask | CPAlternateKeyMask;
// File
var fileMenu = [self _addSubmenu:@"File" to:mainMenu];
[self _addItem:@"New Smart Folder…" action:@selector(newSmartFolder:) key:@"n" modifiers:commandOption to:fileMenu];
[self _addItem:@"New Group…" action:@selector(newGroup:) key:@"g" modifiers:commandOption to:fileMenu];
[self _addItem:@"New Person" action:@selector(newPerson:) key:@"p" modifiers:commandOption to:fileMenu];
[fileMenu addItem:[CPMenuItem separatorItem]];
[self _addItem:@"Edit Smart Folder…" action:@selector(editSelectedSmartFolder:) key:@"e" modifiers:commandOption to:fileMenu];
[self _addItem:@"Rename Group…" action:@selector(renameSelectedGroup:) key:nil modifiers:0 to:fileMenu];
[self _addItem:@"Delete Group or Smart Folder" action:@selector(deleteSelectedSourceItem:) key:nil modifiers:0 to:fileMenu];
[fileMenu addItem:[CPMenuItem separatorItem]];
[self _addItem:@"Reset Library…" action:@selector(resetLibrary:) key:nil modifiers:0 to:fileMenu];
// Edit
var editMenu = [self _addSubmenu:@"Edit" to:mainMenu];
[self _addItem:@"Undo" action:@selector(undo:) key:@"z" modifiers:0 to:editMenu];
[self _addItem:@"Redo" action:@selector(redo:) key:@"Z" modifiers:0 to:editMenu];
[editMenu addItem:[CPMenuItem separatorItem]];
[self _addItem:@"Cut" action:@selector(cut:) key:@"x" modifiers:0 to:editMenu];
[self _addItem:@"Copy" action:@selector(copy:) key:@"c" modifiers:0 to:editMenu];
[self _addItem:@"Paste" action:@selector(paste:) key:@"v" modifiers:0 to:editMenu];
[self _addItem:@"Delete" action:@selector(delete:) key:nil modifiers:0 to:editMenu];
[self _addItem:@"Select All" action:@selector(selectAll:) key:@"a" modifiers:0 to:editMenu];
[editMenu addItem:[CPMenuItem separatorItem]];
[self _addItem:@"Find" action:@selector(focusSearchField:) key:@"f" modifiers:0 to:editMenu];
// View
var viewMenu = [self _addSubmenu:@"View" to:mainMenu];
[self _addItem:@"Hide Sidebar" action:@selector(toggleSidebar:) key:@"s" modifiers:CPCommandKeyMask | CPControlKeyMask to:viewMenu];
[self _addItem:@"Hide Details" action:@selector(toggleDetail:) key:@"d" modifiers:CPCommandKeyMask | CPControlKeyMask to:viewMenu];
[viewMenu addItem:[CPMenuItem separatorItem]];
[self _addItem:@"Show All People" action:@selector(showAllPeople:) key:@"1" modifiers:0 to:viewMenu];
}
@end
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CPDefaultTheme</key>
<string>Aristo2</string>
<key>CPApplicationDelegateClass</key>
<string>AppController</string>
<key>CPBundleName</key>
<string>SmartFolderDemo</string>
<key>CPPrincipalClass</key>
<string>CPApplication</string>
</dict>
</plist>
+94
View File
@@ -0,0 +1,94 @@
/*
* Jakefile
* SmartFoldersTest
*
* Created by You on November 25, 2010.
* Copyright 2010, Your Company All rights reserved.
*/
var ENV = process.env,
cp = require("child_process"),
fs = require("fs"),
path = require("path"),
task = JAKE.task,
FileList = JAKE.FileList,
app = CAPPUCCINO.Jake.applicationtask.app,
configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug";
app ("SmartFoldersTest", function(task)
{
task.setBuildIntermediatesPath(path.join("Build", "SmartFoldersTest.build", configuration));
task.setBuildPath(path.join("Build", configuration));
task.setProductName("SmartFoldersTest");
task.setIdentifier("com.yourcompany.");
task.setVersion("1.0");
task.setAuthor("Your Company");
task.setEmail("feedback @nospam@ yourcompany.com");
task.setSummary("SmartFoldersTest");
task.setSources((new FileList("**/*.j")).exclude(path.join("Build", "**")));
task.setResources(new FileList("Resources/**"));
task.setIndexFilePath("index.html");
task.setInfoPlistPath("Info.plist");
task.setNib2CibFlags("-R Resources/");
if (configuration === "Debug")
task.setCompilerFlags("-DDEBUG -g");
else
task.setCompilerFlags("-O");
});
task ("default", ["SmartFoldersTest"], function()
{
printResults(configuration);
});
task ("build", ["default"]);
task ("debug", function()
{
ENV["CONFIGURATION"] = "Debug";
JAKE.subjake(["."], "build", ENV);
});
task ("release", function()
{
ENV["CONFIGURATION"] = "Release";
JAKE.subjake(["."], "build", ENV);
});
task ("run", ["debug"], function()
{
cp.execSync(["open", path.join("Build", "Debug", "SmartFoldersTest", "index.html")].join(" "), { stdio: "inherit" });
});
task ("run-release", ["release"], function()
{
cp.execSync(["open", path.join("Build", "Release", "SmartFoldersTest", "index.html")].join(" "), { stdio: "inherit" });
});
task ("deploy", ["release"], function()
{
fs.mkdirSync(path.join("Build", "Deployment", "SmartFoldersTest"), { recursive: true });
cp.execSync(["press", "-f", path.join("Build", "Release", "SmartFoldersTest"), path.join("Build", "Deployment", "SmartFoldersTest")].join(" "), { stdio: "inherit" });
printResults("Deployment")
});
task ("desktop", ["release"], function()
{
fs.mkdirSync(path.join("Build", "Desktop", "SmartFoldersTest"), { recursive: true });
require("@objj/nativehost").buildNativeHost(path.join("Build", "Release", "SmartFoldersTest"), path.join("Build", "Desktop", "SmartFoldersTest", "SmartFoldersTest.app"));
printResults("Desktop")
});
task ("run-desktop", ["desktop"], function()
{
cp.execSync([path.join("Build", "Desktop", "SmartFoldersTest", "SmartFoldersTest.app", "Contents", "MacOS", "NativeHost"), "-i"].join(" "), { stdio: "inherit" });
});
function printResults(configuration)
{
console.log("----------------------------");
console.log(configuration+" app built at path: "+path.join("Build", configuration, "SmartFoldersTest"));
console.log("----------------------------");
}
+787
View File
@@ -0,0 +1,787 @@
/*
* Library.j
* SmartFoldersDemo
*
* The application model. Holds all people plus the three source list
* sections (Library, Groups, Smart Folders), persists itself to
* CPUserDefaults (local storage) and observes every Person via KVO so that
* edits made anywhere in the UI are noticed, saved and reflected in the
* smart folder badges.
*
* Change notifications:
* - structural changes (people added/removed, group membership, folders
* added/removed/renamed/reordered/edited) are posted synchronously;
* - property edits on a Person are coalesced and posted on the next run
* loop turn.
*/
@import <Foundation/Foundation.j>
@import "Person.j"
LibraryDidChangeNotification = @"LibraryDidChangeNotification";
LibraryChangeKindKey = @"kind";
LibraryChangeStructural = @"structural";
LibraryChangePersonEdited = @"personEdited";
PeopleDragType = @"SmartFoldersDemoPeopleDragType";
SourceItemDragType = @"SmartFoldersDemoSourceItemDragType";
LibraryDepartments = [@"Engineering", @"Design", @"Marketing", @"Sales", @"Support", @"Finance", @"Legal"];
LibraryCities = [@"Berlin", @"Hamburg", @"Munich", @"Zürich", @"Vienna", @"Paris", @"London", @"Lisbon", @"New York", @"San Francisco", @"Tokyo"];
var LibraryDefaultsKey = @"SmartFoldersDemo.Library.v1",
ObservedPersonKeys = [@"firstName", @"lastName", @"email", @"department", @"city", @"birthDate", @"salary", @"isManager"],
SourceItemUIDCounter = 0;
// --------------------------------------------------------------------------------
// Source list items
// --------------------------------------------------------------------------------
@implementation SourceItem : CPObject
{
CPString uid @accessors;
CPString name @accessors;
}
- (id)initWithName:(CPString)aName
{
if (self = [super init])
{
uid = [self className] + "-" + (++SourceItemUIDCounter);
name = aName;
}
return self;
}
- (BOOL)isSection
{
return NO;
}
- (BOOL)isSelectable
{
return YES;
}
// Can be renamed, deleted and reordered by the user.
- (BOOL)isUserItem
{
return NO;
}
@end
@implementation SourceSection : SourceItem
{
CPMutableArray children @accessors;
}
- (id)initWithName:(CPString)aName
{
if (self = [super initWithName:aName])
children = [];
return self;
}
- (BOOL)isSection
{
return YES;
}
- (BOOL)isSelectable
{
return NO;
}
@end
@implementation AllPeopleItem : SourceItem
@end
@implementation LibraryGroup : SourceItem
{
CPMutableArray memberIDs @accessors;
}
- (id)initWithName:(CPString)aName memberIDs:(CPArray)theIDs
{
if (self = [super initWithName:aName])
memberIDs = [CPMutableArray arrayWithArray:theIDs || []];
return self;
}
- (id)initWithDictionary:(CPDictionary)aDictionary
{
return [self initWithName:[aDictionary objectForKey:@"name"] || @"Group"
memberIDs:[aDictionary objectForKey:@"members"] || []];
}
- (CPDictionary)dictionaryRepresentation
{
return [CPDictionary dictionaryWithObjectsAndKeys:name, @"name", [memberIDs copy], @"members"];
}
- (BOOL)isUserItem
{
return YES;
}
@end
@implementation SmartFolder : SourceItem
{
CPPredicate predicate @accessors;
}
- (id)initWithName:(CPString)aName predicate:(CPPredicate)aPredicate
{
if (self = [super initWithName:aName])
predicate = aPredicate;
return self;
}
- (id)initWithDictionary:(CPDictionary)aDictionary
{
var format = [aDictionary objectForKey:@"predicate"],
aPredicate = nil;
try
{
if (format)
aPredicate = [CPPredicate predicateWithFormat:format];
}
catch (e)
{
CPLog.warn("SmartFoldersDemo: could not parse stored predicate '" + format + "': " + e);
}
return [self initWithName:[aDictionary objectForKey:@"name"] || @"Smart Folder"
predicate:aPredicate || [CPPredicate predicateWithValue:YES]];
}
- (CPDictionary)dictionaryRepresentation
{
return [CPDictionary dictionaryWithObjectsAndKeys:name, @"name", [predicate predicateFormat], @"predicate"];
}
- (BOOL)isUserItem
{
return YES;
}
@end
// --------------------------------------------------------------------------------
// Library
// --------------------------------------------------------------------------------
@implementation Library : CPObject
{
CPMutableArray _people;
Object _peopleByUID;
SourceSection _librarySection;
SourceSection _groupsSection;
SourceSection _smartFoldersSection;
AllPeopleItem _allPeopleItem;
CPArray _sections;
BOOL _editNotificationScheduled;
BOOL _saveScheduled;
}
- (id)init
{
if (self = [super init])
{
_people = [];
_peopleByUID = {};
_allPeopleItem = [[AllPeopleItem alloc] initWithName:@"All People"];
_librarySection = [[SourceSection alloc] initWithName:@"LIBRARY"];
[[_librarySection children] addObject:_allPeopleItem];
_groupsSection = [[SourceSection alloc] initWithName:@"GROUPS"];
_smartFoldersSection = [[SourceSection alloc] initWithName:@"SMART FOLDERS"];
_sections = [_librarySection, _groupsSection, _smartFoldersSection];
if (![self _loadFromDefaults])
[self _populateWithSampleData];
}
return self;
}
// ---------------------------------------------------------------- Accessors
- (CPArray)people
{
return _people;
}
- (CPArray)sections
{
return _sections;
}
- (AllPeopleItem)allPeopleItem
{
return _allPeopleItem;
}
- (SourceSection)groupsSection
{
return _groupsSection;
}
- (SourceSection)smartFoldersSection
{
return _smartFoldersSection;
}
- (CPArray)groups
{
return [_groupsSection children];
}
- (CPArray)smartFolders
{
return [_smartFoldersSection children];
}
- (CPArray)childrenOfItem:(id)anItem
{
if (anItem == nil)
return _sections;
if ([anItem isSection])
return [anItem children];
return [];
}
- (SourceSection)sectionForItem:(id)anItem
{
if ([anItem isKindOfClass:[AllPeopleItem class]])
return _librarySection;
if ([anItem isKindOfClass:[LibraryGroup class]])
return _groupsSection;
if ([anItem isKindOfClass:[SmartFolder class]])
return _smartFoldersSection;
return nil;
}
- (BOOL)containsItem:(id)anItem
{
if (!anItem)
return NO;
if ([_sections indexOfObjectIdenticalTo:anItem] !== CPNotFound)
return YES;
var section = [self sectionForItem:anItem];
return section && [[section children] indexOfObjectIdenticalTo:anItem] !== CPNotFound;
}
- (id)itemWithUID:(CPString)aUID
{
for (var i = 0; i < _sections.length; i++)
{
var section = _sections[i],
children = [section children];
if ([section uid] === aUID)
return section;
for (var j = 0; j < children.length; j++)
if ([children[j] uid] === aUID)
return children[j];
}
return nil;
}
- (Person)personWithUID:(CPString)aUID
{
return _peopleByUID[aUID] || nil;
}
- (CPArray)peopleWithUIDs:(CPArray)theUIDs
{
var result = [];
for (var i = 0; i < theUIDs.length; i++)
{
var person = _peopleByUID[theUIDs[i]];
if (person)
result.push(person);
}
return result;
}
// The unfiltered content for a source item (what goes into the array controller).
- (CPArray)baseContentForItem:(id)anItem
{
if ([anItem isKindOfClass:[LibraryGroup class]])
return [self peopleWithUIDs:[anItem memberIDs]];
if ([anItem isKindOfClass:[AllPeopleItem class]] || [anItem isKindOfClass:[SmartFolder class]])
return _people;
return [];
}
- (CPPredicate)predicateForItem:(id)anItem
{
if ([anItem isKindOfClass:[SmartFolder class]])
return [anItem predicate];
return nil;
}
- (CPArray)peopleMatchingPredicate:(CPPredicate)aPredicate
{
if (!aPredicate)
return _people;
try
{
return [_people filteredArrayUsingPredicate:aPredicate];
}
catch (e)
{
CPLog.warn("SmartFoldersDemo: predicate evaluation failed: " + e);
return nil;
}
}
- (int)countForItem:(id)anItem
{
if ([anItem isSection])
return CPNotFound;
var base = [self baseContentForItem:anItem],
predicate = [self predicateForItem:anItem];
if (!predicate)
return [base count];
var matches = [self peopleMatchingPredicate:predicate];
return matches ? [matches count] : CPNotFound;
}
// ---------------------------------------------------------------- People mutation
- (Person)createPerson
{
var person = [[Person alloc] init];
[person setFirstName:@"New"];
[person setLastName:@"Person"];
[person setEmail:@"new.person@example.com"];
[person setCity:LibraryCities[0]];
[self addPeople:[person]];
return person;
}
- (void)addPeople:(CPArray)thePeople
{
if (![thePeople count])
return;
for (var i = 0; i < thePeople.length; i++)
[self _insertPerson:thePeople[i]];
[self _noteStructuralChange];
}
- (void)removePeople:(CPArray)thePeople
{
if (![thePeople count])
return;
var uids = [];
for (var i = 0; i < thePeople.length; i++)
{
var person = thePeople[i];
if (!_peopleByUID[[person uid]])
continue;
[self _stopObservingPerson:person];
[_people removeObjectIdenticalTo:person];
delete _peopleByUID[[person uid]];
uids.push([person uid]);
}
var groups = [self groups];
for (var i = 0; i < groups.length; i++)
[[groups[i] memberIDs] removeObjectsInArray:uids];
[self _noteStructuralChange];
}
- (void)addPeople:(CPArray)thePeople toGroup:(LibraryGroup)aGroup
{
var members = [aGroup memberIDs],
changed = NO;
for (var i = 0; i < thePeople.length; i++)
{
var uid = [thePeople[i] uid];
if (![members containsObject:uid])
{
[members addObject:uid];
changed = YES;
}
}
if (changed)
[self _noteStructuralChange];
}
- (void)removePeople:(CPArray)thePeople fromGroup:(LibraryGroup)aGroup
{
[[aGroup memberIDs] removeObjectsInArray:[thePeople valueForKey:@"uid"]];
[self _noteStructuralChange];
}
// ---------------------------------------------------------------- Source item mutation
- (LibraryGroup)addGroupWithName:(CPString)aName people:(CPArray)thePeople
{
var group = [[LibraryGroup alloc] initWithName:aName memberIDs:[thePeople valueForKey:@"uid"] || []];
[[_groupsSection children] addObject:group];
[self _noteStructuralChange];
return group;
}
- (SmartFolder)addSmartFolderWithName:(CPString)aName predicate:(CPPredicate)aPredicate
{
var folder = [[SmartFolder alloc] initWithName:aName predicate:aPredicate];
[[_smartFoldersSection children] addObject:folder];
[self _noteStructuralChange];
return folder;
}
- (void)updateSmartFolder:(SmartFolder)aFolder name:(CPString)aName predicate:(CPPredicate)aPredicate
{
[aFolder setName:aName];
[aFolder setPredicate:aPredicate];
[self _noteStructuralChange];
}
- (void)renameItem:(SourceItem)anItem to:(CPString)aName
{
if (![anItem isUserItem] || ![aName length])
return;
[anItem setName:aName];
[self _noteStructuralChange];
}
- (void)removeItem:(SourceItem)anItem
{
var section = [self sectionForItem:anItem];
if (![anItem isUserItem] || !section)
return;
[[section children] removeObjectIdenticalTo:anItem];
[self _noteStructuralChange];
}
// Moves a user item to a new index inside its own section.
// anIndex is the proposed insertion index *before* removal (outline view semantics).
- (void)moveItem:(SourceItem)anItem toIndex:(int)anIndex
{
var section = [self sectionForItem:anItem],
children = [section children],
oldIndex = [children indexOfObjectIdenticalTo:anItem];
if (oldIndex === CPNotFound)
return;
if (anIndex > oldIndex)
anIndex--;
anIndex = MAX(0, MIN(anIndex, [children count] - 1));
if (anIndex === oldIndex)
return;
[children removeObjectAtIndex:oldIndex];
[children insertObject:anItem atIndex:anIndex];
[self _noteStructuralChange];
}
- (void)resetToSampleData
{
var people = [_people copy];
for (var i = 0; i < people.length; i++)
[self _stopObservingPerson:people[i]];
_people = [];
_peopleByUID = {};
[[_groupsSection children] removeAllObjects];
[[_smartFoldersSection children] removeAllObjects];
[[CPUserDefaults standardUserDefaults] removeObjectForKey:LibraryDefaultsKey];
[self _populateWithSampleData];
[self _noteStructuralChange];
}
// ---------------------------------------------------------------- KVO
- (void)_insertPerson:(Person)aPerson
{
if (_peopleByUID[[aPerson uid]])
return;
_people.push(aPerson);
_peopleByUID[[aPerson uid]] = aPerson;
for (var i = 0; i < ObservedPersonKeys.length; i++)
[aPerson addObserver:self forKeyPath:ObservedPersonKeys[i] options:CPKeyValueObservingOptionNew context:nil];
}
- (void)_stopObservingPerson:(Person)aPerson
{
for (var i = 0; i < ObservedPersonKeys.length; i++)
[aPerson removeObserver:self forKeyPath:ObservedPersonKeys[i]];
}
- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)aChange context:(id)aContext
{
if (_editNotificationScheduled)
return;
_editNotificationScheduled = YES;
[CPTimer scheduledTimerWithTimeInterval:0 target:self selector:@selector(_postEditNotification:) userInfo:nil repeats:NO];
}
- (void)_postEditNotification:(CPTimer)aTimer
{
_editNotificationScheduled = NO;
[self _scheduleSave];
[[CPNotificationCenter defaultCenter] postNotificationName:LibraryDidChangeNotification
object:self
userInfo:[CPDictionary dictionaryWithObject:LibraryChangePersonEdited forKey:LibraryChangeKindKey]];
}
- (void)_noteStructuralChange
{
[self _scheduleSave];
[[CPNotificationCenter defaultCenter] postNotificationName:LibraryDidChangeNotification
object:self
userInfo:[CPDictionary dictionaryWithObject:LibraryChangeStructural forKey:LibraryChangeKindKey]];
}
// ---------------------------------------------------------------- Persistence
- (void)_scheduleSave
{
if (_saveScheduled)
return;
_saveScheduled = YES;
[CPTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(_saveTimerFired:) userInfo:nil repeats:NO];
}
- (void)_saveTimerFired:(CPTimer)aTimer
{
_saveScheduled = NO;
[self save];
}
- (void)save
{
var people = [],
groups = [],
folders = [],
groupItems = [self groups],
folderItems = [self smartFolders];
for (var i = 0; i < _people.length; i++)
people.push([_people[i] dictionaryRepresentation]);
for (var i = 0; i < groupItems.length; i++)
groups.push([groupItems[i] dictionaryRepresentation]);
for (var i = 0; i < folderItems.length; i++)
folders.push([folderItems[i] dictionaryRepresentation]);
var defaults = [CPUserDefaults standardUserDefaults],
dictionary = [CPDictionary dictionaryWithObjectsAndKeys:
1, @"version",
people, @"people",
groups, @"groups",
folders, @"smartFolders"];
[defaults setObject:dictionary forKey:LibraryDefaultsKey];
[defaults synchronize];
}
- (BOOL)_loadFromDefaults
{
var dictionary = [[CPUserDefaults standardUserDefaults] objectForKey:LibraryDefaultsKey];
if (!dictionary || ![dictionary isKindOfClass:[CPDictionary class]])
return NO;
try
{
var people = [dictionary objectForKey:@"people"] || [],
groups = [dictionary objectForKey:@"groups"] || [],
folders = [dictionary objectForKey:@"smartFolders"] || [];
for (var i = 0; i < people.length; i++)
[self _insertPerson:[[Person alloc] initWithDictionary:people[i]]];
for (var i = 0; i < groups.length; i++)
[[_groupsSection children] addObject:[[LibraryGroup alloc] initWithDictionary:groups[i]]];
for (var i = 0; i < folders.length; i++)
[[_smartFoldersSection children] addObject:[[SmartFolder alloc] initWithDictionary:folders[i]]];
}
catch (e)
{
CPLog.warn("SmartFoldersDemo: stored library is unreadable, using sample data. " + e);
_people = [];
_peopleByUID = {};
[[_groupsSection children] removeAllObjects];
[[_smartFoldersSection children] removeAllObjects];
return NO;
}
return [_people count] > 0;
}
// ---------------------------------------------------------------- Sample data
- (void)_populateWithSampleData
{
// Deterministic Park-Miller generator so every fresh launch shows identical data.
var seed = 20101125;
function random()
{
seed = (seed * 16807) % 2147483647;
return (seed - 1) / 2147483646;
}
function pick(anArray)
{
return anArray[Math.floor(random() * anArray.length)];
}
// Intentionally includes diacritics to exercise [cd] predicate options.
var firstNames = [@"Anna", @"Ben", @"Chloé", @"David", @"Elif", @"François", @"Greta", @"Hiro", @"Ingrid", @"José",
@"Karl", @"Lena", @"Mateo", @"Nina", @"Oskar", @"Paula", @"Quentin", @"Renée", @"Søren", @"Tomás",
@"Ute", @"Viktor", @"Wiebke", @"Xavier", @"Yuki", @"Zoë", @"Amélie", @"Björn", @"Clara", @"Dario"],
lastNames = [@"Müller", @"Schmidt", @"Bauer", @"García", @"Dubois", @"Rossi", @"Nowak", @"Jensen", @"Tanaka", @"Smith",
@"Johnson", @"Öztürk", @"Novák", @"Silva", @"Fischer", @"Weber", @"Lefèvre", @"Kowalski", @"Andersson", @"Brown",
@"Costa", @"Hoffmann", @"Ivanova", @"Kim", @"Lambert", @"Martín", @"Nilsson", @"O'Brien", @"Peña", @"Wagner"];
function emailFor(aFirst, aLast)
{
var raw = (aFirst + "." + aLast).toLowerCase();
if (raw.normalize)
raw = raw.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
return raw.replace(/ø/g, "o").replace(/[^a-z.]/g, "") + "@example.com";
}
var count = 240;
for (var i = 0; i < count; i++)
{
var uid = "p" + ("000" + i).slice(-4),
person = [[Person alloc] initWithUID:uid],
first = pick(firstNames),
last = pick(lastNames);
[person setFirstName:first];
[person setLastName:last];
[person setEmail:emailFor(first, last)];
[person setDepartment:pick(LibraryDepartments)];
[person setCity:pick(LibraryCities)];
[person setBirthDate:new Date(1955 + Math.floor(random() * 48), Math.floor(random() * 12), 1 + Math.floor(random() * 28))];
[person setSalary:38000 + Math.round(random() * 240) * 500];
[person setIsManager:random() < 0.12];
[self _insertPerson:person];
}
// Groups (static membership)
var someIDs = function(n)
{
var ids = [];
while (ids.length < n)
{
var candidate = _people[Math.floor(random() * _people.length)];
if (ids.indexOf([candidate uid]) === -1)
ids.push([candidate uid]);
}
return ids;
};
[[_groupsSection children] addObject:[[LibraryGroup alloc] initWithName:@"Favorites" memberIDs:someIDs(6)]];
[[_groupsSection children] addObject:[[LibraryGroup alloc] initWithName:@"Hiking Club" memberIDs:someIDs(14)]];
[[_groupsSection children] addObject:[[LibraryGroup alloc] initWithName:@"Book Club" memberIDs:someIDs(9)]];
[[_groupsSection children] addObject:[[LibraryGroup alloc] initWithName:@"Drop People Here" memberIDs:[]]];
// Smart folders. Every predicate below is representable by the row templates
// in SmartFolderEditorController, so CPPredicateEditor -setObjectValue: is
// exercised on all of them.
var folders = [
[@"Engineering over 40", [CPPredicate predicateWithFormat:@"department == %@ AND age > %@", @"Engineering", 40]],
[@"Names starting with A", [CPPredicate predicateWithFormat:@"firstName BEGINSWITH[cd] %@", @"a"]],
[@"Müller (diacritics test)",[CPPredicate predicateWithFormat:@"lastName CONTAINS[cd] %@", @"muller"]],
[@"High earners in Berlin", [CPPredicate predicateWithFormat:@"salary >= %@ AND city ==[cd] %@", 120000, @"berlin"]],
[@"Sales or Marketing", [CPPredicate predicateWithFormat:@"department == %@ OR department == %@", @"Sales", @"Marketing"]],
[@"Not in Support", [CPPredicate predicateWithFormat:@"NOT (department == %@)", @"Support"]],
[@"Nested compound", [CPPredicate predicateWithFormat:@"age < %@ AND (city ==[cd] %@ OR city ==[cd] %@)", 35, @"tokyo", @"lisbon"]]
];
for (var i = 0; i < folders.length; i++)
[[_smartFoldersSection children] addObject:[[SmartFolder alloc] initWithName:folders[i][0] predicate:folders[i][1]]];
}
@end
@@ -0,0 +1,585 @@
/*
* MainWindowController.j
* SmartFoldersDemo
*
* Owns the full-platform (bridge) main window. Instantiates the sidebar and
* people view controllers and loads their views into a vertical CPSplitView.
* Exercises:
* - CPWindowController created in code, window delegate + responder chain
* target for menu / toolbar / button bar actions (sent to nil)
* - CPToolbar with view items (buttons, CPSearchField)
* - CPSplitView delegate API: min/max constraints, collapsing, custom
* resizeSubviewsWithOldSize:, autosave, button bar resize control
* - menu validation (-validateMenuItem:)
* - CPAlert as a sheet
* - CPPasteboard copy / paste
*/
@import <AppKit/AppKit.j>
@import "Library.j"
@import "SourceListViewController.j"
@import "PeopleViewController.j"
@import "SmartFolderEditorController.j"
@import "NameSheetController.j"
var ToolbarSidebarIdentifier = @"SidebarItem",
ToolbarNewFolderIdentifier = @"NewSmartFolderItem",
ToolbarNewGroupIdentifier = @"NewGroupItem",
ToolbarEditIdentifier = @"EditFolderItem",
ToolbarDetailIdentifier = @"DetailItem",
ToolbarSearchIdentifier = @"SearchItem",
SidebarDefaultWidth = 230.0,
SidebarMinWidth = 160.0,
SidebarMaxWidth = 420.0,
ResetAlertResetTag = 1001,
ResetAlertCancelTag = 1002;
@implementation MainWindowController : CPWindowController
{
Library _library;
CPSplitView _splitView;
SourceListViewController _sourceListController;
PeopleViewController _peopleController;
SmartFolderEditorController _folderEditor;
NameSheetController _nameSheet;
CPSearchField _searchField;
CPPredicate _searchPredicate;
id _selectedItem;
float _lastSidebarWidth;
}
- (id)initWithLibrary:(Library)aLibrary
{
var window = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask];
if (self = [super initWithWindow:window])
{
_library = aLibrary;
_lastSidebarWidth = SidebarDefaultWidth;
[window setDelegate:self];
[window setTitle:@"Smart Folders Demo"];
_sourceListController = [[SourceListViewController alloc] initWithLibrary:_library];
[_sourceListController setDelegate:self];
_peopleController = [[PeopleViewController alloc] initWithLibrary:_library];
[self _buildToolbar];
[self _buildSplitView];
[[CPNotificationCenter defaultCenter] addObserver:self
selector:@selector(libraryDidChange:)
name:LibraryDidChangeNotification
object:_library];
[self _selectSourceItem:[_library allPeopleItem]];
}
return self;
}
// ---------------------------------------------------------------- Interface
- (void)_buildSplitView
{
var contentView = [[self window] contentView],
bounds = [contentView bounds];
_splitView = [[CPSplitView alloc] initWithFrame:bounds];
[_splitView setVertical:YES];
[_splitView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[_splitView setDelegate:self];
var sidebar = [_sourceListController view],
people = [_peopleController view];
[sidebar setFrame:CGRectMake(0, 0, SidebarDefaultWidth, CGRectGetHeight(bounds))];
[people setFrame:CGRectMake(SidebarDefaultWidth + 1, 0, MAX(0, CGRectGetWidth(bounds) - SidebarDefaultWidth - 1), CGRectGetHeight(bounds))];
[_splitView addSubview:sidebar];
[_splitView addSubview:people];
[contentView addSubview:_splitView];
if ([_splitView respondsToSelector:@selector(setButtonBar:forDividerAtIndex:)])
[_splitView setButtonBar:[_sourceListController buttonBar] forDividerAtIndex:0];
if ([_splitView respondsToSelector:@selector(setAutosaveName:)])
[_splitView setAutosaveName:@"SmartFoldersDemo.MainSplit"];
}
- (void)_buildToolbar
{
_searchField = [[CPSearchField alloc] initWithFrame:CGRectMake(0, 0, 220, 30)];
[_searchField setPlaceholderString:@"Search people"];
[_searchField setTarget:self];
[_searchField setAction:@selector(searchFieldDidChange:)];
[_searchField setSendsSearchStringImmediately:YES];
[_searchField setRecentsAutosaveName:@"SmartFoldersDemo.RecentSearches"];
[_searchField setSearchMenuTemplate:[_searchField defaultSearchMenuTemplate]];
var toolbar = [[CPToolbar alloc] initWithIdentifier:@"SmartFoldersDemoToolbar"];
[toolbar setDelegate:self];
// Buttons carry their own titles, so labels below them would be redundant.
[toolbar setDisplayMode:CPToolbarDisplayModeIconOnly];
[toolbar setVisible:YES];
[[self window] setToolbar:toolbar];
}
- (CPArray)toolbarAllowedItemIdentifiers:(CPToolbar)aToolbar
{
return [ToolbarSidebarIdentifier, ToolbarNewFolderIdentifier, ToolbarNewGroupIdentifier, ToolbarEditIdentifier,
ToolbarDetailIdentifier, ToolbarSearchIdentifier,
CPToolbarFlexibleSpaceItemIdentifier, CPToolbarSpaceItemIdentifier, CPToolbarSeparatorItemIdentifier];
}
- (CPArray)toolbarDefaultItemIdentifiers:(CPToolbar)aToolbar
{
return [ToolbarSidebarIdentifier, CPToolbarSpaceItemIdentifier,
ToolbarNewFolderIdentifier, ToolbarNewGroupIdentifier, ToolbarEditIdentifier,
CPToolbarFlexibleSpaceItemIdentifier,
ToolbarDetailIdentifier, ToolbarSearchIdentifier];
}
- (CPToolbarItem)toolbar:(CPToolbar)aToolbar itemForItemIdentifier:(CPString)anIdentifier willBeInsertedIntoToolbar:(BOOL)aFlag
{
var item = [[CPToolbarItem alloc] initWithItemIdentifier:anIdentifier];
switch (anIdentifier)
{
case ToolbarSidebarIdentifier:
[self _configureItem:item label:@"Sidebar" buttonTitle:@"Sidebar" action:@selector(toggleSidebar:) toolTip:@"Show or hide the sidebar"];
break;
case ToolbarNewFolderIdentifier:
[self _configureItem:item label:@"New Smart Folder" buttonTitle:@"New Smart Folder" action:@selector(newSmartFolder:) toolTip:@"Create a smart folder from a predicate"];
break;
case ToolbarNewGroupIdentifier:
[self _configureItem:item label:@"New Group" buttonTitle:@"New Group" action:@selector(newGroup:) toolTip:@"Create an empty group (or drop people on GROUPS)"];
break;
case ToolbarEditIdentifier:
[self _configureItem:item label:@"Edit" buttonTitle:@"Edit…" action:@selector(editSelectedSmartFolder:) toolTip:@"Edit the selected smart folder"];
break;
case ToolbarDetailIdentifier:
[self _configureItem:item label:@"Details" buttonTitle:@"Details" action:@selector(toggleDetail:) toolTip:@"Show or hide the detail pane"];
break;
case ToolbarSearchIdentifier:
[item setLabel:@"Search"];
[item setPaletteLabel:@"Search"];
[item setView:_searchField];
[item setMinSize:CGSizeMake(140, 30)];
[item setMaxSize:CGSizeMake(260, 30)];
break;
}
return item;
}
- (void)_configureItem:(CPToolbarItem)anItem label:(CPString)aLabel buttonTitle:(CPString)aTitle action:(SEL)anAction toolTip:(CPString)aToolTip
{
var button = [CPButton buttonWithTitle:aTitle];
[button setBezelStyle:CPRoundedBezelStyle];
[button setTarget:self];
[button setAction:anAction];
[button sizeToFit];
var size = CGSizeMake(MAX(CGRectGetWidth([button frame]), 40), 25);
[anItem setLabel:aLabel];
[anItem setPaletteLabel:aLabel];
[anItem setToolTip:aToolTip];
[anItem setTarget:self];
[anItem setAction:anAction];
[anItem setView:button];
[anItem setMinSize:size];
[anItem setMaxSize:size];
}
// ---------------------------------------------------------------- Split view delegate (sidebar)
- (BOOL)splitView:(CPSplitView)aSplitView canCollapseSubview:(CPView)aSubview
{
return aSubview === [_sourceListController view];
}
- (float)splitView:(CPSplitView)aSplitView constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)anIndex
{
return SidebarMinWidth;
}
- (float)splitView:(CPSplitView)aSplitView constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)anIndex
{
return MIN(SidebarMaxWidth, CGRectGetWidth([aSplitView bounds]) - 300.0);
}
- (void)splitView:(CPSplitView)aSplitView resizeSubviewsWithOldSize:(CGSize)oldSize
{
// Sidebar keeps its width, the people pane takes up the rest.
var subviews = [aSplitView subviews],
sidebar = subviews[0],
main = subviews[1],
size = [aSplitView bounds].size,
divider = [aSplitView dividerThickness],
sidebarWidth = [aSplitView isSubviewCollapsed:sidebar] ? 0 : CGRectGetWidth([sidebar frame]);
[sidebar setFrame:CGRectMake(0, 0, sidebarWidth, size.height)];
[main setFrame:CGRectMake(sidebarWidth + divider, 0, MAX(0, size.width - sidebarWidth - divider), size.height)];
// Custom layout bypasses CPSplitView's own pass: redraw the divider.
[aSplitView setNeedsDisplay:YES];
}
- (BOOL)isSidebarVisible
{
var sidebar = [_sourceListController view];
return ![_splitView isSubviewCollapsed:sidebar] && CGRectGetWidth([sidebar frame]) > 0;
}
// ---------------------------------------------------------------- Source list delegate
- (void)sourceListViewController:(SourceListViewController)aController didSelectItem:(id)anItem
{
_selectedItem = anItem;
[self _refreshPeople];
}
- (void)_selectSourceItem:(id)anItem
{
_selectedItem = anItem;
[_sourceListController reloadAndSelectItem:anItem];
[self _refreshPeople];
}
// ---------------------------------------------------------------- Model -> view
- (void)libraryDidChange:(CPNotification)aNotification
{
var kind = [[aNotification userInfo] objectForKey:LibraryChangeKindKey];
if (![_library containsItem:_selectedItem])
_selectedItem = [_library allPeopleItem];
// Names, order and badges are all rendered by the outline.
[_sourceListController reloadAndSelectItem:_selectedItem];
if (kind === LibraryChangeStructural)
[self _refreshPeople];
else
[_peopleController rearrange];
}
- (CPPredicate)_combinedPredicate
{
var subpredicates = [],
folderPredicate = [_library predicateForItem:_selectedItem];
if (folderPredicate)
subpredicates.push(folderPredicate);
if (_searchPredicate)
subpredicates.push(_searchPredicate);
if (subpredicates.length === 0)
return nil;
if (subpredicates.length === 1)
return subpredicates[0];
return [CPCompoundPredicate andPredicateWithSubpredicates:subpredicates];
}
- (void)_refreshPeople
{
var item = _selectedItem || [_library allPeopleItem];
[_peopleController setPeople:[_library baseContentForItem:item]
filterPredicate:[self _combinedPredicate]
title:[item name]];
}
// ---------------------------------------------------------------- Search
- (void)searchFieldDidChange:(id)sender
{
var searchString = [[_searchField stringValue] stringByTrimmingWhitespace];
if (![searchString length])
_searchPredicate = nil;
else
_searchPredicate = [CPPredicate predicateWithFormat:@"(firstName CONTAINS[cd] %@) OR (lastName CONTAINS[cd] %@) OR (email CONTAINS[cd] %@) OR (city CONTAINS[cd] %@) OR (department CONTAINS[cd] %@)",
searchString, searchString, searchString, searchString, searchString];
[self _refreshPeople];
}
- (void)focusSearchField:(id)sender
{
[[self window] makeFirstResponder:_searchField];
}
// ---------------------------------------------------------------- Source item actions
- (SmartFolderEditorController)_folderEditor
{
if (!_folderEditor)
_folderEditor = [[SmartFolderEditorController alloc] initWithLibrary:_library];
return _folderEditor;
}
- (NameSheetController)_nameSheet
{
if (!_nameSheet)
_nameSheet = [[NameSheetController alloc] init];
return _nameSheet;
}
- (BOOL)_isSheetVisible
{
return !![[self window] attachedSheet];
}
- (void)newSmartFolder:(id)sender
{
if ([self _isSheetVisible])
return;
[[self _folderEditor] beginEditingFolder:nil modalForWindow:[self window] completion:function(folder)
{
[self _selectSourceItem:folder];
}];
}
- (void)editSelectedSmartFolder:(id)sender
{
if (![_selectedItem isKindOfClass:[SmartFolder class]] || [self _isSheetVisible])
return;
var folder = _selectedItem;
[[self _folderEditor] beginEditingFolder:folder modalForWindow:[self window] completion:function(aFolder)
{
[self _selectSourceItem:aFolder];
}];
}
- (void)newGroup:(id)sender
{
if ([self _isSheetVisible])
return;
[[self _nameSheet] beginWithTitle:@"New Group" name:@"Untitled Group" okTitle:@"Create" modalForWindow:[self window] completion:function(name)
{
var group = [_library addGroupWithName:name people:[]];
[self _selectSourceItem:group];
}];
}
- (void)renameSelectedGroup:(id)sender
{
if (![_selectedItem isKindOfClass:[LibraryGroup class]] || [self _isSheetVisible])
return;
var group = _selectedItem;
[[self _nameSheet] beginWithTitle:@"Rename Group" name:[group name] okTitle:@"Rename" modalForWindow:[self window] completion:function(name)
{
[_library renameItem:group to:name];
}];
}
- (void)deleteSelectedSourceItem:(id)sender
{
if (![_selectedItem isUserItem])
return;
[_library removeItem:_selectedItem];
}
// ---------------------------------------------------------------- People actions
- (void)newPerson:(id)sender
{
// A new person would be invisible in a smart folder or a filtered list.
if ([_selectedItem isKindOfClass:[SmartFolder class]])
_selectedItem = [_library allPeopleItem];
if (_searchPredicate)
{
[_searchField setStringValue:@""];
_searchPredicate = nil;
}
var group = [_selectedItem isKindOfClass:[LibraryGroup class]] ? _selectedItem : nil,
person = [_library createPerson];
if (group)
[_library addPeople:[person] toGroup:group];
[_peopleController selectPerson:person edit:YES];
}
- (void)removeSelectedPeople:(id)sender
{
var people = [_peopleController selectedPeople];
if (![people count])
return;
if ([_selectedItem isKindOfClass:[LibraryGroup class]])
[_library removePeople:people fromGroup:_selectedItem];
else
[_library removePeople:people];
}
- (void)delete:(id)sender
{
[self removeSelectedPeople:sender];
}
- (void)copy:(id)sender
{
[_peopleController copySelectionToPasteboard:[CPPasteboard generalPasteboard]];
}
- (void)cut:(id)sender
{
[self copy:sender];
[self removeSelectedPeople:sender];
}
- (void)paste:(id)sender
{
var pasteboard = [CPPasteboard generalPasteboard],
people = [_peopleController peopleFromTabSeparatedString:[pasteboard stringForType:CPStringPboardType]];
if (![people count])
return;
if ([_selectedItem isKindOfClass:[SmartFolder class]])
_selectedItem = [_library allPeopleItem];
[_library addPeople:people];
if ([_selectedItem isKindOfClass:[LibraryGroup class]])
[_library addPeople:people toGroup:_selectedItem];
[[_peopleController arrayController] setSelectedObjects:people];
}
// ---------------------------------------------------------------- View actions
- (void)toggleSidebar:(id)sender
{
if ([self isSidebarVisible])
{
_lastSidebarWidth = CGRectGetWidth([[_sourceListController view] frame]);
[_splitView setPosition:0 ofDividerAtIndex:0];
}
else
[_splitView setPosition:MAX(_lastSidebarWidth, SidebarMinWidth) ofDividerAtIndex:0];
}
- (void)toggleDetail:(id)sender
{
[_peopleController toggleDetail];
}
- (void)showAllPeople:(id)sender
{
[self _selectSourceItem:[_library allPeopleItem]];
}
- (void)resetLibrary:(id)sender
{
if ([self _isSheetVisible])
return;
var alert = [[CPAlert alloc] init];
[alert setAlertStyle:CPWarningAlertStyle];
[alert setMessageText:@"Reset the library?"];
[alert setInformativeText:@"All people, groups and smart folders will be replaced by the sample data. This cannot be undone."];
[alert addButtonWithTitle:@"Reset"];
[alert addButtonWithTitle:@"Cancel"];
// CPAlert reports the clicked button's tag as the return code. Assign our own
// tags rather than relying on a framework-specific numbering scheme.
var buttons = [alert buttons];
[buttons[0] setTag:ResetAlertResetTag];
[buttons[1] setTag:ResetAlertCancelTag];
[alert beginSheetModalForWindow:[self window]
modalDelegate:self
didEndSelector:@selector(resetAlertDidEnd:returnCode:contextInfo:)
contextInfo:nil];
}
- (void)resetAlertDidEnd:(CPAlert)anAlert returnCode:(int)aReturnCode contextInfo:(id)aContext
{
if (aReturnCode !== ResetAlertResetTag)
return;
[_searchField setStringValue:@""];
_searchPredicate = nil;
_selectedItem = [_library allPeopleItem];
[_library resetToSampleData];
}
// ---------------------------------------------------------------- Validation
- (BOOL)validateMenuItem:(CPMenuItem)anItem
{
var action = [anItem action],
sheet = [self _isSheetVisible],
hasPeopleSelection = [[_peopleController selectedPeople] count] > 0;
switch (action)
{
case @selector(newSmartFolder:):
case @selector(newGroup:):
case @selector(resetLibrary:):
return !sheet;
case @selector(editSelectedSmartFolder:):
return !sheet && [_selectedItem isKindOfClass:[SmartFolder class]];
case @selector(renameSelectedGroup:):
return !sheet && [_selectedItem isKindOfClass:[LibraryGroup class]];
case @selector(deleteSelectedSourceItem:):
return !sheet && [_selectedItem isUserItem];
case @selector(copy:):
case @selector(cut:):
case @selector(delete:):
case @selector(removeSelectedPeople:):
return !sheet && hasPeopleSelection;
case @selector(paste:):
return !sheet && [[[CPPasteboard generalPasteboard] types] containsObject:CPStringPboardType];
case @selector(toggleSidebar:):
[anItem setTitle:[self isSidebarVisible] ? @"Hide Sidebar" : @"Show Sidebar"];
return YES;
case @selector(toggleDetail:):
[anItem setTitle:[_peopleController isDetailVisible] ? @"Hide Details" : @"Show Details"];
return YES;
}
return YES;
}
@end
@@ -0,0 +1,102 @@
/*
* NameSheetController.j
* SmartFoldersDemo
*
* Minimal reusable sheet asking for a name (new group, rename group).
* Exercises a second, independent CPWindowController-owned sheet, first
* responder handling and default/escape key equivalents.
*/
@import <AppKit/AppKit.j>
@implementation NameSheetController : CPWindowController
{
CPTextField _titleField;
CPTextField _nameField;
CPButton _okButton;
Function _completion;
}
- (id)init
{
var window = [[CPWindow alloc] initWithContentRect:CGRectMake(0, 0, 380, 140) styleMask:CPDocModalWindowMask];
if (self = [super initWithWindow:window])
[self _buildInterface];
return self;
}
- (void)_buildInterface
{
var content = [[self window] contentView],
width = CGRectGetWidth([content bounds]),
height = CGRectGetHeight([content bounds]);
_titleField = [CPTextField labelWithTitle:@""];
[_titleField setFont:[CPFont boldSystemFontOfSize:13.0]];
[_titleField setFrame:CGRectMake(20, 16, width - 40, 20)];
[content addSubview:_titleField];
_nameField = [[CPTextField alloc] initWithFrame:CGRectMake(20, 46, width - 40, 29)];
[_nameField setEditable:YES];
[_nameField setBezeled:YES];
[content addSubview:_nameField];
var cancelButton = [CPButton buttonWithTitle:@"Cancel"];
[cancelButton setFrame:CGRectMake(width - 200, height - 44, 85, 25)];
[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 setTag:CPOKButton];
[_okButton setTarget:self];
[_okButton setAction:@selector(closeSheet:)];
[content addSubview:_okButton];
[[self window] setDefaultButton:_okButton];
}
// aCallback(name) is invoked only when the user confirms with a non-empty name.
- (void)beginWithTitle:(CPString)aTitle name:(CPString)aName okTitle:(CPString)anOKTitle modalForWindow:(CPWindow)aWindow completion:(Function)aCallback
{
_completion = aCallback;
[_titleField setStringValue:aTitle];
[_nameField setStringValue:aName || @""];
[_okButton setTitle:anOKTitle || @"OK"];
[CPApp beginSheet:[self window]
modalForWindow:aWindow
modalDelegate:self
didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
contextInfo:nil];
[[self window] makeFirstResponder:_nameField];
[_nameField selectText:self];
}
- (void)closeSheet:(id)sender
{
if ([sender tag] === CPOKButton && ![[[_nameField stringValue] stringByTrimmingWhitespace] length])
return;
[CPApp endSheet:[self window] returnCode:[sender tag]];
}
- (void)sheetDidEnd:(CPWindow)aSheet returnCode:(int)aReturnCode contextInfo:(id)aContext
{
[aSheet orderOut:self];
var callback = _completion;
_completion = nil;
if (aReturnCode === CPOKButton && callback)
callback([[_nameField stringValue] stringByTrimmingWhitespace]);
}
@end
@@ -0,0 +1,461 @@
/*
* PeopleViewController.j
* SmartFoldersDemo
*
* Right hand side of the main split view. Exercises:
* - CPArrayController with content, filterPredicate, sortDescriptors and
* selection (incl. setSelectedObjects: across content changes)
* - cell-based CPTableView fully driven by bindings (value, selection,
* sort descriptors), editable columns, formatters (CPDateFormatter,
* CPNumberFormatter), checkbox data views, column reordering/resizing,
* autosaved column state, alternating rows, grid lines
* - the table as drag source (rows -> source list groups)
* - CPPasteboard (copy / paste as tab separated text)
* - nested CPSplitView hosting a child view controller (detail pane)
*/
@import <AppKit/AppKit.j>
@import "Library.j"
@import "PersonDetailViewController.j"
@implementation PeopleViewController : CPViewController
{
Library _library;
CPArrayController _arrayController;
CPTableView _tableView;
CPSplitView _splitView;
CPView _tablePane;
CPTextField _statusField;
PersonDetailViewController _detailController;
CPString _contentTitle;
float _lastDetailHeight;
}
- (id)initWithLibrary:(Library)aLibrary
{
if (self = [super initWithCibName:nil bundle:nil])
{
_library = aLibrary;
_contentTitle = @"";
_lastDetailHeight = PersonDetailPreferredHeight;
_arrayController = [[CPArrayController alloc] init];
[_arrayController setEditable:YES];
[_arrayController setAvoidsEmptySelection:NO];
[_arrayController setPreservesSelection:YES];
[_arrayController setSelectsInsertedObjects:YES];
[_arrayController setSortDescriptors:[
[CPSortDescriptor sortDescriptorWithKey:@"lastName" ascending:YES selector:@selector(caseInsensitiveCompare:)],
[CPSortDescriptor sortDescriptorWithKey:@"firstName" ascending:YES selector:@selector(caseInsensitiveCompare:)]
]];
[_arrayController addObserver:self forKeyPath:@"arrangedObjects" options:0 context:nil];
[_arrayController addObserver:self forKeyPath:@"selectionIndexes" options:0 context:nil];
}
return self;
}
- (CPArrayController)arrayController
{
return _arrayController;
}
- (CPTableView)tableView
{
[self view];
return _tableView;
}
- (void)loadView
{
var frame = CGRectMake(0, 0, 800, 600),
view = [[CPView alloc] initWithFrame:frame];
[view setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
_splitView = [[CPSplitView alloc] initWithFrame:frame];
[_splitView setVertical:NO];
[_splitView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[_splitView setDelegate:self];
_tablePane = [self _buildTablePane:CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame) - _lastDetailHeight)];
[_splitView addSubview:_tablePane];
_detailController = [[PersonDetailViewController alloc] initWithArrayController:_arrayController];
var detailView = [_detailController view];
[detailView setFrame:CGRectMake(0, 0, CGRectGetWidth(frame), _lastDetailHeight)];
[_splitView addSubview:detailView];
[view addSubview:_splitView];
[self setView:view];
if ([_splitView respondsToSelector:@selector(setAutosaveName:)])
[_splitView setAutosaveName:@"SmartFoldersDemo.PeopleSplit"];
[self _updateStatus];
}
- (CPView)_buildTablePane:(CGRect)aFrame
{
var pane = [[CPView alloc] initWithFrame:aFrame],
barHeight = 26.0;
[pane setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(aFrame), CGRectGetHeight(aFrame) - barHeight)];
[scrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[scrollView setAutohidesScrollers:YES];
[scrollView setBorderType:CPNoBorder];
_tableView = [[CPTableView alloc] initWithFrame:[[scrollView contentView] bounds]];
[_tableView setUsesAlternatingRowBackgroundColors:YES];
[_tableView setAllowsMultipleSelection:YES];
[_tableView setAllowsEmptySelection:YES];
[_tableView setAllowsColumnReordering:YES];
[_tableView setAllowsColumnResizing:YES];
[_tableView setAllowsColumnSelection:NO];
[_tableView setGridStyleMask:CPTableViewSolidVerticalGridLineMask];
[_tableView setColumnAutoresizingStyle:CPTableViewLastColumnOnlyAutoresizingStyle];
[_tableView setRowHeight:24.0];
[_tableView setIntercellSpacing:CGSizeMake(6.0, 0.0)];
var dateFormatter = [[CPDateFormatter alloc] init];
[dateFormatter setDateStyle:CPDateFormatterMediumStyle];
[dateFormatter setTimeStyle:CPDateFormatterNoStyle];
var numberFormatter = [[CPNumberFormatter alloc] init];
[numberFormatter setNumberStyle:CPNumberFormatterDecimalStyle];
[self _addColumn:@"lastName" title:@"Last Name" width:120 editable:YES dataView:nil];
[self _addColumn:@"firstName" title:@"First Name" width:110 editable:YES dataView:nil];
[self _addColumn:@"department" title:@"Department" width:110 editable:YES dataView:nil];
[self _addColumn:@"city" title:@"City" width:110 editable:YES dataView:nil];
[self _addColumn:@"age" title:@"Age" width:46 editable:NO dataView:[self _textDataViewWithFormatter:nil alignment:CPRightTextAlignment]];
[self _addColumn:@"birthDate" title:@"Born" width:110 editable:NO dataView:[self _textDataViewWithFormatter:dateFormatter alignment:CPRightTextAlignment]];
[self _addColumn:@"salary" title:@"Salary" width:90 editable:YES dataView:[self _textDataViewWithFormatter:numberFormatter alignment:CPRightTextAlignment]];
// Title-less checkbox, image centred in the cell.
var checkBox = [CPCheckBox checkBoxWithTitle:@""];
[checkBox setImagePosition:CPImageOnly];
[checkBox setAlignment:CPCenterTextAlignment];
var managerColumn = [self _addColumn:@"isManager" title:@"Manager" width:64 editable:YES dataView:checkBox];
[[managerColumn headerView] setAlignment:CPCenterTextAlignment];
[managerColumn setResizingMask:CPTableColumnNoResizing];
[self _addColumn:@"email" title:@"Email" width:220 editable:YES dataView:nil];
// Selection and sorting through bindings.
[_tableView bind:CPSelectionIndexesBinding toObject:_arrayController withKeyPath:@"selectionIndexes" options:nil];
[_tableView bind:@"sortDescriptors" toObject:_arrayController withKeyPath:@"sortDescriptors" options:nil];
// The data source only provides drag & drop; content comes from the bindings
// (the column value bindings above establish the table's content binding).
[_tableView setDataSource:self];
[_tableView setDelegate:self];
[_tableView setDraggingSourceOperationMask:CPDragOperationCopy | CPDragOperationMove forLocal:YES];
[_tableView setDraggingSourceOperationMask:CPDragOperationCopy forLocal:NO];
if ([_tableView respondsToSelector:@selector(setAutosaveName:)])
{
[_tableView setAutosaveName:@"SmartFoldersDemo.PeopleTable"];
if ([_tableView respondsToSelector:@selector(setAutosaveTableColumns:)])
[_tableView setAutosaveTableColumns:YES];
}
[scrollView setDocumentView:_tableView];
[pane addSubview:scrollView];
// --- Button bar with status text
var buttonBar = [[CPButtonBar alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(aFrame) - barHeight, CGRectGetWidth(aFrame), barHeight)];
[buttonBar setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin];
if ([buttonBar respondsToSelector:@selector(setHasResizeControl:)])
[buttonBar setHasResizeControl:NO];
var plusButton = [CPButtonBar plusButton];
[plusButton setTarget:nil];
[plusButton setAction:@selector(newPerson:)];
[plusButton setToolTip:@"Add a person"];
var minusButton = [CPButtonBar minusButton];
[minusButton setTarget:nil];
[minusButton setAction:@selector(removeSelectedPeople:)];
[minusButton setToolTip:@"Remove the selected people (from the group, if a group is selected)"];
[minusButton bind:CPEnabledBinding toObject:_arrayController withKeyPath:@"canRemove" options:nil];
[buttonBar setButtons:[plusButton, minusButton]];
_statusField = [CPTextField labelWithTitle:@""];
[_statusField setFont:[CPFont systemFontOfSize:11.0]];
[_statusField setTextColor:[CPColor colorWithWhite:0.35 alpha:1.0]];
[_statusField setAlignment:CPRightTextAlignment];
[_statusField setVerticalAlignment:CPCenterVerticalTextAlignment];
[_statusField setFrame:CGRectMake(80, 0, CGRectGetWidth(aFrame) - 92, barHeight)];
[_statusField setLineBreakMode:CPLineBreakByTruncatingHead];
[_statusField setAutoresizingMask:CPViewWidthSizable];
[buttonBar addSubview:_statusField];
[pane addSubview:buttonBar];
return pane;
}
- (CPTextField)_textDataViewWithFormatter:(CPFormatter)aFormatter alignment:(CPTextAlignment)anAlignment
{
var field = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
[field setAlignment:anAlignment];
[field setVerticalAlignment:CPCenterVerticalTextAlignment];
[field setLineBreakMode:CPLineBreakByTruncatingTail];
if (aFormatter)
[field setFormatter:aFormatter];
return field;
}
- (CPTableColumn)_addColumn:(CPString)aKey title:(CPString)aTitle width:(float)aWidth editable:(BOOL)isEditable dataView:(CPView)aDataView
{
var column = [[CPTableColumn alloc] initWithIdentifier:aKey],
selector = [@"firstName", @"lastName", @"department", @"city", @"email"].indexOf(aKey) !== -1 ? @selector(caseInsensitiveCompare:) : @selector(compare:);
[[column headerView] setStringValue:aTitle];
[column setWidth:aWidth];
[column setMinWidth:36];
[column setEditable:isEditable];
[column setSortDescriptorPrototype:[CPSortDescriptor sortDescriptorWithKey:aKey ascending:YES selector:selector]];
if (!aDataView)
{
aDataView = [self _textDataViewWithFormatter:nil alignment:CPLeftTextAlignment];
[aDataView setEditable:isEditable];
}
[column setDataView:aDataView];
[_tableView addTableColumn:column];
[column bind:CPValueBinding toObject:_arrayController withKeyPath:@"arrangedObjects." + aKey options:nil];
return column;
}
// ---------------------------------------------------------------- Content
- (void)setPeople:(CPArray)thePeople filterPredicate:(CPPredicate)aPredicate title:(CPString)aTitle
{
var selected = [_arrayController selectedObjects] || [];
_contentTitle = aTitle || @"";
try
{
[_arrayController setFilterPredicate:aPredicate];
}
catch (e)
{
CPLog.warn("SmartFoldersDemo: filter predicate failed: " + e);
[_arrayController setFilterPredicate:nil];
}
[_arrayController setContent:[thePeople copy]];
[_arrayController setSelectedObjects:selected];
[self _updateStatus];
}
- (void)rearrange
{
[_arrayController rearrangeObjects];
[self _updateStatus];
}
- (CPArray)selectedPeople
{
return [_arrayController selectedObjects] || [];
}
- (void)selectPerson:(Person)aPerson edit:(BOOL)shouldEdit
{
[_arrayController setSelectedObjects:[aPerson]];
var row = [[_arrayController arrangedObjects] indexOfObjectIdenticalTo:aPerson];
if (row === CPNotFound)
return;
[_tableView scrollRowToVisible:row];
if (shouldEdit)
{
[[_tableView window] makeFirstResponder:_tableView];
[_tableView editColumn:[_tableView columnWithIdentifier:@"firstName"] row:row withEvent:nil select:YES];
}
}
- (void)focus
{
[[_tableView window] makeFirstResponder:_tableView];
}
// ---------------------------------------------------------------- Detail pane
- (BOOL)isDetailVisible
{
var detailView = [[_splitView subviews] objectAtIndex:1];
return ![_splitView isSubviewCollapsed:detailView] && CGRectGetHeight([detailView frame]) > 0;
}
- (void)toggleDetail
{
var total = CGRectGetHeight([_splitView bounds]),
divider = [_splitView dividerThickness];
if ([self isDetailVisible])
{
_lastDetailHeight = CGRectGetHeight([[[_splitView subviews] objectAtIndex:1] frame]);
[_splitView setPosition:total ofDividerAtIndex:0];
}
else
[_splitView setPosition:total - divider - MAX(_lastDetailHeight, 150) ofDividerAtIndex:0];
}
// Split view delegate (detail pane)
- (BOOL)splitView:(CPSplitView)aSplitView canCollapseSubview:(CPView)aSubview
{
return aSubview === [[aSplitView subviews] objectAtIndex:1];
}
- (float)splitView:(CPSplitView)aSplitView constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)anIndex
{
return 120.0;
}
- (float)splitView:(CPSplitView)aSplitView constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)anIndex
{
return CGRectGetHeight([aSplitView bounds]) - 150.0;
}
- (void)splitView:(CPSplitView)aSplitView resizeSubviewsWithOldSize:(CGSize)oldSize
{
// Keep the detail pane at a fixed height while the window resizes.
var subviews = [aSplitView subviews],
top = subviews[0],
bottom = subviews[1],
size = [aSplitView bounds].size,
divider = [aSplitView dividerThickness],
bottomHeight = [aSplitView isSubviewCollapsed:bottom] ? 0 : CGRectGetHeight([bottom frame]);
bottomHeight = MIN(bottomHeight, MAX(0, size.height - divider - 120));
[top setFrame:CGRectMake(0, 0, size.width, MAX(0, size.height - bottomHeight - divider))];
[bottom setFrame:CGRectMake(0, size.height - bottomHeight, size.width, bottomHeight)];
// A custom resizeSubviewsWithOldSize: bypasses CPSplitView's own layout pass,
// so the divider must be told to redraw at the new width.
[aSplitView setNeedsDisplay:YES];
}
// ---------------------------------------------------------------- Pasteboard helpers
- (CPString)tabSeparatedStringForPeople:(CPArray)thePeople
{
var lines = [[Person tabSeparatedHeader]];
for (var i = 0; i < thePeople.length; i++)
lines.push([thePeople[i] tabSeparatedRepresentation]);
return lines.join("\n");
}
- (CPArray)peopleFromTabSeparatedString:(CPString)aString
{
var lines = (aString || "").split(/\r?\n/),
result = [];
for (var i = 0; i < lines.length; i++)
{
var line = lines[i];
if (!line || line === [Person tabSeparatedHeader])
continue;
var parts = line.split("\t");
if (parts.length < 2)
continue;
var person = [[Person alloc] init];
[person setFirstName:parts[0] || @""];
[person setLastName:parts[1] || @""];
[person setEmail:parts[2] || @""];
[person setDepartment:parts[3] || LibraryDepartments[0]];
[person setCity:parts[4] || @""];
[person setSalary:parseInt(parts[5], 10) || 0];
result.push(person);
}
return result;
}
- (void)copySelectionToPasteboard:(CPPasteboard)aPasteboard
{
var people = [self selectedPeople];
if (![people count])
return;
[aPasteboard declareTypes:[CPStringPboardType, PeopleDragType] owner:nil];
[aPasteboard setString:[self tabSeparatedStringForPeople:people] forType:CPStringPboardType];
[aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[people valueForKey:@"uid"]] forType:PeopleDragType];
}
// ---------------------------------------------------------------- Table data source: drag source
- (BOOL)tableView:(CPTableView)aTableView writeRowsWithIndexes:(CPIndexSet)theRows toPasteboard:(CPPasteboard)aPasteboard
{
var people = [[_arrayController arrangedObjects] objectsAtIndexes:theRows];
if (![people count])
return NO;
[aPasteboard declareTypes:[PeopleDragType, CPStringPboardType] owner:self];
[aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[people valueForKey:@"uid"]] forType:PeopleDragType];
[aPasteboard setString:[self tabSeparatedStringForPeople:people] forType:CPStringPboardType];
return YES;
}
// ---------------------------------------------------------------- Table delegate
- (void)tableViewDeleteKeyPressed:(CPTableView)aTableView
{
[CPApp sendAction:@selector(removeSelectedPeople:) to:nil from:aTableView];
}
// ---------------------------------------------------------------- KVO / status
- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)aChange context:(id)aContext
{
[self _updateStatus];
}
- (void)_updateStatus
{
if (!_statusField)
return;
var shown = [[_arrayController arrangedObjects] count],
total = [[_arrayController content] count],
selected = [[_arrayController selectionIndexes] count],
text = [CPString stringWithFormat:@"%@ — %d of %d people", _contentTitle, shown, total];
if (selected)
text += [CPString stringWithFormat:@", %d selected", selected];
[_statusField setStringValue:text];
}
@end
+141
View File
@@ -0,0 +1,141 @@
/*
* Person.j
* SmartFoldersDemo
*
* The record type shown in the people table. Plain KVC/KVO compliant model
* object: every property is exercised by bindings (table columns, detail
* pane), by CPPredicate evaluation (smart folders, search) and by
* CPSortDescriptor (column sorting).
*
* "age" and "fullName" are derived properties registered with
* +keyPathsForValuesAffecting<Key> so that KVO dependent-key support is
* exercised as well.
*/
@import <Foundation/Foundation.j>
var PersonUIDCounter = 0;
@implementation Person : CPObject
{
CPString uid @accessors;
CPString firstName @accessors;
CPString lastName @accessors;
CPString email @accessors;
CPString department @accessors;
CPString city @accessors;
CPDate birthDate @accessors;
CPNumber salary @accessors;
BOOL isManager @accessors;
}
+ (CPString)nextUID
{
return @"p" + (new Date()).getTime().toString(36) + "-" + (++PersonUIDCounter);
}
+ (CPSet)keyPathsForValuesAffectingAge
{
return [CPSet setWithObject:@"birthDate"];
}
+ (CPSet)keyPathsForValuesAffectingFullName
{
return [CPSet setWithObjects:@"firstName", @"lastName"];
}
- (id)init
{
return [self initWithUID:[Person nextUID]];
}
- (id)initWithUID:(CPString)aUID
{
if (self = [super init])
{
uid = aUID;
firstName = @"";
lastName = @"";
email = @"";
department = @"Engineering";
city = @"";
birthDate = new Date(1990, 0, 1);
salary = 50000;
isManager = NO;
}
return self;
}
- (id)initWithDictionary:(CPDictionary)aDictionary
{
if (self = [self initWithUID:[aDictionary objectForKey:@"uid"] || [Person nextUID]])
{
firstName = [aDictionary objectForKey:@"firstName"] || @"";
lastName = [aDictionary objectForKey:@"lastName"] || @"";
email = [aDictionary objectForKey:@"email"] || @"";
department = [aDictionary objectForKey:@"department"] || @"";
city = [aDictionary objectForKey:@"city"] || @"";
var interval = [aDictionary objectForKey:@"birthDate"];
birthDate = (interval !== nil && interval !== undefined) ? [CPDate dateWithTimeIntervalSince1970:interval] : nil;
salary = Number([aDictionary objectForKey:@"salary"] || 0);
isManager = !![aDictionary objectForKey:@"isManager"];
}
return self;
}
// Property-list safe representation used for CPUserDefaults persistence.
- (CPDictionary)dictionaryRepresentation
{
return [CPDictionary dictionaryWithObjectsAndKeys:
uid, @"uid",
firstName || @"", @"firstName",
lastName || @"", @"lastName",
email || @"", @"email",
department || @"", @"department",
city || @"", @"city",
birthDate ? [birthDate timeIntervalSince1970] : 0, @"birthDate",
Number(salary || 0), @"salary",
isManager ? 1 : 0, @"isManager"];
}
- (int)age
{
if (!birthDate)
return 0;
var now = new Date(),
years = now.getFullYear() - birthDate.getFullYear(),
months = now.getMonth() - birthDate.getMonth();
if (months < 0 || (months === 0 && now.getDate() < birthDate.getDate()))
years--;
return years;
}
- (CPString)fullName
{
return [[CPString stringWithFormat:@"%@ %@", firstName || @"", lastName || @""] stringByTrimmingWhitespace];
}
// One line of tab separated text; used for copy / paste and drag & drop.
- (CPString)tabSeparatedRepresentation
{
return [firstName || @"", lastName || @"", email || @"", department || @"", city || @"", String(salary || 0)].join("\t");
}
+ (CPString)tabSeparatedHeader
{
return ["First Name", "Last Name", "Email", "Department", "City", "Salary"].join("\t");
}
- (CPString)description
{
return [CPString stringWithFormat:@"<Person %@ %@>", uid, [self fullName]];
}
@end
@@ -0,0 +1,180 @@
/*
* PersonDetailViewController.j
* SmartFoldersDemo
*
* Detail pane below the people table. Every control is bound to
* arrayController.selection.<key>, which exercises CPControllerSelectionProxy,
* placeholder binding options (no selection / multiple values / null) and a
* read-only binding to a KVO-dependent derived key ("age").
*
* Controls covered: CPTextField (+formatter), CPComboBox, CPPopUpButton
* (selectedValue binding), CPDatePicker, CPCheckBox, CPBox.
*/
@import <AppKit/AppKit.j>
@import "Library.j"
// Layout grid. Every row is RowHeight tall; labels and controls are centred on
// the row's midline regardless of each control's intrinsic height.
var RowHeight = 30.0,
RowPitch = 36.0,
LabelWidth = 90.0,
LabelGap = 8.0,
FieldWidth = 230.0,
ColumnGap = 40.0,
TextFieldHeight = 29.0,
PopUpHeight = 25.0,
DatePickerHeight= 25.0;
PersonDetailPreferredHeight = 5 * RowPitch + 52.0;
@implementation PersonDetailViewController : CPViewController
{
CPArrayController _arrayController;
}
- (id)initWithArrayController:(CPArrayController)anArrayController
{
if (self = [super initWithCibName:nil bundle:nil])
_arrayController = anArrayController;
return self;
}
- (CPDictionary)_placeholderOptions
{
return [CPDictionary dictionaryWithObjectsAndKeys:
@"No Selection", CPNoSelectionPlaceholderBindingOption,
@"Multiple Values", CPMultipleValuesPlaceholderBindingOption,
@"", CPNullPlaceholderBindingOption];
}
- (void)loadView
{
var frame = CGRectMake(0, 0, 800, PersonDetailPreferredHeight),
view = [[CPView alloc] initWithFrame:frame];
[view setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[view setBackgroundColor:[CPColor colorWithWhite:0.96 alpha:1.0]];
var box = [[CPBox alloc] initWithFrame:CGRectInset(frame, 12, 8)];
[box setTitle:@"Selected Person"];
[box setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[view addSubview:box];
var content = [box contentView],
options = [self _placeholderOptions],
left = 12.0,
right = left + LabelWidth + LabelGap + FieldWidth + ColumnGap;
// ---- Left column
[[self _textFieldInRow:0 column:left label:@"First Name:" in:content]
bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.firstName" options:options];
[[self _textFieldInRow:1 column:left label:@"Last Name:" in:content]
bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.lastName" options:options];
[[self _textFieldInRow:2 column:left label:@"Email:" in:content]
bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.email" options:options];
[self _labelInRow:3 column:left title:@"Department:" in:content];
var department = [[CPPopUpButton alloc] initWithFrame:CGRectMakeZero() pullsDown:NO];
[department addItemsWithTitles:LibraryDepartments];
[self _place:department row:3 x:left + LabelWidth + LabelGap width:FieldWidth height:PopUpHeight in:content];
[department bind:CPSelectedValueBinding toObject:_arrayController withKeyPath:@"selection.department" options:nil];
var manager = [CPCheckBox checkBoxWithTitle:@"Manager"];
[manager setAllowsMixedState:YES];
[manager sizeToFit];
[self _place:manager row:4 x:left + LabelWidth + LabelGap width:CGRectGetWidth([manager frame]) height:CGRectGetHeight([manager frame]) in:content];
[manager bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.isManager" options:nil];
// ---- Right column
[self _labelInRow:0 column:right title:@"City:" in:content];
var city = [[CPComboBox alloc] initWithFrame:CGRectMakeZero()];
[city addItemsWithObjectValues:LibraryCities];
[city setEditable:YES];
[self _place:city row:0 x:right + LabelWidth + LabelGap width:FieldWidth height:TextFieldHeight in:content];
[city bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.city" options:options];
[self _labelInRow:1 column:right title:@"Birth Date:" in:content];
var birthDate = [[CPDatePicker alloc] initWithFrame:CGRectMake(0, 0, 140, DatePickerHeight)];
[birthDate setDatePickerStyle:CPTextFieldAndStepperDatePickerStyle];
[birthDate setDatePickerElements:CPYearMonthDayDatePickerElementFlag];
[self _place:birthDate row:1 x:right + LabelWidth + LabelGap width:140 height:CGRectGetHeight([birthDate frame]) in:content];
[birthDate bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.birthDate" options:nil];
[self _labelInRow:2 column:right title:@"Age:" in:content];
var age = [self _valueLabelInRow:2 x:right + LabelWidth + LabelGap in:content];
[age bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.age" options:options];
var salary = [self _textFieldInRow:3 column:right label:@"Salary:" in:content],
numberFormatter = [[CPNumberFormatter alloc] init];
[numberFormatter setNumberStyle:CPNumberFormatterDecimalStyle];
[salary setFormatter:numberFormatter];
[salary setAlignment:CPRightTextAlignment];
[salary setFrameSize:CGSizeMake(140, CGRectGetHeight([salary frame]))];
[salary bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.salary" options:options];
[self _labelInRow:4 column:right title:@"Record ID:" in:content];
var uid = [self _valueLabelInRow:4 x:right + LabelWidth + LabelGap in:content];
[uid setSelectable:YES];
[uid setTextColor:[CPColor grayColor]];
[uid bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.uid" options:options];
[self setView:view];
}
// ---------------------------------------------------------------- Layout helpers
- (float)_rowTop:(int)aRow
{
return 10.0 + aRow * RowPitch;
}
// Centres aView vertically on the row's midline.
- (void)_place:(CPView)aView row:(int)aRow x:(float)anX width:(float)aWidth height:(float)aHeight in:(CPView)aSuperview
{
var y = [self _rowTop:aRow] + Math.round((RowHeight - aHeight) / 2);
[aView setFrame:CGRectMake(anX, y, aWidth, aHeight)];
[aSuperview addSubview:aView];
}
- (CPTextField)_labelInRow:(int)aRow column:(float)anX title:(CPString)aTitle in:(CPView)aSuperview
{
var label = [CPTextField labelWithTitle:aTitle];
[label setAlignment:CPRightTextAlignment];
[label setVerticalAlignment:CPCenterVerticalTextAlignment];
[label setTextColor:[CPColor colorWithWhite:0.25 alpha:1.0]];
[self _place:label row:aRow x:anX width:LabelWidth height:RowHeight in:aSuperview];
return label;
}
- (CPTextField)_valueLabelInRow:(int)aRow x:(float)anX in:(CPView)aSuperview
{
var label = [CPTextField labelWithTitle:@""];
[label setVerticalAlignment:CPCenterVerticalTextAlignment];
[self _place:label row:aRow x:anX + 4 width:FieldWidth - 4 height:RowHeight in:aSuperview];
return label;
}
- (CPTextField)_textFieldInRow:(int)aRow column:(float)anX label:(CPString)aTitle in:(CPView)aSuperview
{
[self _labelInRow:aRow column:anX title:aTitle in:aSuperview];
var field = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
[field setEditable:YES];
[field setBezeled:YES];
[field setVerticalAlignment:CPCenterVerticalTextAlignment];
[self _place:field row:aRow x:anX + LabelWidth + LabelGap width:FieldWidth height:TextFieldHeight in:aSuperview];
return field;
}
@end
+40
View File
@@ -0,0 +1,40 @@
# SmartFoldersDemo
Non-document-based Cappuccino integration test, built entirely in code (no cib).
Copy `index.html`, `index-debug.html` and `Jakefile` from `Tests/Manual/SmartFoldersTest`
(they are toolchain specific and need no changes apart from the title). No `Resources/`
are required: all icons and badges are drawn with `CPBezierPath`.
## Structure
| File | Role |
|---|---|
| `main.j`, `Info.plist` | entry point, `CPApplicationDelegateClass` = `AppController` |
| `AppController.j` | creates model, main menu, main window controller |
| `MainWindowController.j` | bridge window, toolbar, sidebar split view, routed actions, menu validation |
| `SourceListViewController.j` | view-based `CPOutlineView` source list, badges, drop target, button bar |
| `PeopleViewController.j` | `CPArrayController` + bound `CPTableView`, drag source, pasteboard, detail split |
| `PersonDetailViewController.j` | controls bound to `selection.*` |
| `SmartFolderEditorController.j` | sheet with `CPPredicateEditor` built from row templates |
| `NameSheetController.j` | small reusable name sheet |
| `Library.j`, `Person.j` | model, KVO observation, `CPUserDefaults` persistence, sample data |
## Manual test checklist
1. Launch: sidebar shows LIBRARY / GROUPS / SMART FOLDERS with count badges; table shows 240 people.
2. Click each smart folder: table filters, status line shows "n of 240", badge equals n.
3. Double-click a smart folder: sheet opens with the stored predicate reconstructed in the editor
(incl. nested compound and NOT). Add/remove rows: sheet resizes, match count and format update live.
4. OK: folder renamed/updated, badge updated. Cancel: nothing changes.
5. "Müller (diacritics test)" must match Müller with predicate `muller` (`[cd]`).
6. Type in the search field: filter combines with the selected folder (AND).
7. Sort by every column header; reorder and resize columns; reload: column layout restored.
8. Select people, drag onto a group: badge increases. Drag onto the GROUPS header: new group created and selected.
9. Drag groups / smart folders to reorder them within their section; reload: order persisted.
10. Edit a cell (name, salary, Mgr checkbox) and the detail pane fields: other views and badges follow;
multi-select shows "Multiple Values"; changing Birth Date updates Age (dependent key).
11. Copy (⌘C) then Paste (⌘V): duplicated people appear selected. Delete key removes people
(from the group only, when a group is selected).
12. View menu / toolbar: hide and show sidebar and detail pane; drag the sidebar resize control in the button bar.
13. File ▸ Reset Library…: alert sheet, then sample data restored.
@@ -0,0 +1,390 @@
/*
* 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
@@ -0,0 +1,721 @@
/*
* SourceListViewController.j
* SmartFoldersDemo
*
* Sidebar. Exercises:
* - CPViewController with a programmatic -loadView
* - view-based CPOutlineView (outlineView:viewForTableColumn:item:,
* makeViewWithIdentifier:owner:, CPTableCellView subclass)
* - source list highlight style, expand/collapse, non-selectable headers
* - custom drawing (CPBezierPath) for icons and count badges
* - drag & drop: reordering groups / smart folders inside their section,
* dropping people (from the table) onto a group, or onto the GROUPS
* header to create a new group
* - CPButtonBar with plus / minus / action pop-up and a split view
* resize control
*/
@import <AppKit/AppKit.j>
@import "Library.j"
var SourceCellIdentifier = @"SourceCell",
SourceRowHeight = 26.0;
// Tags of the action pop-up items.
SourceActionNewGroup = 1;
SourceActionNewSmartFolder = 2;
SourceActionEdit = 3;
SourceActionRename = 4;
SourceActionDelete = 5;
// --------------------------------------------------------------------------------
// SourceIconView – small vector icon drawn with CPBezierPath
// --------------------------------------------------------------------------------
@implementation SourceIconView : CPView
{
CPString _iconKind;
BOOL _isHighlighted;
}
- (void)setKind:(CPString)aKind
{
if (_iconKind === aKind)
return;
_iconKind = aKind;
[self setNeedsDisplay:YES];
}
- (void)setSelected:(BOOL)aFlag
{
if (_isHighlighted === aFlag)
return;
_isHighlighted = aFlag;
[self setNeedsDisplay:YES];
}
- (void)drawRect:(CGRect)aRect
{
var bounds = [self bounds],
w = CGRectGetWidth(bounds),
h = CGRectGetHeight(bounds);
if (_iconKind === @"all")
{
var color = _isHighlighted ? [CPColor whiteColor] : [CPColor colorWithRed:0.33 green:0.45 blue:0.62 alpha:1.0],
head = [CPBezierPath bezierPath],
body = [CPBezierPath bezierPath];
[color set];
[head appendBezierPathWithOvalInRect:CGRectMake(w / 2 - 3.5, 1.5, 7, 7)];
[head fill];
[body appendBezierPathWithRoundedRect:CGRectMake(w / 2 - 6.5, 9.5, 13, 6) xRadius:3 yRadius:3];
[body fill];
return;
}
// Folder shape for groups and smart folders.
var fill = _isHighlighted ? [CPColor whiteColor] :
(_iconKind === @"smart" ? [CPColor colorWithRed:0.55 green:0.42 blue:0.78 alpha:1.0]
: [CPColor colorWithRed:0.38 green:0.62 blue:0.88 alpha:1.0]),
folder = [CPBezierPath bezierPath];
[folder moveToPoint:CGPointMake(1, 3)];
[folder lineToPoint:CGPointMake(6, 3)];
[folder lineToPoint:CGPointMake(8, 5)];
[folder lineToPoint:CGPointMake(w - 1, 5)];
[folder lineToPoint:CGPointMake(w - 1, h - 2)];
[folder lineToPoint:CGPointMake(1, h - 2)];
[folder closePath];
[fill set];
[folder fill];
if (_iconKind === @"smart")
{
// Gear-ish dot to distinguish smart folders.
var mark = [CPBezierPath bezierPath],
markColor = _isHighlighted ? [CPColor colorWithRed:0.55 green:0.42 blue:0.78 alpha:1.0] : [CPColor whiteColor];
[markColor set];
[mark appendBezierPathWithOvalInRect:CGRectMake(w / 2 - 2.5, 7.5, 5, 5)];
[mark fill];
}
}
@end
// --------------------------------------------------------------------------------
// SourceBadgeView – rounded count badge
// --------------------------------------------------------------------------------
var BadgeFont = nil,
BadgeHeight = 16.0,
BadgeMinWidth = 24.0,
BadgeHPadding = 7.0;
@implementation SourceBadgeView : CPView
{
CPTextField _badgeLabel;
int _badgeCount;
BOOL _isHighlighted;
}
+ (void)initialize
{
if (self !== [SourceBadgeView class])
return;
BadgeFont = [CPFont boldSystemFontOfSize:11.0];
}
- (id)initWithFrame:(CGRect)aFrame
{
if (self = [super initWithFrame:aFrame])
{
// A plain, inset-free label that is positioned by hand: the text field's
// own vertical centering includes theme insets and ends up a pixel or two low.
_badgeLabel = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
[_badgeLabel setFont:BadgeFont];
[_badgeLabel setAlignment:CPCenterTextAlignment];
[_badgeLabel setEditable:NO];
[_badgeLabel setSelectable:NO];
[_badgeLabel setBezeled:NO];
[_badgeLabel setBordered:NO];
[_badgeLabel setDrawsBackground:NO];
[_badgeLabel setValue:CGInsetMakeZero() forThemeAttribute:@"content-inset"];
[self addSubview:_badgeLabel];
[self _updateColors];
}
return self;
}
+ (CPString)stringForCount:(int)aCount
{
// Keep badges compact: 1234 -> "1.2k", 12345 -> "12k"
if (aCount >= 10000)
return Math.round(aCount / 1000) + "k";
if (aCount >= 1000)
return (Math.round(aCount / 100) / 10) + "k";
return String(aCount);
}
- (void)setCount:(int)aCount
{
_badgeCount = aCount;
var hidden = aCount === CPNotFound || aCount === nil || aCount === undefined;
[self setHidden:hidden];
if (!hidden)
{
[_badgeLabel setStringValue:[SourceBadgeView stringForCount:aCount]];
[self _layoutLabel];
}
[self setNeedsDisplay:YES];
}
- (void)setSelected:(BOOL)aFlag
{
if (_isHighlighted === aFlag)
return;
_isHighlighted = aFlag;
[self _updateColors];
[self setNeedsDisplay:YES];
}
- (float)preferredWidth
{
var size = [[_badgeLabel stringValue] sizeWithFont:BadgeFont];
return MAX(BadgeMinWidth, Math.ceil(size.width) + 2 * BadgeHPadding);
}
- (void)setFrameSize:(CGSize)aSize
{
[super setFrameSize:aSize];
[self _layoutLabel];
}
- (void)_layoutLabel
{
var bounds = [self bounds],
textHeight = Math.ceil([[_badgeLabel stringValue] sizeWithFont:BadgeFont].height);
[_badgeLabel setFrame:CGRectMake(0, Math.round((CGRectGetHeight(bounds) - textHeight) / 2), CGRectGetWidth(bounds), textHeight)];
}
- (void)_updateColors
{
[_badgeLabel setTextColor:_isHighlighted ? [CPColor colorWithRed:0.29 green:0.54 blue:0.82 alpha:1.0] : [CPColor whiteColor]];
}
- (void)drawRect:(CGRect)aRect
{
var bounds = [self bounds],
radius = CGRectGetHeight(bounds) / 2,
path = [CPBezierPath bezierPath];
[path appendBezierPathWithRoundedRect:bounds xRadius:radius yRadius:radius];
[(_isHighlighted ? [CPColor whiteColor] : [CPColor colorWithRed:0.56 green:0.63 blue:0.76 alpha:1.0]) set];
[path fill];
}
@end
// --------------------------------------------------------------------------------
// SourceListCellView – view-based outline cell
// --------------------------------------------------------------------------------
@implementation SourceListCellView : CPTableCellView
{
SourceIconView _iconView;
CPTextField _titleField;
SourceBadgeView _badgeView;
BOOL _isHeader;
}
- (id)initWithFrame:(CGRect)aFrame
{
if (self = [super initWithFrame:aFrame])
{
var height = CGRectGetHeight(aFrame);
_iconView = [[SourceIconView alloc] initWithFrame:CGRectMake(2, (height - 16) / 2, 18, 16)];
[_iconView setAutoresizingMask:CPViewMinYMargin | CPViewMaxYMargin];
[self addSubview:_iconView];
_titleField = [[CPTextField alloc] initWithFrame:CGRectMake(24, 0, CGRectGetWidth(aFrame) - 24, height)];
[_titleField setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[_titleField setVerticalAlignment:CPCenterVerticalTextAlignment];
[_titleField setLineBreakMode:CPLineBreakByTruncatingTail];
[_titleField setEditable:NO];
[_titleField setBezeled:NO];
[_titleField setDrawsBackground:NO];
[self addSubview:_titleField];
_badgeView = [[SourceBadgeView alloc] initWithFrame:CGRectMake(CGRectGetWidth(aFrame) - 30, (height - BadgeHeight) / 2, BadgeMinWidth, BadgeHeight)];
[_badgeView setAutoresizingMask:CPViewMinXMargin | CPViewMinYMargin | CPViewMaxYMargin];
[self addSubview:_badgeView];
[self setIdentifier:SourceCellIdentifier];
}
return self;
}
- (void)configureWithItem:(SourceItem)anItem count:(int)aCount
{
_isHeader = [anItem isSection];
var bounds = [self bounds],
width = CGRectGetWidth(bounds),
height = CGRectGetHeight(bounds);
[_titleField setStringValue:[anItem name]];
if (_isHeader)
{
[_iconView setHidden:YES];
[_badgeView setHidden:YES];
[_titleField setFont:[CPFont boldSystemFontOfSize:11.0]];
[_titleField setFrame:CGRectMake(0, 0, width, height)];
}
else
{
var kind = [anItem isKindOfClass:[SmartFolder class]] ? @"smart" :
[anItem isKindOfClass:[LibraryGroup class]] ? @"group" : @"all";
[_iconView setHidden:NO];
[_iconView setKind:kind];
[_titleField setFont:[CPFont systemFontOfSize:12.0]];
[_badgeView setCount:aCount];
var badgeWidth = [_badgeView isHidden] ? 0 : [_badgeView preferredWidth];
[_badgeView setFrame:CGRectMake(width - badgeWidth - 8, Math.round((height - BadgeHeight) / 2), badgeWidth, BadgeHeight)];
[_titleField setFrame:CGRectMake(24, 0, MAX(0, width - 24 - badgeWidth - 14), height)];
}
[self _updateSelectionAppearance];
}
- (void)setThemeState:(ThemeState)aState
{
[super setThemeState:aState];
[self _updateSelectionAppearance];
}
- (void)unsetThemeState:(ThemeState)aState
{
[super unsetThemeState:aState];
[self _updateSelectionAppearance];
}
- (void)_updateSelectionAppearance
{
// CPTableView marks data views of selected rows with CPThemeStateSelectedDataView
// and, only while the table is focused (first responder in the key window),
// additionally with CPThemeStateFirstResponder. The unfocused selection colour
// is a light gray, so white text is only legible in the focused case.
var selected = [self hasThemeState:CPThemeStateSelectedDataView],
emphasized = selected && [self hasThemeState:CPThemeStateFirstResponder];
[_iconView setSelected:emphasized];
[_badgeView setSelected:emphasized];
if (_isHeader)
[_titleField setTextColor:[CPColor colorWithWhite:0.45 alpha:1.0]];
else
[_titleField setTextColor:emphasized ? [CPColor whiteColor] : [CPColor colorWithWhite:0.15 alpha:1.0]];
}
@end
// --------------------------------------------------------------------------------
// SourceListViewController
// --------------------------------------------------------------------------------
@implementation SourceListViewController : CPViewController
{
Library _library;
CPOutlineView _outlineView;
CPButtonBar _buttonBar;
BOOL _suppressSelectionCallbacks;
id _sourceListDelegate @accessors(property=delegate);
}
- (id)initWithLibrary:(Library)aLibrary
{
if (self = [super initWithCibName:nil bundle:nil])
_library = aLibrary;
return self;
}
- (CPButtonBar)buttonBar
{
[self view];
return _buttonBar;
}
- (void)loadView
{
var frame = CGRectMake(0, 0, 230, 500),
view = [[CPView alloc] initWithFrame:frame],
barHeight = 26.0;
[view setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
// --- Outline in scroll view
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame) - barHeight)];
[scrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[scrollView setAutohidesScrollers:YES];
[scrollView setHasHorizontalScroller:NO];
[scrollView setBorderType:CPNoBorder];
_outlineView = [[CPOutlineView alloc] initWithFrame:[[scrollView contentView] bounds]];
var column = [[CPTableColumn alloc] initWithIdentifier:@"source"];
[column setWidth:CGRectGetWidth(frame) - 20];
[column setResizingMask:CPTableColumnAutoresizingMask];
[column setEditable:NO];
[_outlineView addTableColumn:column];
[_outlineView setOutlineTableColumn:column];
[_outlineView setHeaderView:nil];
[_outlineView setCornerView:nil];
[_outlineView setColumnAutoresizingStyle:CPTableViewLastColumnOnlyAutoresizingStyle];
[_outlineView setSelectionHighlightStyle:CPTableViewSelectionHighlightStyleSourceList];
[_outlineView setBackgroundColor:[CPColor colorWithRed:212.0 / 255.0 green:221.0 / 255.0 blue:230.0 / 255.0 alpha:1.0]];
[_outlineView setRowHeight:SourceRowHeight];
[_outlineView setAllowsMultipleSelection:NO];
[_outlineView setAllowsEmptySelection:YES];
[_outlineView setTarget:self];
[_outlineView setDoubleAction:@selector(outlineViewDoubleClicked:)];
[_outlineView registerForDraggedTypes:[PeopleDragType, SourceItemDragType]];
[_outlineView setDraggingSourceOperationMask:CPDragOperationMove forLocal:YES];
[_outlineView setDataSource:self];
[_outlineView setDelegate:self];
[scrollView setDocumentView:_outlineView];
[view addSubview:scrollView];
// --- Button bar
_buttonBar = [[CPButtonBar alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(frame) - barHeight, CGRectGetWidth(frame), barHeight)];
[_buttonBar setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin];
if ([_buttonBar respondsToSelector:@selector(setHasResizeControl:)])
[_buttonBar setHasResizeControl:YES];
if ([_buttonBar respondsToSelector:@selector(setResizeControlIsLeftAligned:)])
[_buttonBar setResizeControlIsLeftAligned:NO];
var plusButton = [CPButtonBar plusButton];
[plusButton setTarget:self];
[plusButton setAction:@selector(_plusClicked:)];
[plusButton setToolTip:@"New smart folder"];
var minusButton = [CPButtonBar minusButton];
[minusButton setTarget:self];
[minusButton setAction:@selector(_minusClicked:)];
[minusButton setToolTip:@"Delete the selected group or smart folder"];
var actionButton = [CPButtonBar actionPopupButton];
if (![[actionButton itemArray] count])
[actionButton addItemWithTitle:@""];
[self _addActionItem:@"New Group…" tag:SourceActionNewGroup to:actionButton];
[self _addActionItem:@"New Smart Folder…" tag:SourceActionNewSmartFolder to:actionButton];
[[actionButton menu] addItem:[CPMenuItem separatorItem]];
[self _addActionItem:@"Edit Smart Folder…" tag:SourceActionEdit to:actionButton];
[self _addActionItem:@"Rename Group…" tag:SourceActionRename to:actionButton];
[self _addActionItem:@"Delete" tag:SourceActionDelete to:actionButton];
[actionButton setTarget:self];
[actionButton setAction:@selector(_actionPopupChosen:)];
[_buttonBar setButtons:[plusButton, minusButton, actionButton]];
[view addSubview:_buttonBar];
[self setView:view];
[_outlineView reloadData];
[self _expandSections];
}
- (void)_addActionItem:(CPString)aTitle tag:(int)aTag to:(CPPopUpButton)aPopUp
{
[aPopUp addItemWithTitle:aTitle];
[[aPopUp lastItem] setTag:aTag];
}
- (void)_expandSections
{
var sections = [_library sections];
for (var i = 0; i < sections.length; i++)
[_outlineView expandItem:sections[i]];
}
// ---------------------------------------------------------------- Public
- (id)selectedItem
{
var row = [_outlineView selectedRow];
return row >= 0 ? [_outlineView itemAtRow:row] : nil;
}
- (void)selectItem:(id)anItem
{
var row = [_outlineView rowForItem:anItem];
if (row === CPNotFound || row < 0)
return;
[_outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
[_outlineView scrollRowToVisible:row];
}
// Reloads the outline (names, order, badges) without emitting selection callbacks.
- (void)reloadAndSelectItem:(id)anItem
{
_suppressSelectionCallbacks = YES;
[_outlineView reloadData];
[self _expandSections];
var row = anItem ? [_outlineView rowForItem:anItem] : CPNotFound;
if (row !== CPNotFound && row >= 0)
[_outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
else
[_outlineView deselectAll:nil];
_suppressSelectionCallbacks = NO;
}
- (void)focus
{
[[_outlineView window] makeFirstResponder:_outlineView];
}
// ---------------------------------------------------------------- Actions
- (void)_plusClicked:(id)sender
{
[CPApp sendAction:@selector(newSmartFolder:) to:nil from:sender];
}
- (void)_minusClicked:(id)sender
{
[CPApp sendAction:@selector(deleteSelectedSourceItem:) to:nil from:sender];
}
- (void)_actionPopupChosen:(id)sender
{
var selector = nil;
switch ([[sender selectedItem] tag])
{
case SourceActionNewGroup: selector = @selector(newGroup:); break;
case SourceActionNewSmartFolder: selector = @selector(newSmartFolder:); break;
case SourceActionEdit: selector = @selector(editSelectedSmartFolder:); break;
case SourceActionRename: selector = @selector(renameSelectedGroup:); break;
case SourceActionDelete: selector = @selector(deleteSelectedSourceItem:); break;
}
if (selector)
[CPApp sendAction:selector to:nil from:sender];
}
- (void)outlineViewDoubleClicked:(id)sender
{
var row = [_outlineView clickedRow];
if (row < 0)
return;
var item = [_outlineView itemAtRow:row];
if ([item isKindOfClass:[SmartFolder class]])
[CPApp sendAction:@selector(editSelectedSmartFolder:) to:nil from:self];
else if ([item isKindOfClass:[LibraryGroup class]])
[CPApp sendAction:@selector(renameSelectedGroup:) to:nil from:self];
}
// ---------------------------------------------------------------- Data source
- (int)outlineView:(CPOutlineView)anOutlineView numberOfChildrenOfItem:(id)anItem
{
return [[_library childrenOfItem:anItem] count];
}
- (id)outlineView:(CPOutlineView)anOutlineView child:(int)anIndex ofItem:(id)anItem
{
return [[_library childrenOfItem:anItem] objectAtIndex:anIndex];
}
- (BOOL)outlineView:(CPOutlineView)anOutlineView isItemExpandable:(id)anItem
{
return [anItem isSection];
}
- (id)outlineView:(CPOutlineView)anOutlineView objectValueForTableColumn:(CPTableColumn)aColumn byItem:(id)anItem
{
return anItem;
}
// ---------------------------------------------------------------- Delegate (view based)
- (CPView)outlineView:(CPOutlineView)anOutlineView viewForTableColumn:(CPTableColumn)aColumn item:(id)anItem
{
var cell = [anOutlineView makeViewWithIdentifier:SourceCellIdentifier owner:self];
if (!cell)
cell = [[SourceListCellView alloc] initWithFrame:CGRectMake(0, 0, [aColumn width], SourceRowHeight)];
[cell configureWithItem:anItem count:[_library countForItem:anItem]];
return cell;
}
- (BOOL)outlineView:(CPOutlineView)anOutlineView shouldSelectItem:(id)anItem
{
return [anItem isSelectable];
}
- (void)outlineViewSelectionDidChange:(CPNotification)aNotification
{
if (_suppressSelectionCallbacks)
return;
var item = [self selectedItem];
if (item && [_sourceListDelegate respondsToSelector:@selector(sourceListViewController:didSelectItem:)])
[_sourceListDelegate sourceListViewController:self didSelectItem:item];
}
// ---------------------------------------------------------------- Drag & drop
- (BOOL)outlineView:(CPOutlineView)anOutlineView writeItems:(CPArray)theItems toPasteboard:(CPPasteboard)aPasteboard
{
var item = [theItems firstObject];
if ([theItems count] !== 1 || ![item isUserItem])
return NO;
[aPasteboard declareTypes:[SourceItemDragType] owner:self];
[aPasteboard setString:[item uid] forType:SourceItemDragType];
return YES;
}
- (CPDragOperation)outlineView:(CPOutlineView)anOutlineView validateDrop:(id)theInfo proposedItem:(id)anItem proposedChildIndex:(int)anIndex
{
var pasteboard = [theInfo draggingPasteboard],
types = [pasteboard types];
// People from the table
if ([types containsObject:PeopleDragType])
{
if ([anItem isKindOfClass:[LibraryGroup class]] && anIndex === CPOutlineViewDropOnItemIndex)
return CPDragOperationCopy;
// Anywhere on/inside the GROUPS section: create a new group.
if (anItem === [_library groupsSection])
{
[anOutlineView setDropItem:anItem dropChildIndex:CPOutlineViewDropOnItemIndex];
return CPDragOperationCopy;
}
return CPDragOperationNone;
}
// Reordering of groups / smart folders
if ([types containsObject:SourceItemDragType])
{
var dragged = [_library itemWithUID:[pasteboard stringForType:SourceItemDragType]],
section = [_library sectionForItem:dragged];
if (!dragged || !section)
return CPDragOperationNone;
if (anItem === section && anIndex !== CPOutlineViewDropOnItemIndex)
return CPDragOperationMove;
// Dropped onto a sibling: retarget to "insert above sibling".
if (anIndex === CPOutlineViewDropOnItemIndex && [_library sectionForItem:anItem] === section)
{
[anOutlineView setDropItem:section dropChildIndex:[[section children] indexOfObjectIdenticalTo:anItem]];
return CPDragOperationMove;
}
}
return CPDragOperationNone;
}
- (BOOL)outlineView:(CPOutlineView)anOutlineView acceptDrop:(id)theInfo item:(id)anItem childIndex:(int)anIndex
{
var pasteboard = [theInfo draggingPasteboard],
types = [pasteboard types];
if ([types containsObject:PeopleDragType])
{
var uids = [CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:PeopleDragType]],
people = [_library peopleWithUIDs:uids || []];
if (![people count])
return NO;
if ([anItem isKindOfClass:[LibraryGroup class]])
{
[_library addPeople:people toGroup:anItem];
return YES;
}
if (anItem === [_library groupsSection])
{
var name = [people count] === 1 ? [people[0] fullName] : [CPString stringWithFormat:@"%d People", [people count]],
group = [_library addGroupWithName:name people:people];
[self selectItem:group];
return YES;
}
return NO;
}
if ([types containsObject:SourceItemDragType])
{
var dragged = [_library itemWithUID:[pasteboard stringForType:SourceItemDragType]];
if (!dragged)
return NO;
[_library moveItem:dragged toIndex:anIndex];
return YES;
}
return NO;
}
@end
+167
View File
@@ -0,0 +1,167 @@
<!DOCTYPE html>
<html lang="en">
<!--
index.html
__project.name__
Created by __user.name__ on __project.date__.
Copyright __project.year__, __organization.name__ All rights reserved.
-->
<head>
<meta charset="utf-8">
<!--[if lte IE 8]>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
<![endif]-->
<!--[if gte IE 9]>
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
<![endif]-->
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="Resources/icon.png">
<link rel="apple-touch-startup-image" href="Resources/default.png">
<title>__project.name__</title>
<!-- Custom javascript goes here -->
<!-- End custom javascript -->
<script type="text/javascript">
OBJJ_INCLUDE_PATHS = ["../../Frameworks"];
OBJJ_MAIN_FILE = "main.j";
// The below will tell the compiler to not generate debug symbols but will generate type signatures and inline objj_msgSend functions.
// This will affect only Objective-J code that is compiled when loading the application. It will not affect precompiled
// code like the Cappuccino frameworks.
// Uncomment or comment on the line below to change the flags
OBJJ_COMPILER_FLAGS = [/*"IncludeDebugSymbols"*/, "IncludeTypeSignatures"/*, "SourceMap"*/, "InlineMsgSend"];
var progressBar = null;
OBJJ_PROGRESS_CALLBACK = function(percent, appSize, path)
{
percent = percent * 100;
if (!progressBar)
progressBar = document.getElementById("progress-bar");
if (progressBar)
progressBar.style.width = Math.min(percent, 100) + "%";
}
var loadingHTML =
'<div id="loading">' +
' <div id="loading-text">Loading...</div>' +
' <div id="progress-indicator">' +
' <span id="progress-bar" style="width:0%"></span>' +
' </div>' +
'</div>';
</script>
<script type="text/javascript" src="../../Frameworks/Objective-J/Objective-J.js" charset="UTF-8"></script>
<style type="text/css">
html, body, h1, p {
margin: 0;
padding: 0;
}
/* We need a body wrapper because Cappuccino is unhappy if we change the body element */
#cappuccino-body {
/* Position it absolutely so it will fill the height without content */
position: absolute;
top: 0;
bottom: 0;
width: 100%;
/* Put it at the bottom of the stack so it doesn't interfere with UI */
z-index: 0;
}
#cappuccino-body .container {
display: table;
margin: 0 auto;
height: 100%;
}
#cappuccino-body .content {
display: table-cell;
height: 100%;
vertical-align: top;
}
#loading {
position: relative;
top: 35%;
}
#loading-text {
height: 1.5em;
color: #555;
font: normal bold 36px/36px Arial, sans-serif;
}
#progress-indicator {
padding: 0px;
height: 16px;
border: 5px solid #555;
border-radius: 18px;
background-color: white;
}
#progress-bar {
position: relative;
top: -1px;
left: -1px;
display: block;
height: 18px;
/* Compensate for moving the bar left 1px to overlap the indicator border */
border-right: 1px solid #555;
background-color: #555;
}
#noscript {
position: relative;
top: 35%;
padding: 1em 1.5em;
border: 5px solid #555;
border-radius: 16px;
background-color: white;
color: #555;
text-align: center;
font: bold 24px Arial, sans-serif;
}
#noscript a {
color: #98c0ff;
text-decoration: none;
}
</style>
</head>
<body>
<div id="cappuccino-body">
<div class="container">
<div class="content">
<script type="text/javascript">
document.write(loadingHTML);
</script>
</div>
</div>
<noscript style="position:absolute; top:0; left:0; width:100%; height:100%">
<div class="container">
<div class="content">
<div id="noscript">
<p style="font-size:120%; margin-bottom:.75em">JavaScript is required for this application.</p>
<p><a href="http://www.enable-javascript.com" target="_blank">Enable JavaScript</a></p>
</div>
</div>
</div>
</noscript>
</div>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
/*
* AppController.j
* null
*
* Created by You on November 25, 2010.
* Copyright 2010, Your Company All rights reserved.
*/
@import <Foundation/Foundation.j>
@import <AppKit/AppKit.j>
@import "AppController.j"
function main(args, namedArgs)
{
CPApplicationMain(args, namedArgs);
}
+149 -148
View File
@@ -1,148 +1,149 @@
1791907547966|ArrayControllerInitialSelectionTest|
1791907547979|ArrayControllerRemovingFirstTest|
1791907547991|ArrayControllerTest|
1791907548004|AttachedSheet2Test|
1791907548016|AttachedSheetTest|
1791907548029|BindingContentArrayTest|
1791907548041|CGCanvasContextTest|
1791907548054|CGPathTest|
1791907548067|CPAlertTest|
1791907548079|CPAnimatablePropertyContainerTest|
1791907548091|CPAnimationContextTest|
1791907548104|CPAnimationTest|
1791907548117|CPBinderEchoBug|
1791907548129|CPBoxCibTest|
1791907548141|CPBoxTest|
1791907548154|CPBrowserTest|
1791907548167|CPButtonBarTest|
1791907548179|CPButtonKeyEquivalentDefaultTest|
1791907548192|CPButtonTest|
1791907548204|CPByteCountFormatterTest|
1791907548217|CPCollectionViewNibTest|
1791907548229|CPColorCPImageDescriptionTest|
1791907548242|CPColorWellBindingsTest|
1791907548254|CPComboBoxTest|
1791907548267|CPControlPerformClickExceptionTest|
1791907548280|CPControlSizeTest|
1791907548292|CPCursorTest|
1791907548305|CPDateFormatterTest|
1791907548318|CPDatePickerTest|
1791907548331|CPDictionaryControllerTest|
1791907548343|CPDocumentControllerTest|
1791907548356|CPFormatterTest|
1791907548368|CPGraphicsTest|
1791907548381|CPImageAndTextViewImageScalingTest|
1791907548393|CPImageViewAlignmentScaling|
1791907548406|CPImageViewTest|
1791907548419|CPImageViewbindingsTest|
1791907548431|CPLanguageModelChatbotTest|
1791907548444|CPLevelIndicator|
1791907548456|CPLevelIndicatorTest|
1791907548469|CPLocalizationTest|
1791907548481|CPLogTest|
1791907548494|CPMarkdownParserTest|
1791907548506|CPMenuCALayerTest|
1791907548519|CPMenuEnabledBindingsTest|
1791907548531|CPMenuKeyEquivalentsTest|
1791907548543|CPMenuRemoveAllTest|
1791907548556|CPMenuSubmenuPlacementConstraints|
1791907548568|CPMenuTest|
1791907548581|CPOutlineViewCibTest|
1791907548593|CPOutlineViewTest|
1791907548606|CPOutlineViewViewBasedCibTest|
1791907548619|CPPanelTest|
1791907548631|CPPlatformWindow|
1791907548644|CPPlattformWindowMinSize|
1791907548656|CPPopUpButtonBindingsTest|
1791907548668|CPPopUpButtonTest|
1791907548681|CPPopoverTest|
1791907548693|CPPredicateEditorCibTest|
1791907548706|CPProgressIndicatorTest|
1791907548718|CPPropertyListSerializationTest|
1791907548731|CPRadioBindingsTest|
1791907548743|CPRuleEditorCibTest|
1791907548755|CPRuleEditorTest|
1791907548768|CPScrollView2Test|
1791907548780|CPScrollViewTest|
1791907548793|CPSearchFieldPredicateBinding|
1791907548805|CPSearchFieldTest|
1791907548818|CPSegmentedControlBindingsTest|
1791907548830|CPSegmentedControlTest|
1791907548843|CPSoundTest|
1791907548855|CPSplitViewDividerTest|
1791907548868|CPSplitViewTest|
1791907548880|CPStackViewTest|
1791907548893|CPStepperBindingsTest|
1791907548905|CPStepperNibTest|
1791907548918|CPTabView2Test|
1791907548930|CPTabViewBindingsTest|
1791907548943|CPTabViewNibTest|
1791907548955|CPTableViewGroupRowsTest|
1791907548967|CPTextFieldEditingStyleTest|
1791907548980|CPTextFieldHeightTest|
1791907548992|CPTextFieldMovementsTest|
1791907549005|CPTextFieldMultiLineTest|
1791907549017|CPTextFieldTest|
1791907549029|CPTextViewCibTest|
1791907549042|CPTextViewDelegateAndNotificationsTest|
1791907549054|CPTimeZoneTest|
1791907549067|CPTokenFieldTest|
1791907549079|CPToolTipTest|
1791907549092|CPToolbarTest|
1791907549104|CPTreeControllerTest|
1791907549117|CPURLConnectionAsyncTest|
1791907549129|CPURLConnectionSynchronousTest|
1791907549142|CPUserDefaultsControllerTest|
1791907549154|CPUserDefaultsTest|
1791907549166|CPUserNotificationTest|
1791907549179|CPViewControllerTest|
1791907549191|CPVisualEffectViewTest|
1791907549204|CPWebViewTest|
1791907549216|CPWebViewTest2|
1791907549229|CPWindowLiveResizeTest|
1791907549241|CPWindowMovableTest|
1791907549254|CPWindowResizeStyleTest|
1791907549266|ChildWindowsTest|
1791907549279|CompilationTests|
1791907549291|ContinuouslyUpdatesValueBinding|
1791907549304|CopyAndPasteTest|
1791907549316|CrossOriginTest|
1791907549329|FF4096Test|
1791907549341|FontEnhancementTest|
1791907549354|FontMetricsExplorerTest|
1791907549366|KeyEquivalentsTest|
1791907549379|KeyViewLoopTest|
1791907549391|KeyWindowInTheBackgroundActivation|
1791907549404|LayoutTest|
1791907549417|LoadDynamicFrameworksTest|
1791907549429|LoadTimeTest|
1791907549442|LongBindingsTest|
1791907549454|MobileTest|
1791907549467|MultipleValueBindingsTest|
1791907549479|NSBoxTest|
1791907549492|NSBrowserTest|
1791907549504|NewTextFieldBezelTest|
1791907549517|Nib2CibAlignmentTest|
1791907549529|Nib2CibBaselineAlignementTest|
1791907549542|Nib2CibSubviewsOrderTest|
1791907549555|NibAppRemixedTest|
1791907549567|PatternFillTest|
1791907549580|PlatformWindowsTest|
1791907549592|PopUpButtonEscTest|
1791907549604|PredicateWithFormatTest|
1791907549617|ResignFirstResponderWithCPControlTextDidEndEditingTest|
1791907549630|Scaling2Test|
1791907549642|ScalingTest|
1791907549654|ScrollviewThemingTest|
1791907549667|SimpleBindings2Test|
1791907549679|SizeToFitTest|
1791907549692|SmartFoldersTest|
1791907549704|TableTest|
1791907549716|ThemeBrowserTest|
1791907549729|ThemeKitchenSinkTest|
1791907549742|TrackingAreaTest|
1791907549754|UndoRedoWithMenuUpdateTest|
1791907549766|UserlandNSTest|
1791907549779|WindowSheetOrderTest|
1791907549791|layoutEphemeralSubviewTest|
1791907549804|performKeyEquivalentTest|
1792528807445|ArrayControllerInitialSelectionTest|
1792528807460|ArrayControllerRemovingFirstTest|
1792528807474|ArrayControllerTest|
1792528807488|AttachedSheet2Test|
1792528807501|AttachedSheetTest|
1792528807515|BindingContentArrayTest|
1792528807528|CGCanvasContextTest|
1792528807542|CGPathTest|
1792528807556|CPAlertTest|
1792528807569|CPAnimatablePropertyContainerTest|
1792528807583|CPAnimationContextTest|
1792528807596|CPAnimationTest|
1792528807610|CPBinderEchoBug|
1792528807624|CPBoxCibTest|
1792528807637|CPBoxTest|
1792528807651|CPBrowserTest|
1792528807664|CPButtonBarTest|
1792528807678|CPButtonKeyEquivalentDefaultTest|
1792528807691|CPButtonTest|
1792528807705|CPByteCountFormatterTest|
1792528807718|CPCollectionViewNibTest|
1792528807732|CPColorCPImageDescriptionTest|
1792528807745|CPColorWellBindingsTest|
1792528807759|CPComboBoxTest|
1792528807772|CPControlPerformClickExceptionTest|
1792528807786|CPControlSizeTest|
1792528807800|CPCursorTest|
1792528807813|CPDateFormatterTest|
1792528807827|CPDatePickerTest|
1792528807841|CPDictionaryControllerTest|
1792528807854|CPDocumentControllerTest|
1792528807868|CPFormatterTest|
1792528807882|CPGraphicsTest|
1792528807895|CPImageAndTextViewImageScalingTest|
1792528807909|CPImageViewAlignmentScaling|
1792528807922|CPImageViewTest|
1792528807936|CPImageViewbindingsTest|
1792528807949|CPLanguageModelChatbotTest|
1792528807963|CPLevelIndicator|
1792528807976|CPLevelIndicatorTest|
1792528807990|CPLocalizationTest|
1792528808004|CPLogTest|
1792528808017|CPMarkdownParserTest|
1792528808031|CPMenuCALayerTest|
1792528808044|CPMenuEnabledBindingsTest|
1792528808058|CPMenuKeyEquivalentsTest|
1792528808071|CPMenuRemoveAllTest|
1792528808085|CPMenuSubmenuPlacementConstraints|
1792528808099|CPMenuTest|
1792528808112|CPOutlineViewCibTest|
1792528808126|CPOutlineViewTest|
1792528808140|CPOutlineViewViewBasedCibTest|
1792528808153|CPPanelTest|
1792528808167|CPPlatformWindow|
1792528808180|CPPlattformWindowMinSize|
1792528808194|CPPopUpButtonBindingsTest|
1792528808207|CPPopUpButtonTest|
1792528808221|CPPopoverTest|
1792528808234|CPPredicateEditorCibTest|
1792528808248|CPProgressIndicatorTest|
1792528808261|CPPropertyListSerializationTest|
1792528808275|CPRadioBindingsTest|
1792528808288|CPRuleEditorCibTest|
1792528808302|CPRuleEditorTest|
1792528808316|CPScrollView2Test|
1792528808329|CPScrollViewTest|
1792528808343|CPSearchFieldPredicateBinding|
1792528808357|CPSearchFieldTest|
1792528808370|CPSegmentedControlBindingsTest|
1792528808384|CPSegmentedControlTest|
1792528808397|CPSoundTest|
1792528808411|CPSplitViewDividerTest|
1792528808425|CPSplitViewTest|
1792528808438|CPStackViewTest|
1792528808452|CPStepperBindingsTest|
1792528808465|CPStepperNibTest|
1792528808479|CPTabView2Test|
1792528808493|CPTabViewBindingsTest|
1792528808506|CPTabViewNibTest|
1792528808520|CPTableViewGroupRowsTest|
1792528808533|CPTextFieldEditingStyleTest|
1792528808547|CPTextFieldHeightTest|
1792528808561|CPTextFieldMovementsTest|
1792528808574|CPTextFieldMultiLineTest|
1792528808588|CPTextFieldTest|
1792528808601|CPTextViewCibTest|
1792528808615|CPTextViewDelegateAndNotificationsTest|
1792528808629|CPTimeZoneTest|
1792528808642|CPTokenFieldTest|
1792528808656|CPToolTipTest|
1792528808670|CPToolbarTest|
1792528808683|CPTreeControllerTest|
1792528808697|CPURLConnectionAsyncTest|
1792528808714|CPURLConnectionSynchronousTest|
1792528808727|CPUserDefaultsControllerTest|
1792528808741|CPUserDefaultsTest|
1792528808755|CPUserNotificationTest|
1792528808768|CPViewControllerTest|
1792528808782|CPVisualEffectViewTest|
1792528808795|CPWebViewTest|
1792528808809|CPWebViewTest2|
1792528808823|CPWindowLiveResizeTest|
1792528808836|CPWindowMovableTest|
1792528808850|CPWindowResizeStyleTest|
1792528808863|ChildWindowsTest|
1792528808877|CompilationTests|
1792528808890|ContinuouslyUpdatesValueBinding|
1792528808904|CopyAndPasteTest|
1792528808917|CrossOriginTest|
1792528808931|FF4096Test|
1792528808945|FontEnhancementTest|
1792528808958|FontMetricsExplorerTest|
1792528808972|KeyEquivalentsTest|
1792528808985|KeyViewLoopTest|
1792528808999|KeyWindowInTheBackgroundActivation|
1792528809013|LayoutTest|
1792528809026|LoadDynamicFrameworksTest|
1792528809040|LoadTimeTest|
1792528809054|LongBindingsTest|
1792528809067|MobileTest|
1792528809081|MultipleValueBindingsTest|
1792528809095|NSBoxTest|
1792528809108|NSBrowserTest|
1792528809122|NewTextFieldBezelTest|
1792528809135|Nib2CibAlignmentTest|
1792528809149|Nib2CibBaselineAlignementTest|
1792528809162|Nib2CibSubviewsOrderTest|
1792528809176|NibAppRemixedTest|
1792528809190|PatternFillTest|
1792528809203|PlatformWindowsTest|
1792528809217|PopUpButtonEscTest|
1792528809231|PredicateWithFormatTest|
1792528809244|ResignFirstResponderWithCPControlTextDidEndEditingTest|
1792528809258|Scaling2Test|
1792528809271|ScalingTest|
1792528809285|ScrollviewThemingTest|
1792528809299|SimpleBindings2Test|
1792528809312|SizeToFitTest|
1792528809326|SmartFoldersDemo|
1792528809339|SmartFoldersTest|
1792528809353|TableTest|
1792528809366|ThemeBrowserTest|
1792528809380|ThemeKitchenSinkTest|
1792528809394|TrackingAreaTest|
1792528809407|UndoRedoWithMenuUpdateTest|
1792528809421|UserlandNSTest|
1792528809434|WindowSheetOrderTest|
1792528809448|layoutEphemeralSubviewTest|
1792528809461|performKeyEquivalentTest|