Files
cappuccino/Resources/SmartFoldersDemo/SourceListViewController.j
T

722 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* SourceListViewController.j
* SmartFoldersDemo
*
* Sidebar. Exercises:
* - CPViewController with a programmatic -loadView
* - view-based CPOutlineView (outlineView:viewForTableColumn:item:,
* makeViewWithIdentifier:owner:, CPTableCellView subclass)
* - source list highlight style, expand/collapse, non-selectable headers
* - custom drawing (CPBezierPath) for icons and count badges
* - drag & drop: reordering groups / smart folders inside their section,
* dropping people (from the table) onto a group, or onto the GROUPS
* header to create a new group
* - CPButtonBar with plus / minus / action pop-up and a split view
* resize control
*/
@import <AppKit/AppKit.j>
@import "Library.j"
var SourceCellIdentifier = @"SourceCell",
SourceRowHeight = 26.0;
// Tags of the action pop-up items.
SourceActionNewGroup = 1;
SourceActionNewSmartFolder = 2;
SourceActionEdit = 3;
SourceActionRename = 4;
SourceActionDelete = 5;
// --------------------------------------------------------------------------------
// SourceIconView – small vector icon drawn with CPBezierPath
// --------------------------------------------------------------------------------
@implementation SourceIconView : CPView
{
CPString _iconKind;
BOOL _isHighlighted;
}
- (void)setKind:(CPString)aKind
{
if (_iconKind === aKind)
return;
_iconKind = aKind;
[self setNeedsDisplay:YES];
}
- (void)setSelected:(BOOL)aFlag
{
if (_isHighlighted === aFlag)
return;
_isHighlighted = aFlag;
[self setNeedsDisplay:YES];
}
- (void)drawRect:(CGRect)aRect
{
var bounds = [self bounds],
w = CGRectGetWidth(bounds),
h = CGRectGetHeight(bounds);
if (_iconKind === @"all")
{
var color = _isHighlighted ? [CPColor whiteColor] : [CPColor colorWithRed:0.33 green:0.45 blue:0.62 alpha:1.0],
head = [CPBezierPath bezierPath],
body = [CPBezierPath bezierPath];
[color set];
[head appendBezierPathWithOvalInRect:CGRectMake(w / 2 - 3.5, 1.5, 7, 7)];
[head fill];
[body appendBezierPathWithRoundedRect:CGRectMake(w / 2 - 6.5, 9.5, 13, 6) xRadius:3 yRadius:3];
[body fill];
return;
}
// Folder shape for groups and smart folders.
var fill = _isHighlighted ? [CPColor whiteColor] :
(_iconKind === @"smart" ? [CPColor colorWithRed:0.55 green:0.42 blue:0.78 alpha:1.0]
: [CPColor colorWithRed:0.38 green:0.62 blue:0.88 alpha:1.0]),
folder = [CPBezierPath bezierPath];
[folder moveToPoint:CGPointMake(1, 3)];
[folder lineToPoint:CGPointMake(6, 3)];
[folder lineToPoint:CGPointMake(8, 5)];
[folder lineToPoint:CGPointMake(w - 1, 5)];
[folder lineToPoint:CGPointMake(w - 1, h - 2)];
[folder lineToPoint:CGPointMake(1, h - 2)];
[folder closePath];
[fill set];
[folder fill];
if (_iconKind === @"smart")
{
// Gear-ish dot to distinguish smart folders.
var mark = [CPBezierPath bezierPath],
markColor = _isHighlighted ? [CPColor colorWithRed:0.55 green:0.42 blue:0.78 alpha:1.0] : [CPColor whiteColor];
[markColor set];
[mark appendBezierPathWithOvalInRect:CGRectMake(w / 2 - 2.5, 7.5, 5, 5)];
[mark fill];
}
}
@end
// --------------------------------------------------------------------------------
// SourceBadgeView – rounded count badge
// --------------------------------------------------------------------------------
var BadgeFont = nil,
BadgeHeight = 16.0,
BadgeMinWidth = 24.0,
BadgeHPadding = 7.0;
@implementation SourceBadgeView : CPView
{
CPTextField _badgeLabel;
int _badgeCount;
BOOL _isHighlighted;
}
+ (void)initialize
{
if (self !== [SourceBadgeView class])
return;
BadgeFont = [CPFont boldSystemFontOfSize:11.0];
}
- (id)initWithFrame:(CGRect)aFrame
{
if (self = [super initWithFrame:aFrame])
{
// A plain, inset-free label that is positioned by hand: the text field's
// own vertical centering includes theme insets and ends up a pixel or two low.
_badgeLabel = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
[_badgeLabel setFont:BadgeFont];
[_badgeLabel setAlignment:CPCenterTextAlignment];
[_badgeLabel setEditable:NO];
[_badgeLabel setSelectable:NO];
[_badgeLabel setBezeled:NO];
[_badgeLabel setBordered:NO];
[_badgeLabel setDrawsBackground:NO];
[_badgeLabel setValue:CGInsetMakeZero() forThemeAttribute:@"content-inset"];
[self addSubview:_badgeLabel];
[self _updateColors];
}
return self;
}
+ (CPString)stringForCount:(int)aCount
{
// Keep badges compact: 1234 -> "1.2k", 12345 -> "12k"
if (aCount >= 10000)
return Math.round(aCount / 1000) + "k";
if (aCount >= 1000)
return (Math.round(aCount / 100) / 10) + "k";
return String(aCount);
}
- (void)setCount:(int)aCount
{
_badgeCount = aCount;
var hidden = aCount === CPNotFound || aCount === nil || aCount === undefined;
[self setHidden:hidden];
if (!hidden)
{
[_badgeLabel setStringValue:[SourceBadgeView stringForCount:aCount]];
[self _layoutLabel];
}
[self setNeedsDisplay:YES];
}
- (void)setSelected:(BOOL)aFlag
{
if (_isHighlighted === aFlag)
return;
_isHighlighted = aFlag;
[self _updateColors];
[self setNeedsDisplay:YES];
}
- (float)preferredWidth
{
var size = [[_badgeLabel stringValue] sizeWithFont:BadgeFont];
return MAX(BadgeMinWidth, Math.ceil(size.width) + 2 * BadgeHPadding);
}
- (void)setFrameSize:(CGSize)aSize
{
[super setFrameSize:aSize];
[self _layoutLabel];
}
- (void)_layoutLabel
{
var bounds = [self bounds],
textHeight = Math.ceil([[_badgeLabel stringValue] sizeWithFont:BadgeFont].height);
[_badgeLabel setFrame:CGRectMake(0, Math.round((CGRectGetHeight(bounds) - textHeight) / 2), CGRectGetWidth(bounds), textHeight)];
}
- (void)_updateColors
{
[_badgeLabel setTextColor:_isHighlighted ? [CPColor colorWithRed:0.29 green:0.54 blue:0.82 alpha:1.0] : [CPColor whiteColor]];
}
- (void)drawRect:(CGRect)aRect
{
var bounds = [self bounds],
radius = CGRectGetHeight(bounds) / 2,
path = [CPBezierPath bezierPath];
[path appendBezierPathWithRoundedRect:bounds xRadius:radius yRadius:radius];
[(_isHighlighted ? [CPColor whiteColor] : [CPColor colorWithRed:0.56 green:0.63 blue:0.76 alpha:1.0]) set];
[path fill];
}
@end
// --------------------------------------------------------------------------------
// SourceListCellView – view-based outline cell
// --------------------------------------------------------------------------------
@implementation SourceListCellView : CPTableCellView
{
SourceIconView _iconView;
CPTextField _titleField;
SourceBadgeView _badgeView;
BOOL _isHeader;
}
- (id)initWithFrame:(CGRect)aFrame
{
if (self = [super initWithFrame:aFrame])
{
var height = CGRectGetHeight(aFrame);
_iconView = [[SourceIconView alloc] initWithFrame:CGRectMake(2, (height - 16) / 2, 18, 16)];
[_iconView setAutoresizingMask:CPViewMinYMargin | CPViewMaxYMargin];
[self addSubview:_iconView];
_titleField = [[CPTextField alloc] initWithFrame:CGRectMake(24, 0, CGRectGetWidth(aFrame) - 24, height)];
[_titleField setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[_titleField setVerticalAlignment:CPCenterVerticalTextAlignment];
[_titleField setLineBreakMode:CPLineBreakByTruncatingTail];
[_titleField setEditable:NO];
[_titleField setBezeled:NO];
[_titleField setDrawsBackground:NO];
[self addSubview:_titleField];
_badgeView = [[SourceBadgeView alloc] initWithFrame:CGRectMake(CGRectGetWidth(aFrame) - 30, (height - BadgeHeight) / 2, BadgeMinWidth, BadgeHeight)];
[_badgeView setAutoresizingMask:CPViewMinXMargin | CPViewMinYMargin | CPViewMaxYMargin];
[self addSubview:_badgeView];
[self setIdentifier:SourceCellIdentifier];
}
return self;
}
- (void)configureWithItem:(SourceItem)anItem count:(int)aCount
{
_isHeader = [anItem isSection];
var bounds = [self bounds],
width = CGRectGetWidth(bounds),
height = CGRectGetHeight(bounds);
[_titleField setStringValue:[anItem name]];
if (_isHeader)
{
[_iconView setHidden:YES];
[_badgeView setHidden:YES];
[_titleField setFont:[CPFont boldSystemFontOfSize:11.0]];
[_titleField setFrame:CGRectMake(0, 0, width, height)];
}
else
{
var kind = [anItem isKindOfClass:[SmartFolder class]] ? @"smart" :
[anItem isKindOfClass:[LibraryGroup class]] ? @"group" : @"all";
[_iconView setHidden:NO];
[_iconView setKind:kind];
[_titleField setFont:[CPFont systemFontOfSize:12.0]];
[_badgeView setCount:aCount];
var badgeWidth = [_badgeView isHidden] ? 0 : [_badgeView preferredWidth];
[_badgeView setFrame:CGRectMake(width - badgeWidth - 8, Math.round((height - BadgeHeight) / 2), badgeWidth, BadgeHeight)];
[_titleField setFrame:CGRectMake(24, 0, MAX(0, width - 24 - badgeWidth - 14), height)];
}
[self _updateSelectionAppearance];
}
- (void)setThemeState:(ThemeState)aState
{
[super setThemeState:aState];
[self _updateSelectionAppearance];
}
- (void)unsetThemeState:(ThemeState)aState
{
[super unsetThemeState:aState];
[self _updateSelectionAppearance];
}
- (void)_updateSelectionAppearance
{
// CPTableView marks data views of selected rows with CPThemeStateSelectedDataView
// and, only while the table is focused (first responder in the key window),
// additionally with CPThemeStateFirstResponder. The unfocused selection colour
// is a light gray, so white text is only legible in the focused case.
var selected = [self hasThemeState:CPThemeStateSelectedDataView],
emphasized = selected && [self hasThemeState:CPThemeStateFirstResponder];
[_iconView setSelected:emphasized];
[_badgeView setSelected:emphasized];
if (_isHeader)
[_titleField setTextColor:[CPColor colorWithWhite:0.45 alpha:1.0]];
else
[_titleField setTextColor:emphasized ? [CPColor whiteColor] : [CPColor colorWithWhite:0.15 alpha:1.0]];
}
@end
// --------------------------------------------------------------------------------
// SourceListViewController
// --------------------------------------------------------------------------------
@implementation SourceListViewController : CPViewController
{
Library _library;
CPOutlineView _outlineView;
CPButtonBar _buttonBar;
BOOL _suppressSelectionCallbacks;
id _sourceListDelegate @accessors(property=delegate);
}
- (id)initWithLibrary:(Library)aLibrary
{
if (self = [super initWithCibName:nil bundle:nil])
_library = aLibrary;
return self;
}
- (CPButtonBar)buttonBar
{
[self view];
return _buttonBar;
}
- (void)loadView
{
var frame = CGRectMake(0, 0, 230, 500),
view = [[CPView alloc] initWithFrame:frame],
barHeight = 26.0;
[view setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
// --- Outline in scroll view
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame) - barHeight)];
[scrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[scrollView setAutohidesScrollers:YES];
[scrollView setHasHorizontalScroller:NO];
[scrollView setBorderType:CPNoBorder];
_outlineView = [[CPOutlineView alloc] initWithFrame:[[scrollView contentView] bounds]];
var column = [[CPTableColumn alloc] initWithIdentifier:@"source"];
[column setWidth:CGRectGetWidth(frame) - 20];
[column setResizingMask:CPTableColumnAutoresizingMask];
[column setEditable:NO];
[_outlineView addTableColumn:column];
[_outlineView setOutlineTableColumn:column];
[_outlineView setHeaderView:nil];
[_outlineView setCornerView:nil];
[_outlineView setColumnAutoresizingStyle:CPTableViewLastColumnOnlyAutoresizingStyle];
[_outlineView setSelectionHighlightStyle:CPTableViewSelectionHighlightStyleSourceList];
[_outlineView setBackgroundColor:[CPColor colorWithRed:212.0 / 255.0 green:221.0 / 255.0 blue:230.0 / 255.0 alpha:1.0]];
[_outlineView setRowHeight:SourceRowHeight];
[_outlineView setAllowsMultipleSelection:NO];
[_outlineView setAllowsEmptySelection:YES];
[_outlineView setTarget:self];
[_outlineView setDoubleAction:@selector(outlineViewDoubleClicked:)];
[_outlineView registerForDraggedTypes:[PeopleDragType, SourceItemDragType]];
[_outlineView setDraggingSourceOperationMask:CPDragOperationMove forLocal:YES];
[_outlineView setDataSource:self];
[_outlineView setDelegate:self];
[scrollView setDocumentView:_outlineView];
[view addSubview:scrollView];
// --- Button bar
_buttonBar = [[CPButtonBar alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(frame) - barHeight, CGRectGetWidth(frame), barHeight)];
[_buttonBar setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin];
if ([_buttonBar respondsToSelector:@selector(setHasResizeControl:)])
[_buttonBar setHasResizeControl:YES];
if ([_buttonBar respondsToSelector:@selector(setResizeControlIsLeftAligned:)])
[_buttonBar setResizeControlIsLeftAligned:NO];
var plusButton = [CPButtonBar plusButton];
[plusButton setTarget:self];
[plusButton setAction:@selector(_plusClicked:)];
[plusButton setToolTip:@"New smart folder"];
var minusButton = [CPButtonBar minusButton];
[minusButton setTarget:self];
[minusButton setAction:@selector(_minusClicked:)];
[minusButton setToolTip:@"Delete the selected group or smart folder"];
var actionButton = [CPButtonBar actionPopupButton];
if (![[actionButton itemArray] count])
[actionButton addItemWithTitle:@""];
[self _addActionItem:@"New Group…" tag:SourceActionNewGroup to:actionButton];
[self _addActionItem:@"New Smart Folder…" tag:SourceActionNewSmartFolder to:actionButton];
[[actionButton menu] addItem:[CPMenuItem separatorItem]];
[self _addActionItem:@"Edit Smart Folder…" tag:SourceActionEdit to:actionButton];
[self _addActionItem:@"Rename Group…" tag:SourceActionRename to:actionButton];
[self _addActionItem:@"Delete" tag:SourceActionDelete to:actionButton];
[actionButton setTarget:self];
[actionButton setAction:@selector(_actionPopupChosen:)];
[_buttonBar setButtons:[plusButton, minusButton, actionButton]];
[view addSubview:_buttonBar];
[self setView:view];
[_outlineView reloadData];
[self _expandSections];
}
- (void)_addActionItem:(CPString)aTitle tag:(int)aTag to:(CPPopUpButton)aPopUp
{
[aPopUp addItemWithTitle:aTitle];
[[aPopUp lastItem] setTag:aTag];
}
- (void)_expandSections
{
var sections = [_library sections];
for (var i = 0; i < sections.length; i++)
[_outlineView expandItem:sections[i]];
}
// ---------------------------------------------------------------- Public
- (id)selectedItem
{
var row = [_outlineView selectedRow];
return row >= 0 ? [_outlineView itemAtRow:row] : nil;
}
- (void)selectItem:(id)anItem
{
var row = [_outlineView rowForItem:anItem];
if (row === CPNotFound || row < 0)
return;
[_outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
[_outlineView scrollRowToVisible:row];
}
// Reloads the outline (names, order, badges) without emitting selection callbacks.
- (void)reloadAndSelectItem:(id)anItem
{
_suppressSelectionCallbacks = YES;
[_outlineView reloadData];
[self _expandSections];
var row = anItem ? [_outlineView rowForItem:anItem] : CPNotFound;
if (row !== CPNotFound && row >= 0)
[_outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
else
[_outlineView deselectAll:nil];
_suppressSelectionCallbacks = NO;
}
- (void)focus
{
[[_outlineView window] makeFirstResponder:_outlineView];
}
// ---------------------------------------------------------------- Actions
- (void)_plusClicked:(id)sender
{
[CPApp sendAction:@selector(newSmartFolder:) to:nil from:sender];
}
- (void)_minusClicked:(id)sender
{
[CPApp sendAction:@selector(deleteSelectedSourceItem:) to:nil from:sender];
}
- (void)_actionPopupChosen:(id)sender
{
var selector = nil;
switch ([[sender selectedItem] tag])
{
case SourceActionNewGroup: selector = @selector(newGroup:); break;
case SourceActionNewSmartFolder: selector = @selector(newSmartFolder:); break;
case SourceActionEdit: selector = @selector(editSelectedSmartFolder:); break;
case SourceActionRename: selector = @selector(renameSelectedGroup:); break;
case SourceActionDelete: selector = @selector(deleteSelectedSourceItem:); break;
}
if (selector)
[CPApp sendAction:selector to:nil from:sender];
}
- (void)outlineViewDoubleClicked:(id)sender
{
var row = [_outlineView clickedRow];
if (row < 0)
return;
var item = [_outlineView itemAtRow:row];
if ([item isKindOfClass:[SmartFolder class]])
[CPApp sendAction:@selector(editSelectedSmartFolder:) to:nil from:self];
else if ([item isKindOfClass:[LibraryGroup class]])
[CPApp sendAction:@selector(renameSelectedGroup:) to:nil from:self];
}
// ---------------------------------------------------------------- Data source
- (int)outlineView:(CPOutlineView)anOutlineView numberOfChildrenOfItem:(id)anItem
{
return [[_library childrenOfItem:anItem] count];
}
- (id)outlineView:(CPOutlineView)anOutlineView child:(int)anIndex ofItem:(id)anItem
{
return [[_library childrenOfItem:anItem] objectAtIndex:anIndex];
}
- (BOOL)outlineView:(CPOutlineView)anOutlineView isItemExpandable:(id)anItem
{
return [anItem isSection];
}
- (id)outlineView:(CPOutlineView)anOutlineView objectValueForTableColumn:(CPTableColumn)aColumn byItem:(id)anItem
{
return anItem;
}
// ---------------------------------------------------------------- Delegate (view based)
- (CPView)outlineView:(CPOutlineView)anOutlineView viewForTableColumn:(CPTableColumn)aColumn item:(id)anItem
{
var cell = [anOutlineView makeViewWithIdentifier:SourceCellIdentifier owner:self];
if (!cell)
cell = [[SourceListCellView alloc] initWithFrame:CGRectMake(0, 0, [aColumn width], SourceRowHeight)];
[cell configureWithItem:anItem count:[_library countForItem:anItem]];
return cell;
}
- (BOOL)outlineView:(CPOutlineView)anOutlineView shouldSelectItem:(id)anItem
{
return [anItem isSelectable];
}
- (void)outlineViewSelectionDidChange:(CPNotification)aNotification
{
if (_suppressSelectionCallbacks)
return;
var item = [self selectedItem];
if (item && [_sourceListDelegate respondsToSelector:@selector(sourceListViewController:didSelectItem:)])
[_sourceListDelegate sourceListViewController:self didSelectItem:item];
}
// ---------------------------------------------------------------- Drag & drop
- (BOOL)outlineView:(CPOutlineView)anOutlineView writeItems:(CPArray)theItems toPasteboard:(CPPasteboard)aPasteboard
{
var item = [theItems firstObject];
if ([theItems count] !== 1 || ![item isUserItem])
return NO;
[aPasteboard declareTypes:[SourceItemDragType] owner:self];
[aPasteboard setString:[item uid] forType:SourceItemDragType];
return YES;
}
- (CPDragOperation)outlineView:(CPOutlineView)anOutlineView validateDrop:(id)theInfo proposedItem:(id)anItem proposedChildIndex:(int)anIndex
{
var pasteboard = [theInfo draggingPasteboard],
types = [pasteboard types];
// People from the table
if ([types containsObject:PeopleDragType])
{
if ([anItem isKindOfClass:[LibraryGroup class]] && anIndex === CPOutlineViewDropOnItemIndex)
return CPDragOperationCopy;
// Anywhere on/inside the GROUPS section: create a new group.
if (anItem === [_library groupsSection])
{
[anOutlineView setDropItem:anItem dropChildIndex:CPOutlineViewDropOnItemIndex];
return CPDragOperationCopy;
}
return CPDragOperationNone;
}
// Reordering of groups / smart folders
if ([types containsObject:SourceItemDragType])
{
var dragged = [_library itemWithUID:[pasteboard stringForType:SourceItemDragType]],
section = [_library sectionForItem:dragged];
if (!dragged || !section)
return CPDragOperationNone;
if (anItem === section && anIndex !== CPOutlineViewDropOnItemIndex)
return CPDragOperationMove;
// Dropped onto a sibling: retarget to "insert above sibling".
if (anIndex === CPOutlineViewDropOnItemIndex && [_library sectionForItem:anItem] === section)
{
[anOutlineView setDropItem:section dropChildIndex:[[section children] indexOfObjectIdenticalTo:anItem]];
return CPDragOperationMove;
}
}
return CPDragOperationNone;
}
- (BOOL)outlineView:(CPOutlineView)anOutlineView acceptDrop:(id)theInfo item:(id)anItem childIndex:(int)anIndex
{
var pasteboard = [theInfo draggingPasteboard],
types = [pasteboard types];
if ([types containsObject:PeopleDragType])
{
var uids = [CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:PeopleDragType]],
people = [_library peopleWithUIDs:uids || []];
if (![people count])
return NO;
if ([anItem isKindOfClass:[LibraryGroup class]])
{
[_library addPeople:people toGroup:anItem];
return YES;
}
if (anItem === [_library groupsSection])
{
var name = [people count] === 1 ? [people[0] fullName] : [CPString stringWithFormat:@"%d People", [people count]],
group = [_library addGroupWithName:name people:people];
[self selectItem:group];
return YES;
}
return NO;
}
if ([types containsObject:SourceItemDragType])
{
var dragged = [_library itemWithUID:[pasteboard stringForType:SourceItemDragType]];
if (!dragged)
return NO;
[_library moveItem:dragged toIndex:anIndex];
return YES;
}
return NO;
}
@end