From 98ba46872d79e7f31b7164d6d52975a278d5f8d4 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 18 Oct 2026 14:32:32 +0200 Subject: [PATCH 1/9] new: SmartFoldersDemo --- Tests/Manual/SmartFoldersDemo/AppController.j | 109 +++ Tests/Manual/SmartFoldersDemo/Info.plist | 14 + Tests/Manual/SmartFoldersDemo/Jakefile | 94 +++ Tests/Manual/SmartFoldersDemo/Library.j | 787 ++++++++++++++++++ .../SmartFoldersDemo/MainWindowController.j | 581 +++++++++++++ .../SmartFoldersDemo/NameSheetController.j | 102 +++ .../SmartFoldersDemo/PeopleViewController.j | 461 ++++++++++ Tests/Manual/SmartFoldersDemo/Person.j | 141 ++++ .../PersonDetailViewController.j | 180 ++++ Tests/Manual/SmartFoldersDemo/README.md | 40 + .../SmartFolderEditorController.j | 390 +++++++++ .../SmartFoldersDemo/SmartFoldersDemo.zip | Bin 0 -> 34436 bytes .../SourceListViewController.j | 716 ++++++++++++++++ Tests/Manual/SmartFoldersDemo/index.html | 166 ++++ Tests/Manual/SmartFoldersDemo/main.j | 18 + 15 files changed, 3799 insertions(+) create mode 100644 Tests/Manual/SmartFoldersDemo/AppController.j create mode 100644 Tests/Manual/SmartFoldersDemo/Info.plist create mode 100644 Tests/Manual/SmartFoldersDemo/Jakefile create mode 100644 Tests/Manual/SmartFoldersDemo/Library.j create mode 100644 Tests/Manual/SmartFoldersDemo/MainWindowController.j create mode 100644 Tests/Manual/SmartFoldersDemo/NameSheetController.j create mode 100644 Tests/Manual/SmartFoldersDemo/PeopleViewController.j create mode 100644 Tests/Manual/SmartFoldersDemo/Person.j create mode 100644 Tests/Manual/SmartFoldersDemo/PersonDetailViewController.j create mode 100644 Tests/Manual/SmartFoldersDemo/README.md create mode 100644 Tests/Manual/SmartFoldersDemo/SmartFolderEditorController.j create mode 100644 Tests/Manual/SmartFoldersDemo/SmartFoldersDemo.zip create mode 100644 Tests/Manual/SmartFoldersDemo/SourceListViewController.j create mode 100644 Tests/Manual/SmartFoldersDemo/index.html create mode 100644 Tests/Manual/SmartFoldersDemo/main.j diff --git a/Tests/Manual/SmartFoldersDemo/AppController.j b/Tests/Manual/SmartFoldersDemo/AppController.j new file mode 100644 index 000000000..c1d8f9209 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/AppController.j @@ -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 +@import + +@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 diff --git a/Tests/Manual/SmartFoldersDemo/Info.plist b/Tests/Manual/SmartFoldersDemo/Info.plist new file mode 100644 index 000000000..3b70f4e68 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/Info.plist @@ -0,0 +1,14 @@ + + + + + CPDefaultTheme + Aristo3 + CPApplicationDelegateClass + AppController + CPBundleName + SmartFolderDemo + CPPrincipalClass + CPApplication + + diff --git a/Tests/Manual/SmartFoldersDemo/Jakefile b/Tests/Manual/SmartFoldersDemo/Jakefile new file mode 100644 index 000000000..80b5b8cf0 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/Jakefile @@ -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("----------------------------"); +} diff --git a/Tests/Manual/SmartFoldersDemo/Library.j b/Tests/Manual/SmartFoldersDemo/Library.j new file mode 100644 index 000000000..b7ec8c682 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/Library.j @@ -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 +@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 diff --git a/Tests/Manual/SmartFoldersDemo/MainWindowController.j b/Tests/Manual/SmartFoldersDemo/MainWindowController.j new file mode 100644 index 000000000..05b524771 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/MainWindowController.j @@ -0,0 +1,581 @@ +/* + * 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 +@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; + +@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. + if (typeof CPToolbarDisplayModeIconOnly !== "undefined") + [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"]; + + [alert beginSheetModalForWindow:[self window] + modalDelegate:self + didEndSelector:@selector(resetAlertDidEnd:returnCode:contextInfo:) + contextInfo:nil]; +} + +- (void)resetAlertDidEnd:(CPAlert)anAlert returnCode:(int)aReturnCode contextInfo:(id)aContext +{ + // First button: 0 in classic Cappuccino, CPAlertFirstButtonReturn where defined. + var firstButton = (typeof CPAlertFirstButtonReturn !== "undefined") ? CPAlertFirstButtonReturn : 0; + + if (aReturnCode !== firstButton && aReturnCode !== 0) + 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 diff --git a/Tests/Manual/SmartFoldersDemo/NameSheetController.j b/Tests/Manual/SmartFoldersDemo/NameSheetController.j new file mode 100644 index 000000000..fafc614d7 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/NameSheetController.j @@ -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 + +@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 diff --git a/Tests/Manual/SmartFoldersDemo/PeopleViewController.j b/Tests/Manual/SmartFoldersDemo/PeopleViewController.j new file mode 100644 index 000000000..efd3794cd --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/PeopleViewController.j @@ -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 +@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 diff --git a/Tests/Manual/SmartFoldersDemo/Person.j b/Tests/Manual/SmartFoldersDemo/Person.j new file mode 100644 index 000000000..4f15f25cc --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/Person.j @@ -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 so that KVO dependent-key support is + * exercised as well. + */ + +@import + +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:@"", uid, [self fullName]]; +} + +@end diff --git a/Tests/Manual/SmartFoldersDemo/PersonDetailViewController.j b/Tests/Manual/SmartFoldersDemo/PersonDetailViewController.j new file mode 100644 index 000000000..28de9481e --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/PersonDetailViewController.j @@ -0,0 +1,180 @@ +/* + * PersonDetailViewController.j + * SmartFoldersDemo + * + * Detail pane below the people table. Every control is bound to + * arrayController.selection., 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 +@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 diff --git a/Tests/Manual/SmartFoldersDemo/README.md b/Tests/Manual/SmartFoldersDemo/README.md new file mode 100644 index 000000000..101cca2de --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/README.md @@ -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. diff --git a/Tests/Manual/SmartFoldersDemo/SmartFolderEditorController.j b/Tests/Manual/SmartFoldersDemo/SmartFolderEditorController.j new file mode 100644 index 000000000..cd69152f9 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/SmartFolderEditorController.j @@ -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 +@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 diff --git a/Tests/Manual/SmartFoldersDemo/SmartFoldersDemo.zip b/Tests/Manual/SmartFoldersDemo/SmartFoldersDemo.zip new file mode 100644 index 0000000000000000000000000000000000000000..979399d92c8b63ed2a3e128fc8a40d95399b23e6 GIT binary patch literal 34436 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 F{{tDLzSsZ& literal 0 HcmV?d00001 diff --git a/Tests/Manual/SmartFoldersDemo/SourceListViewController.j b/Tests/Manual/SmartFoldersDemo/SourceListViewController.j new file mode 100644 index 000000000..5f92b7752 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/SourceListViewController.j @@ -0,0 +1,716 @@ +/* + * 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 +@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 +{ + var selected = [self hasThemeState:CPThemeStateSelectedDataView]; + + [_iconView setSelected:selected]; + [_badgeView setSelected:selected]; + + if (_isHeader) + [_titleField setTextColor:[CPColor colorWithWhite:0.45 alpha:1.0]]; + else + [_titleField setTextColor:selected ? [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 diff --git a/Tests/Manual/SmartFoldersDemo/index.html b/Tests/Manual/SmartFoldersDemo/index.html new file mode 100644 index 000000000..ac42c98a7 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/index.html @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + __project.name__ + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/SmartFoldersDemo/main.j b/Tests/Manual/SmartFoldersDemo/main.j new file mode 100644 index 000000000..c446f82f7 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/main.j @@ -0,0 +1,18 @@ +/* + * AppController.j + * null + * + * Created by You on November 25, 2010. + * Copyright 2010, Your Company All rights reserved. + */ + +@import +@import + +@import "AppController.j" + + +function main(args, namedArgs) +{ + CPApplicationMain(args, namedArgs); +} From 34b3367468e262ab563c200b10b74344b8d662df Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 18 Oct 2026 14:54:44 +0200 Subject: [PATCH 2/9] textcolorfix --- .../SmartFoldersDemo/SourceListViewController.j | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Tests/Manual/SmartFoldersDemo/SourceListViewController.j b/Tests/Manual/SmartFoldersDemo/SourceListViewController.j index 5f92b7752..49f2b0353 100644 --- a/Tests/Manual/SmartFoldersDemo/SourceListViewController.j +++ b/Tests/Manual/SmartFoldersDemo/SourceListViewController.j @@ -327,15 +327,20 @@ var BadgeFont = nil, - (void)_updateSelectionAppearance { - var selected = [self hasThemeState:CPThemeStateSelectedDataView]; + // 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:selected]; - [_badgeView setSelected:selected]; + [_iconView setSelected:emphasized]; + [_badgeView setSelected:emphasized]; if (_isHeader) [_titleField setTextColor:[CPColor colorWithWhite:0.45 alpha:1.0]]; else - [_titleField setTextColor:selected ? [CPColor whiteColor] : [CPColor colorWithWhite:0.15 alpha:1.0]]; + [_titleField setTextColor:emphasized ? [CPColor whiteColor] : [CPColor colorWithWhite:0.15 alpha:1.0]]; } @end From 1ae67679717e0d0a823bfb0185044d09ba84af2e Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 18 Oct 2026 18:12:55 +0200 Subject: [PATCH 3/9] aristo2 fix --- AppKit/CPSplitView.j | 8 +++++--- Tests/Manual/SmartFoldersDemo/Info.plist | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/AppKit/CPSplitView.j b/AppKit/CPSplitView.j index 67edc2864..1a7aea7a4 100644 --- a/AppKit/CPSplitView.j +++ b/AppKit/CPSplitView.j @@ -1285,8 +1285,11 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle - (void)setButtonBar:(CPButtonBar)aButtonBar forDividerAtIndex:(CPUInteger)dividerIndex { // For compatibility with previous behavior - [aButtonBar setAutomaticResizeControl:YES]; - [self attachButtonBar:aButtonBar]; + if ([aButtonBar respondsToSelector:@selector(setAutomaticResizeControl:)]) + { + [aButtonBar setAutomaticResizeControl:YES]; + [self attachButtonBar:aButtonBar]; + } } - (void)attachButtonBar:(CPButtonBar)aButtonBar @@ -1897,4 +1900,3 @@ var CPSplitViewDelegateKey = @"CPSplitViewDelegateKey", } @end - diff --git a/Tests/Manual/SmartFoldersDemo/Info.plist b/Tests/Manual/SmartFoldersDemo/Info.plist index 3b70f4e68..22941d016 100644 --- a/Tests/Manual/SmartFoldersDemo/Info.plist +++ b/Tests/Manual/SmartFoldersDemo/Info.plist @@ -3,7 +3,7 @@ CPDefaultTheme - Aristo3 + Aristo2 CPApplicationDelegateClass AppController CPBundleName From 7b2528f7c2940f4a90cc090f42586534d914b09b Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 19 Oct 2026 22:22:33 +0200 Subject: [PATCH 4/9] removed: accidentally added zip file --- .../SmartFoldersDemo/SmartFoldersDemo.zip | Bin 34436 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Tests/Manual/SmartFoldersDemo/SmartFoldersDemo.zip diff --git a/Tests/Manual/SmartFoldersDemo/SmartFoldersDemo.zip b/Tests/Manual/SmartFoldersDemo/SmartFoldersDemo.zip deleted file mode 100644 index 979399d92c8b63ed2a3e128fc8a40d95399b23e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34436 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 F{{tDLzSsZ& From f10eb2e0a59a1822dd0373784cdbbda91d5891be Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 20 Oct 2026 18:20:15 +0200 Subject: [PATCH 5/9] fixed: CPAlertFirstButtonReturn.. --- .../SmartFoldersDemo/MainWindowController.j | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Tests/Manual/SmartFoldersDemo/MainWindowController.j b/Tests/Manual/SmartFoldersDemo/MainWindowController.j index 05b524771..575784a74 100644 --- a/Tests/Manual/SmartFoldersDemo/MainWindowController.j +++ b/Tests/Manual/SmartFoldersDemo/MainWindowController.j @@ -30,7 +30,9 @@ var ToolbarSidebarIdentifier = @"SidebarItem", ToolbarSearchIdentifier = @"SearchItem", SidebarDefaultWidth = 230.0, SidebarMinWidth = 160.0, - SidebarMaxWidth = 420.0; + SidebarMaxWidth = 420.0, + ResetAlertResetTag = 1001, + ResetAlertCancelTag = 1002; @implementation MainWindowController : CPWindowController { @@ -122,8 +124,7 @@ var ToolbarSidebarIdentifier = @"SidebarItem", [toolbar setDelegate:self]; // Buttons carry their own titles, so labels below them would be redundant. - if (typeof CPToolbarDisplayModeIconOnly !== "undefined") - [toolbar setDisplayMode:CPToolbarDisplayModeIconOnly]; + [toolbar setDisplayMode:CPToolbarDisplayModeIconOnly]; [toolbar setVisible:YES]; [[self window] setToolbar:toolbar]; @@ -512,6 +513,12 @@ var ToolbarSidebarIdentifier = @"SidebarItem", [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:) @@ -520,10 +527,7 @@ var ToolbarSidebarIdentifier = @"SidebarItem", - (void)resetAlertDidEnd:(CPAlert)anAlert returnCode:(int)aReturnCode contextInfo:(id)aContext { - // First button: 0 in classic Cappuccino, CPAlertFirstButtonReturn where defined. - var firstButton = (typeof CPAlertFirstButtonReturn !== "undefined") ? CPAlertFirstButtonReturn : 0; - - if (aReturnCode !== firstButton && aReturnCode !== 0) + if (aReturnCode !== ResetAlertResetTag) return; [_searchField setStringValue:@""]; From cc5807e5685e0ee40f4394f41b6a1ede6fa0774a Mon Sep 17 00:00:00 2001 From: David Richardson Date: Tue, 20 Oct 2026 15:29:24 -0600 Subject: [PATCH 6/9] SmartFoldersDemo: Add target classification keys for capp-build Defines explicit target type identifiers in Info.plist required by the static compiler. These keys have no effect on the legacy Node build pipeline. --- Tests/Manual/SmartFoldersDemo/Info.plist | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/Manual/SmartFoldersDemo/Info.plist b/Tests/Manual/SmartFoldersDemo/Info.plist index 22941d016..fba1328e4 100644 --- a/Tests/Manual/SmartFoldersDemo/Info.plist +++ b/Tests/Manual/SmartFoldersDemo/Info.plist @@ -2,6 +2,10 @@ + CPBundlePackageType + APPL + CPBundleIdentifier + com.yourorganization.SmartFoldersDemo CPDefaultTheme Aristo2 CPApplicationDelegateClass From 850c348cca3c9a491598267bd0ef2e7d1429c153 Mon Sep 17 00:00:00 2001 From: David Richardson Date: Tue, 20 Oct 2026 16:19:39 -0600 Subject: [PATCH 7/9] Tests: Add SmartFoldersDemo index-debug.html and provision test frameworks - Add missing index-debug.html entry point to SmartFoldersDemo. - Update Tests/Manual/Jakefile configure and clobber tasks to link Release and Debug framework variants (Objective-J, Foundation, AppKit, BlendKit) across all manual tests. - Safeguard symlink replacement logic to correctly handle existing links while protecting physical asset directories. --- Tests/Manual/Jakefile | 132 ++++++++++- .../Manual/SmartFoldersDemo/index-debug.html | 213 ++++++++++++++++++ 2 files changed, 337 insertions(+), 8 deletions(-) create mode 100644 Tests/Manual/SmartFoldersDemo/index-debug.html diff --git a/Tests/Manual/Jakefile b/Tests/Manual/Jakefile index bc143690c..ceab277af 100644 --- a/Tests/Manual/Jakefile +++ b/Tests/Manual/Jakefile @@ -23,8 +23,13 @@ var ENV = process.env, task = JAKE.task, cp = require("child_process"), + fs = require("fs"), path = require("path"); +// The frameworks an application's Frameworks directory links to, in the layout +// capp gen creates: release builds directly inside, debug builds in Debug/. +var ConfiguredFrameworks = ["Objective-J", "Foundation", "AppKit", "BlendKit"]; + /* * Standard build targets are neutered at this directory level to prevent automated * CI runners from triggering exhaustive, recursive compilations across all manual test suites. @@ -47,15 +52,49 @@ task ("release", ["default"]); */ /* - * Traverses every manual test subdirectory containing a Jakefile and symlinks its - * internal 'Frameworks' directory directly to the concrete Build/Frameworks - * directory generated at the project root. + * Gives every manual test application the Frameworks layout capp gen creates: + * Frameworks/ links to the release build in Build/Release, and + * Frameworks/Debug links to the debug build in Build/Debug, so index.html loads + * release frameworks and index-debug.html loads debug frameworks. An existing + * Frameworks link, or a Frameworks directory holding only links, is replaced; + * a Frameworks directory holding real files is left untouched. */ task ("configure", function() { - var manualDir = __dirname; - var targetPath = path.resolve(manualDir, "../../Build/Frameworks"); - cp.execSync("find . -mindepth 2 -iname 'jakefile' -execdir sh -c 'echo \"Configuring $(basename \\\"$PWD\\\")\"; ln -sfn \"$1\" Frameworks' _ '" + targetPath + "' \\;", { stdio: "inherit", cwd: manualDir }); + var manualDir = __dirname, + releasePath = path.resolve(manualDir, "../../Build/Release"), + debugPath = path.resolve(manualDir, "../../Build/Debug"); + + [releasePath, debugPath].forEach(function(buildPath) + { + ConfiguredFrameworks.forEach(function(framework) + { + if (!fs.existsSync(path.join(buildPath, framework))) + throw new Error(path.join(buildPath, framework) + " not found; build the frameworks in both configurations first"); + }); + }); + + manualApplicationDirectories(manualDir).forEach(function(appDir) + { + var name = path.basename(appDir), + frameworksPath = path.join(appDir, "Frameworks"); + + if (!removeConfiguredFrameworks(appDir)) + { + console.log("Skipping " + name + ": Frameworks contains files that are not links"); + return; + } + + console.log("Configuring " + name); + fs.mkdirSync(frameworksPath); + + ConfiguredFrameworks.forEach(function(framework) + { + fs.symlinkSync(path.join(releasePath, framework), path.join(frameworksPath, framework)); + }); + + fs.symlinkSync(debugPath, path.join(frameworksPath, "Debug")); + }); }); /* @@ -69,14 +108,91 @@ task ("clean", function() }); /* - * Sweeps Frameworks symlinks across subsidiary test applications after delegating Build directory removal to clean. + * Removes the Frameworks layout configure created from every manual test + * application, after delegating Build directory removal to clean. A Frameworks + * directory holding real files is left untouched. */ task ("clobber", ["clean"], function() { var manualDir = __dirname; - cp.execSync("find . -mindepth 2 -maxdepth 2 -type l -name 'Frameworks' -exec rm -f {} +", { stdio: "inherit", cwd: manualDir }); + + manualApplicationDirectories(manualDir).forEach(function(appDir) + { + if (!removeConfiguredFrameworks(appDir)) + console.log("Skipping " + path.basename(appDir) + ": Frameworks contains files that are not links"); + }); }); +/* + * Returns the absolute paths of the manual test applications: every directory + * below this one that contains a Jakefile. + */ +function manualApplicationDirectories(manualDir) +{ + var jakefiles = cp.execSync("find . -mindepth 2 -iname 'jakefile'", { cwd: manualDir, encoding: "utf8" }); + + return jakefiles.split("\n").filter(Boolean).map(function(jakefile) + { + return path.resolve(manualDir, path.dirname(jakefile)); + }); +} + +/* + * Removes an application's Frameworks link, or its Frameworks directory when + * that holds only links and directories, and returns true; returns false and + * leaves it in place when it holds any real file. Links are removed, never + * followed. + */ +function removeConfiguredFrameworks(appDir) +{ + var frameworksPath = path.join(appDir, "Frameworks"), + stat; + + try + { + stat = fs.lstatSync(frameworksPath); + } + catch (anError) + { + return true; + } + + if (stat.isSymbolicLink()) + { + fs.unlinkSync(frameworksPath); + return true; + } + + if (stat.isDirectory() && containsOnlyLinks(frameworksPath)) + { + fs.rmSync(frameworksPath, { recursive: true }); + return true; + } + + return false; +} + +/* + * Returns true when aPath is a directory tree holding only symbolic links and + * directories. + */ +function containsOnlyLinks(aPath) +{ + return fs.readdirSync(aPath).every(function(name) + { + var entry = path.join(aPath, name), + stat = fs.lstatSync(entry); + + if (stat.isSymbolicLink()) + return true; + + if (stat.isDirectory()) + return containsOnlyLinks(entry); + + return false; + }); +} + /* * ----------------------------------------------------------------------------- * Manual Execution Targets diff --git a/Tests/Manual/SmartFoldersDemo/index-debug.html b/Tests/Manual/SmartFoldersDemo/index-debug.html new file mode 100644 index 000000000..c5df711dc --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/index-debug.html @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + SmartFoldersDemoUpdated + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + From 53692b1b71c83bb8168c5b23153cef146ed1ba2e Mon Sep 17 00:00:00 2001 From: David Richardson Date: Tue, 20 Oct 2026 17:13:38 -0600 Subject: [PATCH 8/9] Foundation: Declare variables used before declaration and remove dead IE code References to undeclared variables created implicit globals, and an Internet Explorer code path referenced the now unsupported ActiveXObject. * CPDecimal: Declare adderror in CPDecimalAdd and error1 in _SimpleDivide and _SimpleMultiply. * CPFunctionOperation: Declare functionOp in +functionOperationWithFunction:. * CPRunLoop: Declare CPMainRunLoop at file level instead of creating it as an implicit global in +initialize. * CPWebDAVManager: Remove the ActiveXObject path from XMLDocumentFromString. --- Foundation/CPDecimal.j | 7 +++++-- Foundation/CPFunctionOperation.j | 2 +- Foundation/CPRunLoop.j | 3 ++- Foundation/CPWebDAVManager.j | 10 ---------- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Foundation/CPDecimal.j b/Foundation/CPDecimal.j index 8dba1b75d..54bb53353 100644 --- a/Foundation/CPDecimal.j +++ b/Foundation/CPDecimal.j @@ -577,7 +577,8 @@ function CPDecimalAdd(result, leftOperand, rightOperand, roundingMode, longMode) // below is equiv. of simple compare var comp = 0, ll = n1._mantissa.length, - lr = n2._mantissa.length; + lr = n2._mantissa.length, + adderror; if (ll == lr) comp = CPOrderedSame; @@ -790,6 +791,7 @@ function _SimpleDivide(result, leftOperand, rightOperand, roundingMode) k = 0, firsttime = YES, stopk = CPDecimalMaxDigits + 1, + error1, used = 0; // How many digits of l have been used? _CPDecimalSetZero(result); @@ -957,7 +959,8 @@ function _SimpleMultiply(result, leftOperand, rightOperand, roundingMode, powerM var error = CPCalculationNoError, carry = 0, exp = 0, - n = CPDecimalMakeZero(); + n = CPDecimalMakeZero(), + error1; _CPDecimalSetZero(result); diff --git a/Foundation/CPFunctionOperation.j b/Foundation/CPFunctionOperation.j index 34784374f..8232e78aa 100644 --- a/Foundation/CPFunctionOperation.j +++ b/Foundation/CPFunctionOperation.j @@ -79,7 +79,7 @@ */ + (id)functionOperationWithFunction:(JSObject)jsFunction { - functionOp = [[CPFunctionOperation alloc] init]; + var functionOp = [[CPFunctionOperation alloc] init]; [functionOp addExecutionFunction:jsFunction]; return functionOp; diff --git a/Foundation/CPRunLoop.j b/Foundation/CPRunLoop.j index 6867a4d8d..edcc1403e 100644 --- a/Foundation/CPRunLoop.j +++ b/Foundation/CPRunLoop.j @@ -177,7 +177,8 @@ var _CPRunLoopPerformPool = [], @end -var CPRunLoopLastNativeRunLoop = 0; +var CPRunLoopLastNativeRunLoop = 0, + CPMainRunLoop = nil; /*! @class CPRunLoop diff --git a/Foundation/CPWebDAVManager.j b/Foundation/CPWebDAVManager.j index b761d4eea..3a30fadfe 100644 --- a/Foundation/CPWebDAVManager.j +++ b/Foundation/CPWebDAVManager.j @@ -179,16 +179,6 @@ WebDAVPropertiesForURLKeys[CPURLIsDirectoryKey] = @"resourcetype"; var XMLDocumentFromString = function(anXMLString) { - if (typeof window["ActiveXObject"] !== "undefined") - { - var XMLDocument = new ActiveXObject("Microsoft.XMLDOM"); - - XMLDocument.async = false; - XMLDocument.loadXML(anXMLString); - - return XMLDocument; - } - return new DOMParser().parseFromString(anXMLString,"text/xml"); }; From a49a5021df1527e5c41e3aa66fe8b05975ac1a78 Mon Sep 17 00:00:00 2001 From: David Richardson Date: Tue, 20 Oct 2026 17:15:04 -0600 Subject: [PATCH 9/9] AppKit: Fix references to undefined identifiers Several functions and methods referenced identifiers that were never declared or defined. * CGColor: Compare color spaces by identity in CGColorEqualToColor, replacing a call to the undefined CGColorSpaceEqualToColorSpace whose test was inverted. * CGContextCanvas: Declare abs_cos and abs_sin in scale_rotate and rotate_scale. * CGContextText: Call CGPointMakeZero in CGContextGetTextPosition instead of the removed _CGPointMakeZero. * CPColor: Compare pattern orientation with CPColorPatternIsVertical instead of BlendKit's PatternIsVertical. * CPMenu: Declare _CPMenuBarImage with the file's other menu bar variables. * CPScrollView: Test horizontalScrollerClass and verticalScrollerClass in +frameSizeForContentSize:horizontalScrollerClass:verticalScrollerClass:borderType:controlSize:scrollerStyle: instead of the undefined hFlag and vFlag. --- AppKit/CPColor.j | 2 +- AppKit/CPMenu/CPMenu.j | 3 ++- AppKit/CPScrollView.j | 4 ++-- AppKit/CoreGraphics/CGColor.j | 2 +- AppKit/CoreGraphics/CGContextCanvas.j | 8 ++++++-- AppKit/CoreGraphics/CGContextText.j | 2 +- 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/AppKit/CPColor.j b/AppKit/CPColor.j index 1d1199350..505a6f036 100644 --- a/AppKit/CPColor.j +++ b/AppKit/CPColor.j @@ -1494,7 +1494,7 @@ var patternColorsFromPattern = function(pattern, attributes, imageFactory) else { numParts = 3; - isVertical = orientation === PatternIsVertical; + isVertical = orientation === CPColorPatternIsVertical; if (isVertical) { diff --git a/AppKit/CPMenu/CPMenu.j b/AppKit/CPMenu/CPMenu.j index 90436c30a..3e793b898 100644 --- a/AppKit/CPMenu/CPMenu.j +++ b/AppKit/CPMenu/CPMenu.j @@ -51,7 +51,8 @@ CPMenuDidEndTrackingNotification = @"CPMenuDidEndTrackingNotification"; var _CPMenuBarVisible = NO, _CPMenuBarTitle = @"", _CPMenuBarAttributes = nil, - _CPMenuBarSharedWindow = nil; + _CPMenuBarSharedWindow = nil, + _CPMenuBarImage = nil; /*! @ingroup appkit diff --git a/AppKit/CPScrollView.j b/AppKit/CPScrollView.j index 73533032a..45f98d5f0 100644 --- a/AppKit/CPScrollView.j +++ b/AppKit/CPScrollView.j @@ -236,10 +236,10 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay, heightInset = contentSize.height - bounds.size.height, frameSize = CGSizeMake(contentSize.width + widthInset, contentSize.height + heightInset); - if (hFlag) + if (horizontalScrollerClass) frameSize.height += [horizontalScrollerClass scrollerWidthInStyle:scrollerStyle]; - if (vFlag) + if (verticalScrollerClass) frameSize.width += [verticalScrollerClass scrollerWidthForControlSize:scrollerStyle]; return frameSize; diff --git a/AppKit/CoreGraphics/CGColor.j b/AppKit/CoreGraphics/CGColor.j index 371c6861d..0c305285f 100644 --- a/AppKit/CoreGraphics/CGColor.j +++ b/AppKit/CoreGraphics/CGColor.j @@ -212,7 +212,7 @@ function CGColorEqualToColor(lhs, rhs) if (lhs.pattern != rhs.pattern) return false; - if (CGColorSpaceEqualToColorSpace(lhs.colorspace, rhs.colorspace)) + if (lhs.colorspace !== rhs.colorspace) return false; return true; diff --git a/AppKit/CoreGraphics/CGContextCanvas.j b/AppKit/CoreGraphics/CGContextCanvas.j index d8d13e1f7..b2e01306a 100644 --- a/AppKit/CoreGraphics/CGContextCanvas.j +++ b/AppKit/CoreGraphics/CGContextCanvas.j @@ -447,7 +447,9 @@ var scale_rotate = function(a, b, c, d) var sign = (a * d < 0.0 || b * c > 0.0) ? -1.0 : 1.0, a2 = (ATAN2(b, d) + ATAN2(-sign * c, sign * a)) / 2.0, cos = COS(a2), - sin = SIN(a2); + sin = SIN(a2), + abs_cos, + abs_sin; if (cos === 0) { @@ -474,7 +476,9 @@ var rotate_scale = function(a, b, c, d) var sign = (a * d < 0.0 || b * c > 0.0) ? -1.0 : 1.0, a1 = (ATAN2(sign * b, sign * a) + ATAN2(-c, d)) / 2.0, cos = COS(a1), - sin = SIN(a1); + sin = SIN(a1), + abs_cos, + abs_sin; if (cos === 0) { diff --git a/AppKit/CoreGraphics/CGContextText.j b/AppKit/CoreGraphics/CGContextText.j index 92cf30c9d..1aa5b8508 100644 --- a/AppKit/CoreGraphics/CGContextText.j +++ b/AppKit/CoreGraphics/CGContextText.j @@ -37,7 +37,7 @@ function CGContextSetTextMatrix(/* CGContext */ aContext, /* CGAffineTransform * function CGContextGetTextPosition(/* CGContext */ aContext) { - return aContext._textPosition || _CGPointMakeZero(); + return aContext._textPosition || CGPointMakeZero(); } function CGContextSetTextPosition(/* CGContext */ aContext, /* float */ x, /* float */ y)