mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-11-03 13:49:45 +00:00
Protects the selection, editing and drag and drop contracts of tables and outlines by verifying the events, delegate calls and data source calls that applications depend on. A drop retargeted between the children of an item is delivered to that item. * CPOutlineView: Set the retargeted drop operation in setDropItem:dropChildIndex:. * CPTableViewInteractionTest: Verify selection by mouse and keyboard, editing, drag initiation and drop handling, including refusal and retargeting by the delegate and data source. * CPOutlineViewInteractionTest: Verify dragged items, proposed parents and child indexes, drop retargeting, and expansion and its cancellation during a hover.
349 lines
11 KiB
Plaintext
349 lines
11 KiB
Plaintext
/*
|
|
* CPOutlineViewInteractionTest.j
|
|
* AppKit
|
|
*
|
|
* Created by David Richardson on 31/10/2026.
|
|
* Copyright David Richardson 2026.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
@import <AppKit/AppKit.j>
|
|
|
|
/*
|
|
An outline view adds a parent to every row, so it reports a drag by item and a drop
|
|
by parent item and child index. Selection and editing come from CPTableView, and
|
|
CPTableViewInteractionTest.j covers them. These tests cover what an outline view
|
|
changes.
|
|
*/
|
|
|
|
var OutlineInteractionDragType = @"OutlineInteractionDragType";
|
|
|
|
@implementation _CPOutlineInteractionView : CPOutlineView
|
|
{
|
|
CPArray dragCalls @accessors;
|
|
}
|
|
|
|
- (void)dragView:(CPView)aView at:(CGPoint)aLocation offset:(CGSize)mouseOffset event:(CPEvent)anEvent pasteboard:(CPPasteboard)aPasteboard source:(id)aSourceObject slideBack:(BOOL)slideBack
|
|
{
|
|
if (!dragCalls)
|
|
dragCalls = [];
|
|
|
|
dragCalls.push(aPasteboard);
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation _CPOutlineInteractionDataSource : CPObject
|
|
{
|
|
id tree @accessors;
|
|
CPArray writtenItems @accessors;
|
|
CPArray validations @accessors;
|
|
CPArray acceptedDrops @accessors;
|
|
id retargetItem @accessors;
|
|
id retargetIndex @accessors;
|
|
BOOL shouldRetarget @accessors;
|
|
}
|
|
|
|
- (id)init
|
|
{
|
|
self = [super init];
|
|
|
|
if (self)
|
|
{
|
|
tree = {
|
|
@"root": [@"A", @"B", @"C"],
|
|
@"A": [@"A1", @"A2"],
|
|
@"B": [],
|
|
@"C": [@"C1"]
|
|
};
|
|
validations = [];
|
|
acceptedDrops = [];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (CPArray)childrenOfItem:(id)anItem
|
|
{
|
|
return tree[anItem || @"root"] || [];
|
|
}
|
|
|
|
- (id)outlineView:(CPOutlineView)anOutlineView child:(int)anIndex ofItem:(id)anItem
|
|
{
|
|
return [self childrenOfItem:anItem][anIndex];
|
|
}
|
|
|
|
- (BOOL)outlineView:(CPOutlineView)anOutlineView isItemExpandable:(id)anItem
|
|
{
|
|
return [self childrenOfItem:anItem].length > 0;
|
|
}
|
|
|
|
- (int)outlineView:(CPOutlineView)anOutlineView numberOfChildrenOfItem:(id)anItem
|
|
{
|
|
return [self childrenOfItem:anItem].length;
|
|
}
|
|
|
|
- (id)outlineView:(CPOutlineView)anOutlineView objectValueForTableColumn:(CPTableColumn)aColumn byItem:(id)anItem
|
|
{
|
|
return anItem;
|
|
}
|
|
|
|
- (BOOL)outlineView:(CPOutlineView)anOutlineView writeItems:(CPArray)theItems toPasteboard:(CPPasteboard)aPasteboard
|
|
{
|
|
writtenItems = theItems;
|
|
|
|
[aPasteboard declareTypes:[OutlineInteractionDragType] owner:self];
|
|
[aPasteboard setData:@"items" forType:OutlineInteractionDragType];
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (CPDragOperation)outlineView:(CPOutlineView)anOutlineView validateDrop:(id)anInfo proposedItem:(id)anItem proposedChildIndex:(int)anIndex
|
|
{
|
|
validations.push({ @"item": anItem, @"index": anIndex });
|
|
|
|
if (shouldRetarget)
|
|
[anOutlineView setDropItem:retargetItem dropChildIndex:retargetIndex];
|
|
|
|
return CPDragOperationMove;
|
|
}
|
|
|
|
- (BOOL)outlineView:(CPOutlineView)anOutlineView acceptDrop:(id)anInfo item:(id)anItem childIndex:(int)anIndex
|
|
{
|
|
acceptedDrops.push({ @"item": anItem, @"index": anIndex });
|
|
|
|
return YES;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation _CPOutlineInteractionDraggingInfo : CPObject
|
|
{
|
|
CPPasteboard draggingPasteboard @accessors;
|
|
CGPoint draggingLocation @accessors;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation CPOutlineViewInteractionTest : OJTestCase
|
|
{
|
|
CPWindow theWindow;
|
|
_CPOutlineInteractionView outlineView;
|
|
_CPOutlineInteractionDataSource dataSource;
|
|
}
|
|
|
|
- (void)setUp
|
|
{
|
|
// This will init the global var CPApp which are used internally in the AppKit
|
|
[[CPApplication alloc] init];
|
|
|
|
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0) styleMask:CPWindowNotSizable];
|
|
outlineView = [[_CPOutlineInteractionView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)];
|
|
|
|
var column = [[CPTableColumn alloc] initWithIdentifier:@"Column"];
|
|
|
|
[column setWidth:200.0];
|
|
[outlineView addTableColumn:column];
|
|
[outlineView setOutlineTableColumn:column];
|
|
[outlineView setAllowsMultipleSelection:YES];
|
|
[[theWindow contentView] addSubview:outlineView];
|
|
|
|
dataSource = [[_CPOutlineInteractionDataSource alloc] init];
|
|
|
|
[outlineView setDataSource:dataSource];
|
|
[outlineView registerForDraggedTypes:[OutlineInteractionDragType]];
|
|
[outlineView expandItem:@"A"];
|
|
[outlineView reloadData];
|
|
}
|
|
|
|
- (CGPoint)pointInItem:(id)anItem
|
|
{
|
|
return CGPointMake(100.0, CGRectGetMidY([outlineView rectOfRow:[outlineView rowForItem:anItem]]));
|
|
}
|
|
|
|
- (id)draggingInfoAtPoint:(CGPoint)aPoint
|
|
{
|
|
var info = [[_CPOutlineInteractionDraggingInfo alloc] init],
|
|
pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard];
|
|
|
|
[pasteboard declareTypes:[OutlineInteractionDragType] owner:self];
|
|
[info setDraggingPasteboard:pasteboard];
|
|
[info setDraggingLocation:[outlineView convertPoint:aPoint toView:nil]];
|
|
|
|
return info;
|
|
}
|
|
|
|
- (void)sendMouse:(CPEventType)aType atPoint:(CGPoint)aPoint
|
|
{
|
|
var event = [CPEvent mouseEventWithType:aType
|
|
location:[outlineView convertPoint:aPoint toView:nil]
|
|
modifierFlags:0
|
|
timestamp:0
|
|
windowNumber:[theWindow windowNumber]
|
|
context:nil
|
|
eventNumber:0
|
|
clickCount:1
|
|
pressure:0];
|
|
|
|
[CPApp sendEvent:event];
|
|
[outlineView trackMouse:event];
|
|
}
|
|
|
|
- (void)testDraggingARowWritesItsItem
|
|
{
|
|
var start = [self pointInItem:@"A1"];
|
|
|
|
[self sendMouse:CPLeftMouseDown atPoint:start];
|
|
[self sendMouse:CPLeftMouseDragged atPoint:CGPointMake(start.x + 20.0, start.y)];
|
|
|
|
[self assert:[@"A1"] equals:[dataSource writtenItems]];
|
|
[self assert:1 equals:[[outlineView dragCalls] count]];
|
|
}
|
|
|
|
- (void)testDraggingASelectionWritesEachItem
|
|
{
|
|
var firstRow = [outlineView rowForItem:@"A1"];
|
|
|
|
[outlineView selectRowIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(firstRow, 2)] byExtendingSelection:NO];
|
|
|
|
var start = [self pointInItem:@"A2"];
|
|
|
|
[self sendMouse:CPLeftMouseDown atPoint:start];
|
|
[self sendMouse:CPLeftMouseDragged atPoint:CGPointMake(start.x + 20.0, start.y)];
|
|
|
|
[self assert:[@"A1", @"A2"] equals:[dataSource writtenItems]];
|
|
}
|
|
|
|
- (void)testTheMiddleOfARowProposesADropOnThatItem
|
|
{
|
|
[outlineView draggingUpdated:[self draggingInfoAtPoint:[self pointInItem:@"B"]]];
|
|
|
|
[self assert:@"B" equals:[dataSource validations][0].item];
|
|
[self assert:CPOutlineViewDropOnItemIndex equals:[dataSource validations][0].index];
|
|
}
|
|
|
|
- (void)testTheTopOfAChildRowProposesADropInItsParent
|
|
{
|
|
var rect = [outlineView rectOfRow:[outlineView rowForItem:@"A2"]];
|
|
|
|
[outlineView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(100.0, CGRectGetMinY(rect) + 1.0)]];
|
|
|
|
[self assert:@"A" equals:[dataSource validations][0].item];
|
|
[self assert:1 equals:[dataSource validations][0].index];
|
|
}
|
|
|
|
/*
|
|
Between a child and a root row, either parent is possible, and the horizontal
|
|
position of the drag chooses between them.
|
|
*/
|
|
- (void)testAnUnindentedDropAboveARootRowProposesTheRoot
|
|
{
|
|
var rect = [outlineView rectOfRow:[outlineView rowForItem:@"B"]];
|
|
|
|
[outlineView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(5.0, CGRectGetMinY(rect) + 1.0)]];
|
|
|
|
[self assertNull:[dataSource validations][0].item];
|
|
[self assert:1 equals:[dataSource validations][0].index];
|
|
}
|
|
|
|
- (void)testAnIndentedDropAboveARootRowProposesTheEndOfThePreviousParent
|
|
{
|
|
var rect = [outlineView rectOfRow:[outlineView rowForItem:@"B"]];
|
|
|
|
[outlineView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(100.0, CGRectGetMinY(rect) + 1.0)]];
|
|
|
|
[self assert:@"A" equals:[dataSource validations][0].item];
|
|
[self assert:2 equals:[dataSource validations][0].index];
|
|
}
|
|
|
|
- (void)testBelowTheLastRowProposesADropAtTheEndOfTheRoot
|
|
{
|
|
[outlineView draggingUpdated:[self draggingInfoAtPoint:CGPointMake(100.0, 290.0)]];
|
|
|
|
[self assertNull:[dataSource validations][0].item];
|
|
[self assert:3 equals:[dataSource validations][0].index];
|
|
}
|
|
|
|
- (void)testAcceptingADropReceivesTheProposedItem
|
|
{
|
|
var info = [self draggingInfoAtPoint:[self pointInItem:@"B"]];
|
|
|
|
[outlineView draggingUpdated:info];
|
|
|
|
[self assertTrue:[outlineView performDragOperation:info]];
|
|
[self assert:@"B" equals:[dataSource acceptedDrops][0].item];
|
|
[self assert:CPOutlineViewDropOnItemIndex equals:[dataSource acceptedDrops][0].index];
|
|
}
|
|
|
|
- (void)testDataSourceCanRetargetADropBetweenChildren
|
|
{
|
|
[dataSource setShouldRetarget:YES];
|
|
[dataSource setRetargetItem:@"A"];
|
|
[dataSource setRetargetIndex:0];
|
|
|
|
var info = [self draggingInfoAtPoint:[self pointInItem:@"B"]];
|
|
|
|
[outlineView draggingUpdated:info];
|
|
[outlineView performDragOperation:info];
|
|
|
|
[self assert:@"A" equals:[dataSource acceptedDrops][0].item];
|
|
[self assert:0 equals:[dataSource acceptedDrops][0].index];
|
|
}
|
|
|
|
- (void)testDataSourceCanRetargetADropOnAnItem
|
|
{
|
|
[dataSource setShouldRetarget:YES];
|
|
[dataSource setRetargetItem:@"C"];
|
|
[dataSource setRetargetIndex:CPOutlineViewDropOnItemIndex];
|
|
|
|
var info = [self draggingInfoAtPoint:[self pointInItem:@"B"]];
|
|
|
|
[outlineView draggingUpdated:info];
|
|
[outlineView performDragOperation:info];
|
|
|
|
[self assert:@"C" equals:[dataSource acceptedDrops][0].item];
|
|
[self assert:CPOutlineViewDropOnItemIndex equals:[dataSource acceptedDrops][0].index];
|
|
}
|
|
|
|
- (void)testHoveringOverACollapsedItemExpandsIt
|
|
{
|
|
[self assertFalse:[outlineView isItemExpanded:@"C"]];
|
|
|
|
[outlineView setDropItem:@"C" dropChildIndex:CPOutlineViewDropOnItemIndex];
|
|
[outlineView._dragHoverTimer fire];
|
|
|
|
[self assertTrue:[outlineView isItemExpanded:@"C"]];
|
|
}
|
|
|
|
- (void)testAChildIndexCancelsTheHover
|
|
{
|
|
[outlineView setDropItem:@"C" dropChildIndex:CPOutlineViewDropOnItemIndex];
|
|
[outlineView setDropItem:@"C" dropChildIndex:0];
|
|
|
|
[self assertNull:outlineView._dragHoverTimer];
|
|
}
|
|
|
|
- (void)testEndingADragCancelsTheHover
|
|
{
|
|
[outlineView setDropItem:@"C" dropChildIndex:CPOutlineViewDropOnItemIndex];
|
|
[outlineView draggedView:nil endedAt:CGPointMakeZero() operation:CPDragOperationNone];
|
|
|
|
[self assertNull:outlineView._dragHoverTimer];
|
|
[self assertFalse:[outlineView isItemExpanded:@"C"]];
|
|
}
|
|
|
|
@end
|