mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-20 23:35:01 +00:00
Deploying to gh-pages from @ cappuccino/cappuccino@1585123040 🚀
This commit is contained in:
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)
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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("----------------------------");
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
@@ -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|
|
||||
Reference in new issue
Block a user