Merge remote-tracking branch 'upstream/main' into new-cprulerview

This commit is contained in:
daboe01 committed 2026-05-28 21:22:55 +02:00
commit 453e8dbd2e
60 files changed
+5340 -983

No files matched your search

+2
View File
@@ -115,3 +115,5 @@
@import "CPWindow.j"
@import "CPWindowController.j"
@import "CPWorkspace.j"
@import "CPFontPanel.j"
@import "CPTreeController.j"
+72 -16
View File
@@ -36,6 +36,7 @@
BOOL _hasResizeControl;
BOOL _resizeControlIsLeftAligned;
CPArray _buttons;
CPArray _rightButtons;
}
+ (id)plusButton
@@ -104,6 +105,7 @@
if (self)
{
_buttons = [];
_rightButtons = [];
[self setNeedsLayout];
}
@@ -157,6 +159,30 @@
return [CPArray arrayWithArray:_buttons];
}
- (void)setRightButtons:(CPArray)buttons
{
for (var i = [_rightButtons count] - 1; i >= 0; i--)
{
[_rightButtons[i] removeFromSuperview];
[_rightButtons[i] removeObserver:self forKeyPath:@"hidden"];
}
_rightButtons = [CPArray arrayWithArray:buttons];
for (var i = [_rightButtons count] - 1; i >= 0; i--)
{
[_rightButtons[i] addObserver:self forKeyPath:@"hidden" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:nil];
[_rightButtons[i] setBordered:YES];
}
[self setNeedsLayout];
}
- (CPArray)rightButtons
{
return [CPArray arrayWithArray:_rightButtons];
}
- (void)setHasResizeControl:(BOOL)shouldHaveResizeControl
{
if (_hasResizeControl === shouldHaveResizeControl)
@@ -243,46 +269,73 @@
}
}
var currentButtonOffset = _resizeControlIsLeftAligned ? CGRectGetMaxX([self bounds]) + 1 : -1,
bounds = [self bounds],
var rightButtonsNotHidden = [CPArray arrayWithArray:_rightButtons],
rightCount = [rightButtonsNotHidden count];
while (rightCount--)
{
var button = rightButtonsNotHidden[rightCount];
if ([button isHidden])
{
[button removeFromSuperview];
[rightButtonsNotHidden removeObject:button];
}
}
var bounds = [self bounds],
height = CGRectGetHeight(bounds) - 1,
frameWidth = CGRectGetWidth(bounds),
resizeRect = _hasResizeControl ? [self rectForEphemeralSubviewNamed:"resize-control-view"] : CGRectMakeZero(),
resizeWidth = CGRectGetWidth(resizeRect),
availableWidth = frameWidth - resizeWidth - 1;
for (var i = 0, count = [buttonsNotHidden count]; i < count; i++)
var currentLeftOffset = _resizeControlIsLeftAligned ? resizeWidth - 1 : -1,
currentRightOffset = _resizeControlIsLeftAligned ? CGRectGetMaxX(bounds) + 1 : CGRectGetMaxX(bounds) - resizeWidth + 1;
var setupButton = function(button, isRightAligned)
{
var button = buttonsNotHidden[i],
width = CGRectGetWidth([button frame]);
var width = CGRectGetWidth([button frame]);
if (availableWidth > width)
availableWidth -= width;
else
break;
return NO;
if (_resizeControlIsLeftAligned)
if (isRightAligned)
{
[button setFrame:CGRectMake(currentButtonOffset - width, 1, width, height)];
currentButtonOffset -= width - 1;
[button setFrame:CGRectMake(currentRightOffset - width, 1, width, height)];
currentRightOffset -= width - 1;
}
else
{
[button setFrame:CGRectMake(currentButtonOffset, 1, width, height)];
currentButtonOffset += width - 1;
{[button setFrame:CGRectMake(currentLeftOffset, 1, width, height)];
currentLeftOffset += width - 1;
}
[button setValue:normalColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateNormal, CPThemeStateBordered]];
[button setValue:highlightedColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateHighlighted, CPThemeStateBordered, ]];
[button setValue:highlightedColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateHighlighted, CPThemeStateBordered]];
[button setValue:disabledColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateDisabled, CPThemeStateBordered]];
[button setValue:textColor forThemeAttribute:@"text-color" inState:CPThemeStateBordered];
// FIXME shouldn't need this
[button setValue:normalColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateNormal, CPThemeStateBordered, CPPopUpButtonStatePullsDown]];
// FIXME shouldn't need this[button setValue:normalColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateNormal, CPThemeStateBordered, CPPopUpButtonStatePullsDown]];
[button setValue:highlightedColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateHighlighted, CPThemeStateBordered, CPPopUpButtonStatePullsDown]];
[button setValue:disabledColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateDisabled, CPThemeStateBordered, CPPopUpButtonStatePullsDown]];
[self addSubview:button];
return YES;
};
for (var i = 0, count = [buttonsNotHidden count]; i < count; i++)
{
if (!setupButton(buttonsNotHidden[i], _resizeControlIsLeftAligned))
break;
}
for (var i = 0, count = [rightButtonsNotHidden count]; i < count; i++)
{
if (!setupButton(rightButtonsNotHidden[i], YES))
break;
}
if (_hasResizeControl)
@@ -314,7 +367,8 @@
var CPButtonBarHasResizeControlKey = @"CPButtonBarHasResizeControlKey",
CPButtonBarResizeControlIsLeftAlignedKey = @"CPButtonBarResizeControlIsLeftAlignedKey",
CPButtonBarButtonsKey = @"CPButtonBarButtonsKey";
CPButtonBarButtonsKey = @"CPButtonBarButtonsKey",
CPButtonBarRightButtonsKey = @"CPButtonBarRightButtonsKey";
@implementation CPButtonBar (CPCoding)
@@ -325,6 +379,7 @@ var CPButtonBarHasResizeControlKey = @"CPButtonBarHasResizeControlKey",
[aCoder encodeBool:_hasResizeControl forKey:CPButtonBarHasResizeControlKey];
[aCoder encodeBool:_resizeControlIsLeftAligned forKey:CPButtonBarResizeControlIsLeftAlignedKey];
[aCoder encodeObject:_buttons forKey:CPButtonBarButtonsKey];
[aCoder encodeObject:_rightButtons forKey:CPButtonBarRightButtonsKey];
}
- (id)initWithCoder:(CPCoder)aCoder
@@ -332,6 +387,7 @@ var CPButtonBarHasResizeControlKey = @"CPButtonBarHasResizeControlKey",
if (self = [super initWithCoder:aCoder])
{
_buttons = [aCoder decodeObjectForKey:CPButtonBarButtonsKey] || [];
_rightButtons = [aCoder decodeObjectForKey:CPButtonBarRightButtonsKey] || [];
_hasResizeControl = [aCoder decodeBoolForKey:CPButtonBarHasResizeControlKey];
_resizeControlIsLeftAligned = [aCoder decodeBoolForKey:CPButtonBarResizeControlIsLeftAlignedKey];
}
+80 -32
View File
@@ -51,6 +51,7 @@ CPColorPickerViewWidth = 265;
CPColorPickerViewHeight = 370;
CPColorPanelColorDidChangeNotification = @"CPColorPanelColorDidChangeNotification";
CPColorDragType = CPColorPboardType;
var PREVIEW_HEIGHT = 20.0,
TOOLBAR_HEIGHT = 32.0,
@@ -129,8 +130,6 @@ var SharedColorPanel = nil,
if (self)
{
//[[self contentView] setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]];
[self setTitle:@"Color Panel"];
[self setLevel:CPFloatingWindowLevel];
@@ -149,20 +148,37 @@ var SharedColorPanel = nil,
*/
- (void)setColor:(CPColor)aColor
{
if ([_color isEqual:aColor])
return;
_color = aColor;
[_previewView setBackgroundColor:_color];
[CPApp sendAction:@selector(changeColor:) to:nil from:self];
// Check if the color change originated from user interaction inside the panel itself.
// We only broadcast `changeColor:` if the user picked a color via the panel's UI.
// If an external CPColorWell called `setColor:` programmatically, broadcasting it
// back down the responder chain would incorrectly change the previous First Responder.
var currentEvent = [CPApp currentEvent],
isFromPanel = currentEvent && ([currentEvent window] === self);
if (_target && _action)
[CPApp sendAction:_action to:_target from:self];
if (isFromPanel)
{
// Push color via Responder Chain (targets First Responder, i.e., the active CPColorWell)
[CPApp sendAction:@selector(changeColor:) to:nil from:self];
if (_target && _action)
[CPApp sendAction:_action to:_target from:self];
}
[[CPNotificationCenter defaultCenter]
postNotificationName:CPColorPanelColorDidChangeNotification
object:self];
[_activePicker setColor:_color];
[_opacitySlider setFloatValue:[_color alphaComponent]];
if (_activePicker)
[_activePicker setColor:_color];
if (_opacitySlider)
[_opacitySlider setFloatValue:[_color alphaComponent]];
}
/*!
@@ -174,7 +190,7 @@ var SharedColorPanel = nil,
{
[self setColor:aColor];
if (bool)
if (bool && _activePicker)
[_activePicker setColor:_color];
}
@@ -332,7 +348,7 @@ var SharedColorPanel = nil,
buttonForLater = button;
}
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
// Preview
var previewBox = [[CPView alloc] initWithFrame:CGRectMake(76, TOOLBAR_HEIGHT + 10, CGRectGetWidth(bounds) - 86, PREVIEW_HEIGHT)];
_previewView = [[_CPColorPanelPreview alloc] initWithFrame:CGRectInset([previewBox bounds], 2.0, 2.0)];
@@ -350,7 +366,7 @@ var SharedColorPanel = nil,
[_previewLabel setTextColor:[CPColor blackColor]];
[_previewLabel setAlignment:CPRightTextAlignment];
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
// Swatches
var swatchBox = [[CPView alloc] initWithFrame:CGRectMake(76, TOOLBAR_HEIGHT + 10 + PREVIEW_HEIGHT + 5, CGRectGetWidth(bounds) - 86, SWATCH_HEIGHT + 2.0)];
[swatchBox setBackgroundColor:[CPColor colorWithWhite:0.8 alpha:1.0]];
@@ -391,8 +407,6 @@ var SharedColorPanel = nil,
[contentView addSubview:opacityLabel];
[contentView addSubview:_opacitySlider];
_target = nil;
_action = nil;
_activePicker = nil;
[_previewView setBackgroundColor:_color];
@@ -412,8 +426,6 @@ var SharedColorPanel = nil,
@end
CPColorDragType = "CPColorDragType";
var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
/* @ignore */
@@ -423,12 +435,14 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
CPColor _dragColor;
CPColorPanel _colorPanel;
CPCookie _swatchCookie;
CGPoint _mouseDownPoint;
}
- (id)initWithFrame:(CGRect)aFrame
{
self = [super initWithFrame:aFrame];
_mouseDownPoint = CGPointMake(0, 0);
[self setBackgroundColor:[CPColor grayColor]];
[self registerForDraggedTypes:[CPArray arrayWithObjects:CPColorDragType]];
@@ -442,7 +456,6 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
for (var i = 0; i < 50; i++)
{
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
var view = [[CPView alloc] initWithFrame:CGRectMake(13 * i + 1, 1, 12, 12)],
fillView = [[CPView alloc] initWithFrame:CGRectInset([view bounds], 1.0, 1.0)];
@@ -494,7 +507,6 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
- (CPArray)saveColorList
{
var result = [];
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
for (var i = 0; i < _swatches.length; i++)
result.push([[[_swatches[i] subviews][0] backgroundColor] hexString]);
@@ -521,39 +533,44 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
- (void)setColor:(CPColor)aColor atIndex:(int)index
{
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
[[_swatches[index] subviews][0] setBackgroundColor:aColor];
[self saveColorList];
}
- (void)mouseDown:(CPEvent)anEvent
{
_mouseDownPoint = [anEvent locationInWindow];
}
- (void)mouseUp:(CPEvent)anEvent
{
var point = [self convertPoint:[anEvent locationInWindow] fromView:nil],
bounds = [self bounds];
if (!CGRectContainsPoint(bounds, point) || point.x > [self bounds].size.width - 1 || point.x < 1)
return NO;
return;
[_colorPanel setColor:[self colorAtIndex:FLOOR(point.x / 13)] updatePicker:YES];
}
- (void)mouseDragged:(CPEvent)anEvent
{
var point = [self convertPoint:[anEvent locationInWindow] fromView:nil];
var windowPoint = [anEvent locationInWindow];
// Prevent accidental drags from rapid clicking causing small micro-movements
if (ABS(windowPoint.x - _mouseDownPoint.x) < 3 && ABS(windowPoint.y - _mouseDownPoint.y) < 3)
return;
var point = [self convertPoint:windowPoint fromView:nil];
if (point.x > [self bounds].size.width - 1 || point.x < 1)
return NO;
[[CPPasteboard pasteboardWithName:CPDragPboard] declareTypes:[CPArray arrayWithObject:CPColorDragType] owner:self];
return;
var swatch = _swatches[FLOOR(point.x / 13)];
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
_dragColor = [[swatch subviews][0] backgroundColor];
var bounds = CGRectMakeCopy([swatch bounds]);
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
var dragView = [[CPView alloc] initWithFrame:bounds],
dragFillView = [[CPView alloc] initWithFrame:CGRectInset(bounds, 1.0, 1.0)];
@@ -562,11 +579,15 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
[dragView addSubview:dragFillView];
var pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard];
[pasteboard declareTypes:[CPArray arrayWithObject:CPColorDragType] owner:self];
[pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_dragColor] forType:CPColorDragType];
[self dragView:dragView
at:CGPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0)
offset:CGPointMake(0.0, 0.0)
event:anEvent
pasteboard:nil
pasteboard:pasteboard
source:self
slideBack:YES];
}
@@ -577,7 +598,7 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
[aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_dragColor] forType:aType];
}
- (void)performDragOperation:(id /*<CPDraggingInfo>*/)aSender
- (BOOL)performDragOperation:(id /*<CPDraggingInfo>*/)aSender
{
var location = [self convertPoint:[aSender draggingLocation] fromView:nil],
pasteboard = [aSender draggingPasteboard],
@@ -587,6 +608,13 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
return NO;
[self setColor:[CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:CPColorDragType]] atIndex:FLOOR(location.x / 13)];
return YES;
}
- (unsigned)draggingSourceOperationMaskForLocal:(BOOL)isLocal
{
return CPDragOperationCopy;
}
@end
@@ -595,11 +623,13 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
@implementation _CPColorPanelPreview : CPView
{
CPColorPanel _colorPanel;
CGPoint _mouseDownPoint;
}
- (id)initWithFrame:(CGRect)aFrame
{
self = [super initWithFrame:aFrame];
_mouseDownPoint = CGPointMake(0, 0);
[self registerForDraggedTypes:[CPArray arrayWithObjects:CPColorDragType]];
@@ -616,7 +646,7 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
return _colorPanel;
}
- (void)performDragOperation:(id /*<CPDraggingInfo>*/)aSender
- (BOOL)performDragOperation:(id /*<CPDraggingInfo>*/)aSender
{
var pasteboard = [aSender draggingPasteboard];
@@ -625,6 +655,8 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
var color = [CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:CPColorDragType]];
[_colorPanel setColor:color updatePicker:YES];
return YES;
}
- (BOOL)isOpaque
@@ -632,15 +664,22 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
return YES;
}
- (void)mouseDown:(CPEvent)anEvent
{
_mouseDownPoint = [anEvent locationInWindow];
}
- (void)mouseDragged:(CPEvent)anEvent
{
var point = [self convertPoint:[anEvent locationInWindow] fromView:nil];
var windowPoint = [anEvent locationInWindow];
if (ABS(windowPoint.x - _mouseDownPoint.x) < 3 && ABS(windowPoint.y - _mouseDownPoint.y) < 3)
return;
[[CPPasteboard pasteboardWithName:CPDragPboard] declareTypes:[CPColorDragType] owner:self];
var point = [self convertPoint:windowPoint fromView:nil];
var bounds = CGRectMake(0, 0, 15, 15);
// FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox
var dragView = [[CPView alloc] initWithFrame:bounds],
dragFillView = [[CPView alloc] initWithFrame:CGRectInset(bounds, 1.0, 1.0)];
@@ -649,11 +688,15 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
[dragView addSubview:dragFillView];
var pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard];
[pasteboard declareTypes:[CPArray arrayWithObject:CPColorDragType] owner:self];
[pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[self backgroundColor]] forType:CPColorDragType];
[self dragView:dragView
at:CGPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0)
offset:CGPointMake(0.0, 0.0)
event:anEvent
pasteboard:nil
pasteboard:pasteboard
source:self
slideBack:YES];
}
@@ -664,6 +707,11 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie";
[aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[self backgroundColor]] forType:aType];
}
- (unsigned)draggingSourceOperationMaskForLocal:(BOOL)isLocal
{
return CPDragOperationCopy;
}
@end
@import "CPColorPicker.j"
+200 -136
View File
@@ -25,7 +25,7 @@
@import "CPView.j"
@import "CPColor.j"
@import "CPColorPanel.j"
@import "CPPasteboard.j"
var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiveNotification";
@@ -39,10 +39,10 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
*/
@implementation CPColorWell : CPControl
{
BOOL _active;
BOOL _bordered;
CPColor _color;
BOOL _isChangingColorFromPanel; // Guard flag to prevent recursion
CGPoint _mouseDownPoint;
}
+ (Class)_binderClassForBinding:(CPString)aBinding
@@ -77,52 +77,54 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
[theBinding reverseSetValueFor:@"color"];
}
- (BOOL)isFirstResponder
{
return [[self window] firstResponder] === self;
}
- (BOOL)acceptsFirstResponder
{
return [self isEnabled];
}
- (void)activate:(BOOL)shouldBeExclusive
{
[[self window] makeFirstResponder:self];
[[CPColorPanel sharedColorPanel] orderFront:self];
}
- (BOOL)isActive
{
return [self isFirstResponder] && [self isEnabled];
}
/*!
Deactivates the color well.
*/
- (void)deactivate
{
if ([self isFirstResponder])
[[self window] makeFirstResponder:nil];
}
- (id)initWithFrame:(CGRect)aFrame
{
self = [super initWithFrame:aFrame];
if (self)
{
_active = NO;
_color = [CPColor whiteColor];
_mouseDownPoint = CGPointMakeZero();
[self setBordered:YES];
[self registerForDraggedTypes:[CPArray arrayWithObject:CPColorPboardType]];
}
return self;
}
- (void)_registerNotifications
{
var defaultCenter = [CPNotificationCenter defaultCenter];
[defaultCenter
addObserver:self
selector:@selector(colorWellDidBecomeExclusive:)
name:_CPColorWellDidBecomeExclusiveNotification
object:nil];
[defaultCenter
addObserver:self
selector:@selector(colorPanelWillClose:)
name:CPWindowWillCloseNotification
object:[CPColorPanel sharedColorPanel]];
}
- (void)_removeNotifications
{
var defaultCenter = [CPNotificationCenter defaultCenter];
[defaultCenter
removeObserver:self
name:_CPColorWellDidBecomeExclusiveNotification
object:nil];
[defaultCenter
removeObserver:self
name:CPWindowWillCloseNotification
object:[CPColorPanel sharedColorPanel]];
}
#pragma mark -
#pragma mark Draw
/*!
Sets whether the color well is bordered.
@@ -143,7 +145,8 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
return [self hasThemeState:CPThemeStateBordered];
}
// Managing Color From Color Wells
#pragma mark -
#pragma mark Managing Color
/*!
Returns the color well's current color.
@@ -158,12 +161,17 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
*/
- (void)setColor:(CPColor)aColor
{
if (_color == aColor)
if ([_color isEqual:aColor])
return;
_color = aColor;
[self setNeedsLayout];
// Only push back to the panel if we initiated the change (not if the panel pushed it to us)
// AND if we are the current focus.
if (!_isChangingColorFromPanel && [self isFirstResponder])
[[CPColorPanel sharedColorPanel] setColor:_color];
}
/*!
@@ -175,96 +183,175 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
[self setColor:[aSender color]];
}
// Activating and Deactivating Color Wells
/*!
Standard action method sent by CPColorPanel via the Responder Chain.
*/
- (void)changeColor:(id)aSender
{
if ([aSender isKindOfClass:[CPColorPanel class]])
{
_isChangingColorFromPanel = YES;
[self setColor:[aSender color]];
_isChangingColorFromPanel = NO;
// Forward the action to our target (e.g. controller)
[self sendAction:[self action] to:[self target]];
}
}
#pragma mark -
#pragma mark Activating and Deactivating
/*!
Activates the color well, displays the color panel, and makes the panel's current color the same as its own.
If exclusive is \c YES, deactivates any other CPColorWells. \c NO, keeps them active.
@param shouldBeExclusive whether other color wells should be deactivated.
*/
- (void)activate:(BOOL)shouldBeExclusive
- (BOOL)becomeFirstResponder
{
if (shouldBeExclusive)
// FIXME: make this queue!
[[CPNotificationCenter defaultCenter]
postNotificationName:_CPColorWellDidBecomeExclusiveNotification
object:self];
[self setThemeState:CPThemeStateFirstResponder];
var panel = [CPColorPanel sharedColorPanel];
// EXPLICITLY set ourselves as the target.
// This ensures that when the user clicks the panel (making Panel key),
// the panel still knows to send messages back to us.
[panel setTarget:self];
[panel setAction:@selector(changeColor:)];
// Sync panel to our current color
[panel setColor:_color];
if ([self isActive])
return;
_active = YES;
[[CPNotificationCenter defaultCenter]
addObserver:self
selector:@selector(colorPanelDidChangeColor:)
name:CPColorPanelColorDidChangeNotification
object:[CPColorPanel sharedColorPanel]];
}
/*!
Deactivates the color well.
*/
- (void)deactivate
{
if (![self isActive])
return;
_active = NO;
[[CPNotificationCenter defaultCenter]
removeObserver:self
name:CPColorPanelColorDidChangeNotification
object:[CPColorPanel sharedColorPanel]];
}
/*!
Returns \c YES if the color well is active.
*/
- (BOOL)isActive
{
return _active;
}
- (void)colorPanelDidChangeColor:(CPNotification)aNotification
{
[self takeColorFrom:[aNotification object]];
[self sendAction:[self action] to:[self target]];
}
- (void)colorWellDidBecomeExclusive:(CPNotification)aNotification
{
if (self != [aNotification object])
[self deactivate];
}
- (void)colorPanelWillClose:(CPNotification)aNotification
{
[self deactivate];
}
- (void)stopTracking:(CGPoint)lastPoint at:(CGPoint)aPoint mouseIsUp:(BOOL)mouseIsUp
{
[self highlight:NO];
if (!mouseIsUp || !CGRectContainsPoint([self bounds], aPoint) || ![self isEnabled])
[[CPNotificationCenter defaultCenter] postNotificationName:_CPColorWellDidBecomeExclusiveNotification object:self];
return YES;
}
- (BOOL)resignFirstResponder
{
[self unsetThemeState:CPThemeStateFirstResponder];
var panel = [CPColorPanel sharedColorPanel];
// Clean up if we were the target
if ([panel target] == self)
[panel setTarget:nil];
return YES;
}
#pragma mark -
#pragma mark Event Handling
- (void)mouseDown:(CPEvent)anEvent
{
if (![self isEnabled])
return;
_mouseDownPoint = [anEvent locationInWindow];
[self activate:YES];
var colorPanel = [CPColorPanel sharedColorPanel];
[colorPanel setPlatformWindow:[[self window] platformWindow]];
[colorPanel setColor:_color];
[colorPanel orderFront:self];
}
- (void)mouseDragged:(CPEvent)anEvent
{
if (![self isEnabled])
return;
var windowPoint = [anEvent locationInWindow];
// Prevent accidental drags from rapid clicking causing small micro-movements
if (ABS(windowPoint.x - _mouseDownPoint.x) < 3 && ABS(windowPoint.y - _mouseDownPoint.y) < 3)
return;
var bounds = CGRectMake(0, 0, 15, 15);
var dragView = [[CPView alloc] initWithFrame:bounds],
dragFillView = [[CPView alloc] initWithFrame:CGRectInset(bounds, 1.0, 1.0)];
[dragView setBackgroundColor:[CPColor blackColor]];
[dragFillView setBackgroundColor:_color];
[dragView addSubview:dragFillView];
var pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard];
[pasteboard declareTypes:[CPArray arrayWithObject:CPColorPboardType] owner:self];
[pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_color] forType:CPColorPboardType];
var point = [self convertPoint:windowPoint fromView:nil];
[self dragView:dragView
at:CGPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0)
offset:CGPointMake(0.0, 0.0)
event:anEvent
pasteboard:pasteboard
source:self
slideBack:YES];
}
#pragma mark -
#pragma mark Drag and Drop
- (void)draggingEntered:(id)sender
{
var pasteboard = [sender draggingPasteboard];
if ([[pasteboard types] containsObject:CPColorPboardType])
{
[self setThemeState:CPThemeStateHighlighted];
return CPDragOperationCopy;
}
return CPDragOperationNone;
}
- (void)draggingExited:(id)sender
{
[self unsetThemeState:CPThemeStateHighlighted];
}
- (BOOL)performDragOperation:(id)sender
{
var pasteboard = [sender draggingPasteboard];
if ([[pasteboard types] containsObject:CPColorPboardType])
{
var data = [pasteboard dataForType:CPColorPboardType],
newColor = [CPKeyedUnarchiver unarchiveObjectWithData:data];
if (newColor && [newColor isKindOfClass:[CPColor class]])
{
[self setColor:newColor];
[self sendAction:[self action] to:[self target]];
// Activate nicely after drop
[self activate:YES];
[self unsetThemeState:CPThemeStateHighlighted];
return YES;
}
}
return NO;
}
- (void)pasteboard:(CPPasteboard)aPasteboard provideDataForType:(CPString)aType
{
if (aType == CPColorPboardType)
[aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_color] forType:aType];
}
- (unsigned)draggingSourceOperationMaskForLocal:(BOOL)isLocal
{
return CPDragOperationCopy;
}
#pragma mark -
#pragma mark Layout
- (CGRect)contentRectForBounds:(CGRect)bounds
{
var contentInset = [self currentValueForThemeAttribute:@"content-inset"];
return CGRectInsetByInset(bounds, contentInset);
}
@@ -318,7 +405,6 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
positioned:CPWindowAbove
relativeToEphemeralSubviewNamed:@"bezel-view"];
[contentView setBackgroundColor:_color];
var contentBorderView = [self layoutEphemeralSubviewNamed:@"content-border-view"
@@ -328,28 +414,6 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv
[contentBorderView setBackgroundColor:[self currentValueForThemeAttribute:@"content-border-color"]];
}
#pragma mark -
#pragma mark Observers method
- (void)_addObservers
{
if (_isObserving)
return;
[super _addObservers];
[self _registerNotifications];
}
- (void)_removeObservers
{
if (!_isObserving)
return;
[super _removeObservers];
[self _removeNotifications];
}
@end
@implementation CPColorWellValueBinder : CPBinder
@@ -398,9 +462,9 @@ var CPColorWellColorKey = "CPColorWellColorKey",
if (self)
{
_active = NO;
_color = [aCoder decodeObjectForKey:CPColorWellColorKey];
[self setBordered:[aCoder decodeBoolForKey:CPColorWellBorderedKey]];
[self registerForDraggedTypes:[CPArray arrayWithObject:CPColorPboardType]];
}
return self;
+9 -1
View File
@@ -251,7 +251,15 @@
- (void)_displayNextMonth
{
[self setDateValue:[_monthView nextMonth]];
// Copy the date so we don't modify the view's state directly
var nextDate = [[_monthView nextMonth] copy];
// Set to the middle of the month (15th).
// This prevents [setDateValue:]'s timezone adjustment from
// shifting the date back into the previous month (e.g., Nov 1 -> Oct 31).
nextDate.setDate(15);
[self setDateValue:nextDate];
}
- (void)_displayPreviousMonth
@@ -27,12 +27,7 @@
CPDatePickerElementTextFieldBecomeFirstResponder = @"CPDatePickerElementTextFieldBecomeFirstResponder";
CPDatePickerElementTextFieldAMPMChangedNotification = @"CPDatePickerElementTextFieldAMPMChangedNotification";
var CPZeroKeyCode = 48,
CPNineKeyCode = 57,
CPMajAKeyCode = 65,
CPMajPKeyCode = 80,
CPAKeyCode = 97,
CPPKeyCode = 112;
// Removed hardcoded KeyCodes (CPZeroKeyCode, etc) as they are unreliable across browsers/layouts.
CPMonthDateType = 0;
CPDayDateType = 1;
@@ -190,23 +185,38 @@ CPAMPMDateType = 6;
*/
- (void)setValueForKeyEvent:(CPEvent)anEvent
{
var keyCode = [anEvent keyCode];
var keyCode = [anEvent keyCode],
characters = [anEvent characters];
if (keyCode != CPDeleteKeyCode && keyCode != CPDeleteForwardKeyCode && keyCode < CPZeroKeyCode || keyCode > CPNineKeyCode)
// Check if the event is a deletion
var isDelete = (keyCode === CPDeleteKeyCode || keyCode === CPDeleteForwardKeyCode);
// Check if the event is a numeric input.
// By testing the character string against a regex, we support num-pads and
// international keyboards correctly, rather than relying on keyCode ranges.
var isNumeric = (characters && [characters length] > 0 && /^[0-9]$/.test(characters));
// If it is neither a delete command nor a digit, we ignore it.
if (!isDelete && !isNumeric)
return;
var newValue = [self stringValue].replace(/\s/g, ''),
length = [newValue length],
eventKeyValue = parseInt([anEvent characters]).toString();
length = [newValue length];
if (keyCode == CPDeleteKeyCode || keyCode == CPDeleteForwardKeyCode)
if (isDelete)
{
[_timerEdition invalidate];
_timerEdition = nil;
newValue = [newValue substringToIndex:(length - 1)];
// Ensure we don't substring if length is 0
if (length > 0)
newValue = [newValue substringToIndex:(length - 1)];
}
else
{
// Since isNumeric is true, characters is a valid digit string
var eventKeyValue = characters;
if (!_timerEdition)
{
_timerEdition = [CPTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(_timerKeyEvent:) userInfo:nil repeats:NO];
@@ -227,7 +237,12 @@ CPAMPMDateType = 6;
}
}
if (parseInt(newValue) > [self _maxNumberWithMaxDate] || ([_datePicker _isAmericanFormat] && _dateType == CPHourDateType && parseInt(newValue) > 12))
// Safety check for NaN before comparison
var numericValue = parseInt(newValue);
if (isNaN(numericValue))
numericValue = 0;
if (numericValue > [self _maxNumberWithMaxDate] || ([_datePicker _isAmericanFormat] && _dateType == CPHourDateType && numericValue > 12))
return;
_firstEvent = NO;
+163 -32
View File
@@ -48,13 +48,6 @@
@global CPYearMonthDayDatePickerElementFlag
@global CPEraDatePickerElementFlag
var CPZeroKeyCode = 48,
CPNineKeyCode = 57,
CPMajAKeyCode = 65,
CPMajPKeyCode = 80,
CPAKeyCode = 97,
CPPKeyCode = 112;
// This class is used to represente the datePicker with the CPTextFieldAndStepperDatePickerStyle/CPTextFieldDatePickerStyle mode
@implementation _CPDatePickerTextField : CPControl
{
@@ -131,7 +124,7 @@ var CPZeroKeyCode = 48,
// Don't forget to unbind, otherwise several steppers will increase or decrease
[_currentTextField unbind:@"objectValue"];
[_currentTextField makeDeselectable];
_currentTextField = nil
_currentTextField = nil;
// This is usefull when clicking on the stepper when the datePicker is not selected
[_stepper setObjectValue:0];
@@ -221,13 +214,30 @@ var CPZeroKeyCode = 48,
{
[_datePickerElementView _updateResponderTextField];
// We select the firstTextField when the datePicker becomes firstResponder if _currentTextField is null. It can be null just when using tab
if (!_currentTextField)
{
var targetField = nil;
if (flags & CPShiftKeyMask)
[self _selectTextField:_lastTextField];
{
// Try last field; if hidden, find previous visible
if ([_lastTextField isHidden])
targetField = [self _previousVisibleTextFieldFrom:_lastTextField];
else
targetField = _lastTextField;
}
else
[self _selectTextField:_firstTextField];
{
// Try first field; if hidden, find next visible
if ([_firstTextField isHidden])
targetField = [self _nextVisibleTextFieldFrom:_firstTextField];
else
targetField = _firstTextField;
}
// Only select if we actually found a valid visible field
if (targetField)
[self _selectTextField:targetField];
}
}
@@ -334,23 +344,92 @@ var CPZeroKeyCode = 48,
return [super performKeyEquivalent:anEvent];
}
- (_CPDatePickerElementTextField)_nextVisibleTextFieldFrom:(_CPDatePickerElementTextField)aTextField
{
var runner = [aTextField nextTextField];
// If we wrapped back to the start immediately, or runner is nil, we are done.
if (!runner || runner == _firstTextField)
return nil;
// Traverse hidden fields
while (runner && [runner isHidden])
{
// If we hit the absolute last field and it is hidden, we've reached the end.
if (runner == _lastTextField)
return nil;
runner = [runner nextTextField];
// Safety: if we wrapped back to the start inside the loop
if (runner == _firstTextField)
return nil;
}
return runner;
}
- (_CPDatePickerElementTextField)_previousVisibleTextFieldFrom:(_CPDatePickerElementTextField)aTextField
{
var runner = [aTextField previousTextField];
// If we wrapped back to the end immediately, or runner is nil, we are done.
if (!runner || runner == _lastTextField)
return nil;
// Traverse hidden fields
while (runner && [runner isHidden])
{
// If we hit the absolute first field and it is hidden, we've reached the start.
if (runner == _firstTextField)
return nil;
runner = [runner previousTextField];
// Safety: if we wrapped back to the end inside the loop
if (runner == _lastTextField)
return nil;
}
return runner;
}
- (void)insertTab:(id)sender
{
if (!_currentTextField)
return;
if (_currentTextField == _lastTextField)
[[self window] selectNextKeyView:self];
// Ensure boundaries are up to date
[_datePickerElementView _updateResponderTextField];
var nextField = [self _nextVisibleTextFieldFrom:_currentTextField];
if (nextField)
{
[self _selectTextField:nextField];
}
else
[self moveRight:sender];
}
{
// We reached the visual end. Manually find the next external view.
// We cannot rely on [[self window] selectNextKeyView:self] because it might
// loop back into our own internal fields or select 'self' which refuses focus.
var nextView = [_currentTextField nextValidKeyView];
- (void)moveRight:(id)sender
{
if (!_currentTextField)
return;
// Skip any view that is part of this control (descendant)
while (nextView && [nextView isDescendantOf:self])
{
// If we looped back to the current field, we are trapped in a closed loop with no exit.
if (nextView == _currentTextField)
{
nextView = nil;
break;
}
nextView = [nextView nextValidKeyView];
}
[self _selectTextField:[_currentTextField nextTextField]];
if (nextView)
[[self window] makeFirstResponder:nextView];
}
}
- (void)insertBacktab:(id)sender
@@ -358,10 +437,47 @@ var CPZeroKeyCode = 48,
if (!_currentTextField)
return;
if (_currentTextField == _firstTextField)
[[self window] selectPreviousKeyView:self];
[_datePickerElementView _updateResponderTextField];
var prevField = [self _previousVisibleTextFieldFrom:_currentTextField];
if (prevField)
{
[self _selectTextField:prevField];
}
else
[self moveLeft:sender];
{
// We reached the visual start. Manually find the previous external view.
var prevView = [_currentTextField previousValidKeyView];
// Skip any view that is part of this control
while (prevView && [prevView isDescendantOf:self])
{
if (prevView == _currentTextField)
{
prevView = nil;
break;
}
prevView = [prevView previousValidKeyView];
}
if (prevView)
[[self window] makeFirstResponder:prevView];
}
}
- (void)moveRight:(id)sender
{
if (!_currentTextField)
return;
[_datePickerElementView _updateResponderTextField];
// Use the helper to skip hidden fields
var nextField = [self _nextVisibleTextFieldFrom:_currentTextField];
if (nextField)
[self _selectTextField:nextField];
}
- (void)moveLeft:(id)sender
@@ -369,7 +485,13 @@ var CPZeroKeyCode = 48,
if (!_currentTextField)
return;
[self _selectTextField:[_currentTextField previousTextField]];
[_datePickerElementView _updateResponderTextField];
// Use the helper to skip hidden fields to be safe
var prevField = [self _previousVisibleTextFieldFrom:_currentTextField];
if (prevField)
[self _selectTextField:prevField];
}
- (void)moveDown:(id)sender
@@ -401,7 +523,7 @@ var CPZeroKeyCode = 48,
}
/*! KeyDown event
We just care care about the event A/P and every numbers
We just care about the event A/P and every numbers
*/
- (void)keyDown:(CPEvent)anEvent
{
@@ -410,18 +532,27 @@ var CPZeroKeyCode = 48,
[self interpretKeyEvents:[anEvent]];
if ([_datePicker _isAmericanFormat] && [_currentTextField dateType] == CPAMPMDateType && ([anEvent keyCode] == CPAKeyCode || [anEvent keyCode] == CPPKeyCode || [anEvent keyCode] == CPMajAKeyCode || [anEvent keyCode] == CPMajPKeyCode))
var characters = [anEvent characters];
if ([_datePicker _isAmericanFormat] && [_currentTextField dateType] == CPAMPMDateType && [characters length] > 0)
{
if ([anEvent keyCode] == CPAKeyCode || [anEvent keyCode] == CPMajAKeyCode)
var charUpper = [characters uppercaseString];
if (charUpper === "A")
{
[_currentTextField setStringValue:@"AM"];
else
[[CPNotificationCenter defaultCenter] postNotificationName:CPDatePickerElementTextFieldAMPMChangedNotification object:_currentTextField userInfo:nil];
return;
}
else if (charUpper === "P")
{
[_currentTextField setStringValue:@"PM"];
[[CPNotificationCenter defaultCenter] postNotificationName:CPDatePickerElementTextFieldAMPMChangedNotification object:_currentTextField userInfo:nil];
return;
[[CPNotificationCenter defaultCenter] postNotificationName:CPDatePickerElementTextFieldAMPMChangedNotification object:_currentTextField userInfo:nil];
return;
}
}
// Pass the event down to the specific field (which handles numeric input validation via regex)
[_currentTextField setValueForKeyEvent:anEvent];
}
+31 -3
View File
@@ -70,6 +70,7 @@ var _CPEventPeriodicEventPeriod = 0,
BOOL _isARepeat;
unsigned _keyCode;
DOMEvent _DOMEvent;
BOOL _isActionKey;
int _data1;
int _data2;
short _subtype;
@@ -110,17 +111,28 @@ var _CPEventPeriodicEventPeriod = 0,
@param unmodCharacters the string of keys pressed without the presence of any modifiers other than Shift
@param repeatKey \c YES if this is caused by the system repeat as opposed to the user pressing the key again
@param code a number associated with the keyboard key of this event
@param isAnActionKey a BOOL indicating whether this key is an action key (e.g. a function key)
@throws CPInternalInconsistencyException if \c anEventType is not a CPKeyDown,
CPKeyUp or CPFlagsChanged
@return the keyboard event
*/
+ (CPEvent)keyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags
timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code isActionKey:(BOOL)isAnActionKey
{
return [[self alloc] _initKeyEventWithType:anEventType location:aPoint modifierFlags:modifierFlags
timestamp:aTimestamp windowNumber:aWindowNumber context:aGraphicsContext
characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code];
characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code isActionKey:isAnActionKey];
}
// for backwards compatibility only
+ (CPEvent)keyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags
timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code
{
return [[self alloc] _initKeyEventWithType:anEventType location:aPoint modifierFlags:modifierFlags
timestamp:aTimestamp windowNumber:aWindowNumber context:aGraphicsContext
characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code isActionKey:NO];
}
/*!
@@ -252,7 +264,7 @@ var _CPEventPeriodicEventPeriod = 0,
/* @ignore */
- (id)_initKeyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags
timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)isARepeat keyCode:(unsigned short)code
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)isARepeat keyCode:(unsigned short)code isActionKey:(BOOL)isAnActionKey
{
if (self = [self _initWithType:anEventType])
{
@@ -264,6 +276,7 @@ var _CPEventPeriodicEventPeriod = 0,
_charactersIgnoringModifiers = unmodCharacters;
_isARepeat = isARepeat;
_keyCode = code;
_isActionKey = isAnActionKey;
_windowNumber = aWindowNumber;
}
@@ -571,6 +584,21 @@ var _CPEventPeriodicEventPeriod = 0,
return !firstResponderIsText;
}
- (BOOL)_isActionOrCommandEvent
{
// This method is now platform-agnostic. It checks for abstract properties
// of the event, including the _isActionKey flag that was set at creation time.
return (
// Is it a command shortcut?
(_modifierFlags & (CPCommandKeyMask | CPControlKeyMask)) ||
// Is it a key that doesn't produce a character?
([_characters length] === 0) ||
// Was it identified as an action key by the platform-specific layer?
_isActionKey
);
}
/*!
Return YES if this event is a part of processing a browser controlled cut or paste event
where the browser will go ahead and do the work of cutting or pasting within the input
+5 -1
View File
@@ -208,7 +208,11 @@ CPRemoveTraitFontAction = 7;
- (@action)addFontTrait:(id)sender
{
var tag = [sender tag];
var tag = sender;
if ([sender respondsToSelector:@selector(tag)])
tag = [sender tag];
_activeChange = tag == nil ? @{} : @{ @"addTraits": tag };
_fontAction = CPAddTraitFontAction;
+10
View File
@@ -1075,7 +1075,17 @@ var _CPMenuBarVisible = NO,
if (self === [CPApp mainMenu])
[self _flashItemAtIndex:index];
anEvent._isKeyEquivalent = YES; // prevent the menu keystroke from beeing inserted into textview
[self performActionForItemAtIndex:index];
#if PLATFORM(DOM)
// we are done with this event in cappuccino space. do not let the browser do something weird additionally (e.g. command-o).
// but we must not stop copy/paste events as these can only be handled by the browser at this time even if they are in our menu
// (until we move to CPTextView as the fieleditor)
if (characters != "c" && characters != "x" && characters != "v")
_CPDOMEventStop(anEvent._DOMEvent);
#endif
}
else
{
+352 -91
View File
@@ -311,8 +311,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
[self setIndentationPerLevel:16.0];
[self setIndentationMarkerFollowsDataView:YES];
[super setDataSource:[[_CPOutlineViewTableViewDataSource alloc] initWithOutlineView:self]];
[super setDelegate:[[_CPOutlineViewTableViewDelegate alloc] initWithOutlineView:self]];
[super setDataSource:self];
[super setDelegate:self];
[self setDisclosureControlPrototype:[[CPDisclosureButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 10.0, 10.0)]];
}
@@ -773,6 +773,11 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
[self reloadItem:anItem reloadChildren:NO];
}
- (int)_numberOfRows
{
return _itemsForRows ? _itemsForRows.length : 0;
}
/*!
Reloads the data for a given item and optionally the children.
@@ -781,9 +786,12 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
*/
- (void)reloadItem:(id)anItem reloadChildren:(BOOL)shouldReloadChildren
{
_pendingItemToClean = [];
_itemAddedDuringLastLoading = [];
var previousRowCount = _itemsForRows.length;
if (!!shouldReloadChildren || !anItem)
[self _loadItemInfoForItem:anItem intermediate:NO];
else
@@ -791,6 +799,11 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
[self _cleanPendingItem];
// Safely update the table size and force a synchronous layout recalculation
// BEFORE the views are reloaded, avoiding the clipping issue.
if (_itemsForRows.length !== previousRowCount)
[self noteNumberOfRowsChanged];
[super _reloadDataViews];
}
@@ -837,9 +850,20 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
for (var i = [previousItems count] - 1; i >= 0; i--)
{
var item = previousItems[i];
var item = previousItems[i],
found = NO;
if (![children containsObject:item])
// Use strict identity (===) instead of containsObject: (which triggers isEqual:)
for (var j = 0, count = children.length; j < count; j++)
{
if (children[j] === item)
{
found = YES;
break;
}
}
if (!found)
[self _addPendingItem:item];
}
}
@@ -853,7 +877,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
var children = itemInfo.children;
for (var i = [children count]; i >= 0; i--)
// Fixed out-of-bounds index (was previously [children count])
for (var i = children.length - 1; i >= 0; i--)
{
var child = children[i];
[self _addPendingItem:child];
@@ -864,7 +889,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
- (void)_cleanPendingItem
{
for (var i = [_pendingItemToClean count]; i >= 0; i--)
for (var i = [_pendingItemToClean count] - 1; i >= 0; i--)
{
var item = _pendingItemToClean[i];
@@ -908,7 +933,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
var weight = itemInfo.weight,
descendants = anItem ? [anItem] : [];
[_itemAddedDuringLastLoading addObject:anItem];
if (anItem)
[_itemAddedDuringLastLoading addObject:anItem];
if (itemInfo.isExpanded && [self _sendDataSourceShouldDeferDisplayingChildrenOfItem:anItem])
{
@@ -1085,7 +1111,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
var parent = itemInfo.parent;
// Check if the parent is the root item because we never return the actual root item
if (itemInfo[[parent UID]] === _rootItemInfo)
if (parent && _itemInfosForItems[[parent UID]] === _rootItemInfo)
parent = nil;
return parent;
@@ -1486,6 +1512,21 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
return [super frameOfDataViewAtColumn:aColumn row:aRow];
}
- (void)_applyToolTipToDataView:(CPView)aDataView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow
{
if (_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_toolTipForView_rect_tableColumn_item_mouseLocation_)
{
var item = [self itemAtRow:aRow],
tooltip = [self _sendDelegateToolTipForView:aDataView rect:[aDataView frame] tableColumn:aTableColumn item:item mouseLocation:CGPointMakeZero()];
[aDataView setToolTip:tooltip];
}
else
{
[super _applyToolTipToDataView:aDataView forTableColumn:aTableColumn row:aRow];
}
}
/*!
Retargets the drop item for the outlineview.
@@ -1975,43 +2016,28 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
return _implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldSelectItem_;
}
@end
@implementation _CPOutlineViewTableViewDataSource : CPObject <CPTableViewDataSource>
{
CPObject _outlineView;
}
- (id)initWithOutlineView:(CPOutlineView)anOutlineView
{
self = [super init];
if (self)
_outlineView = anOutlineView;
return self;
}
/*** CPTableViewDataSource methods ***/
- (CPInteger)numberOfRowsInTableView:(CPTableView)anOutlineView
{
return _outlineView._itemsForRows.length;
return _itemsForRows.length;
}
- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow
{
return [_outlineView._outlineViewDataSource outlineView:_outlineView objectValueForTableColumn:aTableColumn byItem:_outlineView._itemsForRows[aRow]];
return [_outlineViewDataSource outlineView:self objectValueForTableColumn:aTableColumn byItem:_itemsForRows[aRow]];
}
- (void)tableView:(CPTableView)aTableView setObjectValue:(id)aValue forTableColumn:(CPTableColumn)aColumn row:(CPInteger)aRow
{
if (!(_outlineView._implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_setObjectValue_forTableColumn_byItem_))
if (!(_implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_setObjectValue_forTableColumn_byItem_))
return;
[_outlineView._outlineViewDataSource outlineView:_outlineView setObjectValue:aValue forTableColumn:aColumn byItem:_outlineView._itemsForRows[aRow]];
[_outlineViewDataSource outlineView:self setObjectValue:aValue forTableColumn:aColumn byItem:_itemsForRows[aRow]];
}
- (BOOL)tableView:(CPTableView)aTableColumn writeRowsWithIndexes:(CPIndexSet)theIndexes toPasteboard:(CPPasteboard)thePasteboard
{
if (!(_outlineView._implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_writeItems_toPasteboard_))
if (!(_implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_writeItems_toPasteboard_))
return NO;
var items = [],
@@ -2019,27 +2045,27 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
while (index !== CPNotFound)
{
[items addObject:[_outlineView itemAtRow:index]];
[items addObject:[self itemAtRow:index]];
index = [theIndexes indexGreaterThanIndex:index];
}
return [_outlineView._outlineViewDataSource outlineView:_outlineView writeItems:items toPasteboard:thePasteboard];
return [_outlineViewDataSource outlineView:self writeItems:items toPasteboard:thePasteboard];
}
- (int)_childIndexForDropOperation:(CPTableViewDropOperation)theDropOperation row:(CPInteger)theRow offset:(CGPoint)theOffset
{
if (_outlineView._shouldRetargetChildIndex)
return _outlineView._retargedChildIndex;
if (_shouldRetargetChildIndex)
return _retargedChildIndex;
var childIndex = CPNotFound;
if (theDropOperation === CPTableViewDropAbove)
{
var parentItem = [_outlineView _parentItemForUpperRow:theRow - 1 andLowerRow:theRow atMouseOffset:theOffset],
itemInfo = (parentItem != nil) ? _outlineView._itemInfosForItems[[parentItem UID]] : _outlineView._rootItemInfo,
var parentItem = [self _parentItemForUpperRow:theRow - 1 andLowerRow:theRow atMouseOffset:theOffset],
itemInfo = (parentItem != nil) ? _itemInfosForItems[[parentItem UID]] : _rootItemInfo,
children = itemInfo.children;
childIndex = [children indexOfObject:[_outlineView itemAtRow:theRow]];
childIndex = [children indexOfObject:[self itemAtRow:theRow]];
if (childIndex === CPNotFound)
childIndex = children.length;
@@ -2050,155 +2076,151 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
return childIndex;
}
- (void)_parentItemForDropOperation:(CPTableViewDropOperation)theDropOperation row:(CPInteger)theRow offset:(CGPoint)theOffset
- (id)_parentItemForDropOperation:(CPTableViewDropOperation)theDropOperation row:(CPInteger)theRow offset:(CGPoint)theOffset
{
if (theDropOperation === CPTableViewDropAbove)
return [_outlineView _parentItemForUpperRow:theRow - 1 andLowerRow:theRow atMouseOffset:theOffset];
return [self _parentItemForUpperRow:theRow - 1 andLowerRow:theRow atMouseOffset:theOffset];
return [_outlineView itemAtRow:theRow];
return [self itemAtRow:theRow];
}
- (CPDragOperation)tableView:(CPTableView)aTableView validateDrop:(id /*< CPDraggingInfo >*/)theInfo
proposedRow:(CPInteger)theRow proposedDropOperation:(CPTableViewDropOperation)theOperation
{
if (!(_outlineView._implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_validateDrop_proposedItem_proposedChildIndex_))
if (!(_implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_validateDrop_proposedItem_proposedChildIndex_))
return CPDragOperationNone;
// Make sure the retargeted item and index are reset
_outlineView._retargetedItem = nil;
_outlineView._shouldRetargetItem = NO;
_retargetedItem = nil;
_shouldRetargetItem = NO;
_outlineView._retargedChildIndex = nil;
_outlineView._shouldRetargetChildIndex = NO;
_retargedChildIndex = nil;
_shouldRetargetChildIndex = NO;
var location = [_outlineView convertPoint:[theInfo draggingLocation] fromView:nil],
var location = [self convertPoint:[theInfo draggingLocation] fromView:nil],
parentItem = [self _parentItemForDropOperation:theOperation row:theRow offset:location],
childIndex = [self _childIndexForDropOperation:theOperation row:theRow offset:location];
return [_outlineView._outlineViewDataSource outlineView:_outlineView validateDrop:theInfo proposedItem:parentItem proposedChildIndex:childIndex];
return [_outlineViewDataSource outlineView:self validateDrop:theInfo proposedItem:parentItem proposedChildIndex:childIndex];
}
- (BOOL)tableView:(CPTableView)aTableView acceptDrop:(id /*<CPDraggingInfo>*/)theInfo row:(CPInteger)theRow dropOperation:(CPTableViewDropOperation)theOperation
{
if (!(_outlineView._implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_acceptDrop_item_childIndex_))
if (!(_implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_acceptDrop_item_childIndex_))
return NO;
var location = [_outlineView convertPoint:[theInfo draggingLocation] fromView:nil],
var location = [self convertPoint:[theInfo draggingLocation] fromView:nil],
parentItem = [self _parentItemForDropOperation:theOperation row:theRow offset:location],
childIndex = [self _childIndexForDropOperation:theOperation row:theRow offset:location];
_outlineView._retargetedItem = nil;
_outlineView._shouldRetargetItem = NO;
_retargetedItem = nil;
_shouldRetargetItem = NO;
_outlineView._retargedChildIndex = nil;
_outlineView._shouldRetargetChildIndex = NO;
_retargedChildIndex = nil;
_shouldRetargetChildIndex = NO;
return [_outlineView._outlineViewDataSource outlineView:_outlineView acceptDrop:theInfo item:parentItem childIndex:childIndex];
return [_outlineViewDataSource outlineView:self acceptDrop:theInfo item:parentItem childIndex:childIndex];
}
- (void)tableView:(CPTableView)aTableView sortDescriptorsDidChange:(CPArray)oldSortDescriptors
{
if ((_outlineView._implementedOutlineViewDataSourceMethods &
if ((_implementedOutlineViewDataSourceMethods &
CPOutlineViewDataSource_outlineView_sortDescriptorsDidChange_))
{
[[_outlineView dataSource] outlineView:_outlineView sortDescriptorsDidChange:oldSortDescriptors];
[[self dataSource] outlineView:self sortDescriptorsDidChange:oldSortDescriptors];
}
}
@end
/*** CPTableViewDelegate methods ***/
@implementation _CPOutlineViewTableViewDelegate : CPObject <CPTableViewDelegate>
/*!
@ignore
*/
- (CPString)_sendDelegateToolTipForView:(id)aView rect:(CGRect)aRect tableColumn:(CPTableColumn)aTableColumn item:(id)anItem mouseLocation:(CGPoint)aPoint
{
CPOutlineView _outlineView;
}
if (!(_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_toolTipForView_rect_tableColumn_item_mouseLocation_))
return nil;
- (id)initWithOutlineView:(CPOutlineView)anOutlineView
{
self = [super init];
if (self)
_outlineView = anOutlineView;
return self;
return [_outlineViewDelegate outlineView:self toolTipForView:aView rect:aRect tableColumn:aTableColumn item:anItem mouseLocation:aPoint];
}
- (BOOL)tableView:(CPTableView)theTableView shouldSelectRow:(CPInteger)theRow
{
return SHOULD_SELECT_ITEM(_outlineView, [_outlineView itemAtRow:theRow]);
return SHOULD_SELECT_ITEM(self, [self itemAtRow:theRow]);
}
- (BOOL)selectionShouldChangeInTableView:(CPTableView)theTableView
{
return SELECTION_SHOULD_CHANGE(_outlineView);
return SELECTION_SHOULD_CHANGE(self);
}
- (BOOL)tableView:(CPTableView)aTableView shouldEditTableColumn:(CPTableColumn)aColumn row:(CPInteger)aRow
{
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldEditTableColumn_item_))
return [_outlineView._outlineViewDelegate outlineView:_outlineView shouldEditTableColumn:aColumn item:[_outlineView itemAtRow:aRow]];
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldEditTableColumn_item_))
return [_outlineViewDelegate outlineView:self shouldEditTableColumn:aColumn item:[self itemAtRow:aRow]];
return NO;
}
- (float)tableView:(CPTableView)theTableView heightOfRow:(CPInteger)theRow
{
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_heightOfRowByItem_))
return [_outlineView._outlineViewDelegate outlineView:_outlineView heightOfRowByItem:[_outlineView itemAtRow:theRow]];
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_heightOfRowByItem_))
return [_outlineViewDelegate outlineView:self heightOfRowByItem:[self itemAtRow:theRow]];
return [theTableView rowHeight];
}
- (void)tableView:(CPTableView)aTableView willDisplayView:(id)aView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex
{
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_willDisplayView_forTableColumn_item_))
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_willDisplayView_forTableColumn_item_))
{
var item = [_outlineView itemAtRow:aRowIndex];
[_outlineView._outlineViewDelegate outlineView:_outlineView willDisplayView:aView forTableColumn:aTableColumn item:item];
var item = [self itemAtRow:aRowIndex];
[_outlineViewDelegate outlineView:self willDisplayView:aView forTableColumn:aTableColumn item:item];
}
}
- (void)tableView:(CPTableView)aTableView willRemoveView:(id)aView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex
{
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_willRemoveView_forTableColumn_item_))
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_willRemoveView_forTableColumn_item_))
{
var item = [_outlineView itemAtRow:aRowIndex];
[_outlineView._outlineViewDelegate outlineView:_outlineView willRemoveView:aView forTableColumn:aTableColumn item:item];
var item = [self itemAtRow:aRowIndex];
[_outlineViewDelegate outlineView:self willRemoveView:aView forTableColumn:aTableColumn item:item];
}
}
- (BOOL)tableView:(CPTableView)aTableView isGroupRow:(CPInteger)aRow
{
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_isGroupItem_))
return [_outlineView._outlineViewDelegate outlineView:_outlineView isGroupItem:[_outlineView itemAtRow:aRow]];
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_isGroupItem_))
return [_outlineViewDelegate outlineView:self isGroupItem:[self itemAtRow:aRow]];
return NO;
}
- (CPMenu)tableView:(CPTableView)aTableView menuForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow
{
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_menuForTableColumn_item_))
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_menuForTableColumn_item_))
{
var item = [_outlineView itemAtRow:aRow];
return [_outlineView._outlineViewDelegate outlineView:_outlineView menuForTableColumn:aTableColumn item:item]
var item = [self itemAtRow:aRow];
return [_outlineViewDelegate outlineView:self menuForTableColumn:aTableColumn item:item]
}
// We reimplement CPView menuForEvent: because we can't call it directly. CPTableView implements menuForEvent:
// to call this delegate method.
return [_outlineView menu] || [[_outlineView class] defaultMenu];
return [self menu] || [[self class] defaultMenu];
}
- (CPIndexSet)tableView:(CPTableView)aTableView selectionIndexesForProposedSelection:(CPIndexSet)anIndexSet
{
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_selectionIndexesForProposedSelection_))
return [_outlineView._outlineViewDelegate outlineView:_outlineView selectionIndexesForProposedSelection:anIndexSet];
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_selectionIndexesForProposedSelection_))
return [_outlineViewDelegate outlineView:self selectionIndexesForProposedSelection:anIndexSet];
return anIndexSet;
}
- (BOOL)tableView:(CPTableView)aTableView shouldSelectTableColumn:(CPTableColumn)aTableColumn
{
if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldSelectTableColumn_))
return [_outlineView._outlineViewDelegate outlineView:_outlineView shouldSelectTableColumn:aTableColumn];
if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldSelectTableColumn_))
return [_outlineViewDelegate outlineView:self shouldSelectTableColumn:aTableColumn];
return YES;
}
@@ -2304,8 +2326,8 @@ var CPOutlineViewIndentationPerLevelKey = @"CPOutlineViewIndentationPerLevelKey"
_outlineViewDataSource = [aCoder decodeObjectForKey:CPOutlineViewDataSourceKey];
_outlineViewDelegate = [aCoder decodeObjectForKey:CPOutlineViewDelegateKey];
[super setDataSource:[[_CPOutlineViewTableViewDataSource alloc] initWithOutlineView:self]];
[super setDelegate:[[_CPOutlineViewTableViewDelegate alloc] initWithOutlineView:self]];
[super setDataSource:self];
[super setDelegate:self];
[self _updateIsViewBased];
}
@@ -2344,3 +2366,242 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted)
? [CPColor colorWithCalibratedWhite:0.4 alpha: 1.0]
: [CPColor colorWithCalibratedWhite:0.5 alpha: 1.0]);
};
@implementation CPOutlineView (CPBindings)
+ (void)initialize
{
if (self !== [CPOutlineView class])
return;
[self exposeBinding:@"content"];
[self exposeBinding:@"selectionIndexPaths"];
[self exposeBinding:@"sortDescriptors"];
}
/*!
Returns the currently selected index paths.
This allows the outline view to be KVC-compliant for `selectionIndexPaths`.
*/
- (CPArray)selectionIndexPaths
{
var indexes = [self selectedRowIndexes],
paths = [CPMutableArray array],
index = [indexes firstIndex];
while (index !== CPNotFound)
{
var item = [self itemAtRow:index];
// Check if the item is a CPTreeNode proxy (which it will be when bound to CPTreeController)
if ([item respondsToSelector:@selector(indexPath)])
[paths addObject:[item indexPath]];
index = [indexes indexGreaterThanIndex:index];
}
return paths;
}
@end
@implementation CPOutlineView (CPBinder)
- (id)content { return nil; }
- (void)setContent:(id)aContent { }
- (void)setSelectionIndexPaths:(CPArray)paths { }
+ (Class)_binderClassForBinding:(CPString)aBinding
{
if (aBinding === @"content")
return [_CPOutlineViewContentBinder class];
if (aBinding === @"selectionIndexPaths")
return [_CPOutlineViewSelectionIndexPathsBinder class];
return [super _binderClassForBinding:aBinding];
}
@end
// --- Content Binder ---
/*!
_CPOutlineViewContentBinder acts as the CPOutlineViewDataSource when the outline view
is bound to a CPTreeController's arrangedObjects.
*/
@implementation _CPOutlineViewContentBinder : CPBinder
{
CPTreeNode _rootNode;
}
- (void)setValueFor:(CPString)aBinding
{
var destination = [_info objectForKey:CPObservedObjectKey],
keyPath = [_info objectForKey:CPObservedKeyPathKey],
value = [destination valueForKeyPath:keyPath];
if (!value || ![value isKindOfClass:[CPTreeNode class]])
_rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil];
else
_rootNode = value;
if ([_source dataSource] !== self)
[_source setDataSource:self];
else
[_source reloadData];
}
- (CPTreeNode)rootNode
{
return _rootNode;
}
// -- CPOutlineViewDataSource implementation --
- (id)outlineView:(CPOutlineView)outlineView child:(CPInteger)index ofItem:(id)item
{
var node = item || _rootNode;
return [[node childNodes] objectAtIndex:index];
}
- (BOOL)outlineView:(CPOutlineView)outlineView isItemExpandable:(id)item
{
var node = item || _rootNode;
return ![node isLeaf];
}
- (int)outlineView:(CPOutlineView)outlineView numberOfChildrenOfItem:(id)item
{
var node = item || _rootNode;
return [[node childNodes] count];
}
- (id)outlineView:(CPOutlineView)outlineView objectValueForTableColumn:(CPTableColumn)tableColumn byItem:(id)item
{
var rep = [item respondsToSelector:@selector(representedObject)] ? [item representedObject] : item;
// Dynamically fetch the value using the column's identifier (e.g., "name")
if (rep && [tableColumn identifier] && [tableColumn identifier] !== @"")
return [rep valueForKey:[tableColumn identifier]];
return rep;
}
- (void)outlineView:(CPOutlineView)outlineView setObjectValue:(id)value forTableColumn:(CPTableColumn)tableColumn byItem:(id)item
{
var rep = [item respondsToSelector:@selector(representedObject)] ?[item representedObject] : item;
// Push the inline edit back to the model using the column's identifier
if (rep && [tableColumn identifier] && [tableColumn identifier] !== @"")
[rep setValue:value forKey:[tableColumn identifier]];
}
- (id)content
{
// CPTableView internals probe the binder for its flat content to draw rows.
if (_source && _source._itemsForRows)
return _source._itemsForRows;
return [];
}
@end
// --- Selection Index Paths Binder ---
/*!
_CPOutlineViewSelectionIndexPathsBinder listens for selection changes on the CPOutlineView
and translates the selected rows into CPIndexPaths to push to the CPTreeController.
It also intercepts changes from the CPTreeController and auto-expands the tree to highlight them.
*/
@implementation _CPOutlineViewSelectionIndexPathsBinder : CPBinder
{
BOOL _isSyncingFromModel;
}
- (id)initWithBinding:(CPString)aBinding name:(CPString)aName to:(id)aDestination keyPath:(CPString)aKeyPath options:(CPDictionary)options from:(id)aSource
{
self = [super initWithBinding:aBinding name:aName to:aDestination keyPath:aKeyPath options:options from:aSource];
[[CPNotificationCenter defaultCenter]
addObserver:self
selector:@selector(outlineViewSelectionDidChange:)
name:CPOutlineViewSelectionDidChangeNotification
object:aSource];
}
+ (void)unbind:(CPString)aBinding forObject:(id)anObject
{
if (aBinding === "selectionIndexPaths")
[[CPNotificationCenter defaultCenter]
removeObserver:self
name:CPOutlineViewSelectionDidChangeNotification
object:anObject];
[super unbind:aBinding forObject:anObject];
}
- (void)setValueFor:(CPString)aBinding
{
// 1. SUPPRESS KVO AT THE VERY TOP to avoid circular updates when expanding parents
_isSyncingFromModel = YES;
var destination = [_info objectForKey:CPObservedObjectKey],
keyPath = [_info objectForKey:CPObservedKeyPathKey],
indexPaths = [destination valueForKeyPath:keyPath] || [],
indexes = [CPMutableIndexSet indexSet];
// 2. Fetch the root node directly from the CPTreeController (destination)
var rootNode = [destination respondsToSelector:@selector(arrangedObjects)] ? [destination arrangedObjects] : nil;
if (rootNode)
{
for (var i = 0, count = [indexPaths count]; i < count; i++)
{
var item = [rootNode descendantNodeAtIndexPath:[indexPaths objectAtIndex:i]];
if (item)
{
var parentsToExpand = [CPMutableArray array],
parent = [item parentNode];
while (parent && parent !== rootNode)
{
[parentsToExpand insertObject:parent atIndex:0];
parent = [parent parentNode];
}
for (var j = 0; j < [parentsToExpand count]; j++)
[_source expandItem:parentsToExpand[j]];
var row = [_source rowForItem:item];
if (row !== CPNotFound && row >= 0)
[indexes addIndex:row];
}
}
}
// Adjust the CPOutlineView selection
[_source selectRowIndexes:indexes byExtendingSelection:NO];
// 3. Re-enable KVO after adjustments are done
_isSyncingFromModel = NO;
}
- (void)outlineViewSelectionDidChange:(CPNotification)note
{
// We only want to push the change back if we aren't currently syncing down from the model
if (_isSyncingFromModel)
return;
// In CPBinder, reverseSetValueFor: takes the name of the property on _source
// it should fetch the updated value from. Since CPOutlineView has the selectionIndexPaths method:
[self reverseSetValueFor:@"selectionIndexPaths"];
}
@end
+6 -1
View File
@@ -269,6 +269,8 @@
_currentAnimation = nil;
_sendAction = NO;
var rows = [];
if (predicate != nil)
{
if ((_nestingMode == CPRuleEditorNestingModeSimple || _nestingMode == CPRuleEditorNestingModeCompound)
@@ -276,10 +278,13 @@
predicate = [[CPCompoundPredicate alloc] initWithType:[self _compoundPredicateTypeForRootRows] subpredicates:[CPArray arrayWithObject:predicate]];
var row = [self _rowObjectFromPredicate:predicate];
if (row != nil)
[_boundArrayOwner setValue:[CPArray arrayWithObject:row] forKey:_boundArrayKeyPath];
[rows addObject:row];
}
[_boundArrayOwner setValue:rows forKey:_boundArrayKeyPath];
[self setAnimation:animation];
}
+3 -7
View File
@@ -69,13 +69,9 @@ CPCanvasStringSizingIsFunctional = NO;
CPStringSizeMeasuringContext = CGBitmapGraphicsContextCreate();
// This is to make sure that Canvas based string sizing is functional before we use it.
// Currently, Chrome has issues with certain strings, FF had issues in the past.
// Unfortunately, this test does fit in CPCompatibility.j where things are not sufficiently initialized.
var testingFont = [CPFont systemFontOfSize:12];
var testingText = 'A A A A A A A A';
CPStringSizeMeasuringContext.font = [testingFont cssString];
CPCanvasStringSizingIsFunctional = ROUND(CPStringSizeMeasuringContext.measureText(testingText).width) == ROUND([CPPlatformString sizeOfString:testingText withFont:testingFont forWidth:NULL].width);
// Unfortunately, as of today canvas sizing is not funtional any more. Neither in Chrome nor in FF
CPCanvasStringSizingIsFunctional = NO;
}
#endif
}
+106 -13
View File
@@ -296,6 +296,8 @@ CPTableViewFirstColumnOnlyAutoresizingStyle = 5;
_CPTableDrawView _tableDrawView;
SEL _doubleAction;
id _doubleClickTarget @accessors(property=doubleClickTarget);
id _doubleClickArgument @accessors(property=doubleClickArgument);
CPInteger _clickedRow;
CPInteger _clickedColumn;
unsigned _columnAutoResizingStyle;
@@ -3486,13 +3488,73 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
[removeIndexes addIndex:columnIdx];
}
var rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])];
[self _unloadDataViewsInRows:rowIndexes columns:removeIndexes];
if ([removeIndexes count] > 0)
{
var rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])];
[self _unloadDataViewsInRows:rowIndexes columns:removeIndexes];
[_tableColumns removeObjectsAtIndexes:removeIndexes];
[_tableColumns removeObjectsAtIndexes:removeIndexes];
_dirtyTableColumnRangeIndex = 0;
[self _recalculateTableColumnRanges];
_dirtyTableColumnRangeIndex = 0;
[self _recalculateTableColumnRanges];
// Shift cached index sets downwards to account for the removed columns
var shiftIndexSet = function(indexSet)
{
var newSet = [CPIndexSet indexSet];
[indexSet enumerateIndexesUsingBlock:function(idx, stop)
{
if (![removeIndexes containsIndex:idx])
{
var shift = 0,
remIdx = [removeIndexes firstIndex];
while (remIdx !== CPNotFound && remIdx < idx)
{
shift++;
remIdx = [removeIndexes indexGreaterThanIndex:remIdx];
}
[newSet addIndex:idx - shift];
}
}];
return newSet;
};
_exposedColumns = shiftIndexSet(_exposedColumns);
_selectedColumnIndexes = shiftIndexSet(_selectedColumnIndexes);
// Shift individual index variables
var shiftIndex = function(idx)
{
if (idx === CPNotFound || idx === -1)
return idx;
if ([removeIndexes containsIndex:idx])
return CPNotFound;
var shift = 0,
remIdx = [removeIndexes firstIndex];
while (remIdx !== CPNotFound && remIdx < idx)
{
shift++;
remIdx = [removeIndexes indexGreaterThanIndex:remIdx];
}
return idx - shift;
};
_editingColumn = shiftIndex(_editingColumn);
_draggedColumnIndex = shiftIndex(_draggedColumnIndex);
if (_draggedColumnIndex === CPNotFound)
_draggedColumnIndex = -1;
_clickedColumn = shiftIndex(_clickedColumn);
if (_clickedColumn === CPNotFound)
_clickedColumn = -1;
}
[_differedColumnDataToRemove removeAllObjects];
_needsDifferedTableColumnRemove = NO;
@@ -3616,10 +3678,21 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
[self _setEditingState:NO forView:dataView];
[self _sendDelegateWillDisplayView:dataView forTableColumn:tableColumn row:row];
[self _applyToolTipToDataView:dataView forTableColumn:tableColumn row:row];
return dataView;
}
- (void)_applyToolTipToDataView:(CPView)aDataView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow
{
var tooltip = nil;
if (_implementedDelegateMethods & CPTableViewDelegate_tableView_toolTipForView_rect_tableColumn_row_mouseLocation_)
tooltip = [self _sendDelegateToolTipForView:aDataView rect:[aDataView frame] tableColumn:aTableColumn row:aRow mouseLocation:CGPointMakeZero()];
[aDataView setToolTip:tooltip];
}
- (void)_setObjectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow forView:(CPView)aDataView
{
[self _setObjectValueForTableColumn:aTableColumn row:aRow forView:aDataView useCache:!_invalidateObjectValuesCache];
@@ -3627,17 +3700,28 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
- (void)_setObjectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow forView:(CPView)aDataView useCache:(BOOL)useCache
{
var providedByDataSource = NO;
if (_implementedDataSourceMethods & CPTableViewDataSource_tableView_objectValueForTableColumn_row_)
[aDataView setObjectValue:[self _objectValueForTableColumn:aTableColumn row:aRow useCache:useCache]];
{
var objectValue = [self _objectValueForTableColumn:aTableColumn row:aRow useCache:useCache];
[aDataView setObjectValue:objectValue];
providedByDataSource = YES;
}
// This gives the table column an opportunity to apply its bindings.
// It will override the value set above if there is a binding.
// It will override the value set above if there is an explicit column binding.
var columnHasBindings = [[[CPBinder allBindingsForObject:aTableColumn] allKeys] count] > 0;
if (_contentBindingExplicitlySet)
[self _prepareContentBindedDataView:aDataView forRow:aRow];
else
// For both cell-based and view-based
if (columnHasBindings)
{
[aTableColumn _prepareDataView:aDataView forRow:aRow];
}
// Only forcefully bind the raw content object if the data source didn't already provide a formatted value
else if (_contentBindingExplicitlySet && !providedByDataSource)
{
[self _prepareContentBindedDataView:aDataView forRow:aRow];
}
}
- (void)_prepareContentBindedDataView:(CPView)dataView forRow:(CPInteger)aRow
@@ -4712,7 +4796,12 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
//double click actions
if ([[CPApp currentEvent] clickCount] === 2 && _doubleAction)
[self sendAction:_doubleAction to:_target];
{
var target = _doubleClickTarget || _target,
argument = [self infoForBinding:@"doubleClickArgument"] ? _doubleClickArgument : self;
[CPApp sendAction:_doubleAction to:target from:argument];
}
}
/*
@@ -5958,7 +6047,6 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
/*!
@ignore
Not yet implemented
*/
- (CPString)_sendDelegateToolTipForView:(id)aView rect:(CGRect)aRect tableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex mouseLocation:(CGPoint)aPoint
{
@@ -6077,6 +6165,11 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
{
if (aBinding == @"content")
_contentBindingExplicitlySet = YES;
else if (aBinding == @"doubleClickTarget")
{
if ([options objectForKey:CPSelectorNameBindingOption])
[self setDoubleAction:CPSelectorFromString([options objectForKey:CPSelectorNameBindingOption])];
}
[super bind:aBinding toObject:anObject withKeyPath:aKeyPath options:options];
}
+27 -3
View File
@@ -68,7 +68,11 @@ var CPTextFieldDOMCurrentElement = nil,
CPTextFieldCachedDragFunction = nil,
CPTextFieldBlurHandler = nil,
CPTextFieldInputFunction = nil,
CPTexFieldCurrentCSSSelectableField = nil;
CPTexFieldCurrentCSSSelectableField = nil,
CPTextFieldLastValidationFailureEvent = nil,
CPTextFieldLastValidationFailureString = nil,
CPTextFieldLastValidationFailureField = nil,
CPTextFieldLastValidationFailureResult = NO;
var CPSecureTextFieldCharacter = "\u2022";
@@ -332,7 +336,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
characters:nil
charactersIgnoringModifiers:nil
isARepeat:NO
keyCode:nil];
keyCode:nil
isActionKey:NO];
[CPTextFieldInputOwner keyUp:cappEvent];
@@ -996,7 +1001,26 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
var acceptInvalidValue = NO;
if (_implementedDelegateMethods & CPTextFieldDelegate_control_didFailToFormatString_errorDescription_)
acceptInvalidValue = [_delegate control:self didFailToFormatString:aValue errorDescription:error];
{
var currentEvent = [CPApp currentEvent];
if (currentEvent &&
CPTextFieldLastValidationFailureField === self &&
CPTextFieldLastValidationFailureString === aValue &&
CPTextFieldLastValidationFailureEvent === currentEvent)
{
acceptInvalidValue = CPTextFieldLastValidationFailureResult;
}
else
{
acceptInvalidValue = [_delegate control:self didFailToFormatString:aValue errorDescription:error];
CPTextFieldLastValidationFailureField = self;
CPTextFieldLastValidationFailureString = aValue;
CPTextFieldLastValidationFailureEvent = currentEvent;
CPTextFieldLastValidationFailureResult = acceptInvalidValue;
}
}
if (acceptInvalidValue === NO)
return NO;
+258 -66
View File
@@ -1,34 +1,29 @@
/*
* CPFontPanel.j
* AppKit
*
* TODOs:
* 1. make browser-width for size smaller and fix columns
* 2. add all the missing features from the MacOS X counterpart (sampleview)
*
*
* Created by Daniel Boehringer on 2/JAN/2014.
* All modifications copyright Daniel Boehringer 2013.
* Extensive code formatting and review by Andrew Hankinson
* Based on original work by
* Created by Emmanuel Maillard on 06/03/2010.
* Copyright Emmanuel Maillard 2010.
*
* 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
*/
CPFontPanel.j
AppKit
Created by Daniel Boehringer on 2/JAN/2014.
All modifications copyright Daniel Boehringer 2013.
Extensive code formatting and review by Andrew Hankinson
Based on original work by
Created by Emmanuel Maillard on 06/03/2010.
Copyright Emmanuel Maillard 2010.
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 "CPPanel.j"
@import "CPColorWell.j"
@@ -37,7 +32,6 @@
@import "CPText.j"
@import "CPFontManager.j"
@class CPTextStorage
@class CPLayoutManager
@class CPTextContainer
@@ -46,27 +40,30 @@
/*
Collection indexes
*/
var kTypefaceIndex_Normal = 0,
kTypefaceIndex_Italic = 1,
kTypefaceIndex_Bold = 2,
var kTypefaceIndex_Normal = 0,
kTypefaceIndex_Italic = 1,
kTypefaceIndex_Bold = 2,
kTypefaceIndex_BoldItalic = 3,
kToolbarHeight = 32,
kBorderSpacing = 6,
kInnerSpacing = 2,
kNothingChanged = 0,
kFontNameChanged = 1,
kTypefaceChanged = 2,
kSizeChanged = 3,
kTextColorChanged = 4,
kBackgroundColorChanged = 5,
kUnderlineChanged = 6,
kWeightChanged = 7,
kToolbarHeight = 32,
kPreviewHeight = 70,
kBorderSpacing = 6,
kInnerSpacing = 2,
kNothingChanged = 0,
kFontNameChanged = 1,
kTypefaceChanged = 2,
kSizeChanged = 3,
kTextColorChanged = 4,
kBackgroundColorChanged = 5,
kUnderlineChanged = 6,
kWeightChanged = 7,
_sharedFontPanel;
// FIXME<!> Locale support
var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
_availableSizes = [@"9", @"10", @"11", @"12", @"13", @"14", @"18", @"24", @"36", @"48", @"72", @"96"];
var _availableTraits = [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
_availableSizes = [@"9", @"10", @"11", @"12", @"13", @"14", @"18", @"24", @"36", @"48", @"64", @"72", @"96", @"144", @"288"];
/*!
@ingroup appkit
@@ -77,6 +74,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
id _fontBrowser;
id _traitBrowser;
id _sizeBrowser;
// Preview
_CPFontPanelPreviewView _previewView;
CPArray _availableFonts;
id _textColorWell;
CPColor _textColor;
@@ -85,7 +86,6 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
int _fontChanges;
}
#pragma mark -
#pragma mark Class methods
@@ -108,6 +108,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
return _sharedFontPanel;
}
- (BOOL)acceptsFirstResponder
{
return NO;
}
#pragma mark -
#pragma mark Init methods
@@ -115,7 +119,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
/*! @ignore */
- (id)init
{
if (self = [super initWithContentRect:CGRectMake(100, 90, 450, 394) styleMask:(CPTitledWindowMask | CPClosableWindowMask /*| CPResizableWindowMask*/ )])
if (self = [super initWithContentRect:CGRectMake(100, 90, 450, 420) styleMask:(CPTitledWindowMask | CPClosableWindowMask | CPResizableWindowMask)])
{
[[self contentView] setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]];
[self setTitle:@"Font Panel"];
@@ -152,10 +156,49 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
[aBrowser setDoubleAction:@selector(dblClicked:)];
[aBrowser setAllowsEmptySelection:NO];
[aBrowser setAllowsMultipleSelection:NO];
// Config Scrollers
//[aBrowser setHasHorizontalScroller:NO];
//[aBrowser setHasVerticalScroller:YES];
//[aBrowser setAutohidesScrollers:YES];
//[aBrowser setMaxVisibleColumns:1];
[aBrowser setDelegate:self];
[[self contentView] addSubview:aBrowser];
}
- (void)_layoutBrowsers
{
var contentView = [self contentView],
contentBounds = [contentView bounds],
previewY = kBorderSpacing + kToolbarHeight + kInnerSpacing,
browserY = previewY + kPreviewHeight + 10,
browserHeight = CGRectGetHeight(contentBounds) - browserY - 10,
availableWidth = CGRectGetWidth(contentBounds) - 20; // 10px padding L/R
// Layout Calculations
// Increase sizeWidth slightly to 60 to allow space for the vertical scrollbar without clipping text
var sizeWidth = 90,
spacing = 5,
remainingWidth = availableWidth - sizeWidth - (spacing * 2),
// Split remaining roughly 60% font name, 40% trait
fontWidth = FLOOR(remainingWidth * 0.60),
traitWidth = remainingWidth - fontWidth;
// Apply frames and column constraints
[_fontBrowser setFrame:CGRectMake(10, browserY, fontWidth, browserHeight)];
[_fontBrowser setDefaultColumnWidth:fontWidth];
[_fontBrowser setLastColumn:0];
[_traitBrowser setFrame:CGRectMake(10 + fontWidth + spacing, browserY, traitWidth, browserHeight)];
[_traitBrowser setDefaultColumnWidth:traitWidth];
[_traitBrowser setLastColumn:0];
[_sizeBrowser setFrame:CGRectMake(10 + fontWidth + traitWidth + (spacing * 2), browserY, sizeWidth, browserHeight)];
[_sizeBrowser setDefaultColumnWidth:sizeWidth];
[_sizeBrowser setLastColumn:0];
}
- (void)_setupContents
{
if (_setupDone)
@@ -163,33 +206,53 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
_setupDone = YES;
// We set ourselves as delegate to handle resizing layout manually
[self setDelegate:self];
[self _setupToolbarView];
var contentView = [self contentView],
label = [CPTextField labelWithTitle:@"Font name"],
contentBounds = [contentView bounds],
upperView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(contentBounds), CGRectGetHeight(contentBounds) - (kBorderSpacing + kToolbarHeight + kInnerSpacing))];
contentBounds = [contentView bounds];
[contentView addSubview:_toolbarView];
_fontBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(10, 35, 150, 350)];
_traitBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(155, 35, 150, 350)];
_sizeBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(300, 35, 140, 350)];
// Preview View
var previewY = kBorderSpacing + kToolbarHeight + kInnerSpacing;
_previewView = [[_CPFontPanelPreviewView alloc] initWithFrame:CGRectMake(10, previewY, CGRectGetWidth(contentBounds) - 20, kPreviewHeight)];
[_previewView setAutoresizingMask:CPViewWidthSizable];
[contentView addSubview:_previewView];
// Initialize Browsers with zero rect, _layoutBrowsers will size them
_fontBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
_traitBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
_sizeBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
// Disable autoresizing masks because we are laying out manually in windowDidResize
[_fontBrowser setAutoresizingMask:CPViewNotSizable];
[_traitBrowser setAutoresizingMask:CPViewNotSizable];
[_sizeBrowser setAutoresizingMask:CPViewNotSizable];
[self _setupBrowser:_fontBrowser];
[self _setupBrowser:_traitBrowser];
[self _setupBrowser:_sizeBrowser];
// Perform initial layout
[self _layoutBrowsers];
[[CPNotificationCenter defaultCenter] addObserver:self
selector:@selector(textViewDidChangeSelection:)
name:CPTextViewDidChangeSelectionNotification
object:nil];
}
- (void)windowDidResize:(CPNotification)aNotification
{
[self _layoutBrowsers];
}
- (void)textViewDidChangeSelection:(CPNotification)notification
{
[self _refreshWithTextView:[notification object]];
[self _refreshWithTextView:[notification object]];
}
- (void)_refreshWithTextView:(CPTextView)textView
@@ -197,6 +260,9 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
if (![self isVisible])
return;
if (![textView respondsToSelector:@selector(_attributesForFontPanel)])
return;
var attribs = [textView _attributesForFontPanel],
font = [attribs objectForKey:CPFontAttributeName] || [[textView textStorage] font] || [CPFont systemFontOfSize:12.0],
color = [attribs objectForKey:CPForegroundColorAttributeName];
@@ -217,6 +283,9 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
[self setCurrentTrait:trait];
[self setCurrentSize:[font size] + ""]; //cast to string
// Update Preview
[_previewView setPreviewFont:font];
if (!color)
return;
@@ -253,7 +322,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
{
case kFontNameChanged:
newFont = [CPFont fontWithDescriptor:[[aFont fontDescriptor] fontDescriptorByAddingAttributes:
[CPDictionary dictionaryWithObject:[self currentFont] forKey:CPFontNameAttribute]] size:0.0];
[CPDictionary dictionaryWithObject:[self currentFont] forKey:CPFontNameAttribute]] size:0.0];
break;
case kTypefaceChanged:
@@ -272,12 +341,13 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
newFont = [[CPFontManager sharedFontManager] convertFont:aFont toSize:[self currentSize]];
break;
case kNothingChanged:
case kNothingChanged:
break;
default:
CPLog.trace(@"FIXME: -[" + [self className] + " " + _cmd + "] unhandled _fontChanges: " + _fontChanges);
break;
}
return newFont;
@@ -285,7 +355,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
- (void)setCurrentSize:(CGSize)aSize
{
[_sizeBrowser selectRow:[_availableSizes indexOfObject:aSize] inColumn:0];
[_sizeBrowser selectRow:[_availableSizes indexOfObject:aSize] inColumn:0];
}
- (CPString)currentSize
@@ -295,7 +365,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
- (void)setCurrentFont:(CPFont)aFont
{
[_fontBrowser selectRow:[_availableFonts indexOfObject:[aFont familyName]] inColumn:0];
[_fontBrowser selectRow:[_availableFonts indexOfObject:[aFont familyName]] inColumn:0];
}
- (CPString)currentFont
@@ -320,9 +390,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
case kTypefaceIndex_BoldItalic:
row = 3;
break;
}
[_traitBrowser selectRow:row inColumn:0];
[_traitBrowser selectRow:row inColumn:0];
}
// FIXME<!> Locale support
@@ -370,6 +441,8 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
if ([self currentTrait] != typefaceIndex)
[self setCurrentTrait:typefaceIndex ];
[_previewView setPreviewFont:font];
_fontChanges = kNothingChanged;
}
@@ -382,23 +455,30 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
////////////////////////////////////////////////////////////////////
// TODO: ask CPFontManager for traits //
- (void)browserClicked:(id)aBrowser
{
if (aBrowser === _fontBrowser)
{
_fontChanges = kFontNameChanged;
[[CPFontManager sharedFontManager] modifyFontViaPanel:self];
}
else if (aBrowser === _traitBrowser)
{
_fontChanges = kTypefaceChanged;
[[CPFontManager sharedFontManager] modifyFontViaPanel:self];
}
else if (aBrowser === _sizeBrowser)
{
_fontChanges = kSizeChanged;
[[CPFontManager sharedFontManager] modifyFontViaPanel:self];
}
// Apply change immediately to manager (standard behavior)
[[CPFontManager sharedFontManager] modifyFontViaPanel:self];
// Update our preview manually because convertFont: calls rely on selected rows
// We construct a temporary font to update the preview view immediately
var updatedFont = [self panelConvertFont:[_previewView font]];
if (updatedFont)
[_previewView setPreviewFont:updatedFont];
}
- (void)dblClicked:(id)sender
@@ -414,7 +494,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
if (aBrowser === _traitBrowser)
return [_availableTraits count];
return [_availableSizes count]
return [_availableSizes count];
}
- (id)browser:(id)aBrowser child:(int)index ofItem:(id)anItem
@@ -440,4 +520,116 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
@end
// -----------------------------------------------------------------------------
// _CPFontPanelPreviewView
// A helper class to display a font sample with metrics grid
// -----------------------------------------------------------------------------
@implementation _CPFontPanelPreviewView : CPView
{
CPTextField _sampleText;
CPColor _gridColor;
float _gridSize;
}
- (id)initWithFrame:(CGRect)aRect
{
self = [super initWithFrame:aRect];
if (self)
{
[self setBackgroundColor:[CPColor whiteColor]];
_gridColor = [CPColor colorWithHexString:@"e4f4ff"];
_gridSize = 10.0;
_sampleText = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(aRect), CGRectGetHeight(aRect))];
[_sampleText setStringValue:@"AaYy-0123"];
[_sampleText setAlignment:CPCenterTextAlignment];
[_sampleText setVerticalAlignment:CPCenterVerticalTextAlignment];
[_sampleText setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[_sampleText setTextColor:[CPColor blackColor]];
[self addSubview:_sampleText];
}
return self;
}
- (void)setPreviewFont:(CPFont)aFont
{
[_sampleText setFont:aFont];
[self setNeedsDisplay:YES];
}
- (CPFont)font
{
return [_sampleText font];
}
- (void)drawRect:(CGRect)dirtyRect
{
// Draw Grid (from MetricsView inspiration)
var context = [[CPGraphicsContext currentContext] graphicsPort],
bounds = [self bounds],
maxX = CGRectGetMaxX(bounds),
maxY = CGRectGetMaxY(bounds);
CGContextSetLineWidth(context, 1.0);
CGContextSetStrokeColor(context, _gridColor);
CGContextBeginPath(context);
for (var y = 0.5; y <= maxY; y += _gridSize)
{
CGContextMoveToPoint(context, 0.0, y);
CGContextAddLineToPoint(context, maxX, y);
}
for (var x = 0.5; x <= maxX; x += _gridSize)
{
CGContextMoveToPoint(context, x, 0.0);
CGContextAddLineToPoint(context, x, maxY);
}
CGContextStrokePath(context);
// Draw Baseline/Ascender/Descender (from BaselineView inspiration)
var font = [_sampleText font];
if (!font) return;
var ascender = [font ascender],
descender = [font descender],
lineHeight = [font defaultLineHeightForFont];
// Calculate the baseline.
// CPTextField with CPCenterVerticalTextAlignment usually centers the line height.
// Top of line = midY - (lineHeight / 2.0)
// Baseline = Top of line + ascender
var midY = maxY / 2.0,
baselineY = midY - (lineHeight / 2.0) + ascender;
CGContextSetStrokeColor(context, [CPColor redColor]);
CGContextBeginPath(context);
// Baseline
CGContextMoveToPoint(context, 0, baselineY);
CGContextAddLineToPoint(context, maxX, baselineY);
// Ascender Line
CGContextMoveToPoint(context, 0, baselineY - ascender);
CGContextAddLineToPoint(context, maxX, baselineY - ascender);
// Descender Line
CGContextMoveToPoint(context, 0, baselineY - descender);
CGContextAddLineToPoint(context, maxX, baselineY - descender);
CGContextStrokePath(context);
}
- (void)mouseDown:(CPEvent)anEvent
{
var text = prompt("Enter sample text:", [_sampleText stringValue]);
if (text)
[_sampleText setStringValue:text];
}
@end
[CPFontManager setFontPanelFactory:[CPFontPanel class]];
+12 -9
View File
@@ -286,6 +286,8 @@ _oncontextmenuhandler = function () { return false; };
if (removeRange.length)
_removeInvalidLineFragmentsRange = CPMakeRangeCopy(removeRange);
else
_removeInvalidLineFragmentsRange = nil;
// We erased all lines
if (!startIndex)
@@ -303,13 +305,17 @@ _oncontextmenuhandler = function () { return false; };
- (BOOL)_rescuingInvalidFragmentsWasPossibleForGlyphRange:(CPRange)aRange
{
var l = _lineFragments.length,
location = aRange.location,
found = NO,
targetLine = 0;
// 1. EARLY EXIT: If there are no fragments to rescue (e.g. setting new text), do nothing.
if (!_lineFragmentsForRescue || _lineFragmentsForRescue.length === 0)
return NO;
// try to find the first linefragment of the desired range
for (; targetLine < l; targetLine++)
var l = _lineFragments.length,
location = aRange.location,
found = NO,
targetLine = l - 1; // Start from the END of the array
// 2. REVERSE SEARCH: The fragment we want is almost always at the end.
for (; targetLine >= 0; targetLine--)
{
if (CPLocationInRange(location, _lineFragments[targetLine]._range))
{
@@ -332,9 +338,6 @@ _oncontextmenuhandler = function () { return false; };
newLength = [[_textStorage string].length],
removalSkip = 1;
// if (ABS(newLength - oldLength) > 1)
// return NO;
if (![oldLineFragment isVisuallyIdenticalToFragment:newLineFragment])
{
isIdentical = NO;
+299 -272
View File
@@ -450,19 +450,25 @@ var kDelegateRespondsTo_textShouldBeginEditing
{
[super copy:sender];
if (![self isRichText])
return;
var selectedRange = [self selectedRange],
pasteboard = [CPPasteboard generalPasteboard],
stringForPasting = [[self textStorage] attributedSubstringFromRange:CPMakeRangeCopy(selectedRange)],
richData = [_CPRTFProducer produceRTF:stringForPasting documentAttributes:@{}];
if ([self isRichText])
{
[pasteboard declareTypes:[CPStringPboardType, CPRTFPboardType, _CPSmartPboardType, _CPASPboardType] owner:nil];
[pasteboard setString:[stringForPasting._string stringByReplacingOccurrencesOfString:_CPAttachmentCharacterAsString withString:''] forType:CPStringPboardType];
[pasteboard setString:richData forType:CPRTFPboardType];
[pasteboard setString:_previousSelectionGranularity + '' forType:_CPSmartPboardType];
[pasteboard setString:[[CPKeyedArchiver archivedDataWithRootObject:stringForPasting] rawString] forType:_CPASPboardType];
}
else
{
[pasteboard declareTypes:[CPStringPboardType, _CPSmartPboardType] owner:nil];
[pasteboard setString:stringForPasting._string forType:CPStringPboardType];
[pasteboard setString:_previousSelectionGranularity + '' forType:_CPSmartPboardType];
}
}
- (void)_pasteString:(id)stringForPasting
@@ -470,6 +476,9 @@ var kDelegateRespondsTo_textShouldBeginEditing
if (!stringForPasting)
return;
if (![stringForPasting isKindOfClass:[CPAttributedString class]])
stringForPasting = [[CPAttributedString alloc] initWithString:stringForPasting attributes:nil];
var shouldUseSmartPasting = [self _shouldUseSmartPasting];
if (shouldUseSmartPasting)
@@ -511,8 +520,8 @@ var kDelegateRespondsTo_textShouldBeginEditing
- (void)paste:(id)sender
{
if (![sender isKindOfClass:_CPNativeInputManager] && [[CPApp currentEvent] type] != CPAppKitDefined)
return
if ([[CPApp currentEvent] type] != CPAppKitDefined)
return;
[self _pasteString:[self _stringForPasting]];
}
@@ -525,6 +534,11 @@ var kDelegateRespondsTo_textShouldBeginEditing
return [self isSelectable]; // editable textviews are automatically selectable
}
- (BOOL)acceptsFirstMouse:(CPEvent)anEvent
{
return YES;
}
- (void)_becomeFirstResponder
{
[self updateInsertionPointStateAndRestartTimer:YES];
@@ -1013,35 +1027,6 @@ Sets the selection to a range of characters in response to user action.
}
#endif
// interface to the _CPNativeInputManager
- (void)_activateNativeInputElement:(DOMElement)aNativeField
{
var attributes = [[self typingAttributes] copy];
// make it invisible
[attributes setObject:[CPColor colorWithRed:1 green:1 blue:1 alpha:0] forKey:CPForegroundColorAttributeName];
// FIXME: this hack to provide the visual space for the inputmanager should at least bypass the undomanager
var placeholderString = [[CPAttributedString alloc] initWithString:aNativeField.innerHTML attributes:attributes];
[self insertText:placeholderString];
var caretOrigin = [_layoutManager boundingRectForGlyphRange:CPMakeRange(MAX(0, _selectionRange.location - 1), 1) inTextContainer:_textContainer].origin;
caretOrigin.y += [_layoutManager _characterOffsetAtLocation:MAX(0, _selectionRange.location - 1)];
caretOrigin.x += 2; // two pixel offset to the LHS character
var cumulativeOffset = [self _cumulativeOffset];
#if PLATFORM(DOM)
aNativeField.style.left = (caretOrigin.x + cumulativeOffset.x) + "px";
aNativeField.style.top = (caretOrigin.y + cumulativeOffset.y) + "px";
aNativeField.style.font = [[_typingAttributes objectForKey:CPFontAttributeName] cssString];
aNativeField.style.color = [[_typingAttributes objectForKey:CPForegroundColorAttributeName] cssString];
#endif
[_caret setVisibility:NO]; // hide our caret because now the system caret takes over
}
- (CPArray)selectedRanges
{
return [_selectionRange];
@@ -1052,11 +1037,18 @@ Sets the selection to a range of characters in response to user action.
- (void)keyDown:(CPEvent)event
{
[[_window platformWindow] _propagateCurrentDOMEvent:YES];
[[_window platformWindow] _propagateCurrentDOMEvent:YES]; // for the _CPNativeInputManager (necessary at least on FF and chrome)
if (![_CPNativeInputManager isNativeInputFieldActive] && ![_CPNativeInputManager isDeadKey:event])
if ([event _isActionOrCommandEvent])
{
// This is a navigation key, action key, or command shortcut.
// Let the Cappuccino framework's key binding system handle it.
[self interpretKeyEvents:[event]];
}
// This is a normal printable character ('a', '1', '$', 'é').
// We do nothing, preventing the double-insertion bug. The _CPNativeInputManager
// will capture it from the hidden input field and insert it correctly.
[_caret setPermanentlyVisible:YES];
}
@@ -1117,6 +1109,9 @@ Sets the selection to a range of characters in response to user action.
if (![self isSelectable])
return;
// this is for the ipad-keyboard
[_CPNativeInputManager focusForClipboardOfTextView:self];
[_CPNativeInputManager cancelCurrentInputSessionIfNeeded];
[_caret setVisibility:NO];
@@ -1724,6 +1719,9 @@ Sets the selection to a range of characters in response to user action.
- (void)cut:(id)sender
{
if ([[CPApp currentEvent] type] != CPAppKitDefined)
return;
var selectedRange = [self selectedRange];
if (selectedRange.length < 1)
@@ -1873,7 +1871,11 @@ Sets the selection to a range of characters in response to user action.
}
else
{
[_typingAttributes setObject:[sender selectedFont] forKey:CPFontAttributeName];
attributes = [_textStorage attributesAtIndex:_selectionRange.location
longestEffectiveRange:_selectionRange
inRange:_selectionRange];
oldFont = [attributes objectForKey:CPFontAttributeName] || [self font];
[_typingAttributes setObject:[sender convertFont:oldFont] forKey:CPFontAttributeName];
}
}
else
@@ -1930,6 +1932,87 @@ Sets the selection to a range of characters in response to user action.
[_layoutManager textStorage:_textStorage edited:0 range:CPMakeRangeCopy(range) changeInLength:0 invalidatedRange:CPMakeRangeCopy(range)];
}
#pragma mark -
#pragma mark Style & Alignment methods
- (void)bold:(id)sender
{
// This will trigger changeFont: via the FontManager
[[CPFontManager sharedFontManager] addFontTrait:CPBoldFontMask];
}
- (void)italic:(id)sender
{
// This will trigger changeFont: via the FontManager
[[CPFontManager sharedFontManager] addFontTrait:CPItalicFontMask];
}
- (void)alignLeft:(id)sender
{
[self _setAlignment:CPLeftTextAlignment];
}
- (void)alignCenter:(id)sender
{
[self _setAlignment:CPCenterTextAlignment];
}
- (void)alignRight:(id)sender
{
[self _setAlignment:CPRightTextAlignment];
}
- (void)alignJustified:(id)sender
{
[self _setAlignment:CPJustifiedTextAlignment];
}
- (void)_setAlignment:(CPTextAlignment)anAlignment
{
if (![self _didBeginEditing] || ![self shouldChangeTextInRange:_selectionRange replacementString:nil])
return;
var style = [CPParagraphStyle defaultParagraphStyle],
currentAttributes = _typingAttributes;
// Attempt to grab existing style from selection to preserve other paragraph settings
if (_selectionRange.length > 0)
currentAttributes = [_textStorage attributesAtIndex:_selectionRange.location effectiveRange:nil];
if ([currentAttributes objectForKey:CPParagraphStyleAttributeName])
style = [currentAttributes objectForKey:CPParagraphStyleAttributeName];
// Create new style with modified alignment
var newStyle = [style mutableCopy];
[newStyle setAlignment:anAlignment];
if (_selectionRange.length > 0)
{
// Add rudimentary undo support
var undoManager = [[self window] undoManager];
if (undoManager)
{
[[undoManager prepareWithInvocationTarget:self]
_setAlignment:[style alignment]];
}
[_textStorage addAttribute:CPParagraphStyleAttributeName value:newStyle range:CPMakeRangeCopy(_selectionRange)];
// Notify layout manager of changes
[_layoutManager textStorage:_textStorage
edited:0
range:CPMakeRangeCopy(_selectionRange)
changeInLength:0
invalidatedRange:CPMakeRangeCopy(_selectionRange)];
}
else
{
// Update typing attributes for next character
[_typingAttributes setObject:newStyle forKey:CPParagraphStyleAttributeName];
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
}
}
- (void)underline:(id)sender
{
if (![self _didBeginEditing] || ![self shouldChangeTextInRange:_selectionRange replacementString:nil])
@@ -2216,6 +2299,9 @@ Sets the selection to a range of characters in response to user action.
caretRect.origin.x += _textContainerOrigin.x;
caretRect.origin.y += _textContainerOrigin.y;
caretRect.size.width = MAX(1.0, caretRect.size.width);
caretRect.size.height = MAX(1.0, caretRect.size.height);
return caretRect;
}
- (void)updateInsertionPointStateAndRestartTimer:(BOOL)flag
@@ -2627,59 +2713,28 @@ var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey",
var _CPNativeInputField,
_CPNativeInputFieldKeyDownCalled,
_CPNativeInputFieldKeyUpCalled,
_CPNativeInputFieldKeyPressedCalled,
_CPNativeInputFieldActive;
_isComposing = NO; // Flag to track if an IME/dead key session is active.
var _CPCopyPlaceholder = '-';
@implementation _CPNativeInputManager : CPObject
+ (BOOL)isNativeInputFieldActive
{
return _CPNativeInputFieldActive;
}
+ (void)isDeadKey:(CPEvent)event
{
#if PLATFORM(DOM)
return event._DOMEvent && (event._DOMEvent.key == 'Dead' || event._DOMEvent.key == 'Process');
return event._DOMEvent && (event._DOMEvent.key === 'Dead' || event._DOMEvent.key === 'Process');
#endif
return NO;
}
+ (void)cancelCurrentNativeInputSession
{
#if PLATFORM(DOM)
_CPNativeInputField.innerHTML = '';
#endif
[self _endInputSessionWithString:_CPNativeInputField.innerHTML];
}
+ (void)cancelCurrentInputSessionIfNeeded
{
if (!_CPNativeInputFieldActive)
return;
[self cancelCurrentNativeInputSession];
}
+ (void)_endInputSessionWithString:(CPString)aStr
{
_CPNativeInputFieldActive = NO;
var currentFirstResponder = [[CPApp keyWindow] firstResponder],
placeholderRange = CPMakeRange([currentFirstResponder selectedRange].location - 1, 1);
[currentFirstResponder setSelectedRange:placeholderRange];
[currentFirstResponder insertText:aStr];
_CPNativeInputField.innerHTML = '';
[self hideInputElement];
[currentFirstResponder updateInsertionPointStateAndRestartTimer:YES];
#if PLATFORM(DOM)
if (_CPNativeInputField) {
_CPNativeInputField.innerHTML = '';
}
_isComposing = NO;
#endif
}
+ (void)initialize
@@ -2687,236 +2742,208 @@ var _CPCopyPlaceholder = '-';
#if PLATFORM(DOM)
_CPNativeInputField = document.createElement("div");
_CPNativeInputField.contentEditable = YES;
_CPNativeInputField.style.width = "64px";
_CPNativeInputField.style.zIndex = 10000;
// Style the input field to be invisible but focusable
_CPNativeInputField.style.position = "absolute";
_CPNativeInputField.style.visibility = "visible";
_CPNativeInputField.style.padding = "0px";
_CPNativeInputField.style.margin = "0px";
_CPNativeInputField.style.top = "-1000px";
_CPNativeInputField.style.left = "-1000px";
_CPNativeInputField.style.width = "1px";
_CPNativeInputField.style.height = "1px";
_CPNativeInputField.style.opacity = "0";
_CPNativeInputField.style.overflow = "hidden";
_CPNativeInputField.style.whiteSpace = "pre";
_CPNativeInputField.style.outline = "0px solid transparent";
_CPNativeInputField.style.zIndex = -1; // Put it behind everything
document.body.appendChild(_CPNativeInputField);
_CPNativeInputField.addEventListener("keyup", function(e)
// Central function to handle inserting text into the CPTextView
var handleInput = function(textToInsert)
{
_CPNativeInputFieldKeyUpCalled = YES;
// filter out the shift-up, cursor keys and friends used to access the deadkeys
// fixme: e.which is depreciated(?) -> find a better way to identify the modifier-keyups
if (e.which < 27 || e.which == 91 || e.which == 93) // include apple command keys
{
if (e.which == 13)
_CPNativeInputField.innerHTML = '';
if (_CPNativeInputField.innerHTML.length == 0 || _CPNativeInputField.innerHTML.length > 2) // backspace
[self cancelCurrentInputSessionIfNeeded];
return false; // prevent the default behaviour
}
var currentFirstResponder = [[CPApp keyWindow] firstResponder];
if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)])
return false; // prevent the default behaviour
// chrome-trigger: keypressed is omitted for deadkeys
if (!_CPNativeInputFieldActive && _CPNativeInputFieldKeyPressedCalled == NO && _CPNativeInputField.innerHTML.length && _CPNativeInputField.innerHTML != _CPCopyPlaceholder && _CPNativeInputField.innerHTML.length < 3)
{
_CPNativeInputFieldActive = YES;
[currentFirstResponder _activateNativeInputElement:_CPNativeInputField];
}
else
{
if (_CPNativeInputFieldActive)
[self _endInputSessionWithString:_CPNativeInputField.innerHTML];
// prevent the copy placeholder beeing removed by cursor keys
if (_CPNativeInputFieldKeyPressedCalled)
_CPNativeInputField.innerHTML = '';
}
_CPNativeInputFieldKeyDownCalled = NO;
return false; // prevent the default behaviour
}, true);
_CPNativeInputField.addEventListener("keydown", function(e)
{
// this protects from heavy typing and the shift key
if (_CPNativeInputFieldKeyDownCalled)
return true;
_CPNativeInputFieldKeyDownCalled = YES;
_CPNativeInputFieldKeyUpCalled = NO;
_CPNativeInputFieldKeyPressedCalled = NO;
var currentFirstResponder = [[CPApp keyWindow] firstResponder];
// webkit-browsers: cursor keys do not emit keypressed and would otherwise activate deadkey mode
if (!CPBrowserIsEngine(CPGeckoBrowserEngine) && e.which >= 37 && e.which <= 40)
_CPNativeInputFieldKeyPressedCalled = YES;
if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)])
if (!textToInsert)
return;
// FF-trigger: here the best way to detect a dead key is the missing keyup event
if (CPBrowserIsEngine(CPGeckoBrowserEngine))
setTimeout(function(){
_CPNativeInputFieldKeyDownCalled = NO;
var currentFirstResponder = [[CPApp keyWindow] firstResponder];
if (!_CPNativeInputFieldActive && _CPNativeInputFieldKeyUpCalled == NO && _CPNativeInputField.innerHTML.length && _CPNativeInputField.innerHTML != _CPCopyPlaceholder && _CPNativeInputField.innerHTML.length < 3 && !e.repeat)
{
_CPNativeInputFieldActive = YES;
[currentFirstResponder _activateNativeInputElement:_CPNativeInputField];
}
else if (!_CPNativeInputFieldActive)
[self hideInputElement];
}, 200);
if (currentFirstResponder && [currentFirstResponder respondsToSelector:@selector(insertText:)])
var event = [CPApp currentEvent];
return false;
}, true); // capture mode
if (!event._isKeyEquivalent)
setTimeout(function(){
[currentFirstResponder insertText:textToInsert]
}, 20);
_CPNativeInputField.addEventListener("keypress", function(e)
{
_CPNativeInputFieldKeyUpCalled = YES;
_CPNativeInputFieldKeyPressedCalled = YES;
return false;
}, true); // capture mode
_CPNativeInputField.onpaste = function(e)
{
var nativeClipboard = (e.originalEvent || e).clipboardData,
richtext,
pasteboard = [CPPasteboard generalPasteboard],
currentFirstResponder = [[CPApp keyWindow] firstResponder],
isPlain = NO;
if ([currentFirstResponder respondsToSelector:@selector(isRichText)] && ![currentFirstResponder isRichText])
isPlain = YES;
// this is the rich chrome / FF codepath (where we can use RTF directly)
if ((richtext = nativeClipboard.getData('text/rtf')) && !(!!(e.originalEvent || e).shiftKey) && !isPlain)
{
e.preventDefault();
// setTimeout to prevent flickering in FF
setTimeout(function(){
[currentFirstResponder insertText:[[_CPRTFParser new] parseRTF:richtext]]
}, 20);
return false;
}
// plain is the same in all browsers...
var data = e.clipboardData.getData('text/plain'),
cappString = [pasteboard stringForType:CPStringPboardType];
if (cappString != data)
{
[pasteboard declareTypes:[CPStringPboardType] owner:nil];
[pasteboard setString:data forType:CPStringPboardType];
}
setTimeout(function(){ // prevent dom-flickering (only needed for FF)
[currentFirstResponder paste:self];
}, 20);
return false;
// Clear the field immediately after grabbing its content.
_CPNativeInputField.innerHTML = '';
};
if (CPBrowserIsEngine(CPGeckoBrowserEngine))
{
_CPNativeInputField.oncopy = function(e)
{
var pasteboard = [CPPasteboard generalPasteboard],
string,
currentFirstResponder = [[CPApp keyWindow] firstResponder];
// Intercept problematic keys before the browser acts.
_CPNativeInputField.addEventListener('keydown', function(e) {
[currentFirstResponder copy:self];
var stringForPasting = [pasteboard stringForType:CPStringPboardType];
e.clipboardData.setData('text/plain', stringForPasting);
return false;
};
_CPNativeInputField.oncut = function(e)
{
var pasteboard = [CPPasteboard generalPasteboard],
string,
currentFirstResponder = [[CPApp keyWindow] firstResponder];
// prevent dom-flickering
setTimeout(function(){
[currentFirstResponder cut:self];
}, 20);
// this is necessary because cut will only execute in the future
[currentFirstResponder copy:self];
var stringForPasting = [pasteboard stringForType:CPStringPboardType];
e.clipboardData.setData('text/plain', stringForPasting);
return false;
if (e.key === 'Enter' || (e.key === 'Backspace' && _CPNativeInputField.innerHTML === '')) {
// Prevent browser default action:
// - 'Enter': Prevents inserting <div><br></div>.
// - 'Backspace' on empty: Prevents inserting junk characters on iPadOS.
e.preventDefault();
}
}
});
// This listener handles all other character input.
_CPNativeInputField.addEventListener('input', function(e)
{
// If we are in a composition (e.g., IME), do nothing yet.
if (_isComposing)
return;
// Safety net: ignore deletion events, as they are handled by keydown.
if (e.inputType && e.inputType.startsWith('delete'))
{
_CPNativeInputField.innerHTML = '';
return;
}
// Robustness: Use 'textContent' instead of 'innerHTML' to strip any
// unexpected HTML tags the browser might have inserted.
var textToInsert = e.target.textContent;
handleInput(textToInsert);
});
// Fires when a composition session starts (e.g., user presses a dead key or starts an IME).
_CPNativeInputField.addEventListener('compositionstart', function(e) {
_isComposing = YES;
});
// Fires when the composition is finished.
_CPNativeInputField.addEventListener('compositionend', function(e) {
// The composition is over. `e.data` has the final string (e.g., "é").
handleInput(e.data);
_isComposing = NO;
});
// PASTE handler
_CPNativeInputField.onpaste = function(e)
{
e.preventDefault();
var nativeClipboard = (e.originalEvent || e).clipboardData;
var currentFirstResponder = [[CPApp keyWindow] firstResponder];
// Can we accept richtext? Then this is our preference (fixme: shift key to force plain text paste)
if ([currentFirstResponder isRichText])
{
var richtext = nativeClipboard.getData('text/rtf');
// prefer RTF form the outside of cappuccino
if (richtext)
richtext = [[_CPRTFParser new] parseRTF:richtext];
else
{
var pasteboard = [CPPasteboard generalPasteboard];
// If no RTF is available, try to get the internal represatation of richtext from the pasteboard
var richData = [pasteboard stringForType:_CPASPboardType];
if (richData)
richtext = [CPKeyedUnarchiver unarchiveObjectWithData:[CPData dataWithRawString:richData]];
}
if (richtext)
{
[currentFirstResponder _pasteString:richtext];
return;
}
// If no richtext is available, fall back to plain text
}
var nativeString = nativeClipboard.getData('text/plain');
// Use setTimeout to prevent flickering
setTimeout(function()
{
[currentFirstResponder _pasteString:nativeString || [pasteboard stringForType:CPStringPboardType] || ''];
}, 20);
};
// COPY handler
_CPNativeInputField.oncopy = function(e)
{
e.preventDefault();
var pasteboard = [CPPasteboard generalPasteboard];
var nativeClipboard = (e.originalEvent || e).clipboardData;
// First, copy the data to populate the CP clipboard
[[[CPApp keyWindow] firstResponder] copy:self];
// Now, copy the data over to the native clipboard
var stringForPasting = [pasteboard stringForType:CPStringPboardType] || '';
nativeClipboard.setData('text/plain', stringForPasting);
var rtfForPasting = [pasteboard stringForType:CPRTFPboardType];
if (rtfForPasting)
nativeClipboard.setData('text/rtf', rtfForPasting);
};
// CUT handler
_CPNativeInputField.oncut = function(e)
{
e.preventDefault();
var pasteboard = [CPPasteboard generalPasteboard];
var nativeClipboard = (e.originalEvent || e).clipboardData;
var currentFirstResponder = [[CPApp keyWindow] firstResponder];
// First, copy the data to populate the CP clipboard
[currentFirstResponder copy:self];
// Now, copy the data to the native clipboard
var stringForPasting = [pasteboard stringForType:CPStringPboardType] || '';
nativeClipboard.setData('text/plain', stringForPasting);
var rtfForPasting = [pasteboard stringForType:CPRTFPboardType];
if (rtfForPasting)
nativeClipboard.setData('text/rtf', rtfForPasting);
// Then, perform the delete part of the cut operation in the text view
// Use setTimeout to prevent flickering
setTimeout(function()
{
[currentFirstResponder deleteBackward:self];
}, 20);
};
#endif
}
+ (void)focusForTextView:(CPTextView)currentFirstResponder
{
if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)])
return;
[self hideInputElement];
#if PLATFORM(DOM)
_CPNativeInputField.focus();
#endif
if (_CPNativeInputField && document.activeElement !== _CPNativeInputField)
_CPNativeInputField.focus();
#endif
}
+ (void)focusForClipboardOfTextView:(CPTextView)textview
{
#if PLATFORM(DOM)
if (!_CPNativeInputFieldActive && _CPNativeInputField.innerHTML.length == 0)
_CPNativeInputField.innerHTML = _CPCopyPlaceholder; // make sure we have a selection to allow the native pasteboard work in safari
var selectedRange = [textview selectedRange];
if (selectedRange.length > 0) {
// Put the selected text into the hidden div so the browser can natively copy it.
var textToCopy = [[[textview textStorage] string] substringWithRange:selectedRange];
_CPNativeInputField.innerHTML = textToCopy;
} else {
// For paste, we just need the field to be focusable.
_CPNativeInputField.innerHTML = _CPCopyPlaceholder;
}
[self focusForTextView:textview];
// select all in the contenteditable div (http://stackoverflow.com/questions/12243898/how-to-select-all-text-in-contenteditable-div)
if (document.body.createTextRange)
{
var range = document.body.createTextRange();
range.moveToElementText(_CPNativeInputField);
range.select();
}
else if (window.getSelection)
{
var selection = window.getSelection(),
range = document.createRange();
// Select the content of the hidden div so copy/cut works.
if (window.getSelection && document.createRange) {
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(_CPNativeInputField);
selection.removeAllRanges();
selection.addRange(range);
}
#endif
}
+ (void)hideInputElement
{
#if PLATFORM(DOM)
_CPNativeInputField.style.top = "-10000px";
_CPNativeInputField.style.left = "-10000px";
#endif
}
@end
+11
View File
@@ -365,6 +365,17 @@ var CPSystemTypesetterFactory,
wrapWidth = rangeWidth;
wrapRange._height = _lineHeight;
wrapRange._base = _lineBase;
// Optimization: Start measuring from the next character to avoid O(n^2)
// string width calculation within a line since spaces do not carry ligatures or kerning.
// Only reset the measuring range if the next character is NOT another space.
// This prevents compounded subpixel rounding errors with contiguous spaces.
if (theString.charCodeAt(glyphIndex + 1) !== 32)
{
currentAnchor = rangeWidth;
measuringRange = CPMakeRange(glyphIndex + 1, 0);
}
break;
case 10:
+33 -46
View File
@@ -40,6 +40,15 @@ FIXME: this class should be redone using a 'real' parser
var hexTable = [];
var cp1252Map = {
0x80: 0x20AC, 0x82: 0x201A, 0x83: 0x0192, 0x84: 0x201E, 0x85: 0x2026,
0x86: 0x2020, 0x87: 0x2021, 0x88: 0x02C6, 0x89: 0x2030, 0x8A: 0x0160,
0x8B: 0x2039, 0x8C: 0x0152, 0x8E: 0x017D, 0x91: 0x2018, 0x92: 0x2019,
0x93: 0x201C, 0x94: 0x201D, 0x95: 0x2022, 0x96: 0x2013, 0x97: 0x2014,
0x98: 0x02DC, 0x99: 0x2122, 0x9A: 0x0161, 0x9B: 0x203A, 0x9C: 0x0153,
0x9E: 0x017E, 0x9F: 0x0178
};
// Hold the attributes of the current run
@implementation _RTFAttribute : CPObject
{
@@ -321,17 +330,16 @@ var kRgsymRtf = {
- (BOOL)pushState
{
_states.push["group"];
_states.push(_curState);
return YES;
}
- (BOOL)popState
{
_states.pop();
if (_curState > 0)
_curState--;
if (_states.length > 0)
{
_curState = _states.pop();
}
return YES;
}
@@ -346,25 +354,23 @@ var kRgsymRtf = {
return '';
case "ipfnHex":
ch = _rtf.charAt(++_currentParseIndex);
var hex = '';
while (new RegExp("[a-fA-F0-9\\']").test(ch))
// Konsumiere exakt 2 Zeichen nach dem \'
for (var i = 0; i < 2; i++)
{
if (ch == "'")
var nextCh = _rtf.charAt(++_currentParseIndex);
if (/[a-fA-F0-9]/.test(nextCh))
{
_currentParseIndex++;
continue;
hex += nextCh;
}
else
{
_currentParseIndex--;
break;
}
hex += (ch + '');
ch = _rtf.charAt(++_currentParseIndex);
}
//ch = parseInt(ch, 16);
//console.log("hex : " + hex);
_hexreturn = YES;
_currentParseIndex--;
if (_curState !== 0)
return '';
@@ -596,9 +602,6 @@ var kRgsymRtf = {
}
if (_states.length > 0)
_curState = 1;
return '';
}
}
@@ -734,32 +737,16 @@ var kRgsymRtf = {
{
if (ch.length > 0)
{
if (parseInt(ch, 16) & 0x80)
{
hex += ch.toUpperCase();
}
else
{
[self _appendPlainString: String.fromCharCode(parseInt((hex + ch), 16))];
hex = '';
}
if (hex.length == 4)
{
var temp = parseInt(hex, 16);
if (hexTable && hexTable[hex.toUpperCase()] !== undefined)
temp = parseInt(hexTable[hex.toUpperCase()], 16);
[self _appendPlainString: String.fromCharCode(temp)];
hex = '';
var byteVal = parseInt(ch, 16);
var unicodeVal = byteVal;
// Windows-1252 Mapping für den Bereich 0x80 - 0x9F anwenden
if (byteVal >= 0x80 && byteVal <= 0x9F) {
unicodeVal = cp1252Map[byteVal] || byteVal;
}
[self _appendPlainString: String.fromCharCode(unicodeVal)];
}
else
{
CPLogConsole("hex skipped");
}
_hexreturn = NO;
}
else if (ch !== undefined && _curState === 0)
+1 -1
View File
@@ -551,7 +551,7 @@ CPTokenFieldDeleteButtonType = 1;
CPTokenFieldCachedDragFunction = nil;
document.body.ondrag = CPTokenFieldCachedDragFunction;
document.body.onselectstart = CPTokenFieldCachedSelectStartFunction
document.body.onselectstart = CPTokenFieldCachedSelectStartFunction;
}
#endif
+16
View File
@@ -1021,6 +1021,22 @@ var LABEL_MARGIN = 2.0;
BOOL _FIXME_isHUD;
}
- (BOOL)acceptsFirstResponder
{
if (_view && [_view acceptsFirstResponder])
return YES;
return NO;
}
- (BOOL)becomeFirstResponder
{
if (_view && [_view acceptsFirstResponder])
return [[self window] makeFirstResponder:_view];
return [super becomeFirstResponder];
}
- (id)initWithToolbarItem:(CPToolbarItem)aToolbarItem toolbar:(CPToolbar)aToolbar
{
self = [super init];
+697
View File
@@ -0,0 +1,697 @@
/*
* CPTreeController.j
* AppKit
*
* Daniel Boehringer Mar/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 <Foundation/CPArray.j>
@import <Foundation/CPIndexPath.j>
@import "CPObjectController.j"
@import "CPKeyValueBinding.j"
@import "CPTreeNode.j"
@implementation CPTreeController : CPObjectController
{
BOOL _avoidsEmptySelection;
BOOL _preservesSelection;
BOOL _selectsInsertedObjects;
BOOL _alwaysUsesMultipleValuesMarker;
CPString _childrenKeyPath;
CPString _countKeyPath;
CPString _leafKeyPath;
CPArray _sortDescriptors;
id _arrangedObjects;
CPArray _selectionIndexPaths;
BOOL _disableSetContent;
}
+ (void)initialize
{
if (self !== [CPTreeController class])
return;
[self exposeBinding:@"contentArray"];
[self exposeBinding:@"sortDescriptors"];
}
+ (CPSet)keyPathsForValuesAffectingContentArray
{
return [CPSet setWithObjects:@"content"];
}
+ (CPSet)keyPathsForValuesAffectingArrangedObjects
{
return [CPSet setWithObjects:@"content", @"sortDescriptors", @"childrenKeyPath"];
}
+ (CPSet)keyPathsForValuesAffectingSelectionIndexPath
{
return [CPSet setWithObjects:@"selectionIndexPaths"];
}
+ (CPSet)keyPathsForValuesAffectingSelectedObjects
{
return [CPSet setWithObjects:@"selectionIndexPaths"];
}
+ (CPSet)keyPathsForValuesAffectingSelectedNodes
{
return [CPSet setWithObjects:@"selectionIndexPaths"];
}
+ (CPSet)keyPathsForValuesAffectingCanAddChild
{
return [CPSet setWithObjects:@"selectionIndexPaths"];
}
+ (CPSet)keyPathsForValuesAffectingCanInsert
{
return [CPSet setWithObjects:@"selectionIndexPaths"];
}
+ (CPSet)keyPathsForValuesAffectingCanInsertChild
{
return [CPSet setWithObjects:@"selectionIndexPaths"];
}
- (id)init
{
if (self = [super init])
{
_preservesSelection = YES;
_selectsInsertedObjects = YES;
_avoidsEmptySelection = YES;
_alwaysUsesMultipleValuesMarker = NO;
_childrenKeyPath = @"children";
[self _init];
}
return self;
}
- (void)_init
{
_sortDescriptors = [CPArray array];
_selectionIndexPaths = [CPArray array];
_arrangedObjects = [[CPTreeNode alloc] initWithRepresentedObject:nil];
}
- (void)prepareContent
{[self _setContentArray:[CPArray arrayWithObject:[self newObject]]];
}
- (BOOL)preservesSelection { return _preservesSelection; }
- (void)setPreservesSelection:(BOOL)value { _preservesSelection = value; }
- (BOOL)selectsInsertedObjects { return _selectsInsertedObjects; }
- (void)setSelectsInsertedObjects:(BOOL)value { _selectsInsertedObjects = value; }
- (BOOL)avoidsEmptySelection { return _avoidsEmptySelection; }
- (void)setAvoidsEmptySelection:(BOOL)value { _avoidsEmptySelection = value; }
- (BOOL)alwaysUsesMultipleValuesMarker { return _alwaysUsesMultipleValuesMarker; }
- (void)setAlwaysUsesMultipleValuesMarker:(BOOL)aFlag { _alwaysUsesMultipleValuesMarker = aFlag; }
- (CPArray)sortDescriptors { return _sortDescriptors; }
- (void)setSortDescriptors:(CPArray)value
{
if (_sortDescriptors === value)
return;
_sortDescriptors = [value copy];
[self _rearrangeObjects];
}
- (CPString)childrenKeyPath { return _childrenKeyPath; }
- (void)setChildrenKeyPath:(CPString)aKeyPath
{
if (_childrenKeyPath === aKeyPath) return;
_childrenKeyPath = aKeyPath;[self rearrangeObjects];
}
- (CPString)countKeyPath { return _countKeyPath; }
- (void)setCountKeyPath:(CPString)aKeyPath { _countKeyPath = aKeyPath; }
- (CPString)leafKeyPath { return _leafKeyPath; }
- (void)setLeafKeyPath:(CPString)aKeyPath { _leafKeyPath = aKeyPath; }
- (CPString)childrenKeyPathForNode:(CPTreeNode)node { return [self childrenKeyPath]; }
- (CPString)countKeyPathForNode:(CPTreeNode)node { return [self countKeyPath]; }
- (CPString)leafKeyPathForNode:(CPTreeNode)node { return [self leafKeyPath]; }
- (void)setContent:(id)value
{
if (_disableSetContent) return;
if (!value)
value = [CPArray array];
if (![value isKindOfClass:[CPArray class]])
value = [CPArray arrayWithObject:value];
var oldSelectedObjects = nil,
oldSelectionIndexPaths = nil;
if ([self preservesSelection])
oldSelectedObjects = [self selectedObjects];
else
oldSelectionIndexPaths = [self selectionIndexPaths];
_contentObject = value;
[self _rearrangeObjects];
if ([self preservesSelection])
[self __setSelectedObjects:oldSelectedObjects];
else
[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection];
}
- (void)_setContentArray:(id)anArray {[self setContent:anArray]; }
- (id)contentArray { return _contentObject; }
- (id)arrangedObjects { return _arrangedObjects; }
- (void)rearrangeObjects
{
[self willChangeValueForKey:@"arrangedObjects"];
[self _rearrangeObjects];
[self didChangeValueForKey:@"arrangedObjects"];
}
- (void)_rearrangeObjects
{
var oldSelectedObjects = nil,
oldSelectionIndexPaths = nil;
if ([self preservesSelection])
oldSelectedObjects = [self selectedObjects];
else
oldSelectionIndexPaths = [self selectionIndexPaths];
[self __rebuildArrangedObjectsTree];
if ([self preservesSelection])
[self __setSelectedObjects:oldSelectedObjects];
else
[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection];
}
- (void)__rebuildArrangedObjectsTree
{
var rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil],
contentArray = [self contentArray];
if (contentArray && [contentArray count] > 0)
{
var children = [self _buildTreeNodesForObjects:contentArray];
[[rootNode mutableChildNodes] addObjectsFromArray:children];
}
_arrangedObjects = rootNode;
}
- (CPArray)_buildTreeNodesForObjects:(CPArray)objects
{
var count = [objects count];
if (count === 0)
return [];
var sortedObjects = objects;
if (_sortDescriptors && [_sortDescriptors count] > 0)
sortedObjects = [objects sortedArrayUsingDescriptors:_sortDescriptors];
var nodes = [CPMutableArray arrayWithCapacity:count];
for (var i = 0; i < count; i++)
{
var obj = [sortedObjects objectAtIndex:i],
node = [[CPTreeNode alloc] initWithRepresentedObject:obj];
if (_childrenKeyPath)
{
var childObjects = [obj valueForKeyPath:_childrenKeyPath];
if (childObjects && [childObjects count] > 0)
{
var childNodes = [self _buildTreeNodesForObjects:childObjects];
[[node mutableChildNodes] addObjectsFromArray:childNodes];
}
}
[nodes addObject:node];
}
return nodes;
}
- (CPIndexPath)selectionIndexPath
{
return [_selectionIndexPaths count] > 0 ? [_selectionIndexPaths objectAtIndex:0] : nil;
}
- (BOOL)setSelectionIndexPath:(CPIndexPath)indexPath
{
var paths = indexPath ? [CPArray arrayWithObject:indexPath] : [CPArray array];
return [self setSelectionIndexPaths:paths];
}
- (CPArray)selectionIndexPaths { return _selectionIndexPaths; }
- (BOOL)setSelectionIndexPaths:(CPArray)indexPaths
{
[self _selectionWillChange];
var result = [self __setSelectionIndexPaths:indexPaths avoidEmpty:NO];
[self _selectionDidChange];
return result;
}
- (void)_ensureTreeNodesExistForIndexPaths:(CPArray)indexPaths
{
if (!_childrenKeyPath)
return;
var count = [indexPaths count];
for (var i = 0; i < count; i++)
{
var path = [indexPaths objectAtIndex:i];
var length = [path length];
var currentNode = [self arrangedObjects];
for (var j = 0; j < length; j++)
{
var index = [path indexAtPosition:j];
var obj = [currentNode representedObject];
var expectedChildObjects = nil;
if (!obj && currentNode === [self arrangedObjects])
expectedChildObjects = [self contentArray];
else if (obj)
expectedChildObjects = [obj valueForKeyPath:_childrenKeyPath];
if (expectedChildObjects)
{
var childNodes = [currentNode mutableChildNodes];
var needsRebuild = NO;
if (!childNodes || [childNodes count] !== [expectedChildObjects count])
{
needsRebuild = YES;
}
else
{
for (var k = 0, kCount = [childNodes count]; k < kCount; k++)
{
if ([[childNodes objectAtIndex:k] representedObject] !== [expectedChildObjects objectAtIndex:k])
{
needsRebuild = YES;
break;
}
}
}
if (needsRebuild)
{
[childNodes removeAllObjects];
var newNodes = [self _buildTreeNodesForObjects:expectedChildObjects];[childNodes addObjectsFromArray:newNodes];
}
}
var children = [currentNode childNodes];
if (children && index < [children count])
currentNode = [children objectAtIndex:index];
else
break;
}
}
}
- (BOOL)__setSelectionIndexPaths:(CPArray)indexPaths avoidEmpty:(BOOL)avoidEmpty
{
var newPaths = indexPaths;
if (!newPaths)
newPaths = [CPArray array];
if ([newPaths count] > 0)
[self _ensureTreeNodesExistForIndexPaths:newPaths];
if (![newPaths count] && avoidEmpty)
{
if ([[[self arrangedObjects] childNodes] count] > 0)
newPaths = [CPArray arrayWithObject:[CPIndexPath indexPathWithIndex:0]];
}
if ([_selectionIndexPaths isEqualToArray:newPaths])
return NO;
[self willChangeValueForKey:@"selectionIndexPaths"];
_selectionIndexPaths = [newPaths copy];
var binderClass = [[self class] _binderClassForBinding:@"selectionIndexPaths"];
if (binderClass)
{
var binding = [binderClass getBinding:@"selectionIndexPaths" forObject:self];
if (binding)
[binding reverseSetValueFor:@"selectionIndexPaths"];
}
[self didChangeValueForKey:@"selectionIndexPaths"];
return YES;
}
- (BOOL)addSelectionIndexPaths:(CPArray)indexPaths
{
var newPaths = [_selectionIndexPaths mutableCopy];
[newPaths addObjectsFromArray:indexPaths];
return [self setSelectionIndexPaths:newPaths];
}
- (BOOL)removeSelectionIndexPaths:(CPArray)indexPaths
{
var newPaths = [_selectionIndexPaths mutableCopy];
[newPaths removeObjectsInArray:indexPaths];
return [self setSelectionIndexPaths:newPaths];
}
- (CPArray)selectedNodes
{
var nodes = [CPMutableArray array],
count = [_selectionIndexPaths count];
for (var i = 0; i < count; i++)
{
var node = [[self arrangedObjects] descendantNodeAtIndexPath:[_selectionIndexPaths objectAtIndex:i]];
if (node)
[nodes addObject:node];
}
return nodes;
}
- (CPArray)selectedObjects
{
var objects = [CPMutableArray array],
nodes = [self selectedNodes],
count = [nodes count];
for (var i = 0; i < count; i++)
[objects addObject:[[nodes objectAtIndex:i] representedObject]];
return objects;
}
- (BOOL)__setSelectedObjects:(CPArray)objects
{
if (!objects || [objects count] === 0)
return [self __setSelectionIndexPaths:[CPArray array] avoidEmpty:_avoidsEmptySelection];
var newPaths = [CPMutableArray array];
for (var i = 0, count = [objects count]; i < count; i++)
{
var path = [self _indexPathForObject:[objects objectAtIndex:i] inNode:[self arrangedObjects]];
if (path)
[newPaths addObject:path];
}
return [self __setSelectionIndexPaths:newPaths avoidEmpty:_avoidsEmptySelection];
}
- (CPIndexPath)_indexPathForObject:(id)anObject inNode:(CPTreeNode)node
{
if ([node representedObject] === anObject && [node parentNode] != nil)
return [node indexPath];
var children = [node childNodes];
if (children)
{
for (var i = 0, count = [children count]; i < count; i++)
{
var found = [self _indexPathForObject:anObject inNode:[children objectAtIndex:i]];
if (found)
return found;
}
}
return nil;
}
- (BOOL)canInsert { return [self isEditable]; }
- (BOOL)canInsertChild { return [self isEditable] &&[_selectionIndexPaths count] > 0; }
- (BOOL)canAddChild { return [self canInsertChild]; }
- (void)add:(id)sender
{
if (![self canInsert]) return;
var newObject = [self automaticallyPreparesContent] ? [self newObject] :[self _defaultNewObject],
selectionPath = [self selectionIndexPath];
if (!selectionPath)
selectionPath = [CPIndexPath indexPathWithIndex:[[[self arrangedObjects] childNodes] count]];
var length = [selectionPath length],
lastIndex = [selectionPath indexAtPosition:length - 1],
insertPath = [selectionPath indexPathByRemovingLastIndex];
insertPath = [insertPath indexPathByAddingIndex:lastIndex + 1];
[self insertObject:newObject atArrangedObjectIndexPath:insertPath];
}
- (void)addChild:(id)sender
{
if (![self canAddChild])
return;
var newObject = [self automaticallyPreparesContent] ?[self newObject] : [self _defaultNewObject],
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:[self selectionIndexPath]],
childCount = [[parentNode childNodes] count],
insertPath = [[self selectionIndexPath] indexPathByAddingIndex:childCount];
[self insertObject:newObject atArrangedObjectIndexPath:insertPath];
}
- (void)insert:(id)sender
{
if (![self canInsert]) return;
var newObject = [self automaticallyPreparesContent] ? [self newObject] :[self _defaultNewObject],
indexPath = [self selectionIndexPath] || [CPIndexPath indexPathWithIndex:0];
[self insertObject:newObject atArrangedObjectIndexPath:indexPath];
}
- (void)insertChild:(id)sender
{
if (![self canInsertChild]) return;
var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
insertPath = [[self selectionIndexPath] indexPathByAddingIndex:0];
[self insertObject:newObject atArrangedObjectIndexPath:insertPath];
}
- (void)insertObject:(id)anObject atArrangedObjectIndexPath:(CPIndexPath)indexPath
{
[self insertObjects:[CPArray arrayWithObject:anObject] atArrangedObjectIndexPaths:[CPArray arrayWithObject:indexPath]];
}
- (void)insertObjects:(CPArray)objects atArrangedObjectIndexPaths:(CPArray)indexPaths
{
[self willChangeValueForKey:@"content"];
_disableSetContent = YES;
var count = [objects count];
for (var i = 0; i < count; i++)
{
var object = [objects objectAtIndex:i],
path = [indexPaths objectAtIndex:i],
length = [path length];
if (length === 1)
{[_contentObject insertObject:object atIndex:[path indexAtPosition:0]];
}
else
{
var parentPath = [path indexPathByRemovingLastIndex],
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
if (parentNode)
{
var parentObj = [parentNode representedObject],
childIndex = [path indexAtPosition:length - 1];
var children = [parentObj valueForKeyPath:_childrenKeyPath];
if (!children)
{
children = [CPMutableArray array];
[parentObj setValue:children forKeyPath:_childrenKeyPath];
}
var mutableChildren = [parentObj mutableArrayValueForKeyPath:_childrenKeyPath];
[mutableChildren insertObject:object atIndex:childIndex];
}
}
}
var binding = [[self class] _binderClassForBinding:@"contentArray"];
if (binding)
[[binding getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
_disableSetContent = NO;
[self _rearrangeObjects];
if ([self selectsInsertedObjects])[self setSelectionIndexPaths:indexPaths];
[self didChangeValueForKey:@"content"];
}
- (void)remove:(id)sender
{
[self removeObjectsAtArrangedObjectIndexPaths:_selectionIndexPaths];
}
- (void)removeObjectAtArrangedObjectIndexPath:(CPIndexPath)indexPath
{
[self removeObjectsAtArrangedObjectIndexPaths:[CPArray arrayWithObject:indexPath]];
}
- (void)removeObjectsAtArrangedObjectIndexPaths:(CPArray)indexPaths
{
[self willChangeValueForKey:@"content"];
_disableSetContent = YES;
var sortedPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)],
count = [sortedPaths count];
for (var i = count - 1; i >= 0; i--)
{
var path = [sortedPaths objectAtIndex:i],
length = [path length];
if (length === 1)
{
[_contentObject removeObjectAtIndex:[path indexAtPosition:0]];
}
else
{
var parentPath = [path indexPathByRemovingLastIndex],
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
if (parentNode)
{
var parentObj = [parentNode representedObject],
childIndex = [path indexAtPosition:length - 1],
mutableChildren = [parentObj mutableArrayValueForKeyPath:_childrenKeyPath];
if (mutableChildren && childIndex <[mutableChildren count])
[mutableChildren removeObjectAtIndex:childIndex];
}
}
}
var binding = [[self class] _binderClassForBinding:@"contentArray"];
if (binding)
[[binding getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
_disableSetContent = NO;
[self _rearrangeObjects];
[self didChangeValueForKey:@"content"];
}
- (void)moveNode:(CPTreeNode)node toIndexPath:(CPIndexPath)indexPath
{
[self moveNodes:[CPArray arrayWithObject:node] toIndexPath:indexPath];
}
- (void)moveNodes:(CPArray)nodes toIndexPath:(CPIndexPath)startingIndexPath
{[CPException raise:CPUnsupportedMethodException reason:@"moveNodes:toIndexPath: is not yet implemented in CPTreeController."];
}
@end
var CPTreeControllerAvoidsEmptySelection = @"CPTreeControllerAvoidsEmptySelection",
CPTreeControllerPreservesSelection = @"CPTreeControllerPreservesSelection",
CPTreeControllerSelectsInsertedObjects = @"CPTreeControllerSelectsInsertedObjects",
CPTreeControllerAlwaysUsesMultipleValuesMarker = @"CPTreeControllerAlwaysUsesMultipleValuesMarker",
CPTreeControllerChildrenKeyPath = @"CPTreeControllerChildrenKeyPath",
CPTreeControllerCountKeyPath = @"CPTreeControllerCountKeyPath",
CPTreeControllerLeafKeyPath = @"CPTreeControllerLeafKeyPath";
@implementation CPTreeController (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
self = [super initWithCoder:aCoder];
if (self)
{
_avoidsEmptySelection = [aCoder decodeBoolForKey:CPTreeControllerAvoidsEmptySelection];
_preservesSelection = [aCoder decodeBoolForKey:CPTreeControllerPreservesSelection];
_selectsInsertedObjects = [aCoder decodeBoolForKey:CPTreeControllerSelectsInsertedObjects];
_alwaysUsesMultipleValuesMarker = [aCoder decodeBoolForKey:CPTreeControllerAlwaysUsesMultipleValuesMarker];
_childrenKeyPath = [aCoder decodeObjectForKey:CPTreeControllerChildrenKeyPath] || @"children";
_countKeyPath = [aCoder decodeObjectForKey:CPTreeControllerCountKeyPath];
_leafKeyPath = [aCoder decodeObjectForKey:CPTreeControllerLeafKeyPath];
_sortDescriptors = [CPArray array];
_selectionIndexPaths = [CPArray array];
_arrangedObjects = [[CPTreeNode alloc] initWithRepresentedObject:nil];
if (![self content] && [self automaticallyPreparesContent])
[self prepareContent];
else if (![self content])
[self _setContentArray:[CPArray array]];
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[super encodeWithCoder:aCoder];
[aCoder encodeBool:_avoidsEmptySelection forKey:CPTreeControllerAvoidsEmptySelection];
[aCoder encodeBool:_preservesSelection forKey:CPTreeControllerPreservesSelection];
[aCoder encodeBool:_selectsInsertedObjects forKey:CPTreeControllerSelectsInsertedObjects];
[aCoder encodeBool:_alwaysUsesMultipleValuesMarker forKey:CPTreeControllerAlwaysUsesMultipleValuesMarker];
[aCoder encodeObject:_childrenKeyPath forKey:CPTreeControllerChildrenKeyPath];
[aCoder encodeObject:_countKeyPath forKey:CPTreeControllerCountKeyPath];
[aCoder encodeObject:_leafKeyPath forKey:CPTreeControllerLeafKeyPath];
}
- (void)awakeFromCib
{
[self _selectionWillChange];
[self _selectionDidChange];
}
@end
+83 -39
View File
@@ -22,13 +22,12 @@
@import <Foundation/CPObject.j>
@import <Foundation/CPIndexPath.j>
@import <Foundation/CPArray.j>
@implementation CPTreeNode : CPObject
{
id _representedObject @accessors(readonly, property=representedObject);
CPTreeNode _parentNode @accessors(readonly, property=parentNode);
id _representedObject @accessors(property=representedObject);
CPTreeNode _parentNode @accessors(property=parentNode);
CPMutableArray _childNodes;
}
@@ -52,36 +51,35 @@
- (CPIndexPath)indexPath
{
if (_parentNode != nil)
// If we have a parent, calculate path based on parent's path + our index
if (_parentNode)
{
var path;
var index;
index = [[_parentNode childNodes] indexOfObject:self];
path = [_parentNode indexPath];
if (path != nil)
{
return [path indexPathByAddingIndex:index];
}
else
{
return [CPIndexPath indexPathWithIndex:index];
}
}
else
{
return nil;
// Search the parent's child nodes, not our own!
var index = [[_parentNode childNodes] indexOfObjectIdenticalTo:self];
// If the parent is the root (and technically has no path itself in some implementations),
// we might get nil. Handle that gracefully.
var parentPath = [_parentNode indexPath];
if (parentPath)
return [parentPath indexPathByAddingIndex:index];
return [CPIndexPath indexPathWithIndex:index];
}
// If we are the root, we don't have an index path in the context of a tree controller usually,
// or we are [] (empty path). Returning nil is acceptable for the absolute root.
return nil;
}
- (BOOL)isLeaf
{
return [_childNodes count] <= 0;
return [_childNodes count] == 0;
}
- (CPArray)childNodes
{
// Return a copy to prevent external modification without KVC
return [_childNodes copy];
}
@@ -90,18 +88,29 @@
return [self mutableArrayValueForKey:@"childNodes"];
}
- (void)insertObject:(id)aTreeNode inChildNodesAtIndex:(CPInteger)anIndex
{
[[aTreeNode._parentNode mutableChildNodes] removeObjectIdenticalTo:aTreeNode];
// MARK: - KVC Compliance Methods
aTreeNode._parentNode = self;
- (void)insertObject:(CPTreeNode)aTreeNode inChildNodesAtIndex:(CPInteger)anIndex
{
// Optional: Auto-detach from old parent if strictly moving nodes
if ([aTreeNode isKindOfClass:[CPTreeNode class]] && aTreeNode._parentNode)
{
[[aTreeNode._parentNode mutableChildNodes] removeObjectIdenticalTo:aTreeNode];
}
// Direct ivar access is allowed here since we are inside the class implementation
if ([aTreeNode isKindOfClass:[CPTreeNode class]])
aTreeNode._parentNode = self;
[_childNodes insertObject:aTreeNode atIndex:anIndex];
}
- (void)removeObjectFromChildNodesAtIndex:(CPInteger)anIndex
{
[_childNodes objectAtIndex:anIndex]._parentNode = nil;
var node = [_childNodes objectAtIndex:anIndex];
if ([node isKindOfClass:[CPTreeNode class]])
node._parentNode = nil;
[_childNodes removeObjectAtIndex:anIndex];
}
@@ -110,17 +119,34 @@
{
var oldTreeNode = [_childNodes objectAtIndex:anIndex];
oldTreeNode._parentNode = nil;
aTreeNode._parentNode = self;
if ([oldTreeNode isKindOfClass:[CPTreeNode class]])
oldTreeNode._parentNode = nil;
if ([aTreeNode isKindOfClass:[CPTreeNode class]])
aTreeNode._parentNode = self;
[_childNodes replaceObjectAtIndex:anIndex withObject:aTreeNode];
}
// MARK: - Convenience Accessors
- (id)objectInChildNodesAtIndex:(CPInteger)anIndex
{
return _childNodes[anIndex];
return [_childNodes objectAtIndex:anIndex];
}
- (CPInteger)count
{
return [_childNodes count];
}
- (id)objectAtIndex:(CPInteger)anIndex
{
return [_childNodes objectAtIndex:anIndex];
}
// MARK: - Utility
- (void)sortWithSortDescriptors:(CPArray)sortDescriptors recursively:(BOOL)shouldSortRecursively
{
[_childNodes sortUsingDescriptors:sortDescriptors];
@@ -129,25 +155,39 @@
return;
var count = [_childNodes count];
while (count--)
[_childNodes[count] sortWithSortDescriptors:sortDescriptors recursively:YES];
{
var child = [_childNodes objectAtIndex:count];
if ([child respondsToSelector:@selector(sortWithSortDescriptors:recursively:)])
[child sortWithSortDescriptors:sortDescriptors recursively:YES];
}
}
- (CPTreeNode)descendantNodeAtIndexPath:(CPIndexPath)indexPath
{
var index = 0,
count = [indexPath length],
node = self;
if (!indexPath || [indexPath length] == 0)
return self;
for (; index < count; ++index)
node = [node objectInChildNodesAtIndex:[indexPath indexAtPosition:index]];
var node = self,
length = [indexPath length];
for (var i = 0; i < length; i++)
{
var index = [indexPath indexAtPosition:i],
count = [node count];
if (index >= count || index < 0)
return nil;
node = [node objectAtIndex:index];
}
return node;
}
@end
// Coding implementation remains correct
var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey",
CPTreeNodeParentNodeKey = @"CPTreeNodeParentNodeKey",
CPTreeNodeChildNodesKey = @"CPTreeNodeChildNodesKey";
@@ -163,6 +203,10 @@ var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey",
_representedObject = [aCoder decodeObjectForKey:CPTreeNodeRepresentedObjectKey];
_parentNode = [aCoder decodeObjectForKey:CPTreeNodeParentNodeKey];
_childNodes = [aCoder decodeObjectForKey:CPTreeNodeChildNodesKey];
// Safety check to ensure decoding gave us a CPArray
if (!_childNodes)
_childNodes = [[CPMutableArray alloc] init];
}
return self;
+5 -1
View File
@@ -278,7 +278,11 @@ CPWebViewAppKitScrollMaxPollCount = 3;
if (_effectiveScrollMode === CPWebViewScrollAppKit)
{
var visibleRect = [_frameView visibleRect];
// Use `[_scrollView documentVisibleRect]` instead of `[_frameView visibleRect]`.
// `visibleRect` accounts for window clipping, which collapses to 0x0 if the view is
// animated off-screen, resulting in an incorrectly shrunken web layout.
var visibleRect = [_scrollView documentVisibleRect];
[_frameView setFrameSize:CGSizeMake(CGRectGetMaxX(visibleRect), CGRectGetMaxY(visibleRect))];
// try to get the document size so we can correctly set the frame
+4
View File
@@ -1266,6 +1266,10 @@ CPTexturedBackgroundWindowMask
[_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[_windowView addSubview:_contentView];
// The window view manages the exact layout of the content view (e.g. offsetting for the toolbar).
if ([_windowView respondsToSelector:@selector(tile)])
[_windowView tile];
/*
If the initial first responder has been set to something other than
the window, set it to the window because it will no longer be valid.
+68
View File
@@ -0,0 +1,68 @@
/*
* CAAnimationGroup.j
* AppKit
* Created by Daniel Boehringer.
* Copyright 2025.
*
* Implements grouping for Core Animation.
*/
@import <Foundation/CPArray.j>
@import "CAAnimation.j"
@implementation CAAnimationGroup : CAAnimation
{
CPArray _animations;
}
+ (id)group
{
return [[self alloc] init];
}
- (id)init
{
if (self = [super init])
{
_animations = [];
}
return self;
}
- (void)setAnimations:(CPArray)anArray
{
if (_animations === anArray)
return;
_animations = anArray;
}
- (CPArray)animations
{
return _animations;
}
/*
Iterates through children and executes them recursively.
This effectively runs all grouped animations concurrently.
*/
- (void)runActionForKey:(CPString)aKey object:(id)anObject arguments:(CPDictionary)arguments
{
var count = [_animations count],
i = 0;
for (; i < count; i++)
{
var animation = [_animations objectAtIndex:i];
// Recursively call runActionForKey on the child.
// If the child is a CABasicAnimation, it will call [anObject addAnimation:...]
// If the child is another Group, it will recurse here.
if ([animation respondsToSelector:@selector(runActionForKey:object:arguments:)])
{
[animation runActionForKey:aKey object:anObject arguments:arguments];
}
}
}
@end
+244
View File
@@ -29,6 +29,8 @@
@import "CGGeometry.j"
@import "CPColor.j"
@import "CPView.j"
@import "CAMediaTimingFunction.j"
#define DOM(aLayer) aLayer._DOMElement
@@ -118,6 +120,8 @@ var CALayerRegisteredRunLoopUpdates = nil;
CGAffineTransform _transformToLayer;
CGAffineTransform _transformFromLayer;
CPMutableDictionary _activeAnimations;
}
@global document
@@ -160,6 +164,8 @@ var CALayerRegisteredRunLoopUpdates = nil;
_sublayers = [];
_activeAnimations = [CPMutableDictionary dictionary];
#if PLATFORM(DOM)
_DOMElement = document.createElement("div");
@@ -977,6 +983,244 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex)
return _delegate;
}
/*
Adds an animation to the layer.
Supports CABasicAnimation for Numbers (opacity) and Points (position/anchorPoint).
The animation is exerted by means of periodically applying the keypath on the delegate
Only works if the delegate is set!
*/
- (void)addAnimation:(CAAnimation)anim forKey:(CPString)key
{
if (!anim) return;
// --- 1. Handle Animation Groups ---
// If it's a group, we simply schedule its children individually.
if ([anim respondsToSelector:@selector(animations)] && [anim animations])
{
var animations = [anim animations],
count = [animations count],
i = 0;
for (; i < count; i++)
{
var child = [animations objectAtIndex:i];
// Recurse: Add the child animation.
// We pass 'nil' for the key so the child's own 'keyPath'
// is used as the storage identifier in the dictionary.
[self addAnimation:child forKey:nil];
}
return;
}
// --- 2. Determine KeyPath ---
var keyPath = key;
// If the animation object has an explicit keyPath (like CABasicAnimation), use it.
if ([anim respondsToSelector:@selector(keyPath)] && [anim keyPath])
keyPath = [anim keyPath];
// If we can't determine a property to animate, we must abort.
if (!keyPath) return;
// --- 3. Determine Values ---
var startValue = ([anim respondsToSelector:@selector(fromValue)]) ? [anim fromValue] : nil;
// If startValue is missing, try to read it from the layer.
// We wrap this in a try-catch to prevent crashes if 'keyPath' is invalid.
if (startValue == nil)
{
try {
startValue = [[self delegate] valueForKey:keyPath];
}
catch (e) {
// The keyPath was likely invalid (not KVC compliant), abort.
return;
}
}
var endValue = ([anim respondsToSelector:@selector(toValue)]) ? [anim toValue] : nil;
if (endValue == nil)
return;
var duration = ([anim respondsToSelector:@selector(duration)]) ? [anim duration] : 0.25;
// Default to EaseInEaseOut if not specified
var timingFunction = ([anim respondsToSelector:@selector(timingFunction)]) ? [anim timingFunction] : [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
// --- 4. Prepare Context ---
var context = {
"animation": anim,
"keyPath": keyPath,
"startValue": startValue,
"endValue": endValue,
"duration": duration * 1000.0, // ms
"timingFunction": timingFunction,
"startTime": null,
"requestId": null
};
// --- 5. Render Loop ---
var _self = self;
var renderLoop = function(timestamp) {
if ([_self _renderAnimationStep:context timestamp:timestamp])
context.requestId = window.requestAnimationFrame(renderLoop);
else
context.requestId = null;
};
// --- 6. Storage & Kickoff ---
// Use the keyPath as the identifier if no specific key was provided
var storageKey = (key && key.length > 0) ? key : keyPath;
// Remove any conflicting animation on this specific property/key
[self removeAnimationForKey:storageKey];
context.requestId = window.requestAnimationFrame(renderLoop);
[_activeAnimations setObject:context forKey:storageKey];
}
- (void)removeAnimationForKey:(CPString)key
{
var context = [_activeAnimations objectForKey:key];
if (context)
{
if (context.requestId !== null)
window.cancelAnimationFrame(context.requestId);
[_activeAnimations removeObjectForKey:key];
}
}
- (void)removeAllAnimations
{
var keys = [_activeAnimations allKeys],
count = [keys count];
while (count--)
[self removeAnimationForKey:[keys objectAtIndex:count]];
}
/*
Solves Cubic Bezier for t.
p1, p2 are the control points (x,y). p0 is 0,0, p3 is 1,1.
This is a simplified solver for standard Core Animation timing functions.
*/
- (float)_solveBezier:(float)t forTimingFunction:(CAMediaTimingFunction)tf
{
if (!tf) return t;
// Linear optimization
if (tf === [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear])
return t;
var points = [tf controlPoints]; // [c1x, c1y, c2x, c2y]
var p1x = points[0], p1y = points[1],
p2x = points[2], p2y = points[3];
// Simple polynomial evaluation (De Casteljau's algorithm/Cubic formula subset)
// Since we are usually dealing with standard easing, we can approximate 1D easing on the Y axis
// based on linear time X, or do a full solve.
// For brevity/speed in JS, we often approximate basic easing:
// 3t^2 * (1-t) + t^3 ... standard bezier blending functions
var cx = 3.0 * p1x;
var bx = 3.0 * (p2x - p1x) - cx;
var ax = 1.0 - cx - bx;
var cy = 3.0 * p1y;
var by = 3.0 * (p2y - p1y) - cy;
var ay = 1.0 - cy - by;
// Solve for X given t (time) using Newton-Raphson
var sampleT = t;
for (var i = 0; i < 5; i++) {
var x = ((ax * sampleT + bx) * sampleT + cx) * sampleT - t;
if (Math.abs(x) < 1e-3) break;
var d = (3.0 * ax * sampleT + 2.0 * bx) * sampleT + cx;
if (Math.abs(d) < 1e-6) break;
sampleT = sampleT - x / d;
}
// Solve for Y given derived T
return ((ay * sampleT + by) * sampleT + cy) * sampleT;
}
- (BOOL)_renderAnimationStep:(JSObject)context timestamp:(double)timestamp
{
if (context.startTime === null)
context.startTime = timestamp;
var elapsed = timestamp - context.startTime,
linearProgress = elapsed / context.duration;
if (linearProgress > 1.0) linearProgress = 1.0;
// Apply Timing Function
var progress = [self _solveBezier:linearProgress forTimingFunction:context.timingFunction];
var start = context.startValue,
end = context.endValue,
current = nil;
// Number
if (typeof start === "number")
{
current = start + (end - start) * progress;
}
// Point / Size / Rect
else if (start && start.x !== undefined && start.y !== undefined) // CGPoint
{
current = CGPointMake(start.x + (end.x - start.x) * progress,
start.y + (end.y - start.y) * progress);
}
else if (start && start.width !== undefined && start.height !== undefined) // CGSize
{
current = CGSizeMake(start.width + (end.width - start.width) * progress,
start.height + (end.height - start.height) * progress);
}
else if (start && start.origin !== undefined && start.size !== undefined) // CGRect
{
current = CGRectMake(
start.origin.x + (end.origin.x - start.origin.x) * progress,
start.origin.y + (end.origin.y - start.origin.y) * progress,
start.size.width + (end.size.width - start.size.width) * progress,
start.size.height + (end.size.height - start.size.height) * progress
);
}
if (current !== nil)
[[self delegate] setValue:current forKey:context.keyPath];
if (linearProgress >= 1.0)
{
var anim = context.animation;
// Cleanup
var shouldRemove = [anim respondsToSelector:@selector(isRemovedOnCompletion)] ? [anim isRemovedOnCompletion] : YES;
if (shouldRemove) {
// Find key by context identity to handle groups correctly
var keys = [_activeAnimations allKeys];
for (var i = 0; i < keys.length; i++) {
if ([_activeAnimations objectForKey:keys[i]] === context) {
[_activeAnimations removeObjectForKey:keys[i]];
break;
}
}
}
// Delegate
var delegate = [anim delegate];
if (delegate && [delegate respondsToSelector:@selector(animationDidStop:finished:)])
[delegate animationDidStop:anim finished:YES];
return NO;
}
return YES;
}
/* @ignore */
- (void)_setOwningView:(CPView)anOwningView
{
+1 -1
View File
@@ -305,7 +305,7 @@ var hasEditableTarget = function(aDOMEvent)
location = [[CPApp currentEvent] locationInWindow],
anEvent = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags
timestamp:timestamp windowNumber:windowNumber context:nil
characters:characters charactersIgnoringModifiers:characters isARepeat:NO keyCode:keyCode];
characters:characters charactersIgnoringModifiers:characters isARepeat:NO keyCode:keyCode isActionKey:YES];
anEvent._data1 = @{ "simulated": YES };
anEvent._DOMEvent = aDOMEvent;
+142 -126
View File
@@ -192,6 +192,33 @@ KeyCodesToUnicodeMap[CPKeyCodes.OPEN_SQUARE_BRACKET] = "[";
KeyCodesToUnicodeMap[CPKeyCodes.BACKSLASH] = "\\";
KeyCodesToUnicodeMap[CPKeyCodes.CLOSE_SQUARE_BRACKET] = "]";
var KeyNameToUnicodeMap = {};
KeyNameToUnicodeMap["Backspace"] = CPDeleteCharacter;
KeyNameToUnicodeMap["Delete"] = CPDeleteFunctionKey;
KeyNameToUnicodeMap["Tab"] = CPTabCharacter;
KeyNameToUnicodeMap["Enter"] = CPCarriageReturnCharacter;
KeyNameToUnicodeMap["Escape"] = CPEscapeFunctionKey;
KeyNameToUnicodeMap["PageUp"] = CPPageUpFunctionKey;
KeyNameToUnicodeMap["PageDown"] = CPPageDownFunctionKey;
KeyNameToUnicodeMap["ArrowLeft"] = CPLeftArrowFunctionKey;
KeyNameToUnicodeMap["ArrowUp"] = CPUpArrowFunctionKey;
KeyNameToUnicodeMap["ArrowRight"] = CPRightArrowFunctionKey;
KeyNameToUnicodeMap["ArrowDown"] = CPDownArrowFunctionKey;
KeyNameToUnicodeMap["Home"] = CPHomeFunctionKey;
KeyNameToUnicodeMap["End"] = CPEndFunctionKey;
// Add safeguards for punctuation
KeyNameToUnicodeMap[";"] = ";";
KeyNameToUnicodeMap["-"] = "-";
KeyNameToUnicodeMap["="] = "=";
KeyNameToUnicodeMap[","] = ",";
KeyNameToUnicodeMap["."] = ".";
KeyNameToUnicodeMap["/"] = "/";
KeyNameToUnicodeMap["`"] = "`";
KeyNameToUnicodeMap["'"] = "'";
KeyNameToUnicodeMap["["] = "[";
KeyNameToUnicodeMap["\\"] = "\\";
KeyNameToUnicodeMap["]"] = "]";
var ModifierKeyCodes = [
CPKeyCodes.META,
CPKeyCodes.WEBKIT_RIGHT_META,
@@ -438,7 +465,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
theDocument.addEventListener("keyup", keyEventCallback, NO);
theDocument.addEventListener("keydown", keyEventCallback, NO);
theDocument.addEventListener("keypress", keyEventCallback, NO);
// "keypress" listener removed as it's deprecated and no longer used in the new logic.
theDocument.addEventListener("touchstart", touchEventCallback, {passive: false});
theDocument.addEventListener("touchend", touchEventCallback, {passive: false});
@@ -470,7 +497,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
theDocument.removeEventListener("keyup", keyEventCallback, NO);
theDocument.removeEventListener("keydown", keyEventCallback, NO);
theDocument.removeEventListener("keypress", keyEventCallback, NO);
theDocument.removeEventListener("touchstart", touchEventCallback, NO);
theDocument.removeEventListener("touchend", touchEventCallback, NO);
@@ -503,7 +529,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
theDocument.attachEvent("onkeyup", keyEventCallback);
theDocument.attachEvent("onkeydown", keyEventCallback);
theDocument.attachEvent("onkeypress", keyEventCallback);
// "onkeypress" listener removed.
_DOMWindow.attachEvent("onresize", resizeEventCallback);
@@ -533,7 +559,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
theDocument.detachEvent("onkeyup", keyEventCallback);
theDocument.detachEvent("onkeydown", keyEventCallback);
theDocument.detachEvent("onkeypress", keyEventCallback);
_DOMWindow.detachEvent("onresize", resizeEventCallback);
@@ -709,157 +734,138 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
(_capsLockActive ? CPAlphaShiftKeyMask : 0);
// With a few exceptions, all key events are blocked from propagating to
// the browser. Here the following exceptions are being allowed:
//
// - All keys pressed along with a ctrl or cmd key _unless_ they are in
// one of the two blacklists.
// - Any key listed in the whitelist.
//
// The ctrl/cmd keys are used for browser hotkeys as are the keys listed in
// the whitelist (F1-F12 at the time of writing).
//
// If a key is listed in both the blacklist and whitelist, the blacklist is
// checked first. The key will be blocked from propagating in that case.
// the browser. The logic here allows browser shortcuts (Cmd/Ctrl keys)
// and function keys (F1-F12) to pass through, unless explicitly blacklisted.
StopDOMEventPropagation = YES;
var keyCodeForPropagationCheck = aDOMEvent.keyCode || 0;
var charForPropagationCheck = String.fromCharCode(keyCodeForPropagationCheck).toLowerCase();
// Make sure it is not in the blacklists.
if (!(CharacterKeysToPrevent[String.fromCharCode(aDOMEvent.keyCode || aDOMEvent.charCode).toLowerCase()] || KeyCodesToPrevent[aDOMEvent.keyCode]))
if (!(CharacterKeysToPrevent[charForPropagationCheck] || KeyCodesToPrevent[keyCodeForPropagationCheck]))
{
// It is not in the blacklist, let it through if the ctrl/cmd key is
// also down or it's in the whitelist.
if ((modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) || KeyCodesToAllow[aDOMEvent.keyCode])
if ((modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) || KeyCodesToAllow[keyCodeForPropagationCheck])
StopDOMEventPropagation = NO;
}
var overrideCharacters = nil,
var characters = @"",
charactersIgnoringModifiers = @"";
var keyCode = aDOMEvent.keyCode;
if (keyCode in MozKeyCodeToKeyCodeMap)
keyCode = MozKeyCodeToKeyCodeMap[keyCode];
var isActionKey;
var key = aDOMEvent.key;
if (key) {
isActionKey =
key === 'Enter' ||
key === 'Backspace' ||
key === 'Tab' ||
key === 'Escape' ||
key === 'Delete' ||
key.startsWith('Arrow') ||
key === 'Home' ||
key === 'End' ||
key === 'PageUp' ||
key === 'PageDown';
}
else
{
isActionKey =
(keyCode === 13) || (keyCode === 8) || (keyCode === 9) ||
(keyCode === 27) || (keyCode === 46) || (keyCode >= 37 && keyCode <= 40);
}
switch (aDOMEvent.type)
{
case "keydown":
// Grab and store the keycode now since it is correct and consistent at this point.
if (aDOMEvent.keyCode in MozKeyCodeToKeyCodeMap)
_keyCode = MozKeyCodeToKeyCodeMap[aDOMEvent.keyCode];
else
_keyCode = aDOMEvent.keyCode;
var characters;
// Handle key codes for which String.fromCharCode won't work.
// Refs #1036: In Internet Explorer, both 'which' and 'charCode' are undefined for special keys.
if (aDOMEvent.which === 0 || aDOMEvent.charCode === 0 || (aDOMEvent.which === undefined && aDOMEvent.charCode === undefined))
characters = KeyCodesToUnicodeMap[_keyCode];
// The problem with keyCode is that this property refers to keys on the keyboard and not to characters
// This is why String.fromCharCode does not always work in more recent versions of Firefox
// E.g. pressing a '#' on a German keyboard gives you a charCode of 163, which refers to '£' and not '#'
// The property key works fine, though. From there we can get the actual character more robustly.
// Therefore we prefer key over keyCode whenever possible
if (!characters)
characters = (aDOMEvent.key && aDOMEvent.key.length == 1) ? aDOMEvent.key.toLowerCase() : String.fromCharCode(_keyCode).toLowerCase();
overrideCharacters = (modifierFlags & CPShiftKeyMask || _capsLockActive) ? characters.toUpperCase() : characters;
// check for caps lock state
if (_keyCode === CPKeyCodes.CAPS_LOCK)
if ([ModifierKeyCodes containsObject:keyCode])
{
_capsLockActive = YES;
// Make sure the caps lock flag is set in modifierFlags
modifierFlags |= CPAlphaShiftKeyMask;
}
if ([ModifierKeyCodes containsObject:_keyCode])
{
// A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break.
event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags
timestamp:timestamp windowNumber:windowNumber context:nil
characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:_keyCode];
characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:keyCode isActionKey:YES];
break;
}
break;
}
else if (modifierFlags & (CPControlKeyMask | CPCommandKeyMask))
var isARepeat = !!aDOMEvent.repeat || (_charCodes[keyCode] != nil);
_charCodes[keyCode] = YES;
if (aDOMEvent.key)
{
//we are simply going to skip all keypress events that use cmd/ctrl key
//this lets us be consistent in all browsers and send on the keydown
//which means we can cancel the event early enough, but only if sendEvent needs to
}
else if (CPKeyCodes.firesKeyPressEvent(_keyCode, aDOMEvent.key, _lastKey, aDOMEvent.shiftKey, aDOMEvent.ctrlKey, aDOMEvent.altKey))
{
// this branch is taken by events which fire keydown, keypress, and keyup.
// this is the only time we'll ALLOW character keys to propagate (needed for text fields)
StopDOMEventPropagation = NO;
break;
if (aDOMEvent.key.length === 1)
{
characters = aDOMEvent.key;
}
// Correctly handle dead keys to prevent inserting "Dead"
else if (aDOMEvent.key === "Dead" || aDOMEvent.key === "Process") {
characters = @"";
}
// For other named keys, map them or fall back to an empty string.
else
{
characters = KeyNameToUnicodeMap[aDOMEvent.key] || @"";
}
}
else
{
//this branch is taken by "remedial" key events
// In this state we continue to keypress and send the CPEvent
characters = KeyCodesToUnicodeMap[keyCode];
if (!characters)
{
characters = String.fromCharCode(keyCode);
if (modifierFlags & CPShiftKeyMask || _capsLockActive)
characters = characters.toUpperCase();
else
characters = characters.toLowerCase();
}
}
case "keypress":
// we unconditionally break on keypress events with modifiers,
// because we forced the event to be sent on the keydown
if (aDOMEvent.type === "keypress" && (modifierFlags & (CPControlKeyMask | CPCommandKeyMask)))
break;
var keyCode = _keyCode,
charCode = aDOMEvent.keyCode || aDOMEvent.charCode,
isARepeat = (_charCodes[keyCode] != nil);
_lastKey = keyCode;
_charCodes[keyCode] = charCode;
var characters = overrideCharacters;
// Is this a special key?
if (!characters && (aDOMEvent.which === 0 || aDOMEvent.charCode === 0))
characters = KeyCodesToUnicodeMap[charCode];
if (!characters)
characters = String.fromCharCode(charCode);
charactersIgnoringModifiers = characters.toLowerCase(); // FIXME: This isn't correct. It SHOULD include Shift.
// Safari won't send proper capitalization during cmd-key events
if (!overrideCharacters && (modifierFlags & CPCommandKeyMask) && ((modifierFlags & CPShiftKeyMask) || _capsLockActive))
characters = characters.toUpperCase();
charactersIgnoringModifiers = characters.toLowerCase();
event = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags
timestamp:timestamp windowNumber:windowNumber context:nil
characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:charCode];
characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:keyCode isActionKey:isActionKey];
break;
case "keyup":
var keyCode = aDOMEvent.keyCode,
charCode = _charCodes[keyCode];
_keyCode = -1;
_lastKey = -1;
_charCodes[keyCode] = nil;
// check for caps lock state
if (keyCode === CPKeyCodes.CAPS_LOCK)
{
_capsLockActive = NO;
// Make sure the caps lock flag is cleared in modifierFlags
modifierFlags &= ~CPAlphaShiftKeyMask;
_capsLockActive = !_capsLockActive;
if (_capsLockActive)
modifierFlags |= CPAlphaShiftKeyMask;
else
modifierFlags &= ~CPAlphaShiftKeyMask;
}
if ([ModifierKeyCodes containsObject:keyCode])
{
// A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break.
event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags
timestamp:timestamp windowNumber:windowNumber context:nil
characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:_keyCode];
characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:keyCode isActionKey:YES];
break;
}
var characters = KeyCodesToUnicodeMap[charCode] || String.fromCharCode(charCode);
if (aDOMEvent.key)
{
if (aDOMEvent.key.length === 1) {
characters = aDOMEvent.key;
}
// Ensure keyup events also don't produce "Dead"
else if (aDOMEvent.key === "Dead" || aDOMEvent.key === "Process") {
characters = @"";
}
else
{
characters = KeyNameToUnicodeMap[aDOMEvent.key] || @"";
}
}
else
characters = KeyCodesToUnicodeMap[keyCode] || String.fromCharCode(keyCode);
charactersIgnoringModifiers = characters.toLowerCase();
if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive)
@@ -867,7 +873,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
event = [CPEvent keyEventWithType:CPKeyUp location:location modifierFlags:modifierFlags
timestamp: timestamp windowNumber:windowNumber context:nil
characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode];
characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode isActionKey:isActionKey];
break;
}
@@ -880,12 +886,11 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
if (event && ![_platformPasteboard windowShouldSuppressKeyEvent])
{
[CPApp sendEvent:event];
[_platformPasteboard windowDidSendKeyEvent:event];
}
var didStop = NO;
// Platform pasteboard can overrule the decision to stop propagation either way, or it might have no opinion.
if ([_platformPasteboard windowShouldStopPropagation] || (StopDOMEventPropagation && ![_platformPasteboard windowShouldNotStopPropagation]))
{
didStop = YES;
@@ -1271,6 +1276,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
newEvent.type = CPDOMEventMouseUp;
break;
}
newEvent._isFromTouch = true; // Identify the event as touch-originated for tolerant click counting
[self mouseEvent:newEvent];
return;
}
@@ -1385,7 +1391,8 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
if (aDOMEvent.button !== _firstMouseDownButton)
return;
event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp, location, modifierFlags, timestamp, windowNumber, nil, -1, CPDOMEventGetClickCount(_lastMouseUp, timestamp, location), 0, nil);
var clickCount = CPDOMEventGetClickCount(_lastMouseUp, timestamp, location, aDOMEvent._isFromTouch);
event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp, location, modifierFlags, timestamp, windowNumber, nil, -1, clickCount, 0, nil);
_mouseIsDown = NO;
_lastMouseUp = event;
@@ -1431,15 +1438,17 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
_DOMEventMode = YES;
_mouseIsDown = YES;
var clickCount = CPDOMEventGetClickCount(_lastMouseDown, timestamp, location, aDOMEvent._isFromTouch);
// Fake a down and up event so that event tracking mode will work correctly
[CPApp sendEvent:[CPEvent mouseEventWithType:_mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown location:location modifierFlags:modifierFlags
timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:-1
clickCount:CPDOMEventGetClickCount(_lastMouseDown, timestamp, location) pressure:0]];
clickCount:clickCount pressure:0]];
[CPApp sendEvent:[CPEvent mouseEventWithType:_mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp location:location modifierFlags:modifierFlags
timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:-1
clickCount:CPDOMEventGetClickCount(_lastMouseDown, timestamp, location) pressure:0]];
clickCount:clickCount pressure:0]];
return;
}
@@ -1451,7 +1460,8 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
StopContextMenuDOMEventPropagation = YES;
event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown, location, modifierFlags, timestamp, windowNumber, nil, -1, CPDOMEventGetClickCount(_lastMouseDown, timestamp, location), 0, nil);
var clickCount = CPDOMEventGetClickCount(_lastMouseDown, timestamp, location, aDOMEvent._isFromTouch);
event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown, location, modifierFlags, timestamp, windowNumber, nil, -1, clickCount, 0, nil);
_mouseIsDown = YES;
_lastMouseDown = event;
@@ -1951,19 +1961,25 @@ var _CPEventFromNativeMouseEvent = function(aNativeEvent, anEventType, aPoint, m
return aNativeEvent;
};
var CLICK_SPACE_DELTA = 5.0,
CLICK_TIME_DELTA = (typeof document != "undefined" && document.addEventListener) ? 0.55 : 1.0;
var CLICK_SPACE_DELTA = 5.0,
CLICK_TIME_DELTA = (typeof document != "undefined" && document.addEventListener) ? 0.55 : 1.0,
// Define a more generous time delta for touch events to make double-tapping easier.
TOUCH_CLICK_TIME_DELTA = 0.80; // Increased from 0.55s to 0.80s
CPDOMEventGetClickCount = function(aComparisonEvent, aTimestamp, aLocation)
CPDOMEventGetClickCount = function(aComparisonEvent, aTimestamp, aLocation, isFromTouch)
{
if (!aComparisonEvent)
return 1;
// For touch events, allow a larger pixel delta to accommodate "fat fingers"
// and a longer time delta to accommodate less precise tapping.
var spaceDelta = isFromTouch ? 25.0 : CLICK_SPACE_DELTA;
var timeDelta = isFromTouch ? TOUCH_CLICK_TIME_DELTA : CLICK_TIME_DELTA;
var comparisonLocation = [aComparisonEvent locationInWindow];
return (aTimestamp - [aComparisonEvent timestamp] < CLICK_TIME_DELTA &&
ABS(comparisonLocation.x - aLocation.x) < CLICK_SPACE_DELTA &&
ABS(comparisonLocation.y - aLocation.y) < CLICK_SPACE_DELTA) ? [aComparisonEvent clickCount] + 1 : 1;
return (aTimestamp - [aComparisonEvent timestamp] < timeDelta &&
ABS(comparisonLocation.x - aLocation.x) < spaceDelta &&
ABS(comparisonLocation.y - aLocation.y) < spaceDelta) ? [aComparisonEvent clickCount] + 1 : 1;
};
// Global.
+29 -29
View File
@@ -132,38 +132,36 @@ CPKeyCodes = {
/*!
* Returns true if the key fires a keypress event in the current browser.
* The keypress event is deprecated, but this function helps manage legacy
* event handling by predicting its behavior.
*
* Accoridng to MSDN [1] IE only fires keypress events for the following keys:
* - Letters: A - Z (uppercase and lowercase)
* - Numerals: 0 - 9
* - Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~
* - System: ESC, SPACEBAR, ENTER
*
* That's not entirely correct though, for instance there's no distinction
* between upper and lower case letters.
*
* [1] http://msdn2.microsoft.com/en-us/library/ms536939(VS.85).aspx)
*
* Safari is similar to IE, but does not fire keypress for ESC.
*
* Additionally, IE6 does not fire keydown or keypress events for letters when
* the control or alt keys are held down and the shift key is not. IE7 does
* fire keydown in these cases, though, but not keypress.
*
* @param keyCode A key code.
* @param opt_heldKeyCode Key code of a currently-held key.
* @param opt_shiftKey Whether the shift key is held down.
* @param opt_ctrlKey Whether the control key is held down.
* @param opt_altKey Whether the alt key is held down.
* @return Returns YES if it's a key that fires a keypress event.
* @param {number} keyCode A key code.
* @param {string} key The `key` property from the keyboard event.
* @param {number} opt_heldKeyCode Key code of a currently-held key.
* @param {boolean} opt_shiftKey Whether the shift key is held down.
* @param {boolean} opt_ctrlKey Whether the control key is held down.
* @param {boolean} opt_altKey Whether the alt key is held down.
* @return {boolean} Returns YES if it's a key that fires a keypress event.
*/
CPKeyCodes.firesKeyPressEvent = function(keyCode, key, opt_heldKeyCode, opt_shiftKey, opt_ctrlKey, opt_altKey)
{
// The property key from event is one character wide in case of 'regular' keys (as opposed e.g. to arrow keys)
// Regular keys all fire the keypress event
// Modern approach: Use event.key if available, as it is the most reliable standard.
if (key)
{
// Any key that produces a single, printable character fires a keypress event.
if (key.length === 1)
return true;
if (key && key.length == 1)
return true;
// "Enter" is a special non-printable key that historically fires keypress for compatibility.
if (key === "Enter")
return true;
// For all other non-printable keys (e.g., "ArrowLeft", "Escape", "F1"),
// modern browsers do not fire a keypress event.
return false;
}
// --- Legacy Fallback Logic (for browsers that don't support event.key) ---
if (!CPFeatureIsCompatible(CPJavaScriptRemedialKeySupport))
return true;
@@ -196,9 +194,11 @@ CPKeyCodes.firesKeyPressEvent = function(keyCode, key, opt_heldKeyCode, opt_shif
/*!
* Test for whether or not a given keyCode represents a character key.
* NOTE: This is a legacy function for browsers that don't support `event.key`.
* It is unreliable because `keyCode` represents a physical key, not the character produced.
*
* @param keyCode A key code.
* @return Returns YES if the keyCode is a character key.
* @param {number} keyCode A key code.
* @return {boolean} Returns YES if the keyCode is a character key.
*/
CPKeyCodes.isCharacterKey = function(keyCode)
{
+192 -2
View File
@@ -763,9 +763,12 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti
if (!anObserver)
return;
var forwarder = nil;
var forwarder = nil,
collectionOperatorMatch = aPath.match(/^(.*)\.(@\w+)(?:\.(.*))?$/);
if (aPath.indexOf('.') !== CPNotFound && aPath.charAt(0) !== '@')
if (collectionOperatorMatch)
forwarder = [[_CPKVOCollectionOperatorObserver alloc] initWithTarget:_targetObject observer:anObserver keyPath:aPath options:options context:aContext];
else if (aPath.indexOf('.') !== CPNotFound && aPath.charAt(0) !== '@')
forwarder = [[_CPKVOForwardingObserver alloc] initWithKeyPath:aPath object:_targetObject observer:anObserver options:options context:aContext];
else
[self _replaceModifiersForKey:aPath];
@@ -1265,6 +1268,193 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti
@end
@implementation _CPKVOCollectionOperatorObserver : CPObject
{
id _target;
id _originalObserver;
CPString _fullKeyPath;
unsigned _options;
id _context;
CPString _collectionKeyPath;
CPString _operator;
CPString _valueKeyPath;
CPArray _observedItems;
id _cachedValue;
}
- (id)initWithTarget:(id)aTarget observer:(id)anObserver keyPath:(CPString)aKeyPath options:(unsigned)options context:(id)aContext
{
self = [super init];
if (self)
{
_target = aTarget;
_originalObserver = anObserver;
_fullKeyPath = aKeyPath;
_options = options;
_context = aContext;
_observedItems = [CPArray array];
var match = aKeyPath.match(/^(.*)\.(@\w+)(?:\.(.*))?$/);
if (match)
{
_collectionKeyPath = match[1];
_operator = match[2];
_valueKeyPath = match[3];
}
_cachedValue = [_target valueForKeyPath:_fullKeyPath];
[_target addObserver:self forKeyPath:_collectionKeyPath options:_options context:nil];
[self _setupItemObservers];
}
return self;
}
- (void)_setupItemObservers
{
var collection = [_target valueForKeyPath:_collectionKeyPath];
if (collection && [collection respondsToSelector:@selector(objectEnumerator)])
{
var enumerator = [collection objectEnumerator],
item;
while ((item = [enumerator nextObject]) !== nil)
{
if (_valueKeyPath && [item respondsToSelector:@selector(addObserver:forKeyPath:options:context:)])
[item addObserver:self forKeyPath:_valueKeyPath options:_options context:nil];
[_observedItems addObject:item];
}
}
}
- (void)_tearDownItemObservers
{
if (_valueKeyPath)
{
var count = [_observedItems count];
while (count--)
{
var item = [_observedItems objectAtIndex:count];
if ([item respondsToSelector:@selector(removeObserver:forKeyPath:context:)])
[item removeObserver:self forKeyPath:_valueKeyPath context:nil];
}
}
[_observedItems removeAllObjects];
}
- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context
{
var isPrior = [[change objectForKey:CPKeyValueChangeNotificationIsPriorKey] boolValue];
if (object === _target && [keyPath isEqualToString:_collectionKeyPath])
{
// Only tear down/rebuild observers when the actual change happens, not during the "Prior" notification
if (!isPrior)
{
var kind = [change objectForKey:CPKeyValueChangeKindKey];
if (kind === CPKeyValueChangeSetting)
{
[self _tearDownItemObservers];
[self _setupItemObservers];
}
else if (kind === CPKeyValueChangeInsertion || kind === CPKeyValueChangeReplacement)
{
if (kind === CPKeyValueChangeReplacement)
{
var oldItems = [change objectForKey:CPKeyValueChangeOldKey];
if (oldItems)
{
for (var i = 0, len = oldItems.length; i < len; i++)
{
var item = oldItems[i];
if (_valueKeyPath && [item respondsToSelector:@selector(removeObserver:forKeyPath:context:)])
[item removeObserver:self forKeyPath:_valueKeyPath context:nil];
[_observedItems removeObject:item];
}
}
}
var newItems = [change objectForKey:CPKeyValueChangeNewKey];
if (newItems)
{
for (var i = 0, len = newItems.length; i < len; i++)
{
var item = newItems[i];
if (_valueKeyPath && [item respondsToSelector:@selector(addObserver:forKeyPath:options:context:)])
[item addObserver:self forKeyPath:_valueKeyPath options:_options context:nil];
[_observedItems addObject:item];
}
}
}
else if (kind === CPKeyValueChangeRemoval)
{
var oldItems = [change objectForKey:CPKeyValueChangeOldKey];
if (oldItems)
{
for (var i = 0, len = oldItems.length; i < len; i++)
{
var item = oldItems[i];
if (_valueKeyPath && [item respondsToSelector:@selector(removeObserver:forKeyPath:context:)])
[item removeObserver:self forKeyPath:_valueKeyPath context:nil];
[_observedItems removeObject:item];
}
}
}
}
[self _notifyOriginalObserverIsPrior:isPrior];
}
else if (_valueKeyPath && [keyPath isEqualToString:_valueKeyPath] && [_observedItems containsObject:object])
{
[self _notifyOriginalObserverIsPrior:isPrior];
}
}
- (void)_notifyOriginalObserverIsPrior:(BOOL)isPrior
{
var change = [CPMutableDictionary dictionaryWithObject:CPKeyValueChangeSetting forKey:CPKeyValueChangeKindKey];
if (isPrior)
[change setObject:YES forKey:CPKeyValueChangeNotificationIsPriorKey];
if (_options & CPKeyValueObservingOptionOld)
[change setObject:(_cachedValue !== nil ? _cachedValue : [CPNull null]) forKey:CPKeyValueChangeOldKey];
if (!isPrior)
{
var newValue = [_target valueForKeyPath:_fullKeyPath];
if (_options & CPKeyValueObservingOptionNew)
[change setObject:(newValue !== nil ? newValue : [CPNull null]) forKey:CPKeyValueChangeNewKey];
[_originalObserver observeValueForKeyPath:_fullKeyPath ofObject:_target change:change context:_context];
// Update the cache for the next notification
_cachedValue = newValue;
}
else
{
[_originalObserver observeValueForKeyPath:_fullKeyPath ofObject:_target change:change context:_context];
}
}
- (void)finalize
{
[_target removeObserver:self forKeyPath:_collectionKeyPath];
[self _tearDownItemObservers];
}
@end
@implementation _CPKVOForwardingObserver : CPObject
{
id _object;
+31 -6
View File
@@ -99,13 +99,38 @@ var sharedSystemLocale = nil,
if (language)
{
// Browsers use locale strings such as "en-US", but CPLocale uses "en_US".
language = language.replace("-", "_").substring(0, 5);
// Some browsers have "en_us" at this point, while we want "en_US".
language = language.substring(0, 3).toLowerCase() + language.substring(3, 5).toUpperCase();
// Split the BCP 47 tag into parts (e.g.,["zh", "Hans", "CN"] or ["en", "US"] or ["fr"])
var parts = language.split("-");
if ([availableLocaleIdentifiers indexOfObject:language] !== CPNotFound)
localeIdentifier = language;
if (parts.length > 1)
{
// Chrome behavior: e.g., "de-DE" or "en-US"
var langCode = parts[0].toLowerCase();
var regionCode = parts[parts.length - 1].toUpperCase();
// Update localeIdentifier!
localeIdentifier = langCode + "_" + regionCode;
}
else
{
// Firefox behavior: e.g., "de", "fr", "en"
var langCode = parts[0].toLowerCase();
// Create a mapping of 2-letter language codes to default region codes
var defaultRegions = {
"de": "DE",
"en": "US",
"es": "ES",
"fr": "FR",
"sv": "SE"
};
// Look up the default region, or fallback to the uppercased language code (e.g. "it" -> "IT")
var regionCode = defaultRegions[langCode] || langCode.toUpperCase();
// Update localeIdentifier!
localeIdentifier = langCode + "_" + regionCode;
}
}
}
+3 -9
View File
@@ -3,20 +3,14 @@
# Cappuccino: Build Desktop-Class Web Applications
Cappuccino is an open-source framework that supports building powerful, desktop-class applications running in any modern web browser. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C.
Cappuccino faithfully implements the proven design patterns of NeXTSTEP/Apple's Cocoa frameworks, enabling the creation of incredibly complex and reliable applications with a fraction of the code.
> **✨ Project Status: Active Development & Node.js Transition**
> Cappuccino has been under continuous development since 2008 and is actively maintained. A major transition to a modern, **Node.js-based toolchain** has recently been finalized. The current release is a production-ready Release Candidate, with a formal release scheduled for 2026. It is stable, fast, and ready for new projects.
---
## Why Use Cappuccino?
Cappuccino is not intended for building simple websites. It is for building **applications**—especially complex, data-rich, line-of-business tools where productivity and user experience are paramount.
Cappuccino is for building **applications** in the browser — especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits:
* **💻 True Desktop Behavior, Out-of-the-Box:** Applications built with Cappuccino behave like native desktop software by default. This includes a rich palette of UI controls, **full keyboard navigation and focus management**, and **multi-level undo/redo support** — as you can see in this [Demo application](https://ansb.uniklinik-freiburg.de/UIBuilder/index.html). Also take a look at the [Kitchen Sink demo](https://cappuccino-testbook.5apps.com/#ThemeKitchenSink).
* **💻 True Desktop Behavior, Out-of-the-Box:** Applications built with Cappuccino behave like native desktop software by default. This includes a rich palette of UI controls, **full keyboard navigation and focus management**, and **multi-level undo/redo support** — as you can see in this [Showcase application](https://ansb.uniklinik-freiburg.de/ThemeKitchenSinkA3). Also take a look at the [Cookbook tutorial](https://cappuccino-cookbook.5apps.com/).
* **🚀 Incredible Productivity:** Less code is needed. High-level abstractions and a powerful object-oriented model mean development is focused on application logic, not browser quirks.
* **🏛️ Stable & Mature:** Built on decades of proven API design from Cocoa®, Cappuccino provides a stable foundation, free from the churn common in the JavaScript ecosystem.
* **🧱 True Object-Oriented Architecture:** Objective-J's message-passing architecture promotes loose coupling and clean design, making large-scale applications easier to build and maintain.
@@ -84,7 +78,7 @@ Pure JavaScript and Objective-J can be mixed and matched, even in the same file.
## Find Out More
* **Official Website:** [cappuccino.dev](http://cappuccino.dev)
* **Documentation & Tutorials:** [cappuccino.dev/learn/](http://cappuccino.dev/learn/), [cappuccino cookbook](https://cappuccino-cookbook.5apps.com)
* **Documentation & Tutorials:** [cappuccino.dev/learn/](http://cappuccino.dev/learn/), [Browser online documentation](https://daboe01.github.io/CappDoc/), [cappuccino cookbook](https://cappuccino-cookbook.5apps.com)
* **Gitter Community Chat:** [gitter.im/cappuccino/cappuccino](https://gitter.im/cappuccino/cappuccino)
* **GitHub Wiki:** [github.com/cappuccino/cappuccino/wiki](https://github.com/cappuccino/cappuccino/wiki)
* **FAQ:** [cappuccino.dev/support/faq.html](http://cappuccino.dev/support/faq.html)
+9 -9
View File
@@ -44,11 +44,11 @@
[button setKeyEquivalent:"a"];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0]];
characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:wasClicked];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]];
characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertTrue:wasClicked];
}
@@ -60,11 +60,11 @@
[button setKeyEquivalentModifierMask:CPAlternateKeyMask];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]];
characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:wasClicked];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:CPAlternateKeyMask
timestamp:0 windowNumber:0 context:nil
characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]];
characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertTrue:wasClicked];
}
@@ -76,12 +76,12 @@
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]];
characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:wasClicked];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:CPShiftKeyMask
timestamp:0 windowNumber:0 context:nil
characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]];
characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertTrue:wasClicked];
}
@@ -92,15 +92,15 @@
[button setKeyEquivalent:CPEscapeFunctionKey];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
characters:CPDeleteCharacter charactersIgnoringModifiers:CPDeleteCharacter isARepeat:NO keyCode:0]];
characters:CPDeleteCharacter charactersIgnoringModifiers:CPDeleteCharacter isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:wasClicked];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]];
characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:wasClicked];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]];
characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertTrue:wasClicked];
}
+184
View File
@@ -0,0 +1,184 @@
/*
* CPColorPanelTest.j
* AppKit
*
* Created to test CPColorPanel architectural refactoring.
*/
@import <AppKit/CPColorPanel.j>
/*
* Mock color picker for testing
*/
@implementation _MockColorPicker : CPObject
{
CPArray _receivedColors;
CPView _view;
}
- (id)init
{
if (self = [super init])
{
_receivedColors = [];
_view = [[CPView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
}
return self;
}
- (void)setColor:(CPColor)aColor
{
[_receivedColors addObject:aColor];
}
- (CPArray)receivedColors
{
return _receivedColors;
}
- (CPView)provideNewView:(BOOL)initial
{
return _view;
}
@end
@implementation CPColorPanelTest : OJTestCase
{
CPColorPanel _panel;
}
- (void)setUp
{
// Get shared panel and ensure it's initialized
_panel = [CPColorPanel sharedColorPanel];
[_panel _loadContentsIfNecessary];
}
- (void)tearDown
{
// Reset panel state between tests
[_panel setColor:[CPColor whiteColor]];
}
/*
* Test that setting a color updates the opacity slider to match the color's alpha component.
* This ensures the slider always reflects the current color's opacity.
*/
- (void)testSetColorUpdatesOpacitySlider
{
var initialColor = [CPColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.5];[_panel setColor:initialColor];
[self assert:0.5 equals:[_panel._opacitySlider floatValue]
message:"Opacity slider should match the color's alpha component"];
}
/*
* Test that setOpacity: creates a new color with the correct alpha value.
*/
- (void)testOpacityChangeUpdatesColorAlpha
{
var initialColor = [CPColor colorWithRed:0.5 green:0.5 blue:1.0 alpha:1.0];
[_panel setColor:initialColor];[_panel._opacitySlider setFloatValue:0.3];
[_panel setOpacity:_panel._opacitySlider];
var newColor = [_panel color];
[self assert:0.3 equals:[newColor alphaComponent]
message:"Color's alpha should match slider value"];
// RGB components should remain unchanged
var components = [newColor components];
[self assert:0.5 equals:components[0] message:"Red component should be unchanged"];
[self assert:0.5 equals:components[1] message:"Green component should be unchanged"];
[self assert:1.0 equals:components[2] message:"Blue component should be unchanged"];
}
/*
* Test that the active picker receives setColor: when a color changes.
* Regression test: active picker was not notified of color changes.
*/
- (void)testActivePickerNotifiedOfColorChanges
{
var mockPicker = [[_MockColorPicker alloc] init];
_panel._activePicker = mockPicker;
var color1 = [CPColor redColor];
var color2 = [CPColor blueColor];
[_panel setColor:color1];
[_panel setColor:color2];
var receivedColors = [mockPicker receivedColors];
[self assert:2 equals:[receivedColors count]
message:"Active picker should receive setColor for each color change"];
[self assert:color1 same:receivedColors[0]];
[self assert:color2 same:receivedColors[1]];
}
/*
* Test that a picker receives setColor: when it becomes active.
* Regression test: picker was not notified of current color on activation.
*/
- (void)testPickerNotifiedOnActivation
{
var testColor = [CPColor colorWithRed:0.5 green:0.5 blue:1.0 alpha:1.0];
[_panel setColor:testColor];
var mockPicker = [[_MockColorPicker alloc] init];
_panel._colorPickers = [mockPicker];
var button = [[CPButton alloc] initWithFrame:CGRectMake(0, 0, 32, 32)];
[button setTag:0];
[_panel _setPicker:button];
var receivedColors = [mockPicker receivedColors];
[self assert:1 equals:[receivedColors count]
message:"Picker should receive setColor when activated"];
[self assert:testColor same:receivedColors[0]];
}
/*
* Test that opacity() method returns the current color's alpha component.
*/
- (void)testOpacityMethodReturnsAlpha
{
var testColor = [CPColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.42];
[_panel setColor:testColor];
[self assert:0.42 equals:[_panel opacity]
message:"opacity method should return color's alpha component"];
}
/*
* Test that setColor: with equal color does not trigger updates.
* This is an optimization to prevent unnecessary work.
*/
- (void)testSetColorWithEqualColorDoesNothing
{
var testColor = [CPColor colorWithRed:1.0 green:0.5 blue:0.0 alpha:0.7];
[_panel setColor:testColor];
var mockPicker = [[_MockColorPicker alloc] init];
_panel._activePicker = mockPicker;
// Set same color again
[_panel setColor:testColor];
var receivedColors = [mockPicker receivedColors];
[self assert:0 equals:[receivedColors count]
message:"Setting equal color should not trigger picker update"];
}
/*
* Test color() method returns the current color.
*/
- (void)testColorMethodReturnsCurrentColor
{
var testColor = [CPColor colorWithRed:0.2 green:0.4 blue:0.6 alpha:0.8];[_panel setColor:testColor];
[self assert:testColor same:[_panel color]
message:"color method should return current color"];
}
@end
+73
View File
@@ -31,4 +31,77 @@
[self assert:[CPColor greenColor] equals:[colorWell color] message:"color well archived color"];
}
/**
* Tests that color well objects maintain visual state consistency.
*
* Validates the object's responsibility to synchronize public properties
* with internal theme state management.
*/
- (void)testColorWellMaintainsVisualStateConsistency
{
[colorWell setBordered:NO];
[self assertFalse:[colorWell isBordered] message:"object correctly reports bordered state"];
[colorWell setBordered:YES];
[self assertTrue:[colorWell isBordered] message:"object correctly reports changed bordered state"];
}
/**
* Tests that color well objects properly handle activation responsibilities.
*
* Validates the object's role in responder chain participation and
* exclusive activation behavior.
*/
- (void)testColorWellHandlesActivationResponsibilities
{
// Note: We test activation through observable effects, not internal state
[colorWell activate:YES];
[self assertTrue:[colorWell acceptsFirstResponder] message:"activated object accepts responder chain participation"];
}
/**
* Tests that disabled color well objects behave appropriately.
*
* Validates the object's responsibility to modify its behavior
* when in a disabled state.
*/
- (void)testDisabledColorWellModifiesBehaviorAppropriately
{
[colorWell setEnabled:NO];
[self assertFalse:[colorWell acceptsFirstResponder] message:"disabled object modifies responder behavior"];
[colorWell setEnabled:YES];
[self assertTrue:[colorWell acceptsFirstResponder] message:"re-enabled object restores normal behavior"];
}
/**
* Tests that color well objects interact correctly with other wells.
*
* Validates the object's responsibility to handle exclusive activation
* scenarios with other color well instances.
*/
- (void)testColorWellInteractsCorrectlyWithOtherWells
{
var well1 = colorWell,
well2 = [[CPColorWell alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
[well1 activate:YES]; // Exclusive activation
[well2 activate:NO]; // Non-exclusive should still work
[self assertTrue:[well2 acceptsFirstResponder] message:"objects maintain independence where appropriate"];
}
/**
* Tests basic color assignment functionality.
*
* Validates the core contract: setting a color should change what's
* returned by the color getter.
*/
- (void)testBasicColorAssignment
{
var testColor = [CPColor colorWithCalibratedRed:0.2 green:0.4 blue:0.6 alpha:0.8];
[colorWell setColor:testColor];
[self assert:[colorWell color] equals:testColor message:"object correctly handles color assignment"];
}
@end
+3 -3
View File
@@ -31,7 +31,7 @@
timestamp:400.5 windowNumber:300 context:nil eventNumber:0 clickCount:2 pressure:0.5];
[self assert:@"CPEvent: type=2 loc={50, 50} time=400.5 flags=0x20000 win=undefined winNum=0 ctxt=null evNum=0 click=2 buttonNumber=0 pressure=0.5" equals:[anEvent description]];
anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask | CPCommandKeyMask timestamp:12345.6 windowNumber:10 context:nil characters:"X" charactersIgnoringModifiers:"x" isARepeat:NO keyCode:10];
anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask | CPCommandKeyMask timestamp:12345.6 windowNumber:10 context:nil characters:"X" charactersIgnoringModifiers:"x" isARepeat:NO keyCode:10 isActionKey:NO];
[self assert:@"CPEvent: type=10 loc={0, 0} time=12345.6 flags=0x120000 win=null winNum=10 ctxt=null chars=\"X\" unmodchars=\"x\" repeat=0 keyCode=10" equals:[anEvent description]];
@@ -44,13 +44,13 @@
{
[self assert:0 equals:[CPEvent modifierFlags] message:@"no modifier flags active in a newly started app"];
var anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0];
var anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO];
[CPApp sendEvent:anEvent];
[self assert:CPShiftKeyMask equals:[CPEvent modifierFlags] message:@"shift key pressed"];
// When the key up event is sent the modifier flags are cleared.
anEvent = [CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0];
anEvent = [CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO];
[CPApp sendEvent:anEvent];
[self assert:0 equals:[CPEvent modifierFlags] message:@"shift key released"];
+8 -8
View File
@@ -149,24 +149,24 @@
// Don't match anything.
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
timestamp:0 windowNumber:0 context:nil
characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0]];
characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled];
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0]];
characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled];
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
timestamp:0 windowNumber:0 context:nil
characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0]];
characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || undoWasCalled];
[self assertTrue:openDocumentWasCalled message:"expect openDocumentWasCalled"];
openDocumentWasCalled = NO;
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
timestamp:0 windowNumber:0 context:nil
characters:CPUndoKeyEquivalent charactersIgnoringModifiers:CPUndoKeyEquivalent isARepeat:NO keyCode:0]];
characters:CPUndoKeyEquivalent charactersIgnoringModifiers:CPUndoKeyEquivalent isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled];
[self assertTrue:undoWasCalled];
}
@@ -177,7 +177,7 @@
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]];
characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || openDocumentWasCalled || undoWasCalled];
[self assertTrue:escapeNoModifierWasCalled];
@@ -185,7 +185,7 @@
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
timestamp:0 windowNumber:0 context:nil
characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]];
characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled];
[self assertTrue:escapeWasCalled];
}
@@ -196,7 +196,7 @@
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
timestamp:0 windowNumber:0 context:nil
characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0]];
characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || saveDocumentAsWasCalled || undoWasCalled];
[self assertTrue:saveDocumentWasCalled message:"saveDocumentWasCalled"];
@@ -204,7 +204,7 @@
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask | CPShiftKeyMask
timestamp:0 windowNumber:0 context:nil
characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0]];
characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || saveDocumentWasCalled || undoWasCalled];
[self assertTrue:saveDocumentAsWasCalled message:"saveDocumentAsWasCalled"];
}
+2 -2
View File
@@ -45,7 +45,7 @@
var keyEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
characters:character charactersIgnoringModifiers:character isARepeat:NO keyCode:keyCode];
characters:character charactersIgnoringModifiers:character isARepeat:NO keyCode:keyCode isActionKey:YES];
[responder interpretKeyEvents:[keyEvent]];
[self assert:[selector] equals:responder.doCommandCalls];
}
@@ -57,7 +57,7 @@
var keyEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask
timestamp:0 windowNumber:0 context:nil
characters:CPLeftArrowFunctionKey charactersIgnoringModifiers:CPLeftArrowFunctionKey isARepeat:NO keyCode:CPKeyCodes.LEFT];
characters:CPLeftArrowFunctionKey charactersIgnoringModifiers:CPLeftArrowFunctionKey isARepeat:NO keyCode:CPKeyCodes.LEFT isActionKey:YES];
[responder interpretKeyEvents:[keyEvent]];
[self assert:[@selector(moveLeftAndModifySelection:)] equals:responder.doCommandCalls];
}
+266
View File
@@ -0,0 +1,266 @@
/*
* CPTreeControllerTest.j
*
* Test suite for CPTreeController
*/
@import <Foundation/CPArray.j>
@import <Foundation/CPIndexPath.j>
@import <AppKit/CPTreeController.j>
@import <AppKit/CPTreeNode.j>
@import <AppKit/CPTextField.j>
@class OrgNode
@implementation CPTreeControllerTest : OJTestCase
{
CPTreeController _treeController @accessors(property=treeController);
CPArray _contentArray @accessors(property=contentArray);
CPArray observations;
int aCount @accessors;
}
- (CPArray)makeTestTree
{
var engineering = [OrgNode nodeWithName:@"Engineering"],
marketing = [OrgNode nodeWithName:@"Marketing"];
var webTeam = [OrgNode nodeWithName:@"Web Team"],
backendTeam = [OrgNode nodeWithName:@"Backend Team"];
[engineering setChildren:[CPMutableArray arrayWithObjects:webTeam, backendTeam]];
var dev1 = [OrgNode nodeWithName:@"Francisco"],
dev2 = [OrgNode nodeWithName:@"Ross"];
[webTeam setChildren:[CPMutableArray arrayWithObjects:dev1, dev2]];
return [CPMutableArray arrayWithObjects:engineering, marketing];
}
- (void)setUp
{
[[CPApplication alloc] init];
_contentArray = [self makeTestTree];
_treeController = [[CPTreeController alloc] init];
[_treeController setChildrenKeyPath:@"children"];
[_treeController setContent:[_contentArray copy]];
}
- (void)testInitWithContent
{
[self assert:[_contentArray count] equals:[[_treeController contentArray] count]];
[self assert:[CPTreeNode class] equals:[[[self treeController] arrangedObjects] class] message:@"arranged objects should be a proxy CPTreeNode root"];
}
- (void)testInitWithoutContent
{
var emptyController = [[CPTreeController alloc] init];
[self assert:[CPArray array] equals:[emptyController contentArray]];
[self assert:0 equals:[[[emptyController arrangedObjects] childNodes] count]];
}
- (void)testSetContent
{
var newTree = [CPMutableArray arrayWithObject:[OrgNode nodeWithName:@"Solo Department"]];
[[self treeController] setContent:newTree];
[self assert:newTree equals:[[self treeController] contentArray]];
[self assert:1 equals:[[[[self treeController] arrangedObjects] childNodes] count]];
}
- (void)testSelectionPaths
{
var controller = [self treeController];
var path = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
[controller setSelectionIndexPath:path];
var selectedPath = [controller selectionIndexPath];
[self assert:path equals:selectedPath];
var selectedNodes = [controller selectedNodes];
[self assert:1 equals:[selectedNodes count]];
[self assert:@"Web Team" equals:[[[selectedNodes objectAtIndex:0] representedObject] name]];
}
- (void)testAddChild
{
var controller = [self treeController];
[controller setObjectClass:[OrgNode class]];
var parentPath = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:1];
[controller setSelectionIndexPath:parentPath];
var newDev = [OrgNode nodeWithName:@"Tom"];
var insertPath = [parentPath indexPathByAddingIndex:0];
[controller insertObject:newDev atArrangedObjectIndexPath:insertPath];
var engineering = [[controller contentArray] objectAtIndex:0],
backendTeam = [[engineering children] objectAtIndex:1];
[self assert:1 equals:[[backendTeam children] count] message:@"Child should be added to the model object's children array"];
[self assert:@"Tom" equals:[[[backendTeam children] objectAtIndex:0] name]];
}
- (void)testInsertObjectAtArrangedObjectIndexPath
{
var controller = [self treeController];
var path = [CPIndexPath indexPathWithIndex:1];
var hrDept = [OrgNode nodeWithName:@"Human Resources"];
[controller insertObject:hrDept atArrangedObjectIndexPath:path];
[self assert:3 equals:[[controller contentArray] count]];
[self assert:hrDept equals:[[controller contentArray] objectAtIndex:1]];
var nestedPath = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
var subDept = [OrgNode nodeWithName:@"Sub Dept"];
[controller insertObject:subDept atArrangedObjectIndexPath:nestedPath];
var engChildren = [[[controller contentArray] objectAtIndex:0] children];
[self assert:subDept equals:[engChildren objectAtIndex:0] message:@"Object should be inserted at the correct nested index path"];
}
- (void)testRemoveObjectAtArrangedObjectIndexPath
{
var controller = [self treeController];
var path = [[[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0] indexPathByAddingIndex:0];
[controller removeObjectAtArrangedObjectIndexPath:path];
var engineering = [[controller contentArray] objectAtIndex:0],
webTeam = [[engineering children] objectAtIndex:0];
[self assert:1 equals:[[webTeam children] count] message:@"Francisco should be removed, leaving only Ross"];
[self assert:@"Ross" equals:[[[webTeam children] objectAtIndex:0] name]];
}
- (void)testRemoveObjectsAtArrangedObjectIndexPaths
{
var controller = [self treeController];
var paths = [CPArray arrayWithObjects:[CPIndexPath indexPathWithIndex:0], [CPIndexPath indexPathWithIndex:1]];
[controller removeObjectsAtArrangedObjectIndexPaths:paths];
[self assert:0 equals:[[controller contentArray] count] message:@"All root nodes should be removed"];
}
- (void)testSelectingEmptyIndexPathsExplicitlyWithAvoidsEmptySelection
{
var controller = [self treeController];
[controller setAvoidsEmptySelection:YES];
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
[controller setSelectionIndexPaths:[CPArray array]];
[self assertTrue:([[controller selectionIndexPaths] count] == 0) message:@"Selection should be empty when unselecting explicitly, even with avoidsEmptySelection"];
}
- (void)testAvoidsEmptySelectionWhenRemoving
{
var controller = [self treeController];
[controller setAvoidsEmptySelection:YES];
var path = [CPIndexPath indexPathWithIndex:0];
[controller setSelectionIndexPath:path];
// Remove "Engineering"
[controller removeObjectAtArrangedObjectIndexPath:path];
[self assertTrue:([[controller selectionIndexPaths] count] == 1) message:@"Selection should fallback to the first item when avoidsEmptySelection is YES"];
// "Marketing" is now at index 0
[self assert:[CPIndexPath indexPathWithIndex:0] equals:[controller selectionIndexPath]];
// Test behavior when AvoidsEmptySelection is NO[controller insertObject:[OrgNode nodeWithName:@"New Dept"] atArrangedObjectIndexPath:[CPIndexPath indexPathWithIndex:1]];
[controller setAvoidsEmptySelection:NO];
// Reselect "Marketing" at index 0
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
// Remove "Marketing"
[controller removeObjectAtArrangedObjectIndexPath:[CPIndexPath indexPathWithIndex:0]];
[self assertTrue:([[controller selectionIndexPaths] count] == 0) message:@"Selection should be allowed to be empty when avoidsEmptySelection is NO"];
}
- (void)testChildrenKeyPathOverride
{
var controller = [[CPTreeController alloc] init];
[controller setChildrenKeyPath:@"subItems"];
var data = [OrgNode nodeWithName:@"Root"];
[data setValue:[CPMutableArray arrayWithObject:[OrgNode nodeWithName:@"Sub"]] forKey:@"subItems"];
[controller setContent:[CPMutableArray arrayWithObject:data]];
var path = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
var newItem = [OrgNode nodeWithName:@"New Sub"];
[controller insertObject:newItem atArrangedObjectIndexPath:path];
var subs = [data valueForKey:@"subItems"];
[self assert:2 equals:[subs count]];
[self assert:newItem equals:[subs objectAtIndex:0] message:@"Object should be inserted using the custom childrenKeyPath"];
}
- (void)testContentBinding
{
var controller = [[CPTreeController alloc] init];
[controller bind:@"contentArray" toObject:self withKeyPath:@"contentArray" options:nil];
[self assert:[self contentArray] equals:[controller contentArray]];
[self assert:2 equals:[[[controller arrangedObjects] childNodes] count]];
}
- (void)testSelectedObjects
{
var controller = [self treeController];
var path = [CPIndexPath indexPathWithIndex:1];
[controller setSelectionIndexPath:path];
var selectedObjects = [controller selectedObjects];
[self assert:1 equals:[selectedObjects count]];
[self assert:@"Marketing" equals:[[selectedObjects objectAtIndex:0] name]];
}
@end
/*
* Dummy Model Class for Testing
*/
@implementation OrgNode : CPObject
{
CPString _name @accessors(property=name);
CPMutableArray _children @accessors(property=children);
CPMutableArray _subItems @accessors(property=subItems);
}
+ (id)nodeWithName:(CPString)aName
{
return [[self alloc] initWithName:aName];
}
- (id)initWithName:(CPString)aName
{
if (self = [super init])
{
_name = aName;
_children = [CPMutableArray array];
_subItems = [CPMutableArray array];
}
return self;
}
- (CPString)description
{
return [CPString stringWithFormat:@"<OrgNode %@>", [self name]];
}
@end
+1 -1
View File
@@ -25,7 +25,7 @@
indexPath = [CPIndexPath indexPathWithIndex:1];
[self assert:undefined equals:[treeNode descendantNodeAtIndexPath:indexPath]];
[self assert:nil equals:[treeNode descendantNodeAtIndexPath:indexPath]];
}
@end
+63
View File
@@ -3,6 +3,8 @@
@class CarTester
@class ToManyTester
@class AccountTester
@class TransactionTester
@implementation CPKVOTest : OJTestCase
{
@@ -436,6 +438,35 @@
[self assertTrue:_sawObservation message:@"Never recieved an observation"];
}
- (void)testCollectionOperatorKeyPath
{
var account = [[AccountTester alloc] init],
t1 = [[TransactionTester alloc] initWithAmount:10.0],
t2 = [[TransactionTester alloc] initWithAmount:20.0];
[account setTransactions:[t1, t2]];
// 1. Test updating a child property triggers an aggregate update
[account addObserver:self forKeyPath:@"transactions.@sum.amount" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testCollectionOperatorKeyPath_ItemChange"];
_sawObservation = NO;
[t1 setAmount:15.0]; // KVC sum goes from 30.0 -> 35.0
[self assertTrue:_sawObservation message:@"Never received an observation when child item's property changed"];
[account removeObserver:self forKeyPath:@"transactions.@sum.amount"];
// 2. Test replacing the whole array triggers an aggregate update
[account addObserver:self forKeyPath:@"transactions.@sum.amount" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testCollectionOperatorKeyPath_ArrayReplace"];
var t3 = [[TransactionTester alloc] initWithAmount:5.0];
_sawObservation = NO;
[account setTransactions:[t1, t2, t3]]; // KVC sum goes from 35.0 -> 40.0
[self assertTrue:_sawObservation message:@"Never received an observation when collection was replaced"];
[account removeObserver:self forKeyPath:@"transactions.@sum.amount"];
}
- (void)testPerformance
{
bob = [PersonTester new];
@@ -777,6 +808,16 @@
testNestedNotificationsBobCount += 1;
break;
case "testCollectionOperatorKeyPath_ItemChange":
[self assert:newValue equals:35.0];
[self assert:oldValue equals:30.0];
break;
case "testCollectionOperatorKeyPath_ArrayReplace":
[self assert:newValue equals:40.0];
[self assert:oldValue equals:35.0];
break;
default:
[self assertFalse:YES message:@"unhandled observation, must be an error"];
return;
@@ -955,3 +996,25 @@
}
@end
@implementation TransactionTester : CPObject
{
float amount @accessors;
}
- (id)initWithAmount:(float)anAmount
{
self = [super init];
if (self) {
amount = anAmount;
}
return self;
}
@end
@implementation AccountTester : CPObject
{
CPArray transactions @accessors;
}
@end
@@ -9,6 +9,7 @@
@import <Foundation/Foundation.j>
@import <AppKit/AppKit.j>
@import <AppKit/CAKeyframeAnimation.j>
@import <AppKit/CAAnimationGroup.j>
#define UIAssert(a) [self markTest:_cmd didPass:a];
@@ -403,6 +404,231 @@
[CPAnimationContext endGrouping];
}
- (void)testGroupAnimation:(id)sender
{
// Reset state
[_testView setFrame:_initialTestViewFrame];
[_testView setAlphaValue:1.0];
[_pathView setPath:nil]; // Clear the path view as we aren't using it here
// Ensure the view is layer-backed.
// Without this, [_testView layer] returns nil.
[_testView setWantsLayer:YES];
var layer = [_testView layer];
// 1. Define the start and end positions
// CALayer 'position' corresponds to the center of the view (anchorPoint 0.5,0.5)
var startPos = [layer position];
// Safety check in case layer creation failed (though setWantsLayer:YES should ensure it)
if (!startPos) startPos = CGPointMake(0,0);
var endPos = CGPointMake(startPos.x + 150, startPos.y + 50);
// 2. Create a Position Animation
var moveAnim = [CABasicAnimation animationWithKeyPath:@"position"];
[moveAnim setFromValue:startPos];
[moveAnim setToValue:endPos];
[moveAnim setDuration:1.0];
// 3. Create an Opacity Animation
var fadeAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
[fadeAnim setFromValue:1.0];
[fadeAnim setToValue:0.25];
[fadeAnim setDuration:1.0];
// 4. Group them
// This tests the recursive logic in CAAnimationGroup and the timer logic in CALayer
var group = [CAAnimationGroup group];
[group setAnimations:[moveAnim, fadeAnim]];
[group setDuration:1.0];
// 5. Run the animation on the layer
[layer addAnimation:group forKey:@"groupTest"];
// 6. Verify results after the animation completes (1.0s duration + 0.1s buffer)
[self performSelector:@selector(_verifyGroupAnimation:) withObject:endPos afterDelay:1.1];
}
- (void)_verifyGroupAnimation:(CGPoint)expectedPos
{
var layer = [_testView layer],
currentPos = [layer position],
currentOpacity = [layer opacity];
// Allow for small floating point differences
var posPassed = (Math.abs(currentPos.x - expectedPos.x) < 1.0 && Math.abs(currentPos.y - expectedPos.y) < 1.0);
var opacityPassed = (Math.abs(currentOpacity - 0.25) < 0.05);
[self markTest:@selector(testGroupAnimation:) didPass:(posPassed && opacityPassed)];
// Reset for next test
[self performSelector:@selector(cleanupAfterAnimation) withObject:nil afterDelay:0.5];
}
- (void)testManualRotation:(id)sender
{
// 1. Cleanup previous test view
if (_testView)
[_testView removeFromSuperview];
// 2. Setup the RotatableView
// View is 100x100, but the blue box drawn inside is 70x70 to allow room to spin.
var frame = CGRectMake(390, 450, 100, 100);
_testView = [[RotatableView alloc] initWithFrame:frame];
[[theWindow contentView] addSubview:_testView];
// Ensure layer-backed so we have a layer to animate
[_testView setWantsLayer:YES];
var layer = [_testView layer];
// 3. Define the Animation
// Animation is performed on _testView
[layer setDelegate:_testView];
var rotationAnim = [CABasicAnimation animationWithKeyPath:@"angle"];
// Rotate 360 degrees (2 * PI)
[rotationAnim setFromValue:0.0];
[rotationAnim setToValue:2 * PI];
[rotationAnim setDuration:2.0];
// Use an easing function for smooth start/stop
[rotationAnim setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
// 4. Add to Layer
[layer addAnimation:rotationAnim forKey:@"rotateTest"];
// 5. Verify results after animation
[self performSelector:@selector(_verifyRotation:) withObject:nil afterDelay:2.1];
}
- (void)_verifyRotation:(id)sender
{
var layer = [_testView layer];
var endAngle = [layer angle];
// Check if we reached approx 2*PI (6.28)
var passed = (Math.abs(endAngle - (2 * PI)) < 0.1);
[self markTest:@selector(testManualRotation:) didPass:passed];
// Reset view
[self performSelector:@selector(cleanupAfterAnimation) withObject:nil afterDelay:0.5];
}
@end
/*
A custom view that draws a box with a line in it.
We draw the box smaller than the view bounds to prevent clipping during rotation.
*/
@implementation RotatableView : CPView
{
float _angle;
}
- (void)initWithFrame:(CGRect)aFrame
{
self = [super initWithFrame:aFrame];
_angle = 0;
return self;
}
- (void)setAngle:(float)anAngle
{
_angle = anAngle;
[self display];
}
- (float)angle
{
return _angle;
}
- (void)drawRect:(CGRect)aRect
{
var context = [[CPGraphicsContext currentContext] graphicsPort],
bounds = [self bounds],
cx = CGRectGetWidth(bounds) / 2.0,
cy = CGRectGetHeight(bounds) / 2.0;
// 1. Clear Context
CGContextClearRect(context, bounds);
// 2. Precompute Trig
var cosA = Math.cos(_angle),
sinA = Math.sin(_angle);
/*
Helper closure to transform a local point (x,y) relative to center
into global view coordinates.
*/
var getPoint = function(localX, localY)
{
// Rotation Matrix:
// x' = x*cos - y*sin
// y' = x*sin + y*cos
var rotX = localX * cosA - localY * sinA;
var rotY = localX * sinA + localY * cosA;
// Translate back to view center
return CGPointMake(cx + rotX, cy + rotY);
};
// --- DRAW BLUE SQUARE (70x70) ---
var s = 35.0; // half size
// Calculate the 4 corners manually
var p1 = getPoint(-s, -s); // Top-Left
var p2 = getPoint( s, -s); // Top-Right
var p3 = getPoint( s, s); // Bottom-Right
var p4 = getPoint(-s, s); // Bottom-Left
CGContextBeginPath(context);
CGContextMoveToPoint(context, p1.x, p1.y);
CGContextAddLineToPoint(context, p2.x, p2.y);
CGContextAddLineToPoint(context, p3.x, p3.y);
CGContextAddLineToPoint(context, p4.x, p4.y);
CGContextClosePath(context);
[[CPColor greenColor] setFill];
CGContextFillPath(context);
// --- DRAW RED MARKER (Top-Left Corner) ---
// A 20x20 square in the top-left of the blue box
// Local coords relative to center: x from -35 to -15, y from -35 to -15
var r1 = getPoint(-35, -35);
var r2 = getPoint(-15, -35);
var r3 = getPoint(-15, -15);
var r4 = getPoint(-35, -15);
CGContextBeginPath(context);
CGContextMoveToPoint(context, r1.x, r1.y);
CGContextAddLineToPoint(context, r2.x, r2.y);
CGContextAddLineToPoint(context, r3.x, r3.y);
CGContextAddLineToPoint(context, r4.x, r4.y);
CGContextClosePath(context);
[[CPColor redColor] setFill];
CGContextFillPath(context);
// --- DRAW WHITE POINTER LINE ---
// Line from Center (0,0) to Right Edge (35, 0)
var lineStart = getPoint(0, 0);
var lineEnd = getPoint(35, 0);
CGContextBeginPath(context);
CGContextMoveToPoint(context, lineStart.x, lineStart.y);
CGContextAddLineToPoint(context, lineEnd.x, lineEnd.y);
[[CPColor whiteColor] setStroke];
CGContextSetLineWidth(context, 3.0);
CGContextStrokePath(context);
}
@end
var unCamelCase = function(aString)
@@ -0,0 +1,104 @@
/*
* AppController.j
* CPButtonBar Right Buttons Manual Test
*/
@import <Foundation/CPObject.j>
@import <Foundation/CPKeyedArchiver.j>
@import <Foundation/CPKeyedUnarchiver.j>
@import <AppKit/AppKit.j>
@implementation AppController : CPObject
{
CPButtonBar buttonBar;
}
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
contentView = [theWindow contentView];
var bounds = [contentView bounds];
// 1. Create the ButtonBar
buttonBar = [[CPButtonBar alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(bounds) - 28, CGRectGetWidth(bounds), 28)];
[buttonBar setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin];
// Add resize control to ensure bounds calculations are correct
[buttonBar setHasResizeControl:YES];
// 2. Setup standard Left Buttons
var plusButton = [CPButtonBar plusButton],
minusButton = [CPButtonBar minusButton];
[buttonBar setButtons:[plusButton, minusButton]];
// 3. Setup the new Right Buttons (PR #745)
var actionButton = [CPButtonBar actionPopupButton],
customRightBtn = [[CPButton alloc] initWithFrame:CGRectMake(0, 0, 70, 24)];
[customRightBtn setTitle:@"Settings"];
[buttonBar setRightButtons:[customRightBtn, actionButton]];
[contentView addSubview:buttonBar];
// --- Testing Controls ---
// Button to test resizing alignment layout
var toggleAlignmentBtn = [CPButton buttonWithTitle:@"Toggle Resize Control Alignment"];
[toggleAlignmentBtn setFrameOrigin:CGPointMake(20, 20)];
[toggleAlignmentBtn sizeToFit];
[toggleAlignmentBtn setTarget:self];
[toggleAlignmentBtn setAction:@selector(toggleResizeAlignment:)];
[contentView addSubview:toggleAlignmentBtn];
// Button to test the KVO "hidden" observer on rightButtons
var toggleVisibilityBtn = [CPButton buttonWithTitle:@"Toggle Right Button Visibility"];
[toggleVisibilityBtn setFrameOrigin:CGPointMake(20, 60)];
[toggleVisibilityBtn sizeToFit];
[toggleVisibilityBtn setTarget:self];
[toggleVisibilityBtn setAction:@selector(toggleRightVisibility:)];
[contentView addSubview:toggleVisibilityBtn];
// Button to test CPCoding archiving/unarchiving of rightButtons
var testArchivingBtn = [CPButton buttonWithTitle:@"Test Archiving (CPCoding)"];
[testArchivingBtn setFrameOrigin:CGPointMake(20, 100)];
[testArchivingBtn sizeToFit];
[testArchivingBtn setTarget:self];
[testArchivingBtn setAction:@selector(testArchiving:)];
[contentView addSubview:testArchivingBtn];
[theWindow orderFront:self];
}
- (void)toggleResizeAlignment:(id)sender
{
// Flipping the resize control to the left shouldn't break the right-aligned buttons
[buttonBar setResizeControlIsLeftAligned:![buttonBar resizeControlIsLeftAligned]];
}
- (void)toggleRightVisibility:(id)sender
{
var rightBtns = [buttonBar rightButtons];
if ([rightBtns count] > 0)
{
var btn = rightBtns[0];
[btn setHidden:![btn isHidden]];
}
}
- (void)testArchiving:(id)sender
{
// Test the new CPButtonBarRightButtonsKey in CPCoding
var data = [CPKeyedArchiver archivedDataWithRootObject:buttonBar];
var unarchivedBar = [CPKeyedUnarchiver unarchiveObjectWithData:data];
var bounds = [[buttonBar superview] bounds];
// Place the cloned bar directly above the original one
[unarchivedBar setFrame:CGRectMake(0, CGRectGetHeight(bounds) - 56, CGRectGetWidth(bounds), 28)];
[[buttonBar superview] addSubview:unarchivedBar];
}
@end
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CPApplicationDelegateClass</key>
<string>AppController</string>
<key>CPBundleName</key>
<string>CPButtonBarTest</string>
<key>CPPrincipalClass</key>
<string>CPApplication</string>
</dict>
</plist>
+81
View File
@@ -0,0 +1,81 @@
/*
* Jakefile
* CPBrowserTest
*
* Created by Ross Boucher on March 23, 2010.
* Copyright 2010, Your Company All rights reserved.
*/
var ENV = require("system").env,
FILE = require("file"),
JAKE = require("jake"),
task = JAKE.task,
FileList = JAKE.FileList,
app = require("cappuccino/jake").app,
configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug",
OS = require("os");
app ("CPBrowserTest", function(task)
{
task.setBuildIntermediatesPath(FILE.join("Build", "CPBrowserTest.build", configuration));
task.setBuildPath(FILE.join("Build", configuration));
task.setProductName("CPBrowserTest");
task.setIdentifier("com.yourcompany.CPBrowserTest");
task.setVersion("1.0");
task.setAuthor("Your Company");
task.setEmail("feedback @nospam@ yourcompany.com");
task.setSummary("CPBrowserTest");
task.setSources((new FileList("**/*.j")).exclude(FILE.join("Build", "**")));
task.setResources(new FileList("Resources/*"));
task.setIndexFilePath("index.html");
task.setInfoPlistPath("Info.plist");
if (configuration === "Debug")
task.setCompilerFlags("-DDEBUG -g");
else
task.setCompilerFlags("-O");
});
function printResults(configuration)
{
print("----------------------------");
print(configuration+" app built at path: "+FILE.join("Build", configuration, "CPBrowserTest"));
print("----------------------------");
}
task ("default", ["CPBrowserTest"], 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()
{
OS.system(["open", FILE.join("Build", "Debug", "CPBrowserTest", "index.html")]);
});
task ("run-release", ["release"], function()
{
OS.system(["open", FILE.join("Build", "Release", "CPBrowserTest", "index.html")]);
});
task ("deploy", ["release"], function()
{
FILE.mkdirs(FILE.join("Build", "Deployment", "CPBrowserTest"));
OS.system(["press", "-f", FILE.join("Build", "Release", "CPBrowserTest"), FILE.join("Build", "Deployment", "CPBrowserTest")]);
printResults("Deployment")
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="en">
<!--
index-debug.html
__project.name__
Created by __user.name__ on __project.date__.
Copyright __project.year__, __organization.name__ All rights reserved.
-->
<head>
<meta charset="utf-8">
<!--[if lte IE 8]>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
<![endif]-->
<!--[if gte IE 9]>
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
<![endif]-->
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="Resources/icon.png">
<link rel="apple-touch-startup-image" href="Resources/default.png">
<title>__project.name__</title>
<!-- Custom javascript goes here -->
<!-- End custom javascript -->
<script type="text/javascript">
OBJJ_MAIN_FILE = "main.j";
OBJJ_INCLUDE_PATHS = ["Frameworks/Debug", "Frameworks"];
// The below will tell the compiler to generate debug symbols, type signatures and not inline objj_msgSend functions.
// This will affect only Objective-J code that is compiled when loading the application. It will not affect precompiled
// code like the Cappuccino frameworks.
//
// Debug symbols will give each Objective-J method a Javascript function name. Without a name it will be very hard to find
// the methods in the debugger.
// Type Signatures will give type information to the Objective-J runtime for instance variables and methods.
// Inline objj_msgSend will give a speed increase but decorators will not work. Check below on debug options for
// more information on decorators.
//
// Uncomment or comment on the line below to change the flags
OBJJ_COMPILER_FLAGS = ["IncludeDebugSymbols", "IncludeTypeSignatures", "SourceMap", "InlineMsgSend"];
var progressBar = null;
OBJJ_PROGRESS_CALLBACK = function(percent, appSize, path)
{
percent = percent * 100;
if (!progressBar)
progressBar = document.getElementById("progress-bar");
if (progressBar)
progressBar.style.width = Math.min(percent, 100) + "%";
}
var loadingHTML =
'<div id="loading">' +
' <div id="loading-text">Loading...</div>' +
' <div id="progress-indicator">' +
' <span id="progress-bar" style="width:0%"></span>' +
' </div>' +
'</div>';
</script>
<script type="text/javascript" src="Frameworks/Debug/Objective-J/Objective-J.js" charset="UTF-8"></script>
<script type="text/javascript">
objj_msgSend_reset();
// DEBUG OPTIONS:
// Decorators will only work when the code is compiled without the compiler option 'InlineMsgSend'. Check above.
// Uncomment to enable printing of backtraces on exceptions:
//objj_msgSend_decorate(objj_backtrace_decorator);
// Uncomment to supress exceptions that take place inside a message
//objj_msgSend_decorate(objj_supress_exceptions_decorator)
// Uncomment to enable runtime type checking:
//objj_msgSend_decorate(objj_typecheck_decorator);
// Uncomment (along with both above) to print backtraces on type check errors:
//objj_typecheck_prints_backtrace = true;
// Uncomment to disable the default logger (CPLogConsole if window.console exists, CPLogPopup otherwise):
//CPLogUnregister(CPLogDefault);
// Uncomment to enable a specific logger:
//CPLogRegister(CPLogConsole);
//CPLogRegister(CPLogPopup);
// Tag view DOM elements with a "data-cappuccino-view" attribute that contains
// the class name of the view that created them. Comment this or set to false to disable.
appkit_tag_dom_elements = true;
</script>
<style type="text/css">
html, body, h1, p {
margin: 0;
padding: 0;
}
/* We need a body wrapper because Cappuccino is unhappy if we change the body element */
#cappuccino-body {
/* Position it absolutely so it will fill the height without content */
position: absolute;
top: 0;
bottom: 0;
width: 100%;
/* Put it at the bottom of the stack so it doesn't interfere with UI */
z-index: 0;
}
#cappuccino-body .container {
display: table;
margin: 0 auto;
height: 100%;
}
#cappuccino-body .content {
display: table-cell;
height: 100%;
vertical-align: top;
}
#loading {
position: relative;
top: 35%;
}
#loading-text {
height: 1.5em;
color: #555;
font: normal bold 36px/36px Arial, sans-serif;
}
#progress-indicator {
padding: 0px;
height: 16px;
border: 5px solid #555;
border-radius: 18px;
background-color: white;
}
#progress-bar {
position: relative;
top: -1px;
left: -1px;
display: block;
height: 18px;
/* Compensate for moving the bar left 1px to overlap the indicator border */
border-right: 1px solid #555;
background-color: #555;
}
#noscript {
position: relative;
top: 35%;
padding: 1em 1.5em;
border: 5px solid #555;
border-radius: 16px;
background-color: white;
color: #555;
text-align: center;
font: bold 24px Arial, sans-serif;
}
#noscript a {
color: #98c0ff;
text-decoration: none;
}
</style>
</head>
<body>
<div id="cappuccino-body">
<div class="container">
<div class="content">
<script type="text/javascript">
document.write(loadingHTML);
</script>
</div>
</div>
<noscript style="position:absolute; top:0; left:0; width:100%; height:100%">
<div class="container">
<div class="content">
<div id="noscript">
<p style="font-size:120%; margin-bottom:.75em">JavaScript is required for this application.</p>
<p><a href="http://www.enable-javascript.com" target="_blank">Enable JavaScript</a></p>
</div>
</div>
</div>
</noscript>
</div>
</body>
</html>
+166
View File
@@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="en">
<!--
index.html
__project.name__
Created by __user.name__ on __project.date__.
Copyright __project.year__, __organization.name__ All rights reserved.
-->
<head>
<meta charset="utf-8">
<!--[if lte IE 8]>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
<![endif]-->
<!--[if gte IE 9]>
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
<![endif]-->
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="Resources/icon.png">
<link rel="apple-touch-startup-image" href="Resources/default.png">
<title>__project.name__</title>
<!-- Custom javascript goes here -->
<!-- End custom javascript -->
<script type="text/javascript">
OBJJ_MAIN_FILE = "main.j";
// The below will tell the compiler to not generate debug symbols but will generate type signatures and inline objj_msgSend functions.
// This will affect only Objective-J code that is compiled when loading the application. It will not affect precompiled
// code like the Cappuccino frameworks.
// Uncomment or comment on the line below to change the flags
OBJJ_COMPILER_FLAGS = [/*"IncludeDebugSymbols"*/, "IncludeTypeSignatures"/*, "SourceMap"*/, "InlineMsgSend"];
var progressBar = null;
OBJJ_PROGRESS_CALLBACK = function(percent, appSize, path)
{
percent = percent * 100;
if (!progressBar)
progressBar = document.getElementById("progress-bar");
if (progressBar)
progressBar.style.width = Math.min(percent, 100) + "%";
}
var loadingHTML =
'<div id="loading">' +
' <div id="loading-text">Loading...</div>' +
' <div id="progress-indicator">' +
' <span id="progress-bar" style="width:0%"></span>' +
' </div>' +
'</div>';
</script>
<script type="text/javascript" src="Frameworks/Objective-J/Objective-J.js" charset="UTF-8"></script>
<style type="text/css">
html, body, h1, p {
margin: 0;
padding: 0;
}
/* We need a body wrapper because Cappuccino is unhappy if we change the body element */
#cappuccino-body {
/* Position it absolutely so it will fill the height without content */
position: absolute;
top: 0;
bottom: 0;
width: 100%;
/* Put it at the bottom of the stack so it doesn't interfere with UI */
z-index: 0;
}
#cappuccino-body .container {
display: table;
margin: 0 auto;
height: 100%;
}
#cappuccino-body .content {
display: table-cell;
height: 100%;
vertical-align: top;
}
#loading {
position: relative;
top: 35%;
}
#loading-text {
height: 1.5em;
color: #555;
font: normal bold 36px/36px Arial, sans-serif;
}
#progress-indicator {
padding: 0px;
height: 16px;
border: 5px solid #555;
border-radius: 18px;
background-color: white;
}
#progress-bar {
position: relative;
top: -1px;
left: -1px;
display: block;
height: 18px;
/* Compensate for moving the bar left 1px to overlap the indicator border */
border-right: 1px solid #555;
background-color: #555;
}
#noscript {
position: relative;
top: 35%;
padding: 1em 1.5em;
border: 5px solid #555;
border-radius: 16px;
background-color: white;
color: #555;
text-align: center;
font: bold 24px Arial, sans-serif;
}
#noscript a {
color: #98c0ff;
text-decoration: none;
}
</style>
</head>
<body>
<div id="cappuccino-body">
<div class="container">
<div class="content">
<script type="text/javascript">
document.write(loadingHTML);
</script>
</div>
</div>
<noscript style="position:absolute; top:0; left:0; width:100%; height:100%">
<div class="container">
<div class="content">
<div id="noscript">
<p style="font-size:120%; margin-bottom:.75em">JavaScript is required for this application.</p>
<p><a href="http://www.enable-javascript.com" target="_blank">Enable JavaScript</a></p>
</div>
</div>
</div>
</noscript>
</div>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
/*
* AppController.j
* CPButtonBarTest
*
* Created by Ross Boucher on March 23, 2010.
* Copyright 2010, Your Company All rights reserved.
*/
@import <Foundation/Foundation.j>
@import <AppKit/AppKit.j>
@import "AppController.j"
function main(args, namedArgs)
{
CPApplicationMain(args, namedArgs);
}
@@ -308,4 +308,17 @@ var rowHeights = [ ];
return anItem.customHeight;
}
- (CPString)outlineView:(CPOutlineView)anOutlineView toolTipForView:(CPView)aView rect:(CGRect)aRect tableColumn:(CPTableColumn)aTableColumn item:(id)anItem mouseLocation:(CGPoint)aPoint
{
if (anItem === nil)
return nil;
var title = [anItem title],
childCount = [[anItem children] count],
childString = childCount === 1 ? @"1 child" : childCount + @" children";
// Dynamischer Tooltip basierend auf dem 'Menu' Objekt
return [CPString stringWithFormat:@"Outline Node: %@\nContains: %@", title, childString];
}
@end
@@ -0,0 +1,153 @@
/*
* AppController.j
* TreeControllerBindingsTest
*
* Created for testing CPOutlineView and CPTreeController bindings.
*/
@import <AppKit/AppKit.j>
@import <AppKit/CPTreeController.j>
@implementation AppController : CPObject
{
CPTreeController treeController;
CPTextField logField;
}
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
contentView = [theWindow contentView];
// 1. Create the Data Model
var root1 = [[Node alloc] initWithName:@"Root 1" children:[]],
child1 = [[Node alloc] initWithName:@"Child 1.1" children:[]],
child2 = [[Node alloc] initWithName:@"Child 1.2" children:[]],
root2 = [[Node alloc] initWithName:@"Root 2" children:[]],
child3 = [[Node alloc] initWithName:@"Child 2.1" children:[]];
[root1 setChildren:[child1, child2]];
[root2 setChildren:[child3]];
var contentArray = [root1, root2];
// 2. Setup the Tree Controller
treeController = [[CPTreeController alloc] init];
[treeController setChildrenKeyPath:@"children"];
[treeController setContent:contentArray];
// 3. Setup the Outline View
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(20, 20, 250, 300)];
[scrollView setAutohidesScrollers:YES];
var outlineView = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 250, 300)];
var column = [[CPTableColumn alloc] initWithIdentifier:@"name"];
[[column headerView] setStringValue:@"Node Name"];
[column setWidth:240];
[column setEditable:YES]; // Editable to test bidirectional bindings in the tree
[outlineView addTableColumn:column];
[outlineView setOutlineTableColumn:column];
[outlineView setAllowsMultipleSelection:YES];
[scrollView setDocumentView:outlineView];
[contentView addSubview:scrollView];
// 4. Establish Bindings for the Outline View
[outlineView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
[outlineView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
var scrollView2 = [[CPScrollView alloc] initWithFrame:CGRectMake(300, 20, 250, 300)];
[scrollView2 setAutohidesScrollers:YES];
var outlineView2 = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 250, 300)];
var column2 = [[CPTableColumn alloc] initWithIdentifier:@"name"];
[[column2 headerView] setStringValue:@"Node Name"];
[column2 setWidth:240];
[column2 setEditable:YES]; // Editable to test bidirectional bindings in the tree
[outlineView2 addTableColumn:column2];
[outlineView2 setOutlineTableColumn:column2];
[outlineView2 setAllowsMultipleSelection:YES];
[scrollView2 setDocumentView:outlineView2];
[contentView addSubview:scrollView2];
// 4. Establish Bindings for the Outline View
[outlineView2 bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
[outlineView2 bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
[theWindow orderFront:self];
}
- (void)selectSpecificNode:(id)sender
{
// Programmatically select index path [0, 1] which is "Child 1.2"
// This tests the `_CPOutlineViewSelectionIndexPathsBinder` auto-expand logic.
var path =[CPIndexPath indexPathWithIndexes:[0, 1]];
[treeController setSelectionIndexPath:path];
}
- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context
{
if (keyPath === @"selectionIndexPaths")
{
var selectedObjects = [treeController selectedObjects];
if ([selectedObjects count] > 0)
{
var names = [CPMutableArray array];
for (var i = 0; i <[selectedObjects count]; i++)
[names addObject:[selectedObjects[i] name]];[logField setStringValue:[names componentsJoinedByString:@", "]];
}
else
{
[logField setStringValue:@"Nothing selected"];
}
}
}
@end
// --- Custom Data Model ---
@implementation Node : CPObject
{
CPString name;
CPArray children;
}
- (id)initWithName:(CPString)aName children:(CPArray)someChildren
{
self = [super init];
if (self)
{
name = aName;
children = someChildren;
}
return self;
}
// Explicit accessors to ensure Key-Value Observing (KVO) works flawlessly.
- (void)setName:(CPString)aName
{
[self willChangeValueForKey:@"name"];
name = aName;[self didChangeValueForKey:@"name"];
}
- (CPString)name
{
return name;
}
- (void)setChildren:(CPArray)someChildren
{
[self willChangeValueForKey:@"children"];
children = someChildren;
[self didChangeValueForKey:@"children"];
}
- (CPArray)children
{
return children;
}
@end
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CPApplicationDelegateClass</key>
<string>AppController</string>
<key>CPBundleName</key>
<string>treecontroller</string>
<key>CPPrincipalClass</key>
<string>CPApplication</string>
</dict>
</plist>
@@ -0,0 +1,93 @@
/*
* Jakefile
* tooltips
*
* Created by You on April 26, 2011.
* Copyright 2011, Your Company All rights reserved.
*/
var ENV = require("system").env,
FILE = require("file"),
JAKE = require("jake"),
task = JAKE.task,
FileList = JAKE.FileList,
app = require("cappuccino/jake").app,
configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug",
OS = require("os");
app ("tooltips", function(task)
{
task.setBuildIntermediatesPath(FILE.join("Build", "tooltips.build", configuration));
task.setBuildPath(FILE.join("Build", configuration));
task.setProductName("tooltips");
task.setIdentifier("com.yourcompany.tooltips");
task.setVersion("1.0");
task.setAuthor("Your Company");
task.setEmail("feedback @nospam@ yourcompany.com");
task.setSummary("tooltips");
task.setSources((new FileList("**/*.j")).exclude(FILE.join("Build", "**")));
task.setResources(new FileList("Resources/**"));
task.setIndexFilePath("index.html");
task.setInfoPlistPath("Info.plist");
if (configuration === "Debug")
task.setCompilerFlags("-DDEBUG -g");
else
task.setCompilerFlags("-O");
});
task ("default", ["tooltips"], 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()
{
OS.system(["open", FILE.join("Build", "Debug", "tooltips", "index.html")]);
});
task ("run-release", ["release"], function()
{
OS.system(["open", FILE.join("Build", "Release", "tooltips", "index.html")]);
});
task ("deploy", ["release"], function()
{
FILE.mkdirs(FILE.join("Build", "Deployment", "tooltips"));
OS.system(["press", "-f", FILE.join("Build", "Release", "tooltips"), FILE.join("Build", "Deployment", "tooltips")]);
printResults("Deployment")
});
task ("desktop", ["release"], function()
{
FILE.mkdirs(FILE.join("Build", "Desktop", "tooltips"));
require("cappuccino/nativehost").buildNativeHost(FILE.join("Build", "Release", "tooltips"), FILE.join("Build", "Desktop", "tooltips", "tooltips.app"));
printResults("Desktop")
});
task ("run-desktop", ["desktop"], function()
{
OS.system([FILE.join("Build", "Desktop", "tooltips", "tooltips.app", "Contents", "MacOS", "NativeHost"), "-i"]);
});
function printResults(configuration)
{
print("----------------------------");
print(configuration+" app built at path: "+FILE.join("Build", configuration, "tooltips"));
print("----------------------------");
}
@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="en">
<!--
index-debug.html
__project.name__
Created by __user.name__ on __project.date__.
Copyright __project.year__, __organization.name__ All rights reserved.
-->
<head>
<meta charset="utf-8">
<!--[if lte IE 8]>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
<![endif]-->
<!--[if gte IE 9]>
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
<![endif]-->
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="Resources/icon.png">
<link rel="apple-touch-startup-image" href="Resources/default.png">
<title>__project.name__</title>
<!-- Custom javascript goes here -->
<!-- End custom javascript -->
<script type="text/javascript">
OBJJ_MAIN_FILE = "main.j";
OBJJ_INCLUDE_PATHS = ["Frameworks/Debug", "Frameworks"];
// The below will tell the compiler to generate debug symbols, type signatures and not inline objj_msgSend functions.
// This will affect only Objective-J code that is compiled when loading the application. It will not affect precompiled
// code like the Cappuccino frameworks.
//
// Debug symbols will give each Objective-J method a Javascript function name. Without a name it will be very hard to find
// the methods in the debugger.
// Type Signatures will give type information to the Objective-J runtime for instance variables and methods.
// Inline objj_msgSend will give a speed increase but decorators will not work. Check below on debug options for
// more information on decorators.
//
// Uncomment or comment on the line below to change the flags
OBJJ_COMPILER_FLAGS = ["IncludeDebugSymbols", "IncludeTypeSignatures", "SourceMap", "InlineMsgSend"];
var progressBar = null;
OBJJ_PROGRESS_CALLBACK = function(percent, appSize, path)
{
percent = percent * 100;
if (!progressBar)
progressBar = document.getElementById("progress-bar");
if (progressBar)
progressBar.style.width = Math.min(percent, 100) + "%";
}
var loadingHTML =
'<div id="loading">' +
' <div id="loading-text">Loading...</div>' +
' <div id="progress-indicator">' +
' <span id="progress-bar" style="width:0%"></span>' +
' </div>' +
'</div>';
</script>
<script type="text/javascript" src="Frameworks/Debug/Objective-J/Objective-J.js" charset="UTF-8"></script>
<script type="text/javascript">
objj_msgSend_reset();
// DEBUG OPTIONS:
// Decorators will only work when the code is compiled without the compiler option 'InlineMsgSend'. Check above.
// Uncomment to enable printing of backtraces on exceptions:
//objj_msgSend_decorate(objj_backtrace_decorator);
// Uncomment to supress exceptions that take place inside a message
//objj_msgSend_decorate(objj_supress_exceptions_decorator)
// Uncomment to enable runtime type checking:
//objj_msgSend_decorate(objj_typecheck_decorator);
// Uncomment (along with both above) to print backtraces on type check errors:
//objj_typecheck_prints_backtrace = true;
// Uncomment to disable the default logger (CPLogConsole if window.console exists, CPLogPopup otherwise):
//CPLogUnregister(CPLogDefault);
// Uncomment to enable a specific logger:
//CPLogRegister(CPLogConsole);
//CPLogRegister(CPLogPopup);
// Tag view DOM elements with a "data-cappuccino-view" attribute that contains
// the class name of the view that created them. Comment this or set to false to disable.
appkit_tag_dom_elements = true;
</script>
<style type="text/css">
html, body, h1, p {
margin: 0;
padding: 0;
}
/* We need a body wrapper because Cappuccino is unhappy if we change the body element */
#cappuccino-body {
/* Position it absolutely so it will fill the height without content */
position: absolute;
top: 0;
bottom: 0;
width: 100%;
/* Put it at the bottom of the stack so it doesn't interfere with UI */
z-index: 0;
}
#cappuccino-body .container {
display: table;
margin: 0 auto;
height: 100%;
}
#cappuccino-body .content {
display: table-cell;
height: 100%;
vertical-align: top;
}
#loading {
position: relative;
top: 35%;
}
#loading-text {
height: 1.5em;
color: #555;
font: normal bold 36px/36px Arial, sans-serif;
}
#progress-indicator {
padding: 0px;
height: 16px;
border: 5px solid #555;
border-radius: 18px;
background-color: white;
}
#progress-bar {
position: relative;
top: -1px;
left: -1px;
display: block;
height: 18px;
/* Compensate for moving the bar left 1px to overlap the indicator border */
border-right: 1px solid #555;
background-color: #555;
}
#noscript {
position: relative;
top: 35%;
padding: 1em 1.5em;
border: 5px solid #555;
border-radius: 16px;
background-color: white;
color: #555;
text-align: center;
font: bold 24px Arial, sans-serif;
}
#noscript a {
color: #98c0ff;
text-decoration: none;
}
</style>
</head>
<body>
<div id="cappuccino-body">
<div class="container">
<div class="content">
<script type="text/javascript">
document.write(loadingHTML);
</script>
</div>
</div>
<noscript style="position:absolute; top:0; left:0; width:100%; height:100%">
<div class="container">
<div class="content">
<div id="noscript">
<p style="font-size:120%; margin-bottom:.75em">JavaScript is required for this application.</p>
<p><a href="http://www.enable-javascript.com" target="_blank">Enable JavaScript</a></p>
</div>
</div>
</div>
</noscript>
</div>
</body>
</html>
@@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="en">
<!--
index.html
__project.name__
Created by __user.name__ on __project.date__.
Copyright __project.year__, __organization.name__ All rights reserved.
-->
<head>
<meta charset="utf-8">
<!--[if lte IE 8]>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
<![endif]-->
<!--[if gte IE 9]>
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
<![endif]-->
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="Resources/icon.png">
<link rel="apple-touch-startup-image" href="Resources/default.png">
<title>__project.name__</title>
<!-- Custom javascript goes here -->
<!-- End custom javascript -->
<script type="text/javascript">
OBJJ_MAIN_FILE = "main.j";
// The below will tell the compiler to not generate debug symbols but will generate type signatures and inline objj_msgSend functions.
// This will affect only Objective-J code that is compiled when loading the application. It will not affect precompiled
// code like the Cappuccino frameworks.
// Uncomment or comment on the line below to change the flags
OBJJ_COMPILER_FLAGS = [/*"IncludeDebugSymbols"*/, "IncludeTypeSignatures"/*, "SourceMap"*/, "InlineMsgSend"];
var progressBar = null;
OBJJ_PROGRESS_CALLBACK = function(percent, appSize, path)
{
percent = percent * 100;
if (!progressBar)
progressBar = document.getElementById("progress-bar");
if (progressBar)
progressBar.style.width = Math.min(percent, 100) + "%";
}
var loadingHTML =
'<div id="loading">' +
' <div id="loading-text">Loading...</div>' +
' <div id="progress-indicator">' +
' <span id="progress-bar" style="width:0%"></span>' +
' </div>' +
'</div>';
</script>
<script type="text/javascript" src="Frameworks/Objective-J/Objective-J.js" charset="UTF-8"></script>
<style type="text/css">
html, body, h1, p {
margin: 0;
padding: 0;
}
/* We need a body wrapper because Cappuccino is unhappy if we change the body element */
#cappuccino-body {
/* Position it absolutely so it will fill the height without content */
position: absolute;
top: 0;
bottom: 0;
width: 100%;
/* Put it at the bottom of the stack so it doesn't interfere with UI */
z-index: 0;
}
#cappuccino-body .container {
display: table;
margin: 0 auto;
height: 100%;
}
#cappuccino-body .content {
display: table-cell;
height: 100%;
vertical-align: top;
}
#loading {
position: relative;
top: 35%;
}
#loading-text {
height: 1.5em;
color: #555;
font: normal bold 36px/36px Arial, sans-serif;
}
#progress-indicator {
padding: 0px;
height: 16px;
border: 5px solid #555;
border-radius: 18px;
background-color: white;
}
#progress-bar {
position: relative;
top: -1px;
left: -1px;
display: block;
height: 18px;
/* Compensate for moving the bar left 1px to overlap the indicator border */
border-right: 1px solid #555;
background-color: #555;
}
#noscript {
position: relative;
top: 35%;
padding: 1em 1.5em;
border: 5px solid #555;
border-radius: 16px;
background-color: white;
color: #555;
text-align: center;
font: bold 24px Arial, sans-serif;
}
#noscript a {
color: #98c0ff;
text-decoration: none;
}
</style>
</head>
<body>
<div id="cappuccino-body">
<div class="container">
<div class="content">
<script type="text/javascript">
document.write(loadingHTML);
</script>
</div>
</div>
<noscript style="position:absolute; top:0; left:0; width:100%; height:100%">
<div class="container">
<div class="content">
<div id="noscript">
<p style="font-size:120%; margin-bottom:.75em">JavaScript is required for this application.</p>
<p><a href="http://www.enable-javascript.com" target="_blank">Enable JavaScript</a></p>
</div>
</div>
</div>
</noscript>
</div>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
/*
* AppController.j
* tooltips
*
* Created by You on April 26, 2011.
* Copyright 2011, Your Company All rights reserved.
*/
@import <Foundation/Foundation.j>
@import <AppKit/AppKit.j>
@import "AppController.j"
function main(args, namedArgs)
{
CPApplicationMain(args, namedArgs);
}
@@ -64,6 +64,9 @@ var AppControllerInstance = nil;
- (void)awakeFromCib
{
[[tableView tableColumns][0] setDataView:dataView];
// Set the delegate so we can test the new toolTip feature
[tableView setDelegate:self];
rows = [
[CPDictionary dictionaryWithJSObject:{ id:1, filename: "Jack.png", size:1327, uploading:NO, progress:0 }],
@@ -88,6 +91,27 @@ var AppControllerInstance = nil;
return !uploading;
}
// --------------------------------------------------------------------------------------
// Tool tip test
// --------------------------------------------------------------------------------------
- (CPString)tableView:(CPTableView)aTableView toolTipForView:(CPView)aView rect:(CGRect)aRect tableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex mouseLocation:(CGPoint)aPoint
{
// Make sure the row exists
if (aRowIndex < 0 || aRowIndex >= [rows count])
return nil;
var info = [rows objectAtIndex:aRowIndex],
filename = [info valueForKey:@"filename"],
isUploading = [info valueForKey:@"uploading"],
progress = [info valueForKey:@"progress"];
// Provide a dynamic tooltip based on the state of the data in the row
if (isUploading)
return [CPString stringWithFormat:@"Uploading '%@' (%d%% complete)", filename, progress];
return [CPString stringWithFormat:@"File: %@\nStatus: Idle", filename];
}
- (@action)start:(id)sender
{
// Note we use setUploading:YES instead of uploading = YES.
+14 -4
View File
@@ -164,10 +164,20 @@ tableTestDragType = @"CPTableViewTestDragType";
- (void)removeColumn:(id)sender
{
// if ([[tableView tableColumns] containsObject:randomColumn])
[tableView removeTableColumn:randomColumn];
//else
// [tableView addTableColumn:randomColumn];
var columns = [tableView tableColumns];
// Check if we still have columns left to remove
if (columns && [columns count] > 0)
{
var columnToRemove = [columns lastObject];
[tableView removeTableColumn:columnToRemove];
CPLog.debug(@"Removed column with identifier: " + [columnToRemove identifier]);
}
else
{
CPLog.debug(@"No more columns to remove!");
}
}
- (void)addColumn:(id)sender