/* * 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, ResetAlertResetTag = 1001, ResetAlertCancelTag = 1002; @implementation MainWindowController : CPWindowController { Library _library; CPSplitView _splitView; SourceListViewController _sourceListController; PeopleViewController _peopleController; SmartFolderEditorController _folderEditor; NameSheetController _nameSheet; CPSearchField _searchField; CPPredicate _searchPredicate; id _selectedItem; float _lastSidebarWidth; } - (id)initWithLibrary:(Library)aLibrary { var window = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask]; if (self = [super initWithWindow:window]) { _library = aLibrary; _lastSidebarWidth = SidebarDefaultWidth; [window setDelegate:self]; [window setTitle:@"Smart Folders Demo"]; _sourceListController = [[SourceListViewController alloc] initWithLibrary:_library]; [_sourceListController setDelegate:self]; _peopleController = [[PeopleViewController alloc] initWithLibrary:_library]; [self _buildToolbar]; [self _buildSplitView]; [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(libraryDidChange:) name:LibraryDidChangeNotification object:_library]; [self _selectSourceItem:[_library allPeopleItem]]; } return self; } // ---------------------------------------------------------------- Interface - (void)_buildSplitView { var contentView = [[self window] contentView], bounds = [contentView bounds]; _splitView = [[CPSplitView alloc] initWithFrame:bounds]; [_splitView setVertical:YES]; [_splitView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; [_splitView setDelegate:self]; var sidebar = [_sourceListController view], people = [_peopleController view]; [sidebar setFrame:CGRectMake(0, 0, SidebarDefaultWidth, CGRectGetHeight(bounds))]; [people setFrame:CGRectMake(SidebarDefaultWidth + 1, 0, MAX(0, CGRectGetWidth(bounds) - SidebarDefaultWidth - 1), CGRectGetHeight(bounds))]; [_splitView addSubview:sidebar]; [_splitView addSubview:people]; [contentView addSubview:_splitView]; if ([_splitView respondsToSelector:@selector(setButtonBar:forDividerAtIndex:)]) [_splitView setButtonBar:[_sourceListController buttonBar] forDividerAtIndex:0]; if ([_splitView respondsToSelector:@selector(setAutosaveName:)]) [_splitView setAutosaveName:@"SmartFoldersDemo.MainSplit"]; } - (void)_buildToolbar { _searchField = [[CPSearchField alloc] initWithFrame:CGRectMake(0, 0, 220, 30)]; [_searchField setPlaceholderString:@"Search people"]; [_searchField setTarget:self]; [_searchField setAction:@selector(searchFieldDidChange:)]; [_searchField setSendsSearchStringImmediately:YES]; [_searchField setRecentsAutosaveName:@"SmartFoldersDemo.RecentSearches"]; [_searchField setSearchMenuTemplate:[_searchField defaultSearchMenuTemplate]]; var toolbar = [[CPToolbar alloc] initWithIdentifier:@"SmartFoldersDemoToolbar"]; [toolbar setDelegate:self]; // Buttons carry their own titles, so labels below them would be redundant. [toolbar setDisplayMode:CPToolbarDisplayModeIconOnly]; [toolbar setVisible:YES]; [[self window] setToolbar:toolbar]; } - (CPArray)toolbarAllowedItemIdentifiers:(CPToolbar)aToolbar { return [ToolbarSidebarIdentifier, ToolbarNewFolderIdentifier, ToolbarNewGroupIdentifier, ToolbarEditIdentifier, ToolbarDetailIdentifier, ToolbarSearchIdentifier, CPToolbarFlexibleSpaceItemIdentifier, CPToolbarSpaceItemIdentifier, CPToolbarSeparatorItemIdentifier]; } - (CPArray)toolbarDefaultItemIdentifiers:(CPToolbar)aToolbar { return [ToolbarSidebarIdentifier, CPToolbarSpaceItemIdentifier, ToolbarNewFolderIdentifier, ToolbarNewGroupIdentifier, ToolbarEditIdentifier, CPToolbarFlexibleSpaceItemIdentifier, ToolbarDetailIdentifier, ToolbarSearchIdentifier]; } - (CPToolbarItem)toolbar:(CPToolbar)aToolbar itemForItemIdentifier:(CPString)anIdentifier willBeInsertedIntoToolbar:(BOOL)aFlag { var item = [[CPToolbarItem alloc] initWithItemIdentifier:anIdentifier]; switch (anIdentifier) { case ToolbarSidebarIdentifier: [self _configureItem:item label:@"Sidebar" buttonTitle:@"Sidebar" action:@selector(toggleSidebar:) toolTip:@"Show or hide the sidebar"]; break; case ToolbarNewFolderIdentifier: [self _configureItem:item label:@"New Smart Folder" buttonTitle:@"New Smart Folder" action:@selector(newSmartFolder:) toolTip:@"Create a smart folder from a predicate"]; break; case ToolbarNewGroupIdentifier: [self _configureItem:item label:@"New Group" buttonTitle:@"New Group" action:@selector(newGroup:) toolTip:@"Create an empty group (or drop people on GROUPS)"]; break; case ToolbarEditIdentifier: [self _configureItem:item label:@"Edit" buttonTitle:@"Edit…" action:@selector(editSelectedSmartFolder:) toolTip:@"Edit the selected smart folder"]; break; case ToolbarDetailIdentifier: [self _configureItem:item label:@"Details" buttonTitle:@"Details" action:@selector(toggleDetail:) toolTip:@"Show or hide the detail pane"]; break; case ToolbarSearchIdentifier: [item setLabel:@"Search"]; [item setPaletteLabel:@"Search"]; [item setView:_searchField]; [item setMinSize:CGSizeMake(140, 30)]; [item setMaxSize:CGSizeMake(260, 30)]; break; } return item; } - (void)_configureItem:(CPToolbarItem)anItem label:(CPString)aLabel buttonTitle:(CPString)aTitle action:(SEL)anAction toolTip:(CPString)aToolTip { var button = [CPButton buttonWithTitle:aTitle]; [button setBezelStyle:CPRoundedBezelStyle]; [button setTarget:self]; [button setAction:anAction]; [button sizeToFit]; var size = CGSizeMake(MAX(CGRectGetWidth([button frame]), 40), 25); [anItem setLabel:aLabel]; [anItem setPaletteLabel:aLabel]; [anItem setToolTip:aToolTip]; [anItem setTarget:self]; [anItem setAction:anAction]; [anItem setView:button]; [anItem setMinSize:size]; [anItem setMaxSize:size]; } // ---------------------------------------------------------------- Split view delegate (sidebar) - (BOOL)splitView:(CPSplitView)aSplitView canCollapseSubview:(CPView)aSubview { return aSubview === [_sourceListController view]; } - (float)splitView:(CPSplitView)aSplitView constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)anIndex { return SidebarMinWidth; } - (float)splitView:(CPSplitView)aSplitView constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)anIndex { return MIN(SidebarMaxWidth, CGRectGetWidth([aSplitView bounds]) - 300.0); } - (void)splitView:(CPSplitView)aSplitView resizeSubviewsWithOldSize:(CGSize)oldSize { // Sidebar keeps its width, the people pane takes up the rest. var subviews = [aSplitView subviews], sidebar = subviews[0], main = subviews[1], size = [aSplitView bounds].size, divider = [aSplitView dividerThickness], sidebarWidth = [aSplitView isSubviewCollapsed:sidebar] ? 0 : CGRectGetWidth([sidebar frame]); [sidebar setFrame:CGRectMake(0, 0, sidebarWidth, size.height)]; [main setFrame:CGRectMake(sidebarWidth + divider, 0, MAX(0, size.width - sidebarWidth - divider), size.height)]; // Custom layout bypasses CPSplitView's own pass: redraw the divider. [aSplitView setNeedsDisplay:YES]; } - (BOOL)isSidebarVisible { var sidebar = [_sourceListController view]; return ![_splitView isSubviewCollapsed:sidebar] && CGRectGetWidth([sidebar frame]) > 0; } // ---------------------------------------------------------------- Source list delegate - (void)sourceListViewController:(SourceListViewController)aController didSelectItem:(id)anItem { _selectedItem = anItem; [self _refreshPeople]; } - (void)_selectSourceItem:(id)anItem { _selectedItem = anItem; [_sourceListController reloadAndSelectItem:anItem]; [self _refreshPeople]; } // ---------------------------------------------------------------- Model -> view - (void)libraryDidChange:(CPNotification)aNotification { var kind = [[aNotification userInfo] objectForKey:LibraryChangeKindKey]; if (![_library containsItem:_selectedItem]) _selectedItem = [_library allPeopleItem]; // Names, order and badges are all rendered by the outline. [_sourceListController reloadAndSelectItem:_selectedItem]; if (kind === LibraryChangeStructural) [self _refreshPeople]; else [_peopleController rearrange]; } - (CPPredicate)_combinedPredicate { var subpredicates = [], folderPredicate = [_library predicateForItem:_selectedItem]; if (folderPredicate) subpredicates.push(folderPredicate); if (_searchPredicate) subpredicates.push(_searchPredicate); if (subpredicates.length === 0) return nil; if (subpredicates.length === 1) return subpredicates[0]; return [CPCompoundPredicate andPredicateWithSubpredicates:subpredicates]; } - (void)_refreshPeople { var item = _selectedItem || [_library allPeopleItem]; [_peopleController setPeople:[_library baseContentForItem:item] filterPredicate:[self _combinedPredicate] title:[item name]]; } // ---------------------------------------------------------------- Search - (void)searchFieldDidChange:(id)sender { var searchString = [[_searchField stringValue] stringByTrimmingWhitespace]; if (![searchString length]) _searchPredicate = nil; else _searchPredicate = [CPPredicate predicateWithFormat:@"(firstName CONTAINS[cd] %@) OR (lastName CONTAINS[cd] %@) OR (email CONTAINS[cd] %@) OR (city CONTAINS[cd] %@) OR (department CONTAINS[cd] %@)", searchString, searchString, searchString, searchString, searchString]; [self _refreshPeople]; } - (void)focusSearchField:(id)sender { [[self window] makeFirstResponder:_searchField]; } // ---------------------------------------------------------------- Source item actions - (SmartFolderEditorController)_folderEditor { if (!_folderEditor) _folderEditor = [[SmartFolderEditorController alloc] initWithLibrary:_library]; return _folderEditor; } - (NameSheetController)_nameSheet { if (!_nameSheet) _nameSheet = [[NameSheetController alloc] init]; return _nameSheet; } - (BOOL)_isSheetVisible { return !![[self window] attachedSheet]; } - (void)newSmartFolder:(id)sender { if ([self _isSheetVisible]) return; [[self _folderEditor] beginEditingFolder:nil modalForWindow:[self window] completion:function(folder) { [self _selectSourceItem:folder]; }]; } - (void)editSelectedSmartFolder:(id)sender { if (![_selectedItem isKindOfClass:[SmartFolder class]] || [self _isSheetVisible]) return; var folder = _selectedItem; [[self _folderEditor] beginEditingFolder:folder modalForWindow:[self window] completion:function(aFolder) { [self _selectSourceItem:aFolder]; }]; } - (void)newGroup:(id)sender { if ([self _isSheetVisible]) return; [[self _nameSheet] beginWithTitle:@"New Group" name:@"Untitled Group" okTitle:@"Create" modalForWindow:[self window] completion:function(name) { var group = [_library addGroupWithName:name people:[]]; [self _selectSourceItem:group]; }]; } - (void)renameSelectedGroup:(id)sender { if (![_selectedItem isKindOfClass:[LibraryGroup class]] || [self _isSheetVisible]) return; var group = _selectedItem; [[self _nameSheet] beginWithTitle:@"Rename Group" name:[group name] okTitle:@"Rename" modalForWindow:[self window] completion:function(name) { [_library renameItem:group to:name]; }]; } - (void)deleteSelectedSourceItem:(id)sender { if (![_selectedItem isUserItem]) return; [_library removeItem:_selectedItem]; } // ---------------------------------------------------------------- People actions - (void)newPerson:(id)sender { // A new person would be invisible in a smart folder or a filtered list. if ([_selectedItem isKindOfClass:[SmartFolder class]]) _selectedItem = [_library allPeopleItem]; if (_searchPredicate) { [_searchField setStringValue:@""]; _searchPredicate = nil; } var group = [_selectedItem isKindOfClass:[LibraryGroup class]] ? _selectedItem : nil, person = [_library createPerson]; if (group) [_library addPeople:[person] toGroup:group]; [_peopleController selectPerson:person edit:YES]; } - (void)removeSelectedPeople:(id)sender { var people = [_peopleController selectedPeople]; if (![people count]) return; if ([_selectedItem isKindOfClass:[LibraryGroup class]]) [_library removePeople:people fromGroup:_selectedItem]; else [_library removePeople:people]; } - (void)delete:(id)sender { [self removeSelectedPeople:sender]; } - (void)copy:(id)sender { [_peopleController copySelectionToPasteboard:[CPPasteboard generalPasteboard]]; } - (void)cut:(id)sender { [self copy:sender]; [self removeSelectedPeople:sender]; } - (void)paste:(id)sender { var pasteboard = [CPPasteboard generalPasteboard], people = [_peopleController peopleFromTabSeparatedString:[pasteboard stringForType:CPStringPboardType]]; if (![people count]) return; if ([_selectedItem isKindOfClass:[SmartFolder class]]) _selectedItem = [_library allPeopleItem]; [_library addPeople:people]; if ([_selectedItem isKindOfClass:[LibraryGroup class]]) [_library addPeople:people toGroup:_selectedItem]; [[_peopleController arrayController] setSelectedObjects:people]; } // ---------------------------------------------------------------- View actions - (void)toggleSidebar:(id)sender { if ([self isSidebarVisible]) { _lastSidebarWidth = CGRectGetWidth([[_sourceListController view] frame]); [_splitView setPosition:0 ofDividerAtIndex:0]; } else [_splitView setPosition:MAX(_lastSidebarWidth, SidebarMinWidth) ofDividerAtIndex:0]; } - (void)toggleDetail:(id)sender { [_peopleController toggleDetail]; } - (void)showAllPeople:(id)sender { [self _selectSourceItem:[_library allPeopleItem]]; } - (void)resetLibrary:(id)sender { if ([self _isSheetVisible]) return; var alert = [[CPAlert alloc] init]; [alert setAlertStyle:CPWarningAlertStyle]; [alert setMessageText:@"Reset the library?"]; [alert setInformativeText:@"All people, groups and smart folders will be replaced by the sample data. This cannot be undone."]; [alert addButtonWithTitle:@"Reset"]; [alert addButtonWithTitle:@"Cancel"]; // CPAlert reports the clicked button's tag as the return code. Assign our own // tags rather than relying on a framework-specific numbering scheme. var buttons = [alert buttons]; [buttons[0] setTag:ResetAlertResetTag]; [buttons[1] setTag:ResetAlertCancelTag]; [alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:@selector(resetAlertDidEnd:returnCode:contextInfo:) contextInfo:nil]; } - (void)resetAlertDidEnd:(CPAlert)anAlert returnCode:(int)aReturnCode contextInfo:(id)aContext { if (aReturnCode !== ResetAlertResetTag) return; [_searchField setStringValue:@""]; _searchPredicate = nil; _selectedItem = [_library allPeopleItem]; [_library resetToSampleData]; } // ---------------------------------------------------------------- Validation - (BOOL)validateMenuItem:(CPMenuItem)anItem { var action = [anItem action], sheet = [self _isSheetVisible], hasPeopleSelection = [[_peopleController selectedPeople] count] > 0; switch (action) { case @selector(newSmartFolder:): case @selector(newGroup:): case @selector(resetLibrary:): return !sheet; case @selector(editSelectedSmartFolder:): return !sheet && [_selectedItem isKindOfClass:[SmartFolder class]]; case @selector(renameSelectedGroup:): return !sheet && [_selectedItem isKindOfClass:[LibraryGroup class]]; case @selector(deleteSelectedSourceItem:): return !sheet && [_selectedItem isUserItem]; case @selector(copy:): case @selector(cut:): case @selector(delete:): case @selector(removeSelectedPeople:): return !sheet && hasPeopleSelection; case @selector(paste:): return !sheet && [[[CPPasteboard generalPasteboard] types] containsObject:CPStringPboardType]; case @selector(toggleSidebar:): [anItem setTitle:[self isSidebarVisible] ? @"Hide Sidebar" : @"Show Sidebar"]; return YES; case @selector(toggleDetail:): [anItem setTitle:[_peopleController isDetailVisible] ? @"Hide Details" : @"Show Details"]; return YES; } return YES; } @end