mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-07 17:38:02 +00:00
Merge remote-tracking branch 'upstream/main' into new-cprulerview
This commit is contained in:
commit
453e8dbd2e
60 files changed
+5340
-983
No files matched your search
@@ -115,3 +115,5 @@
|
||||
@import "CPWindow.j"
|
||||
@import "CPWindowController.j"
|
||||
@import "CPWorkspace.j"
|
||||
@import "CPFontPanel.j"
|
||||
@import "CPTreeController.j"
|
||||
+72
-16
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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]];
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -551,7 +551,7 @@ CPTokenFieldDeleteButtonType = 1;
|
||||
CPTokenFieldCachedDragFunction = nil;
|
||||
|
||||
document.body.ondrag = CPTokenFieldCachedDragFunction;
|
||||
document.body.onselectstart = CPTokenFieldCachedSelectStartFunction
|
||||
document.body.onselectstart = CPTokenFieldCachedSelectStartFunction;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"];
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
indexPath = [CPIndexPath indexPathWithIndex:1];
|
||||
|
||||
[self assert:undefined equals:[treeNode descendantNodeAtIndexPath:indexPath]];
|
||||
[self assert:nil equals:[treeNode descendantNodeAtIndexPath:indexPath]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user