mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-23 08:38:53 +00:00
Merge branch 'main' into refactor-remove-nativehost-target
This commit is contained in:
commit
61e885db5f
34 files changed
+3097
-1196
No files matched your search
+103
-146
@@ -28,7 +28,6 @@
|
||||
@import "CPMenu.j"
|
||||
@import "CPMenuItem.j"
|
||||
|
||||
// Orientations matching AppKit standards
|
||||
@typedef CPRulerOrientation
|
||||
CPHorizontalRuler = 0;
|
||||
CPVerticalRuler = 1;
|
||||
@@ -40,7 +39,7 @@ CPRulerOrientationVertical = 1;
|
||||
@class CPTextTab;
|
||||
|
||||
|
||||
// MARK: - CPRulerMarker (Interactive Handles with Dynamic Alignment Icons)
|
||||
// MARK: - CPRulerMarker
|
||||
|
||||
@implementation CPRulerMarker : CPView
|
||||
{
|
||||
@@ -59,13 +58,18 @@ CPRulerOrientationVertical = 1;
|
||||
_imageValue = anImageValue;
|
||||
_representedObject = anObject;
|
||||
|
||||
// CRITICAL: Subviews must not intercept mouse events so CPRulerView receives all mouseDragged: events
|
||||
[self setHitTests:NO];
|
||||
|
||||
_label = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 12, 12)];
|
||||
[_label setFont:[CPFont systemFontOfSize:10.0]];
|
||||
[_label setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]];
|
||||
[_label setAlignment:CPCenterTextAlignment];
|
||||
[_label setHitTests:NO];
|
||||
[self addSubview:_label];
|
||||
|
||||
_customHandleView = [[CPView alloc] initWithFrame:CGRectMakeZero()];
|
||||
[_customHandleView setHitTests:NO];
|
||||
[self addSubview:_customHandleView];
|
||||
|
||||
[self updateMarkerIcon];
|
||||
@@ -90,8 +94,6 @@ CPRulerOrientationVertical = 1;
|
||||
[self updateMarkerIcon];
|
||||
}
|
||||
|
||||
// Dynamically sets the Unicode triangle direction based on the alignment or indent type,
|
||||
// or draws custom split-height grab handles for indentation controls.
|
||||
- (void)updateMarkerIcon
|
||||
{
|
||||
var isIndentMarker = (_representedObject === @"CPFirstLineIndent" || _representedObject === @"CPHeadIndent");
|
||||
@@ -103,28 +105,19 @@ CPRulerOrientationVertical = 1;
|
||||
|
||||
var frame = [self bounds];
|
||||
[_customHandleView setFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
|
||||
|
||||
// Remove old internal rendering to update cleanly
|
||||
[[_customHandleView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
||||
|
||||
var isFirstLine = (_representedObject === @"CPFirstLineIndent");
|
||||
[_customHandleView setBackgroundColor:[CPColor colorWithWhite:0.45 alpha:1.0]];
|
||||
|
||||
// Dark outline/border representation
|
||||
[_customHandleView setBackgroundColor:[CPColor colorWithWhite:0.5 alpha:1.0]];
|
||||
|
||||
// Inner fill (top handle is lighter, bottom is slightly darker)
|
||||
var innerView = [[CPView alloc] initWithFrame:CGRectMake(1.0, 1.0, frame.size.width - 2.0, frame.size.height - 2.0)];
|
||||
|
||||
if (isFirstLine)
|
||||
[innerView setBackgroundColor:[CPColor colorWithWhite:0.92 alpha:1.0]];
|
||||
else
|
||||
[innerView setBackgroundColor:[CPColor colorWithWhite:0.80 alpha:1.0]];
|
||||
|
||||
var innerView = [[CPView alloc] initWithFrame:CGRectMake(1.0, 1.0, Math.max(1.0, frame.size.width - 2.0), Math.max(1.0, frame.size.height - 2.0))];
|
||||
[innerView setHitTests:NO];
|
||||
[innerView setBackgroundColor:isFirstLine ? [CPColor colorWithWhite:0.95 alpha:1.0] : [CPColor colorWithWhite:0.80 alpha:1.0]];
|
||||
[_customHandleView addSubview:innerView];
|
||||
|
||||
// Horizontal indicator line to visually guide drag interactions
|
||||
var gripLine = [[CPView alloc] initWithFrame:CGRectMake(Math.floor(frame.size.width / 2.0) - 1.0, 2.0, 1.0, frame.size.height - 4.0)];
|
||||
[gripLine setBackgroundColor:[CPColor colorWithWhite:0.6 alpha:1.0]];
|
||||
var gripLine = [[CPView alloc] initWithFrame:CGRectMake(Math.floor(frame.size.width / 2.0) - 1.0, 1.0, 1.0, Math.max(1.0, frame.size.height - 3.0))];
|
||||
[gripLine setHitTests:NO];
|
||||
[gripLine setBackgroundColor:[CPColor colorWithWhite:0.5 alpha:1.0]];
|
||||
[innerView addSubview:gripLine];
|
||||
}
|
||||
else
|
||||
@@ -137,33 +130,23 @@ CPRulerOrientationVertical = 1;
|
||||
{
|
||||
var align = [_representedObject alignment];
|
||||
if (align === CPLeftTextAlignment)
|
||||
[_label setStringValue:@"▶"]; // Left-aligned points Right
|
||||
[_label setStringValue:@"▶"];
|
||||
else if (align === CPCenterTextAlignment)
|
||||
[_label setStringValue:@"▼"]; // Center-aligned points Down
|
||||
[_label setStringValue:@"▼"];
|
||||
else if (align === CPRightTextAlignment)
|
||||
[_label setStringValue:@"◀"]; // Right-aligned points Left
|
||||
}
|
||||
else if ([_representedObject isKindOfClass:[CPString class]])
|
||||
{
|
||||
if (_representedObject === @"CPTailIndent")
|
||||
[_label setStringValue:@"⥘"]; // Solid downward triangle for tail indent
|
||||
else
|
||||
[_label setStringValue:@"⇡"]; // Fallback standard up marker
|
||||
[_label setStringValue:@"◀"];
|
||||
}
|
||||
else
|
||||
{
|
||||
[_label setStringValue:@"⇡"]; // Fallback standard up marker
|
||||
[_label setStringValue:@"▲"];
|
||||
}
|
||||
}
|
||||
}
|
||||
// MARK: -
|
||||
// MARK: Context Menu Support
|
||||
|
||||
- (CPMenu)menuForEvent:(CPEvent)anEvent
|
||||
{
|
||||
var menu = [[CPMenu alloc] initWithTitle:@"Marker Context Menu"];
|
||||
|
||||
// If the marker represents a standard tab stop, allow changing its type
|
||||
if ([_representedObject isKindOfClass:[CPTextTab class]])
|
||||
{
|
||||
var itemLeft = [menu addItemWithTitle:@"Left Tab Stop" action:@selector(changeTypeToLeft:) keyEquivalent:@""],
|
||||
@@ -182,17 +165,11 @@ CPRulerOrientationVertical = 1;
|
||||
[menu addItem:[CPMenuItem separatorItem]];
|
||||
}
|
||||
|
||||
// Determine the context-specific delete title
|
||||
var deleteTitle = @"Delete Tab Stop";
|
||||
if ([_representedObject isKindOfClass:[CPString class]])
|
||||
{
|
||||
if (_representedObject === @"CPFirstLineIndent")
|
||||
deleteTitle = @"Delete 1st line indentation marker";
|
||||
else if (_representedObject === @"CPHeadIndent")
|
||||
deleteTitle = @"Delete head indentation marker";
|
||||
else if (_representedObject === @"CPTailIndent")
|
||||
deleteTitle = @"Delete tail indentation marker";
|
||||
}
|
||||
if (_representedObject === @"CPFirstLineIndent")
|
||||
deleteTitle = @"Reset 1st line indentation";
|
||||
else if (_representedObject === @"CPHeadIndent")
|
||||
deleteTitle = @"Reset head indentation";
|
||||
|
||||
var itemDelete = [menu addItemWithTitle:deleteTitle action:@selector(deleteMarker:) keyEquivalent:@""];
|
||||
[itemDelete setTarget:self];
|
||||
@@ -200,66 +177,49 @@ CPRulerOrientationVertical = 1;
|
||||
return menu;
|
||||
}
|
||||
|
||||
- (void)changeTypeToLeft:(id)sender
|
||||
{
|
||||
[self _changeAlignment:CPLeftTextAlignment];
|
||||
}
|
||||
|
||||
- (void)changeTypeToCenter:(id)sender
|
||||
{
|
||||
[self _changeAlignment:CPCenterTextAlignment];
|
||||
}
|
||||
|
||||
- (void)changeTypeToRight:(id)sender
|
||||
{
|
||||
[self _changeAlignment:CPRightTextAlignment];
|
||||
}
|
||||
- (void)changeTypeToLeft:(id)sender { [self _changeAlignment:CPLeftTextAlignment]; }
|
||||
- (void)changeTypeToCenter:(id)sender { [self _changeAlignment:CPCenterTextAlignment]; }
|
||||
- (void)changeTypeToRight:(id)sender { [self _changeAlignment:CPRightTextAlignment]; }
|
||||
|
||||
- (void)_changeAlignment:(CPTextAlignment)alignment
|
||||
{
|
||||
if (![_representedObject isKindOfClass:[CPTextTab class]])
|
||||
return;
|
||||
|
||||
var oldTab = _representedObject;
|
||||
var newTab = [[CPTextTab alloc] initWithType:alignment location:_imageValue];
|
||||
var oldTab = _representedObject,
|
||||
newTab = [[CPTextTab alloc] initWithType:alignment location:_imageValue];
|
||||
|
||||
// Using setRepresentedObject: automatically updates the marker triangle direction
|
||||
[self setRepresentedObject:newTab];
|
||||
|
||||
var client = [_rulerView clientView];
|
||||
if (client && [client respondsToSelector:@selector(rulerView:didUpdateMarker:oldTab:)])
|
||||
{
|
||||
[client rulerView:_rulerView didUpdateMarker:self oldTab:oldTab];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)deleteMarker:(id)sender
|
||||
{
|
||||
var client = [_rulerView clientView];
|
||||
if (client && [client respondsToSelector:@selector(rulerView:didRemoveMarker:)])
|
||||
{
|
||||
[client rulerView:_rulerView didRemoveMarker:self];
|
||||
}
|
||||
|
||||
[_rulerView removeMarker:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// MARK: - CPRulerView (Pure DOM + Interactive Engine)
|
||||
// MARK: - CPRulerView
|
||||
|
||||
@implementation CPRulerView : CPView
|
||||
{
|
||||
CPScrollView _scrollView @accessors(property=scrollView);
|
||||
CPRulerOrientation _orientation @accessors(property=orientation);
|
||||
CPView _clientView @accessors(property=clientView);
|
||||
|
||||
|
||||
float _ruleThickness @accessors(property=ruleThickness);
|
||||
float _reservedThicknessForMarkers;
|
||||
CPArray _markers;
|
||||
|
||||
// Dragger variables
|
||||
CPRulerMarker _draggingMarker;
|
||||
CPRulerMarker _draggingMarker @accessors(getter=draggingMarker);
|
||||
CGPoint _dragStartPoint;
|
||||
float _dragStartLocation;
|
||||
}
|
||||
@@ -287,7 +247,16 @@ CPRulerOrientationVertical = 1;
|
||||
[self updateRuler];
|
||||
}
|
||||
|
||||
// Markers registration
|
||||
- (CPView)clientView
|
||||
{
|
||||
return _clientView || [_scrollView documentView];
|
||||
}
|
||||
|
||||
- (void)setClientView:(CPView)aView
|
||||
{
|
||||
_clientView = aView;
|
||||
}
|
||||
|
||||
- (void)addMarker:(CPRulerMarker)aMarker
|
||||
{
|
||||
if ([_markers containsObject:aMarker])
|
||||
@@ -317,14 +286,20 @@ CPRulerOrientationVertical = 1;
|
||||
[self addSubview:marker];
|
||||
[self _positionMarker:marker];
|
||||
}
|
||||
[self updateRuler];
|
||||
}
|
||||
|
||||
- (CPRulerMarker)_markerAtPoint:(CGPoint)aPoint
|
||||
{
|
||||
for (var i = 0; i < [_markers count]; i++)
|
||||
// Search in reverse order so indent paddles (added last) are prioritized over overlapping 0-location tabs
|
||||
for (var i = [_markers count] - 1; i >= 0; i--)
|
||||
{
|
||||
var marker = [_markers objectAtIndex:i];
|
||||
if (CGRectContainsPoint([marker frame], aPoint))
|
||||
var marker = [_markers objectAtIndex:i],
|
||||
frame = [marker frame];
|
||||
|
||||
// Expanded hit-box by 3px on all sides for easy grabbing
|
||||
var hitFrame = CGRectMake(frame.origin.x - 3.0, frame.origin.y - 2.0, frame.size.width + 6.0, frame.size.height + 4.0);
|
||||
if (CGRectContainsPoint(hitFrame, aPoint))
|
||||
return marker;
|
||||
}
|
||||
return nil;
|
||||
@@ -340,55 +315,48 @@ CPRulerOrientationVertical = 1;
|
||||
isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal),
|
||||
rulerHeight = CGRectGetHeight([self bounds]),
|
||||
rulerWidth = CGRectGetWidth([self bounds]),
|
||||
markerLocation = [aMarker imageValue];
|
||||
markerLocation = [aMarker imageValue],
|
||||
rep = [aMarker representedObject];
|
||||
|
||||
if (isHorizontal)
|
||||
{
|
||||
var x = markerLocation - scrollPoint.x - 6.0, // Center the 12px wide marker
|
||||
var x = markerLocation - scrollPoint.x - 6.0,
|
||||
y = rulerHeight - 11.0,
|
||||
w = 12.0,
|
||||
h = 12.0;
|
||||
|
||||
// Align the First Line Indent (upper half) and Head Indent (lower half) controls
|
||||
if ([aMarker representedObject] === @"CPFirstLineIndent")
|
||||
|
||||
if (rep === @"CPFirstLineIndent")
|
||||
{
|
||||
y = 0.0;
|
||||
h = Math.floor(rulerHeight / 2.0);
|
||||
}
|
||||
else if ([aMarker representedObject] === @"CPHeadIndent")
|
||||
else if (rep === @"CPHeadIndent")
|
||||
{
|
||||
y = Math.floor(rulerHeight / 2.0);
|
||||
h = rulerHeight - y - 1.0; // Subtract 1px to stay cleanly above bottom border
|
||||
h = rulerHeight - y - 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep normal horizontal markers within the bounds of the ruler to prevent clipping
|
||||
if (x < 0.0)
|
||||
x = 0.0;
|
||||
else if (x + 12.0 > rulerWidth)
|
||||
x = rulerWidth - 12.0;
|
||||
if (x < -6.0) x = -6.0;
|
||||
else if (x + 12.0 > rulerWidth) x = rulerWidth - 12.0;
|
||||
}
|
||||
|
||||
|
||||
[aMarker setFrame:CGRectMake(x, y, w, h)];
|
||||
}
|
||||
else
|
||||
{
|
||||
var x = rulerWidth - 11.0,
|
||||
y = markerLocation - scrollPoint.y - 6.0;
|
||||
|
||||
// Keep vertical marker within the bounds of the ruler to prevent clipping
|
||||
if (y < 0.0)
|
||||
y = 0.0;
|
||||
else if (y + 12.0 > rulerHeight)
|
||||
y = rulerHeight - 12.0;
|
||||
|
||||
|
||||
if (y < 0.0) y = 0.0;
|
||||
else if (y + 12.0 > rulerHeight) y = rulerHeight - 12.0;
|
||||
|
||||
[aMarker setFrame:CGRectMake(x, y, 12.0, 12.0)];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
// MARK: Interaction Handlers
|
||||
// MARK: - Interaction Handlers
|
||||
|
||||
- (void)mouseDown:(CPEvent)anEvent
|
||||
{
|
||||
@@ -400,7 +368,6 @@ CPRulerOrientationVertical = 1;
|
||||
|
||||
var rulerLocation = isHorizontal ? (localPoint.x + scrollPoint.x) : (localPoint.y + scrollPoint.y);
|
||||
|
||||
// 1. Check if clicked an existing marker
|
||||
var clickedMarker = [self _markerAtPoint:localPoint];
|
||||
if (clickedMarker)
|
||||
{
|
||||
@@ -408,20 +375,22 @@ CPRulerOrientationVertical = 1;
|
||||
_dragStartPoint = localPoint;
|
||||
_dragStartLocation = [_draggingMarker imageValue];
|
||||
}
|
||||
// 2. Otherwise, create a new marker dynamically where the user clicked
|
||||
else
|
||||
{
|
||||
// Allow client view to constrain initial placement
|
||||
var client = [self clientView];
|
||||
if (client && [client respondsToSelector:@selector(rulerView:willAddMarker:atLocation:)])
|
||||
rulerLocation = [client rulerView:self willAddMarker:nil atLocation:rulerLocation];
|
||||
|
||||
var newMarker = [[CPRulerMarker alloc] initWithRulerView:self
|
||||
markerLocation:rulerLocation
|
||||
imageValue:rulerLocation
|
||||
representedObject:nil];
|
||||
[self addMarker:newMarker];
|
||||
|
||||
// Notify the client view (e.g., CPTextView) that a new marker was added
|
||||
var client = [self clientView];
|
||||
|
||||
if (client && [client respondsToSelector:@selector(rulerView:didAddMarker:)])
|
||||
[client rulerView:self didAddMarker:newMarker];
|
||||
|
||||
|
||||
_draggingMarker = newMarker;
|
||||
_dragStartPoint = localPoint;
|
||||
_dragStartLocation = rulerLocation;
|
||||
@@ -438,34 +407,35 @@ CPRulerOrientationVertical = 1;
|
||||
isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal);
|
||||
|
||||
var delta = isHorizontal ? (localPoint.x - _dragStartPoint.x) : (localPoint.y - _dragStartPoint.y),
|
||||
newLocation = _dragStartLocation + delta;
|
||||
newLocation = Math.max(0.0, _dragStartLocation + delta);
|
||||
|
||||
if (newLocation < 0) newLocation = 0;
|
||||
// Constrain marker location to last possible position
|
||||
var client = [self clientView];
|
||||
|
||||
if (client && [client respondsToSelector:@selector(rulerView:willMoveMarker:toLocation:)])
|
||||
newLocation = [client rulerView:self willMoveMarker:_draggingMarker toLocation:newLocation];
|
||||
|
||||
[_draggingMarker setImageValue:newLocation];
|
||||
|
||||
// Smoothly redraw ruler and margin bounds on every drag step
|
||||
[self updateRuler];
|
||||
|
||||
// Check if dragged off the ruler (more than 15px off the boundary)
|
||||
var draggedOff = isHorizontal ? (localPoint.y < -15 || localPoint.y > CGRectGetHeight([self bounds]) + 15)
|
||||
: (localPoint.x < -15 || localPoint.x > CGRectGetWidth([self bounds]) + 15);
|
||||
[self _positionMarker:_draggingMarker];
|
||||
|
||||
var rep = [_draggingMarker representedObject],
|
||||
isIndent = (rep === @"CPFirstLineIndent" || rep === @"CPHeadIndent" || rep === @"CPTailIndent");
|
||||
|
||||
var draggedOff = !isIndent && (isHorizontal
|
||||
? (localPoint.y < -15 || localPoint.y > CGRectGetHeight([self bounds]) + 15)
|
||||
: (localPoint.x < -15 || localPoint.x > CGRectGetWidth([self bounds]) + 15));
|
||||
|
||||
if (draggedOff)
|
||||
{
|
||||
// Visual feedback: Dim the handle to 40% and turn the triangle icon gray
|
||||
[_draggingMarker setAlphaValue:0.4];
|
||||
[[_draggingMarker label] setTextColor:[CPColor grayColor]];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore standard styling when dragged back into the active strip
|
||||
[_draggingMarker setAlphaValue:1.0];
|
||||
[[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]];
|
||||
}
|
||||
|
||||
// Notify the CPTextView that the marker coordinates shifted
|
||||
var client = [self clientView];
|
||||
|
||||
if (client && [client respondsToSelector:@selector(rulerView:didMoveMarker:)])
|
||||
[client rulerView:self didMoveMarker:_draggingMarker];
|
||||
}
|
||||
@@ -477,10 +447,12 @@ CPRulerOrientationVertical = 1;
|
||||
|
||||
var localPoint = [self convertPoint:[anEvent locationInWindow] fromView:nil],
|
||||
isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal),
|
||||
|
||||
// If dragged more than 15 pixels off the ruler, delete the marker
|
||||
draggedOff = isHorizontal ? (localPoint.y < -15 || localPoint.y > CGRectGetHeight([self bounds]) + 15)
|
||||
: (localPoint.x < -15 || localPoint.x > CGRectGetWidth([self bounds]) + 15);
|
||||
rep = [_draggingMarker representedObject],
|
||||
isIndent = (rep === @"CPFirstLineIndent" || rep === @"CPHeadIndent" || rep === @"CPTailIndent");
|
||||
|
||||
var draggedOff = !isIndent && (isHorizontal
|
||||
? (localPoint.y < -15 || localPoint.y > CGRectGetHeight([self bounds]) + 15)
|
||||
: (localPoint.x < -15 || localPoint.x > CGRectGetWidth([self bounds]) + 15));
|
||||
|
||||
if (draggedOff)
|
||||
{
|
||||
@@ -492,22 +464,19 @@ CPRulerOrientationVertical = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ensure marker style is fully restored if not deleted
|
||||
[_draggingMarker setAlphaValue:1.0];
|
||||
[[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]];
|
||||
}
|
||||
|
||||
|
||||
_draggingMarker = nil;
|
||||
[self updateRuler];
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
// MARK: DOM Layout Builder
|
||||
// MARK: - Layout Builder
|
||||
|
||||
- (void)updateRuler
|
||||
{
|
||||
// Wipe subviews to redraw the dynamic visible tick lines/numbers
|
||||
[self setSubviews:@[]];
|
||||
|
||||
if (!_scrollView)
|
||||
@@ -526,12 +495,11 @@ CPRulerOrientationVertical = 1;
|
||||
rulerHeight = CGRectGetHeight([self bounds]),
|
||||
rulerWidth = CGRectGetWidth([self bounds]);
|
||||
|
||||
// Draw solid horizontal bottom border (pure, razor-sharp CSS DOM view)
|
||||
var bottomBorder = [[CPView alloc] initWithFrame:CGRectMake(0, rulerHeight - 1, rulerWidth, 1)];
|
||||
[bottomBorder setHitTests:NO];
|
||||
[bottomBorder setBackgroundColor:[CPColor colorWithWhite:0.75 alpha:1.0]];
|
||||
[self addSubview:bottomBorder];
|
||||
|
||||
// Find indent markers to determine background highlight boundaries
|
||||
var firstLineMarker = nil,
|
||||
headMarker = nil;
|
||||
for (var i = 0; i < [_markers count]; i++)
|
||||
@@ -545,31 +513,30 @@ CPRulerOrientationVertical = 1;
|
||||
|
||||
var halfHeight = Math.floor(rulerHeight / 2.0);
|
||||
|
||||
// Draw First Line Indent background - top half (lighter gray)
|
||||
if (firstLineMarker)
|
||||
{
|
||||
var firstLineX = [firstLineMarker imageValue] - scrollPoint.x;
|
||||
if (firstLineX > 0)
|
||||
{
|
||||
var firstLineBg = [[CPView alloc] initWithFrame:CGRectMake(0, 0, firstLineX, halfHeight)];
|
||||
[firstLineBg setHitTests:NO];
|
||||
[firstLineBg setBackgroundColor:[CPColor colorWithWhite:0.93 alpha:1.0]];
|
||||
[self addSubview:firstLineBg];
|
||||
}
|
||||
}
|
||||
|
||||
// Draw Head Indent background - bottom half (slightly darker gray)
|
||||
if (headMarker)
|
||||
{
|
||||
var headX = [headMarker imageValue] - scrollPoint.x;
|
||||
if (headX > 0)
|
||||
{
|
||||
var headBg = [[CPView alloc] initWithFrame:CGRectMake(0, halfHeight, headX, rulerHeight - halfHeight - 1.0)];
|
||||
[headBg setHitTests:NO];
|
||||
[headBg setBackgroundColor:[CPColor colorWithWhite:0.86 alpha:1.0]];
|
||||
[self addSubview:headBg];
|
||||
}
|
||||
}
|
||||
|
||||
// Render ruler tick lines and labels on top of shaded areas
|
||||
for (var val = start; val <= end; val += 10)
|
||||
{
|
||||
if (val < 0) continue;
|
||||
@@ -579,30 +546,21 @@ CPRulerOrientationVertical = 1;
|
||||
tickHeight = isMajor ? 8.0 : 4.0,
|
||||
tickY = rulerHeight - tickHeight - 1.0;
|
||||
|
||||
// Tick mark CSS line view
|
||||
var tick = [[CPView alloc] initWithFrame:CGRectMake(screenX, tickY, 1.0, tickHeight)];
|
||||
[tick setHitTests:NO];
|
||||
[tick setBackgroundColor:[CPColor colorWithWhite:0.65 alpha:1.0]];
|
||||
[self addSubview:tick];
|
||||
|
||||
// Unit label
|
||||
if (isMajor)
|
||||
{
|
||||
var labelX = screenX - 20.0,
|
||||
alignment = CPCenterTextAlignment;
|
||||
|
||||
// Adjust label frame and alignment if it lands near left/right bounds
|
||||
if (labelX < 0.0)
|
||||
{
|
||||
labelX = Math.max(0.0, screenX);
|
||||
alignment = CPLeftTextAlignment;
|
||||
}
|
||||
else if (labelX + 40.0 > rulerWidth)
|
||||
{
|
||||
labelX = rulerWidth - 40.0;
|
||||
alignment = CPRightTextAlignment;
|
||||
}
|
||||
if (labelX < 0.0) { labelX = Math.max(0.0, screenX); alignment = CPLeftTextAlignment; }
|
||||
else if (labelX + 40.0 > rulerWidth) { labelX = rulerWidth - 40.0; alignment = CPRightTextAlignment; }
|
||||
|
||||
var label = [[CPTextField alloc] initWithFrame:CGRectMake(labelX, 1.0, 40.0, 12.0)];
|
||||
[label setHitTests:NO];
|
||||
[label setStringValue:[CPString stringWithFormat:@"%d", val]];
|
||||
[label setFont:[CPFont systemFontOfSize:8.0]];
|
||||
[label setTextColor:[CPColor colorWithWhite:0.4 alpha:1.0]];
|
||||
@@ -659,12 +617,11 @@ CPRulerOrientationVertical = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Reposition and display active markers
|
||||
// Add back existing markers without re-creating them
|
||||
for (var i = 0; i < [_markers count]; i++)
|
||||
{
|
||||
var marker = [_markers objectAtIndex:i];
|
||||
if ([marker superview] !== self)
|
||||
[self addSubview:marker];
|
||||
[self addSubview:marker];
|
||||
[self _positionMarker:marker];
|
||||
}
|
||||
}
|
||||
|
||||
+225
-147
@@ -2472,6 +2472,10 @@ Sets the selection to a range of characters in response to user action.
|
||||
if (!ruler)
|
||||
return;
|
||||
|
||||
// Do not rebuild markers if user is currently dragging one
|
||||
if ([ruler draggingMarker])
|
||||
return;
|
||||
|
||||
var selectedRange = [self selectedRange],
|
||||
paragraphStyle = [CPParagraphStyle defaultParagraphStyle],
|
||||
currentAttributes = _typingAttributes;
|
||||
@@ -2480,10 +2484,10 @@ Sets the selection to a range of characters in response to user action.
|
||||
if (textLength > 0)
|
||||
{
|
||||
var charIndex = selectedRange.location;
|
||||
|
||||
// Safety bounds checks for cursor placements
|
||||
|
||||
if (charIndex >= textLength)
|
||||
charIndex = textLength - 1;
|
||||
|
||||
if (charIndex < 0)
|
||||
charIndex = 0;
|
||||
|
||||
@@ -2497,7 +2501,6 @@ Sets the selection to a range of characters in response to user action.
|
||||
tabStops = [paragraphStyle tabStops],
|
||||
count = [tabStops count];
|
||||
|
||||
// A. Load existing tab stop markers onto the ruler
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var tab = [tabStops objectAtIndex:i],
|
||||
@@ -2508,7 +2511,6 @@ Sets the selection to a range of characters in response to user action.
|
||||
[markers addObject:marker];
|
||||
}
|
||||
|
||||
// B. Add Indentation Handles (First line indent & Head indent)
|
||||
var firstLineMarker = [[CPRulerMarker alloc] initWithRulerView:ruler
|
||||
markerLocation:[paragraphStyle firstLineHeadIndent]
|
||||
imageValue:[paragraphStyle firstLineHeadIndent]
|
||||
@@ -2524,7 +2526,6 @@ Sets the selection to a range of characters in response to user action.
|
||||
[ruler setMarkers:markers];
|
||||
}
|
||||
|
||||
// Local comparison function helper for tab sorting
|
||||
var compareTabStops = function(obj1, obj2, context) {
|
||||
if ([obj1 location] < [obj2 location]) return CPOrderedAscending;
|
||||
if ([obj1 location] > [obj2 location]) return CPOrderedDescending;
|
||||
@@ -2533,72 +2534,6 @@ var compareTabStops = function(obj1, obj2, context) {
|
||||
|
||||
- (void)rulerView:(CPRulerView)rulerView didAddMarker:(CPRulerMarker)marker
|
||||
{
|
||||
if (![self _didBeginEditing] || ![self shouldChangeTextInRange:_selectionRange replacementString:nil])
|
||||
return;
|
||||
|
||||
var selectedRange = [self selectedRange],
|
||||
paragraphStyle = [CPParagraphStyle defaultParagraphStyle],
|
||||
currentAttributes = _typingAttributes;
|
||||
|
||||
// Retrieve active attributes at the cursor position if there is no selection
|
||||
var textLength = [_textStorage length],
|
||||
charIndex = selectedRange.location;
|
||||
|
||||
if (textLength > 0)
|
||||
{
|
||||
if (charIndex >= textLength)
|
||||
charIndex = textLength - 1;
|
||||
if (charIndex < 0)
|
||||
charIndex = 0;
|
||||
|
||||
currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil];
|
||||
}
|
||||
|
||||
if ([currentAttributes objectForKey:CPParagraphStyleAttributeName])
|
||||
paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName];
|
||||
|
||||
var mutableStyle = [paragraphStyle mutableCopy];
|
||||
|
||||
// Create a new Left-aligned tab stop where the user clicked
|
||||
var newTab = [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:[marker imageValue]],
|
||||
tabs = [[mutableStyle tabStops] mutableCopy];
|
||||
|
||||
[tabs addObject:newTab];
|
||||
|
||||
// Sort tabs by location ascending
|
||||
[tabs sortUsingFunction:compareTabStops context:nil];
|
||||
|
||||
[mutableStyle setTabStops:tabs];
|
||||
[marker setRepresentedObject:newTab];
|
||||
|
||||
// Find the target text range to modify (selection or containing paragraph)
|
||||
var targetRange = selectedRange;
|
||||
if (targetRange.length === 0)
|
||||
targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph];
|
||||
|
||||
if (targetRange.length > 0)
|
||||
{
|
||||
[_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)];
|
||||
|
||||
[_layoutManager textStorage:_textStorage
|
||||
edited:0
|
||||
range:CPMakeRangeCopy(targetRange)
|
||||
changeInLength:0
|
||||
invalidatedRange:CPMakeRangeCopy(targetRange)];
|
||||
}
|
||||
|
||||
if (selectedRange.length === 0)
|
||||
{
|
||||
[_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName];
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)rulerView:(CPRulerView)rulerView didMoveMarker:(CPRulerMarker)marker
|
||||
{
|
||||
if (![self _didBeginEditing] || ![self shouldChangeTextInRange:_selectionRange replacementString:nil])
|
||||
return;
|
||||
|
||||
var selectedRange = [self selectedRange],
|
||||
paragraphStyle = [CPParagraphStyle defaultParagraphStyle],
|
||||
currentAttributes = _typingAttributes;
|
||||
@@ -2610,6 +2545,7 @@ var compareTabStops = function(obj1, obj2, context) {
|
||||
{
|
||||
if (charIndex >= textLength)
|
||||
charIndex = textLength - 1;
|
||||
|
||||
if (charIndex < 0)
|
||||
charIndex = 0;
|
||||
|
||||
@@ -2620,69 +2556,31 @@ var compareTabStops = function(obj1, obj2, context) {
|
||||
paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName];
|
||||
|
||||
var mutableStyle = [paragraphStyle mutableCopy],
|
||||
oldTab = [marker representedObject];
|
||||
newTab = [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:[marker imageValue]],
|
||||
tabs = [[mutableStyle tabStops] mutableCopy];
|
||||
|
||||
if (!oldTab)
|
||||
return;
|
||||
[tabs addObject:newTab];
|
||||
[tabs sortUsingFunction:compareTabStops context:nil];
|
||||
|
||||
// A. Handle standard tab stops
|
||||
if ([oldTab isKindOfClass:[CPTextTab class]])
|
||||
{
|
||||
var newTab = [[CPTextTab alloc] initWithType:[oldTab alignment] location:[marker imageValue]],
|
||||
tabs = [[mutableStyle tabStops] mutableCopy];
|
||||
[mutableStyle setTabStops:tabs];
|
||||
[marker setRepresentedObject:newTab];
|
||||
|
||||
[tabs removeObject:oldTab];
|
||||
[tabs addObject:newTab];
|
||||
|
||||
// Sort tabs by location ascending
|
||||
[tabs sortUsingFunction:compareTabStops context:nil];
|
||||
|
||||
[mutableStyle setTabStops:tabs];
|
||||
[marker setRepresentedObject:newTab];
|
||||
}
|
||||
// B. Handle Indentation Marker drags (First Line, Left, and Right indents)
|
||||
else if ([oldTab isKindOfClass:[CPString class]])
|
||||
{
|
||||
if (oldTab === @"CPFirstLineIndent")
|
||||
[mutableStyle setFirstLineHeadIndent:[marker imageValue]];
|
||||
else if (oldTab === @"CPHeadIndent")
|
||||
[mutableStyle setHeadIndent:[marker imageValue]];
|
||||
else if (oldTab === @"CPTailIndent")
|
||||
[mutableStyle setTailIndent:[marker imageValue]];
|
||||
}
|
||||
|
||||
// Find the target text range to modify (selection or containing paragraph)
|
||||
var targetRange = selectedRange;
|
||||
if (targetRange.length === 0)
|
||||
if (targetRange.length === 0 && textLength > 0)
|
||||
targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph];
|
||||
|
||||
if (targetRange.length > 0)
|
||||
{
|
||||
[_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)];
|
||||
|
||||
[_layoutManager textStorage:_textStorage
|
||||
edited:0
|
||||
range:CPMakeRangeCopy(targetRange)
|
||||
changeInLength:0
|
||||
invalidatedRange:CPMakeRangeCopy(targetRange)];
|
||||
[_layoutManager textStorage:_textStorage edited:0 range:CPMakeRangeCopy(targetRange) changeInLength:0 invalidatedRange:CPMakeRangeCopy(targetRange)];
|
||||
}
|
||||
|
||||
if (selectedRange.length === 0)
|
||||
{
|
||||
[_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName];
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
|
||||
}
|
||||
|
||||
[_layoutManager _validateLayoutAndGlyphs];
|
||||
[self sizeToFit];
|
||||
[self setNeedsDisplay:YES];
|
||||
[_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName];
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
|
||||
}
|
||||
|
||||
- (void)rulerView:(CPRulerView)rulerView didRemoveMarker:(CPRulerMarker)marker
|
||||
- (void)rulerView:(CPRulerView)rulerView didMoveMarker:(CPRulerMarker)marker
|
||||
{
|
||||
if (![self _didBeginEditing] || ![self shouldChangeTextInRange:_selectionRange replacementString:nil])
|
||||
return;
|
||||
|
||||
var selectedRange = [self selectedRange],
|
||||
paragraphStyle = [CPParagraphStyle defaultParagraphStyle],
|
||||
currentAttributes = _typingAttributes;
|
||||
@@ -2694,12 +2592,78 @@ var compareTabStops = function(obj1, obj2, context) {
|
||||
{
|
||||
if (charIndex >= textLength)
|
||||
charIndex = textLength - 1;
|
||||
|
||||
if (charIndex < 0)
|
||||
charIndex = 0;
|
||||
|
||||
currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil];
|
||||
}
|
||||
|
||||
if ([currentAttributes objectForKey:CPParagraphStyleAttributeName])
|
||||
paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName];
|
||||
|
||||
var mutableStyle = [paragraphStyle mutableCopy],
|
||||
rep = [marker representedObject];
|
||||
|
||||
if (!rep)
|
||||
return;
|
||||
|
||||
// A. Handle standard tab stops
|
||||
if ([rep isKindOfClass:[CPTextTab class]])
|
||||
{
|
||||
var newTab = [[CPTextTab alloc] initWithType:[rep alignment] location:[marker imageValue]],
|
||||
tabs = [[mutableStyle tabStops] mutableCopy];
|
||||
|
||||
[tabs removeObject:rep];
|
||||
[tabs addObject:newTab];
|
||||
[tabs sortUsingFunction:compareTabStops context:nil];
|
||||
|
||||
[mutableStyle setTabStops:tabs];
|
||||
[marker setRepresentedObject:newTab];
|
||||
}
|
||||
|
||||
// B. Handle Indentation Marker drags directly by string literal
|
||||
else if (rep === @"CPFirstLineIndent")
|
||||
[mutableStyle setFirstLineHeadIndent:[marker imageValue]];
|
||||
else if (rep === @"CPHeadIndent")
|
||||
[mutableStyle setHeadIndent:[marker imageValue]];
|
||||
else if (rep === @"CPTailIndent")
|
||||
[mutableStyle setTailIndent:[marker imageValue]];
|
||||
|
||||
var targetRange = selectedRange;
|
||||
|
||||
if (targetRange.length === 0 && textLength > 0)
|
||||
targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph];
|
||||
|
||||
if (targetRange.length > 0)
|
||||
{
|
||||
[_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)];
|
||||
[_layoutManager textStorage:_textStorage edited:0 range:CPMakeRangeCopy(targetRange) changeInLength:0 invalidatedRange:CPMakeRangeCopy(targetRange)];
|
||||
}
|
||||
|
||||
[_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName];
|
||||
|
||||
[_layoutManager _validateLayoutAndGlyphs];
|
||||
[self sizeToFit];
|
||||
[self setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
- (void)rulerView:(CPRulerView)rulerView didRemoveMarker:(CPRulerMarker)marker
|
||||
{
|
||||
var selectedRange = [self selectedRange],
|
||||
paragraphStyle = [CPParagraphStyle defaultParagraphStyle],
|
||||
currentAttributes = _typingAttributes;
|
||||
|
||||
var textLength = [_textStorage length],
|
||||
charIndex = selectedRange.location;
|
||||
|
||||
if (textLength > 0)
|
||||
{
|
||||
if (charIndex >= textLength) charIndex = textLength - 1;
|
||||
if (charIndex < 0) charIndex = 0;
|
||||
currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil];
|
||||
}
|
||||
|
||||
if ([currentAttributes objectForKey:CPParagraphStyleAttributeName])
|
||||
paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName];
|
||||
|
||||
@@ -2711,31 +2675,20 @@ var compareTabStops = function(obj1, obj2, context) {
|
||||
|
||||
var tabs = [[mutableStyle tabStops] mutableCopy];
|
||||
[tabs removeObject:oldTab];
|
||||
|
||||
[mutableStyle setTabStops:tabs];
|
||||
|
||||
// Find the target text range to modify (selection or containing paragraph)
|
||||
var targetRange = selectedRange;
|
||||
|
||||
if (targetRange.length === 0)
|
||||
if (targetRange.length === 0 && textLength > 0)
|
||||
targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph];
|
||||
|
||||
if (targetRange.length > 0)
|
||||
{
|
||||
[_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)];
|
||||
|
||||
[_layoutManager textStorage:_textStorage
|
||||
edited:0
|
||||
range:CPMakeRangeCopy(targetRange)
|
||||
changeInLength:0
|
||||
invalidatedRange:CPMakeRangeCopy(targetRange)];
|
||||
[_layoutManager textStorage:_textStorage edited:0 range:CPMakeRangeCopy(targetRange) changeInLength:0 invalidatedRange:CPMakeRangeCopy(targetRange)];
|
||||
}
|
||||
|
||||
if (selectedRange.length === 0)
|
||||
{
|
||||
[_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName];
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
|
||||
}
|
||||
[_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName];
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
|
||||
}
|
||||
|
||||
- (void)rulerView:(CPRulerView)rulerView didUpdateMarker:(CPRulerMarker)marker oldTab:(id)oldTab
|
||||
@@ -2759,19 +2712,11 @@ var compareTabStops = function(obj1, obj2, context) {
|
||||
|
||||
[tabs removeObject:oldTab];
|
||||
[tabs addObject:newTab];
|
||||
|
||||
// Sort tabs ascending
|
||||
[tabs sortUsingFunction:compareTabStops context:nil];
|
||||
|
||||
[mutableStyle setTabStops:tabs];
|
||||
|
||||
[_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)];
|
||||
|
||||
[_layoutManager textStorage:_textStorage
|
||||
edited:0
|
||||
range:CPMakeRangeCopy(targetRange)
|
||||
changeInLength:0
|
||||
invalidatedRange:CPMakeRangeCopy(targetRange)];
|
||||
[_layoutManager textStorage:_textStorage edited:0 range:CPMakeRangeCopy(targetRange) changeInLength:0 invalidatedRange:CPMakeRangeCopy(targetRange)];
|
||||
|
||||
if (selectedRange.length === 0)
|
||||
{
|
||||
@@ -2779,12 +2724,145 @@ var compareTabStops = function(obj1, obj2, context) {
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
|
||||
}
|
||||
|
||||
// Force layouts and view canvas update
|
||||
[_layoutManager _validateLayoutAndGlyphs];
|
||||
[self sizeToFit];
|
||||
[self setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
- (float)rulerView:(CPRulerView)aRulerView willMoveMarker:(CPRulerMarker)aMarker toLocation:(float)proposedLocation
|
||||
{
|
||||
var rep = [aMarker representedObject];
|
||||
|
||||
// Indent markers can be dragged anywhere >= 0
|
||||
if (rep === @"CPFirstLineIndent" || rep === @"CPHeadIndent" || rep === @"CPTailIndent")
|
||||
return Math.max(0.0, proposedLocation);
|
||||
|
||||
var textLength = [_textStorage length];
|
||||
if (textLength === 0)
|
||||
return Math.max(0.0, proposedLocation);
|
||||
|
||||
var selectedRange = [self selectedRange],
|
||||
targetRange = selectedRange;
|
||||
|
||||
if (targetRange.length === 0)
|
||||
targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph];
|
||||
|
||||
if (targetRange.length === 0)
|
||||
return Math.max(0.0, proposedLocation);
|
||||
|
||||
[_layoutManager _validateLayoutAndGlyphs];
|
||||
|
||||
var theString = [_textStorage string],
|
||||
minLocation = 0.0,
|
||||
containerWidth = [_textContainer containerSize].width,
|
||||
maxLocation = containerWidth,
|
||||
currentParagraphStyle = [_textStorage attribute:CPParagraphStyleAttributeName atIndex:targetRange.location effectiveRange:nil] || [CPParagraphStyle defaultParagraphStyle],
|
||||
tabStops = [currentParagraphStyle tabStops] || [],
|
||||
tabCount = [tabStops count];
|
||||
|
||||
// 1. Right boundary: Tail indent (if configured)
|
||||
var tailIndent = [currentParagraphStyle tailIndent];
|
||||
if (tailIndent > 0.0)
|
||||
maxLocation = Math.min(maxLocation, tailIndent);
|
||||
else if (tailIndent < 0.0)
|
||||
maxLocation = Math.min(maxLocation, containerWidth + tailIndent);
|
||||
|
||||
// 2. Identify active tab stop index
|
||||
var targetTabIndex = -1;
|
||||
for (var i = 0; i < tabCount; i++)
|
||||
{
|
||||
var tab = [tabStops objectAtIndex:i];
|
||||
if (tab === rep || ([tab location] === [rep location] && [tab alignment] === [rep alignment]))
|
||||
{
|
||||
targetTabIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Constrain to neighboring tab stops (Left and Right)
|
||||
var safetySpacer = 2.0;
|
||||
|
||||
if (targetTabIndex !== -1)
|
||||
{
|
||||
// Left neighbor constraint (previous tab stop)
|
||||
if (targetTabIndex > 0)
|
||||
{
|
||||
var prevTab = [tabStops objectAtIndex:targetTabIndex - 1];
|
||||
minLocation = Math.max(minLocation, [prevTab location] + safetySpacer);
|
||||
}
|
||||
|
||||
// Right neighbor constraint (next tab stop)
|
||||
if (targetTabIndex + 1 < tabCount)
|
||||
{
|
||||
var nextTab = [tabStops objectAtIndex:targetTabIndex + 1];
|
||||
maxLocation = Math.min(maxLocation, [nextTab location] - safetySpacer);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For new markers being added between existing tab stops
|
||||
for (var i = 0; i < tabCount; i++)
|
||||
{
|
||||
var tabLoc = [[tabStops objectAtIndex:i] location];
|
||||
if (tabLoc < proposedLocation)
|
||||
minLocation = Math.max(minLocation, tabLoc + safetySpacer);
|
||||
else if (tabLoc > proposedLocation)
|
||||
maxLocation = Math.min(maxLocation, tabLoc - safetySpacer);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Constrain to preceding text on active lines
|
||||
var start = targetRange.location,
|
||||
end = CPMaxRange(targetRange),
|
||||
tabCountInLine = 0,
|
||||
lineStart = start;
|
||||
|
||||
for (var i = start; i < end; i++)
|
||||
{
|
||||
var charCode = theString.charCodeAt(i);
|
||||
if (charCode === 10 || charCode === 13)
|
||||
{
|
||||
tabCountInLine = 0;
|
||||
lineStart = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (charCode === 9) // '\t'
|
||||
{
|
||||
if (tabCountInLine === targetTabIndex || targetTabIndex === -1)
|
||||
{
|
||||
var precedingX = 0.0;
|
||||
if (i === lineStart)
|
||||
{
|
||||
var isFirstLine = (lineStart === 0 || theString.charCodeAt(lineStart - 1) === 10 || theString.charCodeAt(lineStart - 1) === 13);
|
||||
precedingX = isFirstLine ? [currentParagraphStyle firstLineHeadIndent] : [currentParagraphStyle headIndent];
|
||||
}
|
||||
else
|
||||
{
|
||||
var glyphRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(i - 1, 1) inTextContainer:_textContainer];
|
||||
precedingX = CGRectGetMaxX(glyphRect);
|
||||
}
|
||||
|
||||
var textLimit = precedingX + safetySpacer;
|
||||
if (textLimit > minLocation)
|
||||
minLocation = textLimit;
|
||||
}
|
||||
tabCountInLine++;
|
||||
}
|
||||
}
|
||||
|
||||
if (minLocation > maxLocation)
|
||||
minLocation = maxLocation;
|
||||
|
||||
// 5. Clamp proposedLocation within [minLocation, maxLocation]
|
||||
return Math.min(maxLocation, Math.max(minLocation, proposedLocation));
|
||||
}
|
||||
|
||||
- (float)rulerView:(CPRulerView)aRulerView willAddMarker:(CPRulerMarker)aMarker atLocation:(float)proposedLocation
|
||||
{
|
||||
return [self rulerView:aRulerView willMoveMarker:aMarker toLocation:proposedLocation];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPTextView (CPTextViewDelegate)
|
||||
@@ -3089,8 +3167,8 @@ var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey",
|
||||
{
|
||||
var rect = [_textView._layoutManager boundingRectForGlyphRange:CPMakeRange(aLoc, 1) inTextContainer:_textView._textContainer];
|
||||
|
||||
if (aLoc >= [_textView._layoutManager numberOfCharacters])
|
||||
rect.origin.x = CGRectGetMaxX(rect);
|
||||
if (aLoc >= [_textView._layoutManager numberOfCharacters])
|
||||
rect.origin.x = CGRectGetMaxX(rect);
|
||||
|
||||
[self setRect:rect];
|
||||
}
|
||||
|
||||
@@ -138,32 +138,28 @@ var CPSystemTypesetterFactory,
|
||||
return [_layoutManager textContainers];
|
||||
}
|
||||
|
||||
// Retrieves correct CPTextTab stop accounting for CPArray properties
|
||||
// Retrieves correct CPTextTab stop accounting for custom stops and default intervals
|
||||
- (CPTextTab)textTabForWidth:(double)aWidth writingDirection:(CPWritingDirection)direction
|
||||
{
|
||||
var tabStops = [_currentParagraph tabStops];
|
||||
var tabStops = [_currentParagraph tabStops],
|
||||
defaultInterval = [_currentParagraph defaultTabInterval] || 28.0;
|
||||
|
||||
if (!tabStops)
|
||||
tabStops = [[CPParagraphStyle defaultParagraphStyle] tabStops];
|
||||
var l = tabStops ? [tabStops count] : 0;
|
||||
|
||||
var l = [tabStops count];
|
||||
|
||||
if (l === 0)
|
||||
return nil;
|
||||
|
||||
// Find the first tab stop that is strictly greater than the current width
|
||||
for (var i = 0; i < l; i++)
|
||||
// 1. If custom tab stops exist ahead of current position, use the first one encountered
|
||||
if (l > 0)
|
||||
{
|
||||
var tab = [tabStops objectAtIndex:i];
|
||||
for (var i = 0; i < l; i++)
|
||||
{
|
||||
var tab = [tabStops objectAtIndex:i];
|
||||
|
||||
if ([tab location] > aWidth)
|
||||
return tab;
|
||||
if ([tab location] > aWidth)
|
||||
return tab;
|
||||
}
|
||||
}
|
||||
|
||||
// If aWidth exceeds the last tab stop, dynamically calculate the next
|
||||
// tab location using the default tab interval.
|
||||
var defaultInterval = [_currentParagraph defaultTabInterval] || 28.0;
|
||||
var nextLocation = CEIL((aWidth + 1.0) / defaultInterval) * defaultInterval;
|
||||
// 2. Otherwise (or when all custom tab stops are behind the text), advance to the next default interval
|
||||
var nextLocation = (Math.floor(aWidth / defaultInterval) + 1) * defaultInterval;
|
||||
|
||||
return [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:nextLocation];
|
||||
}
|
||||
@@ -205,7 +201,7 @@ var CPSystemTypesetterFactory,
|
||||
[_layoutManager setLocation:CGPointMake(myX, _lineBase) forStartOfGlyphRange:lineRange];
|
||||
[_layoutManager _setAdvancements:advancements forGlyphRange:lineRange];
|
||||
|
||||
//fix the _lineFragments when fontsizes differ
|
||||
// fix the _lineFragments when fontsizes differ
|
||||
var l = _lineFragments.length;
|
||||
|
||||
for (var i = 0 ; i < l ; i++)
|
||||
@@ -251,7 +247,7 @@ var CPSystemTypesetterFactory,
|
||||
isTabStop = NO,
|
||||
isAttachment = NO,
|
||||
isWordWrapped = NO,
|
||||
numberOfGlyphs= [_textStorage length],
|
||||
numberOfGlyphs = [_textStorage length],
|
||||
leading,
|
||||
numLines = 0,
|
||||
theString = [_textStorage string],
|
||||
@@ -293,7 +289,7 @@ var CPSystemTypesetterFactory,
|
||||
|
||||
for (; numLines != maxNumLines && glyphIndex < numberOfGlyphs; glyphIndex++)
|
||||
{
|
||||
// check whether there any change in the attributes from here on
|
||||
// check whether there is any change in the attributes from here on
|
||||
if (!CPLocationInRange(glyphIndex, _attributesRange))
|
||||
{
|
||||
_currentAttributes = [_textStorage attributesAtIndex:glyphIndex effectiveRange:_attributesRange];
|
||||
@@ -327,15 +323,6 @@ var CPSystemTypesetterFactory,
|
||||
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
|
||||
isFirstLineOfLayout = NO;
|
||||
}
|
||||
|
||||
// Calculate the right wrapping margin based on tail indent
|
||||
var tailIndent = [_currentParagraph tailIndent];
|
||||
if (tailIndent > 0.0)
|
||||
rightMargin = tailIndent;
|
||||
else if (tailIndent < 0.0)
|
||||
rightMargin = containerSizeWidth + tailIndent;
|
||||
else
|
||||
rightMargin = containerSizeWidth;
|
||||
|
||||
// Handle the layout's very first line indentation
|
||||
if (isFirstLineOfLayout)
|
||||
@@ -419,10 +406,10 @@ var CPSystemTypesetterFactory,
|
||||
// We are processing characters, so we are no longer at the start of a physical line
|
||||
isStartOfPhysicalLine = NO;
|
||||
|
||||
var currentCharCode = theString.charCodeAt(glyphIndex), // use pure javascript methods for performance reasons
|
||||
var currentCharCode = theString.charCodeAt(glyphIndex),
|
||||
rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor;
|
||||
|
||||
switch (currentCharCode) // faster than sending actionForControlCharacterAtIndex: called for each char.
|
||||
switch (currentCharCode)
|
||||
{
|
||||
case CPAttachmentCharacter:
|
||||
{
|
||||
@@ -454,7 +441,8 @@ var CPSystemTypesetterFactory,
|
||||
}
|
||||
case 9: // '\t'
|
||||
{
|
||||
var nextTab = [self textTabForWidth:rangeWidth + lineOrigin.x writingDirection:0];
|
||||
// Measure against the actual text position before the tab stop
|
||||
var nextTab = [self textTabForWidth:prevRangeWidth + lineOrigin.x writingDirection:0];
|
||||
|
||||
isTabStop = YES;
|
||||
|
||||
@@ -501,7 +489,7 @@ var CPSystemTypesetterFactory,
|
||||
}
|
||||
else
|
||||
{
|
||||
rangeWidth += 28.0; // standard fallback spacer
|
||||
rangeWidth = prevRangeWidth + 28.0; // standard fallback spacer
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -511,10 +499,6 @@ var CPSystemTypesetterFactory,
|
||||
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;
|
||||
@@ -544,7 +528,7 @@ var CPSystemTypesetterFactory,
|
||||
|
||||
isNewline = YES;
|
||||
isWordWrapped = YES;
|
||||
glyphIndex = CPMaxRange(lineRange) - 1; // start the line starts directly at current character
|
||||
glyphIndex = CPMaxRange(lineRange) - 1;
|
||||
}
|
||||
|
||||
if (isNewline || isTabStop || isAttachment)
|
||||
@@ -576,8 +560,6 @@ var CPSystemTypesetterFactory,
|
||||
containerSizeHeight = containerSize.height;
|
||||
}
|
||||
|
||||
// If this is a soft wrap (isWordWrapped), next line gets headIndent.
|
||||
// If it was a paragraph return, it gets firstLineHeadIndent.
|
||||
isFirstLineOfParagraph = !isWordWrapped;
|
||||
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
|
||||
|
||||
@@ -603,7 +585,7 @@ var CPSystemTypesetterFactory,
|
||||
}
|
||||
}
|
||||
|
||||
// this is to "flush" the remaining characters
|
||||
// Flush remaining characters
|
||||
if (lineRange.length)
|
||||
[self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:NO];
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ function _points2twips(a) { return (a) * 20.0; }
|
||||
|
||||
if (!num)
|
||||
[colorDict setObject:num = [CPNumber numberWithInt:[colorDict count] + 1]
|
||||
forKey:[color cssString]];
|
||||
forKey:[color cssString]];
|
||||
|
||||
return [num intValue];
|
||||
}
|
||||
|
||||
+90
-71
@@ -50,6 +50,9 @@
|
||||
|
||||
[self exposeBinding:@"contentArray"];
|
||||
[self exposeBinding:@"sortDescriptors"];
|
||||
[self exposeBinding:@"selectionIndexPaths"];
|
||||
[self exposeBinding:@"selectionIndexPath"];
|
||||
[self exposeBinding:@"selectedObjects"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingContentArray
|
||||
@@ -59,7 +62,7 @@
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingArrangedObjects
|
||||
{
|
||||
return [CPSet setWithObjects:@"content", @"sortDescriptors", @"childrenKeyPath"];
|
||||
return [CPSet setWithObjects:@"content", @"contentArray", @"sortDescriptors", @"childrenKeyPath"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectionIndexPath
|
||||
@@ -69,27 +72,27 @@
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectedObjects
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths", @"arrangedObjects"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectedNodes
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanAddChild
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths", @"arrangedObjects"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanInsert
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
return [CPSet setWithObjects:@"editable"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanInsertChild
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths", @"editable"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanAddChild
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths", @"editable"];
|
||||
}
|
||||
|
||||
- (id)init
|
||||
@@ -115,7 +118,8 @@
|
||||
}
|
||||
|
||||
- (void)prepareContent
|
||||
{[self _setContentArray:[CPArray arrayWithObject:[self newObject]]];
|
||||
{
|
||||
[self _setContentArray:[CPArray arrayWithObject:[self newObject]]];
|
||||
}
|
||||
|
||||
- (BOOL)preservesSelection { return _preservesSelection; }
|
||||
@@ -144,7 +148,8 @@
|
||||
- (void)setChildrenKeyPath:(CPString)aKeyPath
|
||||
{
|
||||
if (_childrenKeyPath === aKeyPath) return;
|
||||
_childrenKeyPath = aKeyPath;[self rearrangeObjects];
|
||||
_childrenKeyPath = aKeyPath;
|
||||
[self rearrangeObjects];
|
||||
}
|
||||
|
||||
- (CPString)countKeyPath { return _countKeyPath; }
|
||||
@@ -166,14 +171,21 @@
|
||||
if (![value isKindOfClass:[CPArray class]])
|
||||
value = [CPArray arrayWithObject:value];
|
||||
|
||||
if (_contentObject === value)
|
||||
return;
|
||||
|
||||
var oldSelectedObjects = nil,
|
||||
oldSelectionIndexPaths = nil;
|
||||
oldSelectionIndexPaths = nil;
|
||||
|
||||
if ([self preservesSelection])
|
||||
oldSelectedObjects = [self selectedObjects];
|
||||
else
|
||||
oldSelectionIndexPaths = [self selectionIndexPaths];
|
||||
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"content"];
|
||||
[self willChangeValueForKey:@"contentArray"];
|
||||
|
||||
_contentObject = value;
|
||||
|
||||
[self _rearrangeObjects];
|
||||
@@ -182,23 +194,32 @@
|
||||
[self __setSelectedObjects:oldSelectedObjects];
|
||||
else
|
||||
[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection];
|
||||
|
||||
[self didChangeValueForKey:@"contentArray"];
|
||||
[self didChangeValueForKey:@"content"];
|
||||
[self _selectionDidChange];
|
||||
}
|
||||
|
||||
- (void)_setContentArray:(id)anArray {[self setContent:anArray]; }
|
||||
|
||||
- (void)_setContentArray:(id)anArray { [self setContent:anArray]; }
|
||||
- (id)contentArray { return _contentObject; }
|
||||
- (id)arrangedObjects { return _arrangedObjects; }
|
||||
|
||||
- (void)rearrangeObjects
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"arrangedObjects"];
|
||||
|
||||
[self _rearrangeObjects];
|
||||
|
||||
[self didChangeValueForKey:@"arrangedObjects"];
|
||||
[self _selectionDidChange];
|
||||
}
|
||||
|
||||
- (void)_rearrangeObjects
|
||||
{
|
||||
var oldSelectedObjects = nil,
|
||||
oldSelectionIndexPaths = nil;
|
||||
oldSelectionIndexPaths = nil;
|
||||
|
||||
if ([self preservesSelection])
|
||||
oldSelectedObjects = [self selectedObjects];
|
||||
@@ -216,7 +237,7 @@
|
||||
- (void)__rebuildArrangedObjectsTree
|
||||
{
|
||||
var rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil],
|
||||
contentArray = [self contentArray];
|
||||
contentArray = [self contentArray];
|
||||
|
||||
if (contentArray && [contentArray count] > 0)
|
||||
{
|
||||
@@ -244,7 +265,7 @@
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var obj = [sortedObjects objectAtIndex:i],
|
||||
node = [[CPTreeNode alloc] initWithRepresentedObject:obj];
|
||||
node = [[CPTreeNode alloc] initWithRepresentedObject:obj];
|
||||
|
||||
if (_childrenKeyPath)
|
||||
{
|
||||
@@ -278,11 +299,7 @@
|
||||
|
||||
- (BOOL)setSelectionIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
var result = [self __setSelectionIndexPaths:indexPaths avoidEmpty:NO];
|
||||
[self _selectionDidChange];
|
||||
|
||||
return result;
|
||||
return [self __setSelectionIndexPaths:indexPaths avoidEmpty:NO];
|
||||
}
|
||||
|
||||
- (void)_ensureTreeNodesExistForIndexPaths:(CPArray)indexPaths
|
||||
@@ -334,7 +351,8 @@
|
||||
if (needsRebuild)
|
||||
{
|
||||
[childNodes removeAllObjects];
|
||||
var newNodes = [self _buildTreeNodesForObjects:expectedChildObjects];[childNodes addObjectsFromArray:newNodes];
|
||||
var newNodes = [self _buildTreeNodesForObjects:expectedChildObjects];
|
||||
[childNodes addObjectsFromArray:newNodes];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,21 +384,13 @@
|
||||
if ([_selectionIndexPaths isEqualToArray:newPaths])
|
||||
return NO;
|
||||
|
||||
[self _selectionWillChange];
|
||||
[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"];
|
||||
[self _selectionDidChange];
|
||||
|
||||
return YES;
|
||||
}
|
||||
@@ -388,9 +398,7 @@
|
||||
- (BOOL)addSelectionIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
var newPaths = [_selectionIndexPaths mutableCopy];
|
||||
|
||||
[newPaths addObjectsFromArray:indexPaths];
|
||||
|
||||
return [self setSelectionIndexPaths:newPaths];
|
||||
}
|
||||
|
||||
@@ -404,7 +412,7 @@
|
||||
- (CPArray)selectedNodes
|
||||
{
|
||||
var nodes = [CPMutableArray array],
|
||||
count = [_selectionIndexPaths count];
|
||||
count = [_selectionIndexPaths count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
@@ -418,13 +426,17 @@
|
||||
- (CPArray)selectedObjects
|
||||
{
|
||||
var objects = [CPMutableArray array],
|
||||
nodes = [self selectedNodes],
|
||||
count = [nodes count];
|
||||
nodes = [self selectedNodes],
|
||||
count = [nodes count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
[objects addObject:[[nodes objectAtIndex:i] representedObject]];
|
||||
{
|
||||
var representedObject = [[nodes objectAtIndex:i] representedObject];
|
||||
if (representedObject)
|
||||
[objects addObject:representedObject];
|
||||
}
|
||||
|
||||
return objects;
|
||||
return [_CPObservableArray arrayWithArray:objects];
|
||||
}
|
||||
|
||||
- (BOOL)__setSelectedObjects:(CPArray)objects
|
||||
@@ -462,22 +474,22 @@
|
||||
}
|
||||
|
||||
- (BOOL)canInsert { return [self isEditable]; }
|
||||
- (BOOL)canInsertChild { return [self isEditable] &&[_selectionIndexPaths count] > 0; }
|
||||
- (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];
|
||||
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];
|
||||
lastIndex = [selectionPath indexAtPosition:length - 1],
|
||||
insertPath = [selectionPath indexPathByRemovingLastIndex];
|
||||
|
||||
insertPath = [insertPath indexPathByAddingIndex:lastIndex + 1];
|
||||
|
||||
@@ -489,10 +501,10 @@
|
||||
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];
|
||||
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];
|
||||
}
|
||||
@@ -501,8 +513,8 @@
|
||||
{
|
||||
if (![self canInsert]) return;
|
||||
|
||||
var newObject = [self automaticallyPreparesContent] ? [self newObject] :[self _defaultNewObject],
|
||||
indexPath = [self selectionIndexPath] || [CPIndexPath indexPathWithIndex:0];
|
||||
var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
|
||||
indexPath = [self selectionIndexPath] || [CPIndexPath indexPathWithIndex:0];
|
||||
|
||||
[self insertObject:newObject atArrangedObjectIndexPath:indexPath];
|
||||
}
|
||||
@@ -512,7 +524,7 @@
|
||||
if (![self canInsertChild]) return;
|
||||
|
||||
var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
|
||||
insertPath = [[self selectionIndexPath] indexPathByAddingIndex:0];
|
||||
insertPath = [[self selectionIndexPath] indexPathByAddingIndex:0];
|
||||
|
||||
[self insertObject:newObject atArrangedObjectIndexPath:insertPath];
|
||||
}
|
||||
@@ -524,6 +536,7 @@
|
||||
|
||||
- (void)insertObjects:(CPArray)objects atArrangedObjectIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"content"];
|
||||
_disableSetContent = YES;
|
||||
|
||||
@@ -531,21 +544,22 @@
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var object = [objects objectAtIndex:i],
|
||||
path = [indexPaths objectAtIndex:i],
|
||||
length = [path length];
|
||||
path = [indexPaths objectAtIndex:i],
|
||||
length = [path length];
|
||||
|
||||
if (length === 1)
|
||||
{[_contentObject insertObject:object atIndex:[path indexAtPosition:0]];
|
||||
{
|
||||
[_contentObject insertObject:object atIndex:[path indexAtPosition:0]];
|
||||
}
|
||||
else
|
||||
{
|
||||
var parentPath = [path indexPathByRemovingLastIndex],
|
||||
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
|
||||
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
|
||||
|
||||
if (parentNode)
|
||||
{
|
||||
var parentObj = [parentNode representedObject],
|
||||
childIndex = [path indexAtPosition:length - 1];
|
||||
childIndex = [path indexAtPosition:length - 1];
|
||||
|
||||
var children = [parentObj valueForKeyPath:_childrenKeyPath];
|
||||
if (!children)
|
||||
@@ -568,9 +582,11 @@
|
||||
_disableSetContent = NO;
|
||||
[self _rearrangeObjects];
|
||||
|
||||
if ([self selectsInsertedObjects])[self setSelectionIndexPaths:indexPaths];
|
||||
if ([self selectsInsertedObjects])
|
||||
[self setSelectionIndexPaths:indexPaths];
|
||||
|
||||
[self didChangeValueForKey:@"content"];
|
||||
[self _selectionDidChange];
|
||||
}
|
||||
|
||||
- (void)remove:(id)sender
|
||||
@@ -585,16 +601,17 @@
|
||||
|
||||
- (void)removeObjectsAtArrangedObjectIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
[self willChangeValueForKey:@"content"];
|
||||
_disableSetContent = YES;
|
||||
|
||||
var sortedPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)],
|
||||
count = [sortedPaths count];
|
||||
count = [sortedPaths count];
|
||||
|
||||
for (var i = count - 1; i >= 0; i--)
|
||||
{
|
||||
var path = [sortedPaths objectAtIndex:i],
|
||||
length = [path length];
|
||||
length = [path length];
|
||||
|
||||
if (length === 1)
|
||||
{
|
||||
@@ -603,15 +620,15 @@
|
||||
else
|
||||
{
|
||||
var parentPath = [path indexPathByRemovingLastIndex],
|
||||
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
|
||||
parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
|
||||
|
||||
if (parentNode)
|
||||
{
|
||||
var parentObj = [parentNode representedObject],
|
||||
childIndex = [path indexAtPosition:length - 1],
|
||||
mutableChildren = [parentObj mutableArrayValueForKeyPath:_childrenKeyPath];
|
||||
childIndex = [path indexAtPosition:length - 1],
|
||||
mutableChildren = [parentObj mutableArrayValueForKeyPath:_childrenKeyPath];
|
||||
|
||||
if (mutableChildren && childIndex <[mutableChildren count])
|
||||
if (mutableChildren && childIndex < [mutableChildren count])
|
||||
[mutableChildren removeObjectAtIndex:childIndex];
|
||||
}
|
||||
}
|
||||
@@ -624,6 +641,7 @@
|
||||
_disableSetContent = NO;
|
||||
[self _rearrangeObjects];
|
||||
[self didChangeValueForKey:@"content"];
|
||||
[self _selectionDidChange];
|
||||
}
|
||||
|
||||
- (void)moveNode:(CPTreeNode)node toIndexPath:(CPIndexPath)indexPath
|
||||
@@ -632,18 +650,19 @@
|
||||
}
|
||||
|
||||
- (void)moveNodes:(CPArray)nodes toIndexPath:(CPIndexPath)startingIndexPath
|
||||
{[CPException raise:CPUnsupportedMethodException reason:@"moveNodes:toIndexPath: is not yet implemented in CPTreeController."];
|
||||
{
|
||||
[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";
|
||||
CPTreeControllerPreservesSelection = @"CPTreeControllerPreservesSelection",
|
||||
CPTreeControllerSelectsInsertedObjects = @"CPTreeControllerSelectsInsertedObjects",
|
||||
CPTreeControllerAlwaysUsesMultipleValuesMarker = @"CPTreeControllerAlwaysUsesMultipleValuesMarker",
|
||||
CPTreeControllerChildrenKeyPath = @"CPTreeControllerChildrenKeyPath",
|
||||
CPTreeControllerCountKeyPath = @"CPTreeControllerCountKeyPath",
|
||||
CPTreeControllerLeafKeyPath = @"CPTreeControllerLeafKeyPath";
|
||||
|
||||
@implementation CPTreeController (CPCoding)
|
||||
|
||||
|
||||
@@ -483,7 +483,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
_DOMWindow.addEventListener("pagehide", function()
|
||||
{
|
||||
_DOMWindow.removeEventListener("unload", arguments.callee, NO);
|
||||
_DOMWindow.removeEventListener("pagehide", arguments.callee, NO);
|
||||
|
||||
[self blurEvent:nil];
|
||||
[self _notifyPlatformWindowWillClose];
|
||||
|
||||
@@ -252,8 +252,8 @@ if (Error.prototype._userInfo !== null)
|
||||
|
||||
function _CPMethodCallString(anObject, aSelector)
|
||||
{
|
||||
var prefix = class_isMetaClass(anObject.isa) ? "+" : "-";
|
||||
return prefix + "[" + [anObject className] + " " + aSelector + "]: ";
|
||||
var prefix = class_isMetaClass(anObject.isa) ? "+" : "-";
|
||||
return prefix + "[" + [anObject className] + " " + aSelector + "]: ";
|
||||
}
|
||||
|
||||
function _CPRaiseInvalidAbstractInvocation(anObject, aSelector)
|
||||
|
||||
@@ -0,0 +1,615 @@
|
||||
/*
|
||||
* CPLanguageModel.j
|
||||
* Foundation
|
||||
*
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@import "CPObject.j"
|
||||
@import "CPString.j"
|
||||
@import "CPError.j"
|
||||
@import "CPDictionary.j"
|
||||
@import "CPBundle.j"
|
||||
@import "CPUserDefaults.j"
|
||||
|
||||
// File-scoped fallback configuration parameters
|
||||
var CPLanguageModelSessionFallbackServiceType = @"ollama",
|
||||
CPLanguageModelSessionFallbackEndpoint = @"http://localhost:11434/api/generate",
|
||||
CPLanguageModelSessionFallbackModel = @"gemma4:e4b",
|
||||
CPLanguageModelSessionFallbackAPIKey = @"",
|
||||
CPLanguageModelSessionFallbackAPIKeyUserDefaultKey = @"",
|
||||
CPLanguageModelSessionEndorsesFallback = NO;
|
||||
|
||||
|
||||
/*!
|
||||
@ingroup foundation
|
||||
@class CPSystemLanguageModel
|
||||
|
||||
CPSystemLanguageModel provides a standard query interface to inspect the
|
||||
availability of client-side, on-device large language models (such as Gemma Nano on Chrome)
|
||||
in the active web browser runtime.
|
||||
*/
|
||||
@implementation CPSystemLanguageModel : CPObject
|
||||
|
||||
var sharedInstance = nil;
|
||||
|
||||
/*!
|
||||
Returns the singleton system language model monitor.
|
||||
@return the default CPSystemLanguageModel instance
|
||||
*/
|
||||
+ (id)defaultModel
|
||||
{
|
||||
|
||||
if (!sharedInstance)
|
||||
sharedInstance = [[CPSystemLanguageModel alloc] init];
|
||||
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
/*!
|
||||
Asynchronously queries the active browser environment to determine if on-device
|
||||
language models are supported and readily available to execute prompts.
|
||||
@param completionHandler a callback block executed with a boolean parameter (supported)
|
||||
*/
|
||||
- (void)supportsLocaleWithCompletionHandler:(Function)completionHandler
|
||||
{
|
||||
if (typeof window === "undefined" || !completionHandler)
|
||||
{
|
||||
if (completionHandler)
|
||||
completionHandler(NO);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
(async function() {
|
||||
var supported = false;
|
||||
|
||||
try {
|
||||
if (window.ai && window.ai.languageModel) {
|
||||
// Pass language options to align with the creation options
|
||||
var options = {
|
||||
expectedInputs: [{ type: "text", languages: ["en"] }],
|
||||
expectedOutputs: [{ type: "text", languages: ["en"] }]
|
||||
};
|
||||
|
||||
if (typeof window.ai.languageModel.availability === 'function') {
|
||||
var avail = await window.ai.languageModel.availability(options);
|
||||
supported = (avail === "readily" || avail === "available" || avail === "after-download");
|
||||
} else if (typeof window.ai.languageModel.capabilities === 'function') {
|
||||
var caps = await window.ai.languageModel.capabilities(options);
|
||||
supported = (caps.available === "readily" || caps.available === "after-download");
|
||||
} else {
|
||||
supported = true;
|
||||
}
|
||||
}
|
||||
else if (window.LanguageModel) {
|
||||
supported = true;
|
||||
}
|
||||
} catch (e) {
|
||||
supported = false;
|
||||
}
|
||||
|
||||
completionHandler(supported);
|
||||
})();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/*!
|
||||
@ingroup foundation
|
||||
@class CPLanguageModelSession
|
||||
|
||||
CPLanguageModelSession manages an active session with a local on-device
|
||||
language model. If the active browser does not support on-device models, the session
|
||||
gracefully and transparently falls back to configured remote server endpoints.
|
||||
|
||||
@discussion
|
||||
CPLanguageModelSession handles text generation prompts. If on-device AI
|
||||
(like Gemma Nano) is supported by the browser, it is utilized directly.
|
||||
Otherwise, or if CPLanguageModelSessionEndorsesFallback is configured to YES,
|
||||
the session automatically falls back to configured network-based providers
|
||||
(such as local Ollama, Groq, or OpenRouter).
|
||||
|
||||
Fallback configurations can be populated globally using the application's Info.plist
|
||||
via the following keys:
|
||||
<pre>
|
||||
CPEndorseLanguageModelFallback - YES to bypass on-device models and force fallback
|
||||
CPDefaultLanguageModelService - "ollama" | "groq" | "gemini" | "openrouter"
|
||||
CPDefaultLanguageModelEndpoint - API Endpoint (e.g. Ollama URL)
|
||||
CPDefaultLanguageModelModel - Model name string
|
||||
CPDefaultLanguageModelAPIKeyUserDefaultKey - CPUserDefaults key containing the actual API token
|
||||
CPDefaultLanguageModelAPIKey - Authentication token string (Unsecure direct fallback)
|
||||
</pre>
|
||||
*/
|
||||
@implementation CPLanguageModelSession : CPObject
|
||||
{
|
||||
id _chromeSession @accessors(property=chromeSession);
|
||||
CPString _instructions @accessors(property=instructions);
|
||||
CPString _fallbackServiceType @accessors(property=fallbackServiceType);
|
||||
CPString _fallbackEndpoint @accessors(property=fallbackEndpoint);
|
||||
CPString _fallbackModel @accessors(property=fallbackModel);
|
||||
CPString _fallbackAPIKey @accessors(property=fallbackAPIKey);
|
||||
}
|
||||
|
||||
/*!
|
||||
Initializes fallback defaults and "Endorsement" flags from the application's Info.plist.
|
||||
*/
|
||||
+ (void)initialize
|
||||
{
|
||||
if (self === [CPLanguageModelSession class])
|
||||
{
|
||||
var bundle = [CPBundle mainBundle];
|
||||
|
||||
CPLanguageModelSessionEndorsesFallback = !![bundle objectForInfoDictionaryKey:@"CPEndorseLanguageModelFallback"];
|
||||
CPLanguageModelSessionFallbackServiceType = [bundle objectForInfoDictionaryKey:@"CPDefaultLanguageModelService"] || @"ollama";
|
||||
CPLanguageModelSessionFallbackEndpoint = [bundle objectForInfoDictionaryKey:@"CPDefaultLanguageModelEndpoint"] || @"http://localhost:11434/api/generate";
|
||||
CPLanguageModelSessionFallbackModel = [bundle objectForInfoDictionaryKey:@"CPDefaultLanguageModelModel"] || @"gemma4:e4b";
|
||||
CPLanguageModelSessionFallbackAPIKey = [bundle objectForInfoDictionaryKey:@"CPDefaultLanguageModelAPIKey"] || @"";
|
||||
CPLanguageModelSessionFallbackAPIKeyUserDefaultKey = [bundle objectForInfoDictionaryKey:@"CPDefaultLanguageModelAPIKeyUserDefaultKey"] || @"";
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Configures whether the session should bypass native browser AI models and force fallback network routing.
|
||||
@param shouldEndorse YES to bypass native AI; NO to prioritize native AI if available
|
||||
*/
|
||||
+ (void)setEndorsesFallback:(BOOL)shouldEndorse
|
||||
{
|
||||
CPLanguageModelSessionEndorsesFallback = shouldEndorse;
|
||||
}
|
||||
|
||||
/*!
|
||||
Indicates if the session bypasses native browser AI models.
|
||||
@return YES if forced fallback is active; NO otherwise
|
||||
*/
|
||||
+ (BOOL)endorsesFallback
|
||||
{
|
||||
return CPLanguageModelSessionEndorsesFallback;
|
||||
}
|
||||
|
||||
/*!
|
||||
Configures fallback details dynamically, overriding any defaults loaded from Info.plist.
|
||||
@param serviceType the service type (e.g. @"ollama", @"groq", @"gemini", @"openrouter")
|
||||
@param endpoint the network target URL
|
||||
@param model the model identifier
|
||||
@param apiKey the API key string
|
||||
*/
|
||||
+ (void)setFallbackServiceType:(CPString)serviceType endpoint:(CPString)endpoint model:(CPString)model apiKey:(CPString)apiKey
|
||||
{
|
||||
CPLanguageModelSessionFallbackServiceType = serviceType;
|
||||
CPLanguageModelSessionFallbackEndpoint = endpoint;
|
||||
CPLanguageModelSessionFallbackModel = model;
|
||||
CPLanguageModelSessionFallbackAPIKey = apiKey;
|
||||
}
|
||||
|
||||
/*!
|
||||
Configures the CPUserDefaults key used to dynamically look up the API key.
|
||||
@param keyName the user defaults key name containing the actual credentials
|
||||
*/
|
||||
+ (void)setFallbackAPIKeyUserDefaultKey:(CPString)keyName
|
||||
{
|
||||
CPLanguageModelSessionFallbackAPIKeyUserDefaultKey = keyName;
|
||||
}
|
||||
|
||||
/*!
|
||||
Gets the CPUserDefaults key name used to dynamically look up the API key.
|
||||
@return the user defaults key name
|
||||
*/
|
||||
+ (CPString)fallbackAPIKeyUserDefaultKey
|
||||
{
|
||||
return CPLanguageModelSessionFallbackAPIKeyUserDefaultKey;
|
||||
}
|
||||
|
||||
/*!
|
||||
Initializes a language model session with specific system instructions.
|
||||
@param instructions the system instructions or context prompt
|
||||
@return the initialized session
|
||||
*/
|
||||
- (id)initWithInstructions:(CPString)instructions
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_instructions = instructions;
|
||||
_chromeSession = nil;
|
||||
_fallbackServiceType = nil;
|
||||
_fallbackEndpoint = nil;
|
||||
_fallbackModel = nil;
|
||||
_fallbackAPIKey = nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
Initializes a language model session with specific system instructions and an explicit programmatic API key.
|
||||
@param instructions the system instructions or context prompt
|
||||
@param apiKey the fallback API key to use specifically for this session
|
||||
@return the initialized session
|
||||
*/
|
||||
- (id)initWithInstructions:(CPString)instructions apiKey:(CPString)apiKey
|
||||
{
|
||||
self = [self initWithInstructions:instructions];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_fallbackAPIKey = apiKey;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
Initializes a language model session with instructions and explicit fallback settings.
|
||||
@param instructions the system instructions or context prompt
|
||||
@param options dictionary containing custom fallback configuration (e.g. @{ @"serviceType": ..., @"apiKey": ... })
|
||||
@return the initialized session
|
||||
*/
|
||||
- (id)initWithInstructions:(CPString)instructions fallbackOptions:(CPDictionary)options
|
||||
{
|
||||
self = [self initWithInstructions:instructions];
|
||||
|
||||
if (self)
|
||||
{
|
||||
if (options)
|
||||
{
|
||||
_fallbackServiceType = [options objectForKey:@"serviceType"];
|
||||
_fallbackEndpoint = [options objectForKey:@"endpoint"];
|
||||
_fallbackModel = [options objectForKey:@"model"];
|
||||
_fallbackAPIKey = [options objectForKey:@"apiKey"];
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
Sends a query prompt to the language model session.
|
||||
@param prompt the query text to analyze
|
||||
@param completionHandler a callback receiving the response string or a CPError instance
|
||||
*/
|
||||
- (void)respondToPrompt:(CPString)prompt options:(id)options completionHandler:(Function)completionHandler
|
||||
{
|
||||
// If the developer forced fallback, bypass native browser execution
|
||||
if (CPLanguageModelSessionEndorsesFallback)
|
||||
{
|
||||
[self _executeRemoteFallbackWithPrompt:prompt options:options completionHandler:completionHandler];
|
||||
return;
|
||||
}
|
||||
|
||||
if (_chromeSession)
|
||||
{
|
||||
[self _executePrompt:prompt options:options completionHandler:completionHandler];
|
||||
return;
|
||||
}
|
||||
|
||||
var instructions = [self instructions];
|
||||
|
||||
[CPLanguageModelSession _getChromeFactoryWithCompletionHandler:function(factory, error) {
|
||||
if (error) {
|
||||
[self _executeRemoteFallbackWithPrompt:prompt options:options completionHandler:completionHandler];
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the required expected input and output parameters
|
||||
var sessionOptions = {
|
||||
expectedInputs: [{ type: "text", languages: ["en"] }],
|
||||
expectedOutputs: [{ type: "text", languages: ["en"] }]
|
||||
};
|
||||
if (instructions) {
|
||||
sessionOptions.systemPrompt = instructions;
|
||||
}
|
||||
|
||||
factory.create(sessionOptions).then(function(session) {
|
||||
[self setChromeSession:session];
|
||||
[self _executePrompt:prompt options:options completionHandler:completionHandler];
|
||||
}).catch(function(err) {
|
||||
[self _executeRemoteFallbackWithPrompt:prompt options:options completionHandler:completionHandler];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
/*!
|
||||
Sends a query prompt and streams the response chunk-by-chunk for live UI rendering.
|
||||
@param prompt the query text to analyze
|
||||
@param chunkHandler a callback block executed as text increments are received
|
||||
@param completionHandler a final callback block executed when generation completes
|
||||
*/
|
||||
- (void)respondToPrompt:(CPString)prompt
|
||||
onChunkReceived:(Function)chunkHandler
|
||||
completed:(Function)completionHandler
|
||||
{
|
||||
if (CPLanguageModelSessionEndorsesFallback)
|
||||
{
|
||||
[self _executeRemoteFallbackWithPrompt:prompt options:nil completionHandler:function(res, err) {
|
||||
if (!err && chunkHandler)
|
||||
chunkHandler(res);
|
||||
completionHandler(res, err);
|
||||
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
||||
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
if (_chromeSession)
|
||||
{
|
||||
[self _executePromptStreaming:prompt onChunkReceived:chunkHandler completed:completionHandler];
|
||||
return;
|
||||
}
|
||||
|
||||
var instructions = [self instructions];
|
||||
|
||||
[CPLanguageModelSession _getChromeFactoryWithCompletionHandler:function(factory, error) {
|
||||
if (error) {
|
||||
[self _executeRemoteFallbackWithPrompt:prompt options:nil completionHandler:function(res, err) {
|
||||
if (!err && chunkHandler)
|
||||
chunkHandler(res);
|
||||
completionHandler(res, err);
|
||||
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
||||
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the required expected input and output parameters
|
||||
var options = {
|
||||
expectedInputs: [{ type: "text", languages: ["en"] }],
|
||||
expectedOutputs: [{ type: "text", languages: ["en"] }]
|
||||
};
|
||||
|
||||
if (instructions)
|
||||
options.systemPrompt = instructions;
|
||||
|
||||
factory.create(options).then(function(session) {
|
||||
[self setChromeSession:session];
|
||||
[self _executePromptStreaming:prompt onChunkReceived:chunkHandler completed:completionHandler];
|
||||
}).catch(function(err)
|
||||
{
|
||||
[self _executeRemoteFallbackWithPrompt:prompt options:nil completionHandler:function(res, err) {
|
||||
if (!err && chunkHandler)
|
||||
chunkHandler(res);
|
||||
|
||||
completionHandler(res, err);
|
||||
|
||||
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
||||
|
||||
}];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
/*!
|
||||
Closes the session and releases associated memory resources on-device.
|
||||
*/
|
||||
- (void)destroy
|
||||
{
|
||||
if (_chromeSession && typeof _chromeSession.destroy === "function")
|
||||
{
|
||||
_chromeSession.destroy();
|
||||
_chromeSession = nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private Helper Methods
|
||||
|
||||
+ (void)_getChromeFactoryWithCompletionHandler:(Function)completionHandler
|
||||
{
|
||||
if (typeof window === "undefined")
|
||||
{
|
||||
var cpError = [CPError errorWithDomain:@"CPLanguageModelErrorDomain" code:-1 userInfo:[CPDictionary dictionaryWithObject:@"Execution environment is not a browser window." forKey:CPLocalizedDescriptionKey]];
|
||||
completionHandler(nil, cpError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.ai && window.ai.languageModel)
|
||||
completionHandler(window.ai.languageModel, nil);
|
||||
else if (window.LanguageModel)
|
||||
completionHandler(window.LanguageModel, nil);
|
||||
else
|
||||
completionHandler(nil, [CPError errorWithDomain:@"CPLanguageModelErrorDomain" code:0 userInfo:nil]);
|
||||
}
|
||||
|
||||
- (void)_executePrompt:(CPString)prompt options:(id)options completionHandler:(Function)completionHandler
|
||||
{
|
||||
var promptPromise = options ? _chromeSession.prompt(prompt, options) : _chromeSession.prompt(prompt);
|
||||
|
||||
promptPromise.then(function(result) {
|
||||
completionHandler(result, nil);
|
||||
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
||||
}).catch(function(err) {
|
||||
var cpError = [CPError errorWithDomain:@"CPLanguageModelErrorDomain"
|
||||
code:2
|
||||
userInfo:[CPDictionary dictionaryWithObject:err.message forKey:CPLocalizedDescriptionKey]];
|
||||
completionHandler(nil, cpError);
|
||||
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
||||
});
|
||||
}
|
||||
|
||||
- (CPString)_resolvedFallbackServiceType
|
||||
{
|
||||
return _fallbackServiceType || CPLanguageModelSessionFallbackServiceType;
|
||||
}
|
||||
|
||||
- (CPString)_resolvedFallbackEndpoint
|
||||
{
|
||||
return _fallbackEndpoint || CPLanguageModelSessionFallbackEndpoint;
|
||||
}
|
||||
|
||||
- (CPString)_resolvedFallbackModel
|
||||
{
|
||||
return _fallbackModel || CPLanguageModelSessionFallbackModel;
|
||||
}
|
||||
|
||||
- (CPString)_resolvedFallbackAPIKey
|
||||
{
|
||||
// 1. Session instance explicit key has highest priority
|
||||
if (_fallbackAPIKey)
|
||||
return _fallbackAPIKey;
|
||||
|
||||
// 2. Class fallback API key set programmatically takes second priority
|
||||
if (CPLanguageModelSessionFallbackAPIKey)
|
||||
return CPLanguageModelSessionFallbackAPIKey;
|
||||
|
||||
// 3. Dynamic lookup from standard user defaults takes final priority
|
||||
if (CPLanguageModelSessionFallbackAPIKeyUserDefaultKey)
|
||||
{
|
||||
var defaults = [CPUserDefaults standardUserDefaults],
|
||||
apiKey = [defaults objectForKey:CPLanguageModelSessionFallbackAPIKeyUserDefaultKey];
|
||||
if (apiKey)
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
return @"";
|
||||
}
|
||||
|
||||
- (void)_executeRemoteFallbackWithPrompt:(CPString)prompt options:(id)options completionHandler:(Function)completionHandler
|
||||
{
|
||||
var systemPrompt = [self instructions],
|
||||
serviceType = [self _resolvedFallbackServiceType],
|
||||
endpoint = [self _resolvedFallbackEndpoint],
|
||||
model = [self _resolvedFallbackModel],
|
||||
apiKey = [self _resolvedFallbackAPIKey];
|
||||
|
||||
var reqUrl = @"",
|
||||
headers = { "Content-Type": "application/json" },
|
||||
payload = {};
|
||||
|
||||
if (serviceType === @"groq")
|
||||
{
|
||||
reqUrl = "https://api.groq.com/openai/v1/chat/completions";
|
||||
headers["Authorization"] = "Bearer " + apiKey;
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{ "role": "system", "content": systemPrompt },
|
||||
{ "role": "user", "content": prompt }
|
||||
],
|
||||
"temperature": 0
|
||||
};
|
||||
}
|
||||
else if (serviceType === @"gemini")
|
||||
{
|
||||
reqUrl = "https://generativelanguage.googleapis.com/v1beta/models/" + model + ":generateContent?key=" + apiKey;
|
||||
payload = {
|
||||
"contents": [
|
||||
{ "parts": [{ "text": systemPrompt + "\n\n" + prompt }] }
|
||||
],
|
||||
"generationConfig": { "temperature": 0 }
|
||||
};
|
||||
}
|
||||
else if (serviceType === @"openrouter")
|
||||
{
|
||||
reqUrl = "https://openrouter.ai/api/v1/chat/completions";
|
||||
headers["Authorization"] = "Bearer " + apiKey;
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{ "role": "system", "content": systemPrompt },
|
||||
{ "role": "user", "content": prompt }
|
||||
],
|
||||
"temperature": 0
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
reqUrl = endpoint || "http://localhost:11434/api/generate";
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": systemPrompt + "\n\n" + prompt,
|
||||
"stream": false,
|
||||
"options": { "temperature": 0 }
|
||||
};
|
||||
}
|
||||
|
||||
fetch(reqUrl, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP error! Status: " + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
var responseText = "";
|
||||
|
||||
if (serviceType === "groq" || serviceType === "openrouter") {
|
||||
responseText = (data.choices && data.choices[0] && data.choices[0].message) ? data.choices[0].message.content : "";
|
||||
} else if (serviceType === "gemini") {
|
||||
responseText = (data.candidates && data.candidates[0] && data.candidates[0].content && data.candidates[0].content.parts) ? data.candidates[0].content.parts[0].text : "";
|
||||
} else {
|
||||
responseText = data.response || "";
|
||||
}
|
||||
|
||||
completionHandler(responseText, nil);
|
||||
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
||||
})
|
||||
.catch(function(err) {
|
||||
var cpError = [CPError errorWithDomain:@"CPLanguageModelErrorDomain"
|
||||
code:4
|
||||
userInfo:[CPDictionary dictionaryWithObject:err.message forKey:CPLocalizedDescriptionKey]];
|
||||
completionHandler(nil, cpError);
|
||||
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
||||
});
|
||||
}
|
||||
|
||||
- (void)_executePromptStreaming:(CPString)prompt
|
||||
onChunkReceived:(Function)chunkHandler
|
||||
completed:(Function)completionHandler
|
||||
{
|
||||
var stream;
|
||||
|
||||
try {
|
||||
stream = _chromeSession.promptStreaming(prompt);
|
||||
} catch (err) {
|
||||
var cpError = [CPError errorWithDomain:@"CPLanguageModelErrorDomain"
|
||||
code:3
|
||||
userInfo:[CPDictionary dictionaryWithObject:err.message forKey:CPLocalizedDescriptionKey]];
|
||||
completionHandler(nil, cpError);
|
||||
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
||||
return;
|
||||
}
|
||||
|
||||
(async function() {
|
||||
var lastChunk = "";
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
lastChunk = chunk;
|
||||
if (chunkHandler) {
|
||||
chunkHandler(chunk);
|
||||
}
|
||||
}
|
||||
if (completionHandler)
|
||||
{
|
||||
completionHandler(lastChunk, nil);
|
||||
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
||||
|
||||
}
|
||||
} catch (err) {
|
||||
if (completionHandler) {
|
||||
var cpError = [CPError errorWithDomain:@"CPLanguageModelErrorDomain"
|
||||
code:2
|
||||
userInfo:[CPDictionary dictionaryWithObject:err.message forKey:CPLocalizedDescriptionKey]];
|
||||
completionHandler(nil, cpError);
|
||||
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; // pump run loop to force GUI update
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -231,8 +231,8 @@ var NumberRegex = new RegExp('(-)?(\\d*)(\\.(\\d*))?');
|
||||
_minimumFractionDigits = 0;
|
||||
_maximumFractionDigits = 3;
|
||||
// Invalidate the cached number handler.
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
_numberHandler = nil;
|
||||
break;
|
||||
|
||||
@@ -240,8 +240,8 @@ var NumberRegex = new RegExp('(-)?(\\d*)(\\.(\\d*))?');
|
||||
_minimumFractionDigits = 2;
|
||||
_maximumFractionDigits = 2;
|
||||
// Invalidate the cached number handler.
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
_numberHandler = nil;
|
||||
break;
|
||||
}
|
||||
@@ -251,8 +251,8 @@ var NumberRegex = new RegExp('(-)?(\\d*)(\\.(\\d*))?');
|
||||
{
|
||||
_roundingMode = aRoundingMode;
|
||||
// Invalidate the cached number handler.
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
_numberHandler = nil;
|
||||
}
|
||||
|
||||
@@ -260,8 +260,8 @@ var NumberRegex = new RegExp('(-)?(\\d*)(\\.(\\d*))?');
|
||||
{
|
||||
_minimumFractionDigits = aNumber;
|
||||
// Invalidate the cached number handler.
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
_numberHandler = nil;
|
||||
}
|
||||
|
||||
@@ -269,8 +269,8 @@ var NumberRegex = new RegExp('(-)?(\\d*)(\\.(\\d*))?');
|
||||
{
|
||||
_maximumFractionDigits = aNumber;
|
||||
// Invalidate the cached number handler.
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
_numberHandler = nil;
|
||||
}
|
||||
|
||||
@@ -278,8 +278,8 @@ var NumberRegex = new RegExp('(-)?(\\d*)(\\.(\\d*))?');
|
||||
{
|
||||
_minimum = aNumber;
|
||||
// Invalidate the cached number handler.
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
_numberHandler = nil;
|
||||
}
|
||||
|
||||
@@ -287,8 +287,8 @@ var NumberRegex = new RegExp('(-)?(\\d*)(\\.(\\d*))?');
|
||||
{
|
||||
_maximum = aNumber;
|
||||
// Invalidate the cached number handler.
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
// It rebuilds on next use.
|
||||
// Replaces pre-processor directive, which is incompatible with the new compiler
|
||||
_numberHandler = nil;
|
||||
}
|
||||
|
||||
|
||||
+370
-370
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
- (id)initWithCapacity:(unsigned)aCapacity
|
||||
{
|
||||
return [self init];
|
||||
return [self init];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -32,7 +32,7 @@
|
||||
*/
|
||||
+ (id)setWithCapacity:(CPUInteger)aCapacity
|
||||
{
|
||||
return [[self alloc] initWithCapacity:aCapacity];
|
||||
return [[self alloc] initWithCapacity:aCapacity];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -41,16 +41,16 @@
|
||||
*/
|
||||
- (void)filterUsingPredicate:(CPPredicate)aPredicate
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
var object,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
if (![aPredicate evaluateWithObject:object])
|
||||
{
|
||||
[self removeObject:object];
|
||||
}
|
||||
}
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
if (![aPredicate evaluateWithObject:object])
|
||||
{
|
||||
[self removeObject:object];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -59,7 +59,7 @@
|
||||
*/
|
||||
- (void)removeObject:(id)anObject
|
||||
{
|
||||
_CPRaiseInvalidAbstractInvocation(self, _cmd);
|
||||
_CPRaiseInvalidAbstractInvocation(self, _cmd);
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -68,13 +68,13 @@
|
||||
*/
|
||||
- (void)removeObjectsInArray:(CPArray)anArray
|
||||
{
|
||||
var index = 0,
|
||||
count = [anArray count];
|
||||
var index = 0,
|
||||
count = [anArray count];
|
||||
|
||||
for (; index < count; ++index)
|
||||
{
|
||||
[self removeObject:[anArray objectAtIndex:index]];
|
||||
}
|
||||
for (; index < count; ++index)
|
||||
{
|
||||
[self removeObject:[anArray objectAtIndex:index]];
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -82,13 +82,13 @@
|
||||
*/
|
||||
- (void)removeAllObjects
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
var object,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
[self removeObject:object];
|
||||
}
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
[self removeObject:object];
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -97,12 +97,12 @@
|
||||
*/
|
||||
- (void)addObjectsFromArray:(CPArray)objects
|
||||
{
|
||||
var count = [objects count];
|
||||
var count = [objects count];
|
||||
|
||||
while (count--)
|
||||
{
|
||||
[self addObject:objects[count]];
|
||||
}
|
||||
while (count--)
|
||||
{
|
||||
[self addObject:objects[count]];
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -111,13 +111,13 @@
|
||||
*/
|
||||
- (void)unionSet:(CPSet)aSet
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [aSet objectEnumerator];
|
||||
var object,
|
||||
objectEnumerator = [aSet objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
[self addObject:object];
|
||||
}
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
[self addObject:object];
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -126,13 +126,13 @@
|
||||
*/
|
||||
- (void)minusSet:(CPSet)aSet
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [aSet objectEnumerator];
|
||||
var object,
|
||||
objectEnumerator = [aSet objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
[self removeObject:object];
|
||||
}
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
[self removeObject:object];
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -141,24 +141,24 @@
|
||||
*/
|
||||
- (void)intersectSet:(CPSet)aSet
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [self objectEnumerator],
|
||||
objectsToRemove = [];
|
||||
var object,
|
||||
objectEnumerator = [self objectEnumerator],
|
||||
objectsToRemove = [];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
if (![aSet containsObject:object])
|
||||
{
|
||||
objectsToRemove.push(object);
|
||||
}
|
||||
}
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
if (![aSet containsObject:object])
|
||||
{
|
||||
objectsToRemove.push(object);
|
||||
}
|
||||
}
|
||||
|
||||
var count = [objectsToRemove count];
|
||||
var count = [objectsToRemove count];
|
||||
|
||||
while (count--)
|
||||
{
|
||||
[self removeObject:objectsToRemove[count]];
|
||||
}
|
||||
while (count--)
|
||||
{
|
||||
[self removeObject:objectsToRemove[count]];
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -167,8 +167,8 @@
|
||||
*/
|
||||
- (void)setSet:(CPSet)aSet
|
||||
{
|
||||
[self removeAllObjects];
|
||||
[self unionSet:aSet];
|
||||
[self removeAllObjects];
|
||||
[self unionSet:aSet];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -180,69 +180,69 @@
|
||||
|
||||
- (id)valueForKeyPath:(CPString)aKeyPath
|
||||
{
|
||||
if (!aKeyPath)
|
||||
{
|
||||
[self valueForUndefinedKey:@"<empty path>"];
|
||||
}
|
||||
if (!aKeyPath)
|
||||
{
|
||||
[self valueForUndefinedKey:@"<empty path>"];
|
||||
}
|
||||
|
||||
if (aKeyPath.charAt(0) === "@")
|
||||
{
|
||||
var dotIndex = aKeyPath.indexOf("."),
|
||||
operator,
|
||||
parameter;
|
||||
if (aKeyPath.charAt(0) === "@")
|
||||
{
|
||||
var dotIndex = aKeyPath.indexOf("."),
|
||||
operator,
|
||||
parameter;
|
||||
|
||||
if (dotIndex !== -1)
|
||||
{
|
||||
operator = aKeyPath.substring(1, dotIndex);
|
||||
parameter = aKeyPath.substring(dotIndex + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
operator = aKeyPath.substring(1);
|
||||
}
|
||||
if (dotIndex !== -1)
|
||||
{
|
||||
operator = aKeyPath.substring(1, dotIndex);
|
||||
parameter = aKeyPath.substring(dotIndex + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
operator = aKeyPath.substring(1);
|
||||
}
|
||||
|
||||
return [_CPCollectionKVCOperator performOperation:operator withCollection:self propertyPath:parameter];
|
||||
}
|
||||
else
|
||||
{
|
||||
var valuesForKeySet = [CPSet set],
|
||||
containedObject,
|
||||
containedObjectValue,
|
||||
containedObjectEnumerator = [self objectEnumerator];
|
||||
return [_CPCollectionKVCOperator performOperation:operator withCollection:self propertyPath:parameter];
|
||||
}
|
||||
else
|
||||
{
|
||||
var valuesForKeySet = [CPSet set],
|
||||
containedObject,
|
||||
containedObjectValue,
|
||||
containedObjectEnumerator = [self objectEnumerator];
|
||||
|
||||
while ((containedObject = [containedObjectEnumerator nextObject]) != nil)
|
||||
{
|
||||
containedObjectValue = [containedObject valueForKeyPath:aKeyPath];
|
||||
while ((containedObject = [containedObjectEnumerator nextObject]) != nil)
|
||||
{
|
||||
containedObjectValue = [containedObject valueForKeyPath:aKeyPath];
|
||||
|
||||
if (containedObjectValue == nil)
|
||||
{
|
||||
containedObjectValue = [CPNull null];
|
||||
}
|
||||
if (containedObjectValue == nil)
|
||||
{
|
||||
containedObjectValue = [CPNull null];
|
||||
}
|
||||
|
||||
[valuesForKeySet addObject:containedObjectValue];
|
||||
}
|
||||
[valuesForKeySet addObject:containedObjectValue];
|
||||
}
|
||||
|
||||
return valuesForKeySet;
|
||||
}
|
||||
return valuesForKeySet;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)valueForKey:(CPString)aKey
|
||||
{
|
||||
// If the key starts with @, it is an operator path.
|
||||
// If not, it is a property path that we want applied to all members.
|
||||
// In either case, valueForKeyPath: handles both scenarios correctly.
|
||||
return [self valueForKeyPath:aKey];
|
||||
// If the key starts with @, it is an operator path.
|
||||
// If not, it is a property path that we want applied to all members.
|
||||
// In either case, valueForKeyPath: handles both scenarios correctly.
|
||||
return [self valueForKeyPath:aKey];
|
||||
}
|
||||
|
||||
- (void)setValue:(id)aValue forKey:(CPString)aKey
|
||||
{
|
||||
var containedObject,
|
||||
containedObjectEnumerator = [self objectEnumerator];
|
||||
var containedObject,
|
||||
containedObjectEnumerator = [self objectEnumerator];
|
||||
|
||||
while ((containedObject = [containedObjectEnumerator nextObject]) != nil)
|
||||
{
|
||||
[containedObject setValue:aValue forKey:aKey];
|
||||
}
|
||||
while ((containedObject = [containedObjectEnumerator nextObject]) != nil)
|
||||
{
|
||||
[containedObject setValue:aValue forKey:aKey];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -254,22 +254,22 @@
|
||||
|
||||
- (id)mutableSetValueForKey:(id)aKey
|
||||
{
|
||||
return [[_CPKVCSet alloc] initWithKey:aKey forProxyObject:self];
|
||||
return [[_CPKVCSet alloc] initWithKey:aKey forProxyObject:self];
|
||||
}
|
||||
|
||||
- (id)mutableSetValueForKeyPath:(id)aKeyPath
|
||||
{
|
||||
var dotIndex = aKeyPath.indexOf(".");
|
||||
var dotIndex = aKeyPath.indexOf(".");
|
||||
|
||||
if (dotIndex < 0)
|
||||
{
|
||||
return [self mutableSetValueForKey:aKeyPath];
|
||||
}
|
||||
if (dotIndex < 0)
|
||||
{
|
||||
return [self mutableSetValueForKey:aKeyPath];
|
||||
}
|
||||
|
||||
var firstPart = aKeyPath.substring(0, dotIndex),
|
||||
lastPart = aKeyPath.substring(dotIndex + 1);
|
||||
var firstPart = aKeyPath.substring(0, dotIndex),
|
||||
lastPart = aKeyPath.substring(dotIndex + 1);
|
||||
|
||||
return [[self valueForKeyPath:firstPart] mutableSetValueForKeyPath:lastPart];
|
||||
return [[self valueForKeyPath:firstPart] mutableSetValueForKeyPath:lastPart];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -279,387 +279,387 @@
|
||||
|
||||
@implementation _CPKVCSet : CPMutableSet
|
||||
{
|
||||
id _proxyObject;
|
||||
id _key;
|
||||
id _proxyObject;
|
||||
id _key;
|
||||
|
||||
SEL _accessSEL;
|
||||
Function _access;
|
||||
SEL _accessSEL;
|
||||
Function _access;
|
||||
|
||||
SEL _setSEL;
|
||||
Function _set;
|
||||
SEL _setSEL;
|
||||
Function _set;
|
||||
|
||||
SEL _countSEL;
|
||||
Function _count;
|
||||
SEL _countSEL;
|
||||
Function _count;
|
||||
|
||||
SEL _enumeratorSEL;
|
||||
Function _enumerator;
|
||||
SEL _enumeratorSEL;
|
||||
Function _enumerator;
|
||||
|
||||
SEL _memberSEL;
|
||||
Function _member;
|
||||
SEL _memberSEL;
|
||||
Function _member;
|
||||
|
||||
SEL _addSEL;
|
||||
Function _add;
|
||||
SEL _addSEL;
|
||||
Function _add;
|
||||
|
||||
SEL _addManySEL;
|
||||
Function _addMany;
|
||||
SEL _addManySEL;
|
||||
Function _addMany;
|
||||
|
||||
SEL _removeSEL;
|
||||
Function _remove;
|
||||
SEL _removeSEL;
|
||||
Function _remove;
|
||||
|
||||
SEL _removeManySEL;
|
||||
Function _removeMany;
|
||||
SEL _removeManySEL;
|
||||
Function _removeMany;
|
||||
|
||||
SEL _intersectSEL;
|
||||
Function _intersect;
|
||||
SEL _intersectSEL;
|
||||
Function _intersect;
|
||||
}
|
||||
|
||||
+ (id)alloc
|
||||
{
|
||||
var set = [CPMutableSet set];
|
||||
var set = [CPMutableSet set];
|
||||
|
||||
set.isa = self;
|
||||
set.isa = self;
|
||||
|
||||
var ivars = class_copyIvarList(self),
|
||||
count = ivars.length;
|
||||
var ivars = class_copyIvarList(self),
|
||||
count = ivars.length;
|
||||
|
||||
while (count--)
|
||||
{
|
||||
set[ivar_getName(ivars[count])] = nil;
|
||||
}
|
||||
while (count--)
|
||||
{
|
||||
set[ivar_getName(ivars[count])] = nil;
|
||||
}
|
||||
|
||||
return set;
|
||||
return set;
|
||||
}
|
||||
|
||||
- (id)initWithKey:(id)aKey forProxyObject:(id)anObject
|
||||
{
|
||||
self = [super init];
|
||||
self = [super init];
|
||||
|
||||
_key = aKey;
|
||||
_proxyObject = anObject;
|
||||
_key = aKey;
|
||||
_proxyObject = anObject;
|
||||
|
||||
var capitalizedKey = _key.charAt(0).toUpperCase() + _key.substring(1);
|
||||
var capitalizedKey = _key.charAt(0).toUpperCase() + _key.substring(1);
|
||||
|
||||
_accessSEL = sel_getName(_key);
|
||||
if ([_proxyObject respondsToSelector:_accessSEL])
|
||||
{
|
||||
_access = [_proxyObject methodForSelector:_accessSEL];
|
||||
}
|
||||
_accessSEL = sel_getName(_key);
|
||||
if ([_proxyObject respondsToSelector:_accessSEL])
|
||||
{
|
||||
_access = [_proxyObject methodForSelector:_accessSEL];
|
||||
}
|
||||
|
||||
_setSEL = sel_getName(@"set" + capitalizedKey + ":");
|
||||
if ([_proxyObject respondsToSelector:_setSEL])
|
||||
{
|
||||
_set = [_proxyObject methodForSelector:_setSEL];
|
||||
}
|
||||
_setSEL = sel_getName(@"set" + capitalizedKey + ":");
|
||||
if ([_proxyObject respondsToSelector:_setSEL])
|
||||
{
|
||||
_set = [_proxyObject methodForSelector:_setSEL];
|
||||
}
|
||||
|
||||
_countSEL = sel_getName(@"countOf" + capitalizedKey);
|
||||
if ([_proxyObject respondsToSelector:_countSEL])
|
||||
{
|
||||
_count = [_proxyObject methodForSelector:_countSEL];
|
||||
}
|
||||
_countSEL = sel_getName(@"countOf" + capitalizedKey);
|
||||
if ([_proxyObject respondsToSelector:_countSEL])
|
||||
{
|
||||
_count = [_proxyObject methodForSelector:_countSEL];
|
||||
}
|
||||
|
||||
_enumeratorSEL = sel_getName(@"enumeratorOf" + capitalizedKey);
|
||||
if ([_proxyObject respondsToSelector:_enumeratorSEL])
|
||||
{
|
||||
_enumerator = [_proxyObject methodForSelector:_enumeratorSEL];
|
||||
}
|
||||
_enumeratorSEL = sel_getName(@"enumeratorOf" + capitalizedKey);
|
||||
if ([_proxyObject respondsToSelector:_enumeratorSEL])
|
||||
{
|
||||
_enumerator = [_proxyObject methodForSelector:_enumeratorSEL];
|
||||
}
|
||||
|
||||
_memberSEL = sel_getName(@"memberOf" + capitalizedKey + ":");
|
||||
if ([_proxyObject respondsToSelector:_memberSEL])
|
||||
{
|
||||
_member = [_proxyObject methodForSelector:_memberSEL];
|
||||
}
|
||||
_memberSEL = sel_getName(@"memberOf" + capitalizedKey + ":");
|
||||
if ([_proxyObject respondsToSelector:_memberSEL])
|
||||
{
|
||||
_member = [_proxyObject methodForSelector:_memberSEL];
|
||||
}
|
||||
|
||||
_addSEL = sel_getName(@"add" + capitalizedKey + "Object:");
|
||||
if ([_proxyObject respondsToSelector:_addSEL])
|
||||
{
|
||||
_add = [_proxyObject methodForSelector:_addSEL];
|
||||
}
|
||||
_addSEL = sel_getName(@"add" + capitalizedKey + "Object:");
|
||||
if ([_proxyObject respondsToSelector:_addSEL])
|
||||
{
|
||||
_add = [_proxyObject methodForSelector:_addSEL];
|
||||
}
|
||||
|
||||
_addManySEL = sel_getName(@"add" + capitalizedKey + ":");
|
||||
if ([_proxyObject respondsToSelector:_addManySEL])
|
||||
{
|
||||
_addMany = [_proxyObject methodForSelector:_addManySEL];
|
||||
}
|
||||
_addManySEL = sel_getName(@"add" + capitalizedKey + ":");
|
||||
if ([_proxyObject respondsToSelector:_addManySEL])
|
||||
{
|
||||
_addMany = [_proxyObject methodForSelector:_addManySEL];
|
||||
}
|
||||
|
||||
_removeSEL = sel_getName(@"remove" + capitalizedKey + "Object:");
|
||||
if ([_proxyObject respondsToSelector:_removeSEL])
|
||||
{
|
||||
_remove = [_proxyObject methodForSelector:_removeSEL];
|
||||
}
|
||||
_removeSEL = sel_getName(@"remove" + capitalizedKey + "Object:");
|
||||
if ([_proxyObject respondsToSelector:_removeSEL])
|
||||
{
|
||||
_remove = [_proxyObject methodForSelector:_removeSEL];
|
||||
}
|
||||
|
||||
_removeManySEL = sel_getName(@"remove" + capitalizedKey + ":");
|
||||
if ([_proxyObject respondsToSelector:_removeManySEL])
|
||||
{
|
||||
_removeMany = [_proxyObject methodForSelector:_removeManySEL];
|
||||
}
|
||||
_removeManySEL = sel_getName(@"remove" + capitalizedKey + ":");
|
||||
if ([_proxyObject respondsToSelector:_removeManySEL])
|
||||
{
|
||||
_removeMany = [_proxyObject methodForSelector:_removeManySEL];
|
||||
}
|
||||
|
||||
_intersectSEL = sel_getName(@"intersect" + capitalizedKey + ":");
|
||||
if ([_proxyObject respondsToSelector:_intersectSEL])
|
||||
{
|
||||
_intersect = [_proxyObject methodForSelector:_intersectSEL];
|
||||
}
|
||||
_intersectSEL = sel_getName(@"intersect" + capitalizedKey + ":");
|
||||
if ([_proxyObject respondsToSelector:_intersectSEL])
|
||||
{
|
||||
_intersect = [_proxyObject methodForSelector:_intersectSEL];
|
||||
}
|
||||
|
||||
return self;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)_representedObject
|
||||
{
|
||||
if (_access)
|
||||
{
|
||||
return _access(_proxyObject, _accessSEL);
|
||||
}
|
||||
if (_access)
|
||||
{
|
||||
return _access(_proxyObject, _accessSEL);
|
||||
}
|
||||
|
||||
return [_proxyObject valueForKey:_key];
|
||||
return [_proxyObject valueForKey:_key];
|
||||
}
|
||||
|
||||
- (void)_setRepresentedObject:(id)anObject
|
||||
{
|
||||
if (_set)
|
||||
{
|
||||
return _set(_proxyObject, _setSEL, anObject);
|
||||
}
|
||||
if (_set)
|
||||
{
|
||||
return _set(_proxyObject, _setSEL, anObject);
|
||||
}
|
||||
|
||||
[_proxyObject setValue:anObject forKey:_key];
|
||||
[_proxyObject setValue:anObject forKey:_key];
|
||||
}
|
||||
|
||||
- (CPUInteger)count
|
||||
{
|
||||
if (_count)
|
||||
{
|
||||
return _count(_proxyObject, _countSEL);
|
||||
}
|
||||
if (_count)
|
||||
{
|
||||
return _count(_proxyObject, _countSEL);
|
||||
}
|
||||
|
||||
return [[self _representedObject] count];
|
||||
return [[self _representedObject] count];
|
||||
}
|
||||
|
||||
- (CPEnumerator)objectEnumerator
|
||||
{
|
||||
if (_enumerator)
|
||||
{
|
||||
return _enumerator(_proxyObject, _enumeratorSEL);
|
||||
}
|
||||
if (_enumerator)
|
||||
{
|
||||
return _enumerator(_proxyObject, _enumeratorSEL);
|
||||
}
|
||||
|
||||
return [[self _representedObject] objectEnumerator];
|
||||
return [[self _representedObject] objectEnumerator];
|
||||
}
|
||||
|
||||
- (id)member:(id)anObject
|
||||
{
|
||||
if (_member)
|
||||
{
|
||||
return _member(_proxyObject, _memberSEL, anObject);
|
||||
}
|
||||
if (_member)
|
||||
{
|
||||
return _member(_proxyObject, _memberSEL, anObject);
|
||||
}
|
||||
|
||||
return [[self _representedObject] member:anObject];
|
||||
return [[self _representedObject] member:anObject];
|
||||
}
|
||||
|
||||
- (void)addObject:(id)anObject
|
||||
{
|
||||
if (_add)
|
||||
{
|
||||
_add(_proxyObject, _addSEL, anObject);
|
||||
}
|
||||
else if (_addMany)
|
||||
{
|
||||
var objectSet = [CPSet setWithObject:anObject];
|
||||
_addMany(_proxyObject, _addManySEL, objectSet);
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target addObject:anObject];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
if (_add)
|
||||
{
|
||||
_add(_proxyObject, _addSEL, anObject);
|
||||
}
|
||||
else if (_addMany)
|
||||
{
|
||||
var objectSet = [CPSet setWithObject:anObject];
|
||||
_addMany(_proxyObject, _addManySEL, objectSet);
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target addObject:anObject];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addObjectsFromArray:(CPArray)objects
|
||||
{
|
||||
if (_addMany)
|
||||
{
|
||||
var objectSet = [CPSet setWithArray:objects];
|
||||
_addMany(_proxyObject, _addManySEL, objectSet);
|
||||
}
|
||||
else if (_add)
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [objects objectEnumerator];
|
||||
if (_addMany)
|
||||
{
|
||||
var objectSet = [CPSet setWithArray:objects];
|
||||
_addMany(_proxyObject, _addManySEL, objectSet);
|
||||
}
|
||||
else if (_add)
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [objects objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
_add(_proxyObject, _addSEL, object);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target addObjectsFromArray:objects];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
_add(_proxyObject, _addSEL, object);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target addObjectsFromArray:objects];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)unionSet:(CPSet)aSet
|
||||
{
|
||||
if (_addMany)
|
||||
{
|
||||
_addMany(_proxyObject, _addManySEL, aSet);
|
||||
}
|
||||
else if (_add)
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [aSet objectEnumerator];
|
||||
if (_addMany)
|
||||
{
|
||||
_addMany(_proxyObject, _addManySEL, aSet);
|
||||
}
|
||||
else if (_add)
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [aSet objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
_add(_proxyObject, _addSEL, object);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target unionSet:aSet];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
_add(_proxyObject, _addSEL, object);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target unionSet:aSet];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeObject:(id)anObject
|
||||
{
|
||||
if (_remove)
|
||||
{
|
||||
_remove(_proxyObject, _removeSEL, anObject);
|
||||
}
|
||||
else if (_removeMany)
|
||||
{
|
||||
var objectSet = [CPSet setWithObject:anObject];
|
||||
_removeMany(_proxyObject, _removeManySEL, objectSet);
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target removeObject:anObject];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
if (_remove)
|
||||
{
|
||||
_remove(_proxyObject, _removeSEL, anObject);
|
||||
}
|
||||
else if (_removeMany)
|
||||
{
|
||||
var objectSet = [CPSet setWithObject:anObject];
|
||||
_removeMany(_proxyObject, _removeManySEL, objectSet);
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target removeObject:anObject];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)minusSet:(CPSet)aSet
|
||||
{
|
||||
if (_removeMany)
|
||||
{
|
||||
_removeMany(_proxyObject, _removeManySEL, aSet);
|
||||
}
|
||||
else if (_remove)
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [aSet objectEnumerator];
|
||||
if (_removeMany)
|
||||
{
|
||||
_removeMany(_proxyObject, _removeManySEL, aSet);
|
||||
}
|
||||
else if (_remove)
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [aSet objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
_remove(_proxyObject, _removeSEL, object);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target minusSet:aSet];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
_remove(_proxyObject, _removeSEL, object);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target minusSet:aSet];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeObjectsInArray:(CPArray)objects
|
||||
{
|
||||
if (_removeMany)
|
||||
{
|
||||
var objectSet = [CPSet setWithArray:objects];
|
||||
_removeMany(_proxyObject, _removeManySEL, objectSet);
|
||||
}
|
||||
else if (_remove)
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [objects objectEnumerator];
|
||||
if (_removeMany)
|
||||
{
|
||||
var objectSet = [CPSet setWithArray:objects];
|
||||
_removeMany(_proxyObject, _removeManySEL, objectSet);
|
||||
}
|
||||
else if (_remove)
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [objects objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
_remove(_proxyObject, _removeSEL, object);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target removeObjectsInArray:objects];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
_remove(_proxyObject, _removeSEL, object);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target removeObjectsInArray:objects];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeAllObjects
|
||||
{
|
||||
if (_removeMany)
|
||||
{
|
||||
var allObjectsSet = [[self _representedObject] copy];
|
||||
_removeMany(_proxyObject, _removeManySEL, allObjectsSet);
|
||||
}
|
||||
else if (_remove)
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [[[self _representedObject] copy] objectEnumerator];
|
||||
if (_removeMany)
|
||||
{
|
||||
var allObjectsSet = [[self _representedObject] copy];
|
||||
_removeMany(_proxyObject, _removeManySEL, allObjectsSet);
|
||||
}
|
||||
else if (_remove)
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [[[self _representedObject] copy] objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
_remove(_proxyObject, _removeSEL, object);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target removeAllObjects];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
_remove(_proxyObject, _removeSEL, object);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target removeAllObjects];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)intersectSet:(CPSet)aSet
|
||||
{
|
||||
if (_intersect)
|
||||
{
|
||||
_intersect(_proxyObject, _intersectSEL, aSet);
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target intersectSet:aSet];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
if (_intersect)
|
||||
{
|
||||
_intersect(_proxyObject, _intersectSEL, aSet);
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = [[self _representedObject] copy];
|
||||
[target intersectSet:aSet];
|
||||
[self _setRepresentedObject:target];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setSet:(CPSet)set
|
||||
{
|
||||
[self _setRepresentedObject:set];
|
||||
[self _setRepresentedObject:set];
|
||||
}
|
||||
|
||||
- (CPArray)allObjects
|
||||
{
|
||||
return [[self _representedObject] allObjects];
|
||||
return [[self _representedObject] allObjects];
|
||||
}
|
||||
|
||||
- (id)anyObject
|
||||
{
|
||||
return [[self _representedObject] anyObject];
|
||||
return [[self _representedObject] anyObject];
|
||||
}
|
||||
|
||||
- (BOOL)containsObject:(id)anObject
|
||||
{
|
||||
return [[self _representedObject] containsObject:anObject];
|
||||
return [[self _representedObject] containsObject:anObject];
|
||||
}
|
||||
|
||||
- (BOOL)intersectsSet:(CPSet)aSet
|
||||
{
|
||||
return [[self _representedObject] intersectsSet:aSet];
|
||||
return [[self _representedObject] intersectsSet:aSet];
|
||||
}
|
||||
|
||||
- (BOOL)isEqualToSet:(CPSet)aSet
|
||||
{
|
||||
return [[self _representedObject] isEqualToSet:aSet];
|
||||
return [[self _representedObject] isEqualToSet:aSet];
|
||||
}
|
||||
|
||||
- (id)copy
|
||||
{
|
||||
return [[self _representedObject] copy];
|
||||
return [[self _representedObject] copy];
|
||||
}
|
||||
|
||||
@end
|
||||
+110
-110
@@ -34,263 +34,263 @@
|
||||
|
||||
+ (id)alloc
|
||||
{
|
||||
if (self === [CPSet class] || self === [CPMutableSet class])
|
||||
return [_CPPlaceholderSet alloc];
|
||||
if (self === [CPSet class] || self === [CPMutableSet class])
|
||||
return [_CPPlaceholderSet alloc];
|
||||
|
||||
return [super alloc];
|
||||
return [super alloc];
|
||||
}
|
||||
|
||||
+ (id)set
|
||||
{
|
||||
return [[self alloc] init];
|
||||
return [[self alloc] init];
|
||||
}
|
||||
|
||||
+ (id)setWithArray:(CPArray)anArray
|
||||
{
|
||||
return [[self alloc] initWithArray:anArray];
|
||||
return [[self alloc] initWithArray:anArray];
|
||||
}
|
||||
|
||||
+ (id)setWithObject:(id)anObject
|
||||
{
|
||||
return [[self alloc] initWithObjects:anObject];
|
||||
return [[self alloc] initWithObjects:anObject];
|
||||
}
|
||||
|
||||
+ (id)setWithObjects:(id)objects count:(CPUInteger)count
|
||||
{
|
||||
return [[self alloc] initWithObjects:objects count:count];
|
||||
return [[self alloc] initWithObjects:objects count:count];
|
||||
}
|
||||
|
||||
+ (id)setWithObjects:(id)anObject, ...
|
||||
{
|
||||
var argumentsArray = Array.prototype.slice.apply(arguments);
|
||||
var argumentsArray = Array.prototype.slice.apply(arguments);
|
||||
|
||||
argumentsArray[0] = [self alloc];
|
||||
argumentsArray[1] = @selector(initWithObjects:);
|
||||
argumentsArray[0] = [self alloc];
|
||||
argumentsArray[1] = @selector(initWithObjects:);
|
||||
|
||||
return objj_msgSend.apply(this, argumentsArray);
|
||||
return objj_msgSend.apply(this, argumentsArray);
|
||||
}
|
||||
|
||||
+ (id)setWithSet:(CPSet)set
|
||||
{
|
||||
return [[self alloc] initWithSet:set];
|
||||
return [[self alloc] initWithSet:set];
|
||||
}
|
||||
|
||||
- (id)setByAddingObject:(id)anObject
|
||||
{
|
||||
return [[self class] setWithArray:[[self allObjects] arrayByAddingObject:anObject]];
|
||||
return [[self class] setWithArray:[[self allObjects] arrayByAddingObject:anObject]];
|
||||
}
|
||||
|
||||
- (id)setByAddingObjectsFromSet:(CPSet)aSet
|
||||
{
|
||||
return [self setByAddingObjectsFromArray:[aSet allObjects]];
|
||||
return [self setByAddingObjectsFromArray:[aSet allObjects]];
|
||||
}
|
||||
|
||||
- (id)setByAddingObjectsFromArray:(CPArray)anArray
|
||||
{
|
||||
return [[self class] setWithArray:[[self allObjects] arrayByAddingObjectsFromArray:anArray]];
|
||||
return [[self class] setWithArray:[[self allObjects] arrayByAddingObjectsFromArray:anArray]];
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
return [self initWithObjects:nil count:0];
|
||||
return [self initWithObjects:nil count:0];
|
||||
}
|
||||
|
||||
- (id)initWithArray:(CPArray)anArray
|
||||
{
|
||||
return [self initWithObjects:anArray count:[anArray count]];
|
||||
return [self initWithObjects:anArray count:[anArray count]];
|
||||
}
|
||||
|
||||
- (id)initWithObjects:(id)anObject, ...
|
||||
{
|
||||
var index = 2,
|
||||
count = arguments.length;
|
||||
var index = 2,
|
||||
count = arguments.length;
|
||||
|
||||
for (; index < count; ++index)
|
||||
if (arguments[index] === nil)
|
||||
break;
|
||||
for (; index < count; ++index)
|
||||
if (arguments[index] === nil)
|
||||
break;
|
||||
|
||||
return [self initWithObjects:Array.prototype.slice.call(arguments, 2, index) count:index - 2];
|
||||
return [self initWithObjects:Array.prototype.slice.call(arguments, 2, index) count:index - 2];
|
||||
}
|
||||
|
||||
- (id)initWithObjects:(CPArray)objects count:(CPUInteger)aCount
|
||||
{
|
||||
if (self === _CPSharedPlaceholderSet)
|
||||
return [[_CPConcreteMutableSet alloc] initWithObjects:objects count:aCount];
|
||||
if (self === _CPSharedPlaceholderSet)
|
||||
return [[_CPConcreteMutableSet alloc] initWithObjects:objects count:aCount];
|
||||
|
||||
return [super init];
|
||||
return [super init];
|
||||
}
|
||||
|
||||
- (id)initWithSet:(CPSet)aSet
|
||||
{
|
||||
return [self initWithArray:[aSet allObjects]];
|
||||
return [self initWithArray:[aSet allObjects]];
|
||||
}
|
||||
|
||||
- (id)initWithSet:(CPSet)aSet copyItems:(BOOL)shouldCopyItems
|
||||
{
|
||||
if (shouldCopyItems)
|
||||
return [aSet valueForKey:@"copy"];
|
||||
if (shouldCopyItems)
|
||||
return [aSet valueForKey:@"copy"];
|
||||
|
||||
return [self initWithSet:aSet];
|
||||
return [self initWithSet:aSet];
|
||||
}
|
||||
|
||||
- (CPUInteger)count
|
||||
{
|
||||
_CPRaiseInvalidAbstractInvocation(self, _cmd);
|
||||
_CPRaiseInvalidAbstractInvocation(self, _cmd);
|
||||
}
|
||||
|
||||
- (CPArray)allObjects
|
||||
{
|
||||
var objects = [],
|
||||
object,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
var objects = [],
|
||||
object,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
objects.push(object);
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
objects.push(object);
|
||||
|
||||
return objects;
|
||||
return objects;
|
||||
}
|
||||
|
||||
- (id)anyObject
|
||||
{
|
||||
return [[self objectEnumerator] nextObject];
|
||||
return [[self objectEnumerator] nextObject];
|
||||
}
|
||||
|
||||
- (BOOL)containsObject:(id)anObject
|
||||
{
|
||||
return [self member:anObject] != nil;
|
||||
return [self member:anObject] != nil;
|
||||
}
|
||||
|
||||
- (CPSet)filteredSetUsingPredicate:(CPPredicate)aPredicate
|
||||
{
|
||||
var objects = [],
|
||||
object,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
var objects = [],
|
||||
object,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
if ([aPredicate evaluateWithObject:object])
|
||||
objects.push(object);
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
if ([aPredicate evaluateWithObject:object])
|
||||
objects.push(object);
|
||||
|
||||
return [[[self class] alloc] initWithArray:objects];
|
||||
return [[[self class] alloc] initWithArray:objects];
|
||||
}
|
||||
|
||||
- (void)makeObjectsPerformSelector:(SEL)aSelector
|
||||
{
|
||||
[self makeObjectsPerformSelector:aSelector withObjects:nil];
|
||||
[self makeObjectsPerformSelector:aSelector withObjects:nil];
|
||||
}
|
||||
|
||||
- (void)makeObjectsPerformSelector:(SEL)aSelector withObject:(id)anObject
|
||||
{
|
||||
[self makeObjectsPerformSelector:aSelector withObjects:[anObject]];
|
||||
[self makeObjectsPerformSelector:aSelector withObjects:[anObject]];
|
||||
}
|
||||
|
||||
- (void)makeObjectsPerformSelector:(SEL)aSelector withObjects:(CPArray)objects
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [self objectEnumerator],
|
||||
argumentsArray = [nil, aSelector].concat(objects || []);
|
||||
var object,
|
||||
objectEnumerator = [self objectEnumerator],
|
||||
argumentsArray = [nil, aSelector].concat(objects || []);
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
argumentsArray[0] = object;
|
||||
objj_msgSend.apply(this, argumentsArray);
|
||||
}
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
{
|
||||
argumentsArray[0] = object;
|
||||
objj_msgSend.apply(this, argumentsArray);
|
||||
}
|
||||
}
|
||||
|
||||
- (id)member:(id)anObject
|
||||
{
|
||||
_CPRaiseInvalidAbstractInvocation(self, _cmd);
|
||||
_CPRaiseInvalidAbstractInvocation(self, _cmd);
|
||||
}
|
||||
|
||||
- (CPEnumerator)objectEnumerator
|
||||
{
|
||||
_CPRaiseInvalidAbstractInvocation(self, _cmd);
|
||||
_CPRaiseInvalidAbstractInvocation(self, _cmd);
|
||||
}
|
||||
|
||||
- (void)enumerateObjectsUsingBlock:(Function)aFunction
|
||||
{
|
||||
var object,
|
||||
objectEnumerator = [self objectEnumerator],
|
||||
shouldStop = NO;
|
||||
var object,
|
||||
objectEnumerator = [self objectEnumerator],
|
||||
shouldStop = NO;
|
||||
|
||||
while (!shouldStop && (object = [objectEnumerator nextObject]) != nil) {
|
||||
if (aFunction(object, @ref(shouldStop)) !== undefined) {
|
||||
throw "DEPRECATED: The method enumerateObjectsUsingBlock: does not support returning a value in the block to stop the iteration.";
|
||||
}
|
||||
}
|
||||
while (!shouldStop && (object = [objectEnumerator nextObject]) != nil) {
|
||||
if (aFunction(object, @ref(shouldStop)) !== undefined) {
|
||||
throw "DEPRECATED: The method enumerateObjectsUsingBlock: does not support returning a value in the block to stop the iteration.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (CPSet)objectsPassingTest:(Function)aFunction
|
||||
{
|
||||
var objects = [],
|
||||
object = nil,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
var objects = [],
|
||||
object = nil,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
if (aFunction(object))
|
||||
objects.push(object);
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
if (aFunction(object))
|
||||
objects.push(object);
|
||||
|
||||
return [[[self class] alloc] initWithArray:objects];
|
||||
return [[[self class] alloc] initWithArray:objects];
|
||||
}
|
||||
|
||||
- (BOOL)isSubsetOfSet:(CPSet)aSet
|
||||
{
|
||||
var object = nil,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
var object = nil,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
if (![aSet containsObject:object])
|
||||
return NO;
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
if (![aSet containsObject:object])
|
||||
return NO;
|
||||
|
||||
return YES;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)intersectsSet:(CPSet)aSet
|
||||
{
|
||||
if (self === aSet)
|
||||
return [self count] > 0;
|
||||
if (self === aSet)
|
||||
return [self count] > 0;
|
||||
|
||||
var object = nil,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
var object = nil,
|
||||
objectEnumerator = [self objectEnumerator];
|
||||
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
if ([aSet containsObject:object])
|
||||
return YES;
|
||||
while ((object = [objectEnumerator nextObject]) != nil)
|
||||
if ([aSet containsObject:object])
|
||||
return YES;
|
||||
|
||||
return NO;
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (CPArray)sortedArrayUsingDescriptors:(CPArray)someSortDescriptors
|
||||
{
|
||||
return [[self allObjects] sortedArrayUsingDescriptors:someSortDescriptors];
|
||||
return [[self allObjects] sortedArrayUsingDescriptors:someSortDescriptors];
|
||||
}
|
||||
|
||||
- (BOOL)isEqualToSet:(CPSet)aSet
|
||||
{
|
||||
return [self isEqual:aSet];
|
||||
return [self isEqual:aSet];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(CPSet)aSet
|
||||
{
|
||||
return self === aSet ||
|
||||
[aSet isKindOfClass:[CPSet class]] &&
|
||||
([self count] === [aSet count] &&
|
||||
[aSet isSubsetOfSet:self]);
|
||||
return self === aSet ||
|
||||
[aSet isKindOfClass:[CPSet class]] &&
|
||||
([self count] === [aSet count] &&
|
||||
[aSet isSubsetOfSet:self]);
|
||||
}
|
||||
|
||||
- (CPString)description
|
||||
{
|
||||
var string = "{(\n",
|
||||
objects = [self allObjects],
|
||||
index = 0,
|
||||
count = [objects count];
|
||||
var string = "{(\n",
|
||||
objects = [self allObjects],
|
||||
index = 0,
|
||||
count = [objects count];
|
||||
|
||||
for (; index < count; ++index)
|
||||
{
|
||||
var object = objects[index];
|
||||
string += "\t" + String(object).split('\n').join("\n\t") + "\n";
|
||||
}
|
||||
for (; index < count; ++index)
|
||||
{
|
||||
var object = objects[index];
|
||||
string += "\t" + String(object).split('\n').join("\n\t") + "\n";
|
||||
}
|
||||
|
||||
return string + ")}";
|
||||
return string + ")}";
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -302,12 +302,12 @@
|
||||
|
||||
- (id)copy
|
||||
{
|
||||
return [[self class] setWithSet:self];
|
||||
return [[self class] setWithSet:self];
|
||||
}
|
||||
|
||||
- (id)mutableCopy
|
||||
{
|
||||
return [self copy];
|
||||
return [self copy];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -321,12 +321,12 @@ var CPSetObjectsKey = @"CPSetObjectsKey";
|
||||
|
||||
- (id)initWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
return [self initWithArray:[aCoder decodeObjectForKey:CPSetObjectsKey]];
|
||||
return [self initWithArray:[aCoder decodeObjectForKey:CPSetObjectsKey]];
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(CPCoder)aCoder
|
||||
{
|
||||
[aCoder encodeObject:[self allObjects] forKey:CPSetObjectsKey];
|
||||
[aCoder encodeObject:[self allObjects] forKey:CPSetObjectsKey];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -342,10 +342,10 @@ var _CPSharedPlaceholderSet = nil;
|
||||
|
||||
+ (id)alloc
|
||||
{
|
||||
if (!_CPSharedPlaceholderSet)
|
||||
_CPSharedPlaceholderSet = [super alloc];
|
||||
if (!_CPSharedPlaceholderSet)
|
||||
_CPSharedPlaceholderSet = [super alloc];
|
||||
|
||||
return _CPSharedPlaceholderSet;
|
||||
return _CPSharedPlaceholderSet;
|
||||
}
|
||||
|
||||
@end
|
||||
+115
-115
@@ -36,14 +36,14 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
@implementation CPTimer : CPObject
|
||||
{
|
||||
CPTimeInterval _timeInterval;
|
||||
CPInvocation _invocation;
|
||||
Function _callback;
|
||||
CPTimeInterval _timeInterval;
|
||||
CPInvocation _invocation;
|
||||
Function _callback;
|
||||
|
||||
BOOL _repeats;
|
||||
BOOL _isValid;
|
||||
CPDate _fireDate;
|
||||
id _userInfo;
|
||||
BOOL _repeats;
|
||||
BOOL _isValid;
|
||||
CPDate _fireDate;
|
||||
id _userInfo;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -51,11 +51,11 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
+ (CPTimer)scheduledTimerWithTimeInterval:(CPTimeInterval)seconds invocation:(CPInvocation)anInvocation repeats:(BOOL)shouldRepeat
|
||||
{
|
||||
const timer = [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds invocation:anInvocation repeats:shouldRepeat];
|
||||
const timer = [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds invocation:anInvocation repeats:shouldRepeat];
|
||||
|
||||
[[CPRunLoop currentRunLoop] addTimer:timer forMode:CPDefaultRunLoopMode];
|
||||
[[CPRunLoop currentRunLoop] addTimer:timer forMode:CPDefaultRunLoopMode];
|
||||
|
||||
return timer;
|
||||
return timer;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -63,11 +63,11 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
+ (CPTimer)scheduledTimerWithTimeInterval:(CPTimeInterval)seconds target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)shouldRepeat
|
||||
{
|
||||
const timer = [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds target:aTarget selector:aSelector userInfo:userInfo repeats:shouldRepeat];
|
||||
const timer = [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds target:aTarget selector:aSelector userInfo:userInfo repeats:shouldRepeat];
|
||||
|
||||
[[CPRunLoop currentRunLoop] addTimer:timer forMode:CPDefaultRunLoopMode];
|
||||
[[CPRunLoop currentRunLoop] addTimer:timer forMode:CPDefaultRunLoopMode];
|
||||
|
||||
return timer;
|
||||
return timer;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -75,11 +75,11 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
+ (CPTimer)scheduledTimerWithTimeInterval:(CPTimeInterval)seconds callback:(Function)aFunction repeats:(BOOL)shouldRepeat
|
||||
{
|
||||
const timer = [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds callback:aFunction repeats:shouldRepeat];
|
||||
const timer = [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds callback:aFunction repeats:shouldRepeat];
|
||||
|
||||
[[CPRunLoop currentRunLoop] addTimer:timer forMode:CPDefaultRunLoopMode];
|
||||
[[CPRunLoop currentRunLoop] addTimer:timer forMode:CPDefaultRunLoopMode];
|
||||
|
||||
return timer;
|
||||
return timer;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -87,7 +87,7 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
+ (CPTimer)timerWithTimeInterval:(CPTimeInterval)seconds invocation:(CPInvocation)anInvocation repeats:(BOOL)shouldRepeat
|
||||
{
|
||||
return [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds invocation:anInvocation repeats:shouldRepeat];
|
||||
return [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds invocation:anInvocation repeats:shouldRepeat];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -95,7 +95,7 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
+ (CPTimer)timerWithTimeInterval:(CPTimeInterval)seconds target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)shouldRepeat
|
||||
{
|
||||
return [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds target:aTarget selector:aSelector userInfo:userInfo repeats:shouldRepeat];
|
||||
return [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds target:aTarget selector:aSelector userInfo:userInfo repeats:shouldRepeat];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -103,7 +103,7 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
+ (CPTimer)timerWithTimeInterval:(CPTimeInterval)seconds callback:(Function)aFunction repeats:(BOOL)shouldRepeat
|
||||
{
|
||||
return [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds callback:aFunction repeats:shouldRepeat];
|
||||
return [[self alloc] initWithFireDate:[CPDate dateWithTimeIntervalSinceNow:seconds] interval:seconds callback:aFunction repeats:shouldRepeat];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -111,18 +111,18 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
- (id)initWithFireDate:(CPDate)aDate interval:(CPTimeInterval)seconds invocation:(CPInvocation)anInvocation repeats:(BOOL)shouldRepeat
|
||||
{
|
||||
self = [super init];
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_timeInterval = (seconds <= 0) ? CPTimerDefaultTimeInterval : seconds;
|
||||
_invocation = anInvocation;
|
||||
_repeats = shouldRepeat;
|
||||
_isValid = YES;
|
||||
_fireDate = aDate;
|
||||
}
|
||||
if (self)
|
||||
{
|
||||
_timeInterval = (seconds <= 0) ? CPTimerDefaultTimeInterval : seconds;
|
||||
_invocation = anInvocation;
|
||||
_repeats = shouldRepeat;
|
||||
_isValid = YES;
|
||||
_fireDate = aDate;
|
||||
}
|
||||
|
||||
return self;
|
||||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -130,18 +130,18 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
- (id)initWithFireDate:(CPDate)aDate interval:(CPTimeInterval)seconds target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)shouldRepeat
|
||||
{
|
||||
const invocation = [CPInvocation invocationWithMethodSignature:1];
|
||||
const invocation = [CPInvocation invocationWithMethodSignature:1];
|
||||
|
||||
[invocation setTarget:aTarget];
|
||||
[invocation setSelector:aSelector];
|
||||
[invocation setArgument:self atIndex:2];
|
||||
[invocation setTarget:aTarget];
|
||||
[invocation setSelector:aSelector];
|
||||
[invocation setArgument:self atIndex:2];
|
||||
|
||||
self = [self initWithFireDate:aDate interval:seconds invocation:invocation repeats:shouldRepeat];
|
||||
self = [self initWithFireDate:aDate interval:seconds invocation:invocation repeats:shouldRepeat];
|
||||
|
||||
if (self)
|
||||
_userInfo = userInfo;
|
||||
if (self)
|
||||
_userInfo = userInfo;
|
||||
|
||||
return self;
|
||||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -149,18 +149,18 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
- (id)initWithFireDate:(CPDate)aDate interval:(CPTimeInterval)seconds callback:(Function)aFunction repeats:(BOOL)shouldRepeat
|
||||
{
|
||||
self = [super init];
|
||||
self = [super init];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_timeInterval = (seconds <= 0) ? CPTimerDefaultTimeInterval : seconds;
|
||||
_callback = aFunction;
|
||||
_repeats = shouldRepeat;
|
||||
_isValid = YES;
|
||||
_fireDate = aDate;
|
||||
}
|
||||
if (self)
|
||||
{
|
||||
_timeInterval = (seconds <= 0) ? CPTimerDefaultTimeInterval : seconds;
|
||||
_callback = aFunction;
|
||||
_repeats = shouldRepeat;
|
||||
_isValid = YES;
|
||||
_fireDate = aDate;
|
||||
}
|
||||
|
||||
return self;
|
||||
return self;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -168,7 +168,7 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
- (CPTimeInterval)timeInterval
|
||||
{
|
||||
return _timeInterval;
|
||||
return _timeInterval;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -176,7 +176,7 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
- (CPDate)fireDate
|
||||
{
|
||||
return _fireDate;
|
||||
return _fireDate;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -184,7 +184,7 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
- (void)setFireDate:(CPDate)aDate
|
||||
{
|
||||
_fireDate = aDate;
|
||||
_fireDate = aDate;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -192,21 +192,21 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
- (void)fire
|
||||
{
|
||||
if (!_isValid)
|
||||
return;
|
||||
if (!_isValid)
|
||||
return;
|
||||
|
||||
if (_callback)
|
||||
_callback();
|
||||
else
|
||||
[_invocation invoke];
|
||||
if (_callback)
|
||||
_callback();
|
||||
else
|
||||
[_invocation invoke];
|
||||
|
||||
if (!_isValid)
|
||||
return;
|
||||
if (!_isValid)
|
||||
return;
|
||||
|
||||
if (_repeats)
|
||||
_fireDate = [CPDate dateWithTimeIntervalSinceNow:_timeInterval];
|
||||
else
|
||||
[self invalidate];
|
||||
if (_repeats)
|
||||
_fireDate = [CPDate dateWithTimeIntervalSinceNow:_timeInterval];
|
||||
else
|
||||
[self invalidate];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -214,7 +214,7 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
- (BOOL)isValid
|
||||
{
|
||||
return _isValid;
|
||||
return _isValid;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -222,10 +222,10 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
- (void)invalidate
|
||||
{
|
||||
_isValid = NO;
|
||||
_userInfo = nil;
|
||||
_invocation = nil;
|
||||
_callback = nil;
|
||||
_isValid = NO;
|
||||
_userInfo = nil;
|
||||
_invocation = nil;
|
||||
_callback = nil;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -233,7 +233,7 @@ const CPTimerDefaultTimeInterval = 0.1;
|
||||
*/
|
||||
- (id)userInfo
|
||||
{
|
||||
return _userInfo;
|
||||
return _userInfo;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -250,68 +250,68 @@ const CPTimersForTimeoutIDs = {};
|
||||
|
||||
const _CPTimerBridgeTimer = function(codeOrFunction, aDelay, shouldRepeat, functionArgs)
|
||||
{
|
||||
const timeoutID = CPTimersTimeoutID++;
|
||||
let theFunction = nil;
|
||||
const timeoutID = CPTimersTimeoutID++;
|
||||
let theFunction = nil;
|
||||
|
||||
if (typeof codeOrFunction === "string")
|
||||
{
|
||||
// FIXME: Anti-pattern: Dynamic Evaluation. Evaluating string payloads via `new Function`
|
||||
// is a strict Content Security Policy (CSP) violation.
|
||||
theFunction = function()
|
||||
{
|
||||
new Function(codeOrFunction)();
|
||||
if (typeof codeOrFunction === "string")
|
||||
{
|
||||
// FIXME: Anti-pattern: Dynamic Evaluation. Evaluating string payloads via `new Function`
|
||||
// is a strict Content Security Policy (CSP) violation.
|
||||
theFunction = function()
|
||||
{
|
||||
new Function(codeOrFunction)();
|
||||
|
||||
if (!shouldRepeat)
|
||||
delete CPTimersForTimeoutIDs[timeoutID];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!functionArgs)
|
||||
functionArgs = [];
|
||||
if (!shouldRepeat)
|
||||
delete CPTimersForTimeoutIDs[timeoutID];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!functionArgs)
|
||||
functionArgs = [];
|
||||
|
||||
theFunction = function()
|
||||
{
|
||||
codeOrFunction.apply(window, functionArgs);
|
||||
theFunction = function()
|
||||
{
|
||||
codeOrFunction.apply(window, functionArgs);
|
||||
|
||||
if (!shouldRepeat)
|
||||
delete CPTimersForTimeoutIDs[timeoutID];
|
||||
}
|
||||
}
|
||||
if (!shouldRepeat)
|
||||
delete CPTimersForTimeoutIDs[timeoutID];
|
||||
}
|
||||
}
|
||||
|
||||
// A call such as setTimeout(f) is technically invalid but browsers seem to treat it as setTimeout(f, 0), so so will we.
|
||||
aDelay = aDelay | 0.0;
|
||||
// A call such as setTimeout(f) is technically invalid but browsers seem to treat it as setTimeout(f, 0), so so will we.
|
||||
aDelay = aDelay | 0.0;
|
||||
|
||||
CPTimersForTimeoutIDs[timeoutID] = [CPTimer scheduledTimerWithTimeInterval:aDelay / 1000 callback:theFunction repeats:shouldRepeat];
|
||||
CPTimersForTimeoutIDs[timeoutID] = [CPTimer scheduledTimerWithTimeInterval:aDelay / 1000 callback:theFunction repeats:shouldRepeat];
|
||||
|
||||
return timeoutID;
|
||||
return timeoutID;
|
||||
};
|
||||
|
||||
// Avoid "TypeError: Result of expression 'window' [undefined] is not an object" when running unit tests.
|
||||
if (typeof(window) !== 'undefined')
|
||||
{
|
||||
window.setTimeout = function(codeOrFunction, aDelay)
|
||||
{
|
||||
return _CPTimerBridgeTimer(codeOrFunction, aDelay, NO, Array.prototype.slice.apply(arguments, [2]));
|
||||
};
|
||||
window.setTimeout = function(codeOrFunction, aDelay)
|
||||
{
|
||||
return _CPTimerBridgeTimer(codeOrFunction, aDelay, NO, Array.prototype.slice.apply(arguments, [2]));
|
||||
};
|
||||
|
||||
window.clearTimeout = function(aTimeoutID)
|
||||
{
|
||||
const timer = CPTimersForTimeoutIDs[aTimeoutID];
|
||||
window.clearTimeout = function(aTimeoutID)
|
||||
{
|
||||
const timer = CPTimersForTimeoutIDs[aTimeoutID];
|
||||
|
||||
if (timer)
|
||||
[timer invalidate];
|
||||
if (timer)
|
||||
[timer invalidate];
|
||||
|
||||
delete CPTimersForTimeoutIDs[aTimeoutID];
|
||||
};
|
||||
delete CPTimersForTimeoutIDs[aTimeoutID];
|
||||
};
|
||||
|
||||
window.setInterval = function(codeOrFunction, aDelay, functionArgs)
|
||||
{
|
||||
return _CPTimerBridgeTimer(codeOrFunction, aDelay, YES, Array.prototype.slice.apply(arguments, [2]));
|
||||
};
|
||||
window.setInterval = function(codeOrFunction, aDelay, functionArgs)
|
||||
{
|
||||
return _CPTimerBridgeTimer(codeOrFunction, aDelay, YES, Array.prototype.slice.apply(arguments, [2]));
|
||||
};
|
||||
|
||||
window.clearInterval = function(aTimeoutID)
|
||||
{
|
||||
window.clearTimeout(aTimeoutID);
|
||||
};
|
||||
window.clearInterval = function(aTimeoutID)
|
||||
{
|
||||
window.clearTimeout(aTimeoutID);
|
||||
};
|
||||
}
|
||||
@@ -161,14 +161,14 @@ var CPURLConnectionDelegate = nil;
|
||||
|
||||
Typical use can be like this:
|
||||
|
||||
- (async @action)doAction:(id)sender {
|
||||
const { response, data, error } = await [CPURLConnection sendAsynchronousRequest:[CPURLRequest requestWithURL:@"http://cappuccino.dev"]];
|
||||
if (error == nil) {
|
||||
//do the stuff...
|
||||
} else {
|
||||
// Handle errors
|
||||
}
|
||||
}
|
||||
- (async @action)doAction:(id)sender {
|
||||
const { response, data, error } = await [CPURLConnection sendAsynchronousRequest:[CPURLRequest requestWithURL:@"http://cappuccino.dev"]];
|
||||
if (error == nil) {
|
||||
//do the stuff...
|
||||
} else {
|
||||
// Handle errors
|
||||
}
|
||||
}
|
||||
*/
|
||||
+ (async JSObject /* { response: CPURLResponse, data: CPData, error: CPError } */)sendAsynchronousRequest:(CPURLRequest)aRequest
|
||||
{
|
||||
@@ -218,15 +218,15 @@ var CPURLConnectionDelegate = nil;
|
||||
|
||||
- (void)_initWithRequest:(CPURLRequest)aRequest
|
||||
{
|
||||
_request = aRequest;
|
||||
_request = aRequest;
|
||||
_originalRequest = [aRequest copy];
|
||||
_isCanceled = NO;
|
||||
_isCanceled = NO;
|
||||
|
||||
var URL = [_request URL],
|
||||
scheme = [URL scheme];
|
||||
var URL = [_request URL],
|
||||
scheme = [URL scheme];
|
||||
|
||||
// Browsers use "file:", Titanium uses "app:"
|
||||
_isLocalFileConnection = scheme === "file" ||
|
||||
// Browsers use "file:", Titanium uses "app:"
|
||||
_isLocalFileConnection = scheme === "file" ||
|
||||
((scheme === "http" || scheme === "https") &&
|
||||
window.location &&
|
||||
(window.location.protocol === "file:" || window.location.protocol === "app:"));
|
||||
|
||||
@@ -12,18 +12,18 @@
|
||||
*/
|
||||
@implementation _MockColorPicker : CPObject
|
||||
{
|
||||
CPArray _receivedColors;
|
||||
CPView _view;
|
||||
CPArray _receivedColors;
|
||||
CPView _view;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_receivedColors = [];
|
||||
_view = [[CPView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
|
||||
}
|
||||
return self;
|
||||
if (self = [super init])
|
||||
{
|
||||
_receivedColors = [];
|
||||
_view = [[CPView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setColor:(CPColor)aColor
|
||||
@@ -33,32 +33,32 @@
|
||||
|
||||
- (CPArray)receivedColors
|
||||
{
|
||||
return _receivedColors;
|
||||
return _receivedColors;
|
||||
}
|
||||
|
||||
- (CPView)provideNewView:(BOOL)initial
|
||||
{
|
||||
return _view;
|
||||
return _view;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPColorPanelTest : OJTestCase
|
||||
{
|
||||
CPColorPanel _panel;
|
||||
CPColorPanel _panel;
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// Get shared panel and ensure it's initialized
|
||||
_panel = [CPColorPanel sharedColorPanel];
|
||||
[_panel _loadContentsIfNecessary];
|
||||
// Get shared panel and ensure it's initialized
|
||||
_panel = [CPColorPanel sharedColorPanel];
|
||||
[_panel _loadContentsIfNecessary];
|
||||
}
|
||||
|
||||
- (void)tearDown
|
||||
{
|
||||
// Reset panel state between tests
|
||||
[_panel setColor:[CPColor whiteColor]];
|
||||
// Reset panel state between tests
|
||||
[_panel setColor:[CPColor whiteColor]];
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -67,10 +67,10 @@
|
||||
*/
|
||||
- (void)testSetColorUpdatesOpacitySlider
|
||||
{
|
||||
var initialColor = [CPColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.5];[_panel setColor:initialColor];
|
||||
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"];
|
||||
[self assert:0.5 equals:[_panel._opacitySlider floatValue]
|
||||
message:"Opacity slider should match the color's alpha component"];
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -78,19 +78,19 @@
|
||||
*/
|
||||
- (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 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"];
|
||||
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"];
|
||||
// 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"];
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -99,20 +99,20 @@
|
||||
*/
|
||||
- (void)testActivePickerNotifiedOfColorChanges
|
||||
{
|
||||
var mockPicker = [[_MockColorPicker alloc] init];
|
||||
_panel._activePicker = mockPicker;
|
||||
var mockPicker = [[_MockColorPicker alloc] init];
|
||||
_panel._activePicker = mockPicker;
|
||||
|
||||
var color1 = [CPColor redColor];
|
||||
var color2 = [CPColor blueColor];
|
||||
var color1 = [CPColor redColor];
|
||||
var color2 = [CPColor blueColor];
|
||||
|
||||
[_panel setColor:color1];
|
||||
[_panel setColor:color2];
|
||||
[_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]];
|
||||
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]];
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -121,21 +121,21 @@
|
||||
*/
|
||||
- (void)testPickerNotifiedOnActivation
|
||||
{
|
||||
var testColor = [CPColor colorWithRed:0.5 green:0.5 blue:1.0 alpha:1.0];
|
||||
[_panel setColor:testColor];
|
||||
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 mockPicker = [[_MockColorPicker alloc] init];
|
||||
_panel._colorPickers = [mockPicker];
|
||||
|
||||
var button = [[CPButton alloc] initWithFrame:CGRectMake(0, 0, 32, 32)];
|
||||
[button setTag:0];
|
||||
var button = [[CPButton alloc] initWithFrame:CGRectMake(0, 0, 32, 32)];
|
||||
[button setTag:0];
|
||||
|
||||
[_panel _setPicker:button];
|
||||
[_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]];
|
||||
var receivedColors = [mockPicker receivedColors];
|
||||
[self assert:1 equals:[receivedColors count]
|
||||
message:"Picker should receive setColor when activated"];
|
||||
[self assert:testColor same:receivedColors[0]];
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -143,11 +143,11 @@
|
||||
*/
|
||||
- (void)testOpacityMethodReturnsAlpha
|
||||
{
|
||||
var testColor = [CPColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.42];
|
||||
[_panel setColor:testColor];
|
||||
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"];
|
||||
[self assert:0.42 equals:[_panel opacity]
|
||||
message:"opacity method should return color's alpha component"];
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -156,18 +156,18 @@
|
||||
*/
|
||||
- (void)testSetColorWithEqualColorDoesNothing
|
||||
{
|
||||
var testColor = [CPColor colorWithRed:1.0 green:0.5 blue:0.0 alpha:0.7];
|
||||
[_panel setColor:testColor];
|
||||
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;
|
||||
var mockPicker = [[_MockColorPicker alloc] init];
|
||||
_panel._activePicker = mockPicker;
|
||||
|
||||
// Set same color again
|
||||
[_panel setColor:testColor];
|
||||
// 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"];
|
||||
var receivedColors = [mockPicker receivedColors];
|
||||
[self assert:0 equals:[receivedColors count]
|
||||
message:"Setting equal color should not trigger picker update"];
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -175,10 +175,10 @@
|
||||
*/
|
||||
- (void)testColorMethodReturnsCurrentColor
|
||||
{
|
||||
var testColor = [CPColor colorWithRed:0.2 green:0.4 blue:0.6 alpha:0.8];[_panel setColor:testColor];
|
||||
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"];
|
||||
[self assert:testColor same:[_panel color]
|
||||
message:"color method should return current color"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -17,22 +17,21 @@
|
||||
CPTreeController _treeController @accessors(property=treeController);
|
||||
CPArray _contentArray @accessors(property=contentArray);
|
||||
|
||||
CPArray observations;
|
||||
int aCount @accessors;
|
||||
CPMutableArray _observedKeyPaths;
|
||||
}
|
||||
|
||||
- (CPArray)makeTestTree
|
||||
{
|
||||
var engineering = [OrgNode nodeWithName:@"Engineering"],
|
||||
marketing = [OrgNode nodeWithName:@"Marketing"];
|
||||
marketing = [OrgNode nodeWithName:@"Marketing"];
|
||||
|
||||
var webTeam = [OrgNode nodeWithName:@"Web Team"],
|
||||
backendTeam = [OrgNode nodeWithName:@"Backend Team"];
|
||||
backendTeam = [OrgNode nodeWithName:@"Backend Team"];
|
||||
|
||||
[engineering setChildren:[CPMutableArray arrayWithObjects:webTeam, backendTeam]];
|
||||
|
||||
var dev1 = [OrgNode nodeWithName:@"Francisco"],
|
||||
dev2 = [OrgNode nodeWithName:@"Ross"];
|
||||
dev2 = [OrgNode nodeWithName:@"Ross"];
|
||||
|
||||
[webTeam setChildren:[CPMutableArray arrayWithObjects:dev1, dev2]];
|
||||
|
||||
@@ -43,12 +42,23 @@
|
||||
{
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
_observedKeyPaths = [CPMutableArray array];
|
||||
_contentArray = [self makeTestTree];
|
||||
_treeController = [[CPTreeController alloc] init];
|
||||
[_treeController setChildrenKeyPath:@"children"];
|
||||
[_treeController setContent:[_contentArray copy]];
|
||||
}
|
||||
|
||||
- (void)tearDown
|
||||
{
|
||||
_observedKeyPaths = nil;
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)aChange context:(id)aContext
|
||||
{
|
||||
[_observedKeyPaths addObject:aKeyPath];
|
||||
}
|
||||
|
||||
- (void)testInitWithContent
|
||||
{
|
||||
[self assert:[_contentArray count] equals:[[_treeController contentArray] count]];
|
||||
@@ -99,7 +109,7 @@
|
||||
[controller insertObject:newDev atArrangedObjectIndexPath:insertPath];
|
||||
|
||||
var engineering = [[controller contentArray] objectAtIndex:0],
|
||||
backendTeam = [[engineering children] objectAtIndex:1];
|
||||
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]];
|
||||
}
|
||||
@@ -132,7 +142,7 @@
|
||||
[controller removeObjectAtArrangedObjectIndexPath:path];
|
||||
|
||||
var engineering = [[controller contentArray] objectAtIndex:0],
|
||||
webTeam = [[engineering children] 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]];
|
||||
@@ -176,7 +186,8 @@
|
||||
// "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]];
|
||||
// Test behavior when AvoidsEmptySelection is NO
|
||||
[controller insertObject:[OrgNode nodeWithName:@"New Dept"] atArrangedObjectIndexPath:[CPIndexPath indexPathWithIndex:1]];
|
||||
[controller setAvoidsEmptySelection:NO];
|
||||
|
||||
// Reselect "Marketing" at index 0
|
||||
@@ -229,6 +240,130 @@
|
||||
[self assert:@"Marketing" equals:[[selectedObjects objectAtIndex:0] name]];
|
||||
}
|
||||
|
||||
/*
|
||||
* New Tests for Selection Bindings, KVO, and UI Action Status
|
||||
*/
|
||||
|
||||
- (void)testExposedBindings
|
||||
{
|
||||
var exposedBindings = [CPTreeController exposedBindings];
|
||||
|
||||
[self assertTrue:[exposedBindings containsObject:@"contentArray"] message:@"contentArray should be exposed"];
|
||||
[self assertTrue:[exposedBindings containsObject:@"sortDescriptors"] message:@"sortDescriptors should be exposed"];
|
||||
[self assertTrue:[exposedBindings containsObject:@"selectionIndexPaths"] message:@"selectionIndexPaths should be exposed"];
|
||||
[self assertTrue:[exposedBindings containsObject:@"selectionIndexPath"] message:@"selectionIndexPath should be exposed"];
|
||||
[self assertTrue:[exposedBindings containsObject:@"selectedObjects"] message:@"selectedObjects should be exposed"];
|
||||
}
|
||||
|
||||
- (void)testDetailBindingToSelectionProxyUpdatesOnSelectionChange
|
||||
{
|
||||
var controller = [self treeController],
|
||||
textField = [[CPTextField alloc] init];
|
||||
|
||||
[textField bind:@"value" toObject:controller withKeyPath:@"selection.name" options:nil];
|
||||
|
||||
// Select "Engineering" (index 0)
|
||||
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
|
||||
[self assert:@"Engineering" equals:[textField stringValue] message:@"Bound view should reflect root selection"];
|
||||
|
||||
// Select "Web Team" (index [0, 0])
|
||||
var nestedPath = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
|
||||
[controller setSelectionIndexPath:nestedPath];
|
||||
[self assert:@"Web Team" equals:[textField stringValue] message:@"Bound view should reflect nested selection"];
|
||||
|
||||
// Select "Marketing" (index 1)
|
||||
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:1]];
|
||||
[self assert:@"Marketing" equals:[textField stringValue] message:@"Bound view should reflect changed selection"];
|
||||
}
|
||||
|
||||
- (void)testDetailBindingUpdatesOnSetContent
|
||||
{
|
||||
var controller = [self treeController],
|
||||
textField = [[CPTextField alloc] init];
|
||||
|
||||
[textField bind:@"value" toObject:controller withKeyPath:@"selection.name" options:nil];
|
||||
|
||||
// Select "Engineering"
|
||||
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
|
||||
[self assert:@"Engineering" equals:[textField stringValue]];
|
||||
|
||||
// Replace content
|
||||
var newDept = [OrgNode nodeWithName:@"Design Dept"];
|
||||
[controller setContent:[CPMutableArray arrayWithObject:newDept]];
|
||||
|
||||
// Detail binding should update to the preserved selection or new root
|
||||
[self assert:@"Design Dept" equals:[textField stringValue] message:@"Detail binding must update when content changes"];
|
||||
}
|
||||
|
||||
- (void)testSelectedObjectsKVOTriggeredOnContentChange
|
||||
{
|
||||
var controller = [self treeController];
|
||||
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
|
||||
|
||||
[controller addObserver:self forKeyPath:@"selectedObjects" options:0 context:nil];
|
||||
|
||||
// Set new content
|
||||
var newDept = [OrgNode nodeWithName:@"Operations"];
|
||||
[controller setContent:[CPMutableArray arrayWithObject:newDept]];
|
||||
|
||||
[self assertTrue:[_observedKeyPaths containsObject:@"selectedObjects"] message:@"selectedObjects KVO notification must fire when content changes"];
|
||||
|
||||
[controller removeObserver:self forKeyPath:@"selectedObjects"];
|
||||
}
|
||||
|
||||
- (void)testCanInsertDependsOnEditable
|
||||
{
|
||||
var controller = [self treeController];
|
||||
|
||||
[controller addObserver:self forKeyPath:@"canInsert" options:0 context:nil];
|
||||
|
||||
[controller setEditable:YES];
|
||||
[self assertTrue:[controller canInsert]];
|
||||
|
||||
[_observedKeyPaths removeAllObjects];
|
||||
|
||||
[controller setEditable:NO];
|
||||
[self assertFalse:[controller canInsert] message:@"canInsert should be NO when editable is NO"];
|
||||
[self assertTrue:[_observedKeyPaths containsObject:@"canInsert"] message:@"canInsert KVO must fire when editable changes"];
|
||||
|
||||
[controller removeObserver:self forKeyPath:@"canInsert"];
|
||||
}
|
||||
|
||||
- (void)testCanAddChildAndCanInsertChildDependOnEditableAndSelection
|
||||
{
|
||||
var controller = [self treeController];
|
||||
[controller setEditable:YES];
|
||||
|
||||
[controller addObserver:self forKeyPath:@"canAddChild" options:0 context:nil];
|
||||
[controller addObserver:self forKeyPath:@"canInsertChild" options:0 context:nil];
|
||||
|
||||
// Empty selection -> canAddChild/canInsertChild should be NO
|
||||
[controller setSelectionIndexPaths:[CPArray array]];
|
||||
[self assertFalse:[controller canAddChild]];
|
||||
[self assertFalse:[controller canInsertChild]];
|
||||
|
||||
[_observedKeyPaths removeAllObjects];
|
||||
|
||||
// Select an item -> canAddChild/canInsertChild should become YES
|
||||
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
|
||||
[self assertTrue:[controller canAddChild]];
|
||||
[self assertTrue:[controller canInsertChild]];
|
||||
[self assertTrue:[_observedKeyPaths containsObject:@"canAddChild"] message:@"canAddChild KVO should fire on selection change"];
|
||||
[self assertTrue:[_observedKeyPaths containsObject:@"canInsertChild"] message:@"canInsertChild KVO should fire on selection change"];
|
||||
|
||||
[_observedKeyPaths removeAllObjects];
|
||||
|
||||
// Set editable to NO -> canAddChild/canInsertChild should become NO
|
||||
[controller setEditable:NO];
|
||||
[self assertFalse:[controller canAddChild]];
|
||||
[self assertFalse:[controller canInsertChild]];
|
||||
[self assertTrue:[_observedKeyPaths containsObject:@"canAddChild"] message:@"canAddChild KVO should fire when editable changes"];
|
||||
[self assertTrue:[_observedKeyPaths containsObject:@"canInsertChild"] message:@"canInsertChild KVO should fire when editable changes"];
|
||||
|
||||
[controller removeObserver:self forKeyPath:@"canAddChild"];
|
||||
[controller removeObserver:self forKeyPath:@"canInsertChild"];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
|
||||
function main(args, namedArgs)
|
||||
{
|
||||
CPLogRegister(CPLogDefault);
|
||||
CPLogRegister(CPLogDefault);
|
||||
CPApplicationMain(args, namedArgs);
|
||||
}
|
||||
@@ -62,9 +62,9 @@
|
||||
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask];
|
||||
[theWindow setAcceptsMouseMovedEvents:YES];
|
||||
var contentView = [theWindow contentView],
|
||||
x = 20,
|
||||
y = 20,
|
||||
yInc = 25,
|
||||
x = 20,
|
||||
y = 20,
|
||||
yInc = 25,
|
||||
imageCursorTester = [[CursorTester alloc] initWithText:@"Image cursor"
|
||||
origin:CGPointMake(x, y)
|
||||
cursor:[[CPCursor alloc] initWithImage:[[CPImage alloc] initWithContentsOfFile:[[CPBundle mainBundle] resourcePath] + @"spinner.gif"]
|
||||
@@ -83,7 +83,7 @@
|
||||
cursor:[CPCursor pointingHandCursor]],
|
||||
resizeDownCursorTester = [[CursorTester alloc] initWithText:@"Resize down cursor"
|
||||
origin:CGPointMake(x, y+=yInc)
|
||||
cursor:[CPCursor resizeDownCursor]],
|
||||
cursor:[CPCursor resizeDownCursor]],
|
||||
resizeUpCursorTester = [[CursorTester alloc] initWithText:@"Resize up cursor"
|
||||
origin:CGPointMake(x, y+=yInc)
|
||||
cursor:[CPCursor resizeUpCursor]],
|
||||
@@ -119,7 +119,7 @@
|
||||
cursor:[CPCursor closedHandCursor]],
|
||||
disappearingItemCursorTester = [[CursorTester alloc] initWithText:@"Disappearing item cursor"
|
||||
origin:CGPointMake(x, y+=yInc)
|
||||
cursor:[CPCursor disappearingItemCursor]],
|
||||
cursor:[CPCursor disappearingItemCursor]],
|
||||
verticalTextCursorTester = [[CursorTester alloc] initWithText:@"Vertical text cursor"
|
||||
origin:CGPointMake(x, y+=yInc)
|
||||
cursor:[CPCursor IBeamCursorForVerticalLayout]];
|
||||
|
||||
@@ -0,0 +1,657 @@
|
||||
// AppController.j
|
||||
// Manual test application for CPLanguageModelSession & CPSystemLanguageModel
|
||||
// With custom SpeechBubbleBox drawing and editable System Prompt controls.
|
||||
//
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
@import <Foundation/CPObject.j>
|
||||
@import <Foundation/CPLanguageModel.j>
|
||||
|
||||
// --- SUBCLASS: SPEECH BUBBLE VIEW ---
|
||||
@implementation SpeechBubbleBox : CPView
|
||||
{
|
||||
BOOL _isUser;
|
||||
CPColor _bubbleColor;
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)aFrame isUser:(BOOL)isUser fillColor:(CPColor)aColor
|
||||
{
|
||||
self = [super initWithFrame:aFrame];
|
||||
if (self) {
|
||||
_isUser = isUser;
|
||||
_bubbleColor = aColor;
|
||||
[self setAutoresizingMask:CPViewWidthSizable];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)aRect
|
||||
{
|
||||
var context = [[CPGraphicsContext currentContext] graphicsPort];
|
||||
var bounds = [self bounds];
|
||||
var w = CGRectGetWidth(bounds);
|
||||
var h = CGRectGetHeight(bounds) - 10.0; // 10px spacing for the bottom triangle pointer
|
||||
var r = 6.0; // Corner radius
|
||||
var th = 10.0; // Tail height
|
||||
|
||||
// --- 1. FILL PATH ---
|
||||
CGContextBeginPath(context);
|
||||
|
||||
// Top-left
|
||||
CGContextMoveToPoint(context, r, 0);
|
||||
|
||||
// Top edge
|
||||
CGContextAddLineToPoint(context, w - r, 0);
|
||||
CGContextAddArcToPoint(context, w, 0, w, r, r);
|
||||
|
||||
// Right edge
|
||||
CGContextAddLineToPoint(context, w, h - r);
|
||||
CGContextAddArcToPoint(context, w, h, w - r, h, r);
|
||||
|
||||
// Bottom edge with triangular tail (RHS vs LHS)
|
||||
if (_isUser) {
|
||||
CGContextAddLineToPoint(context, w - 21, h);
|
||||
CGContextAddLineToPoint(context, w - 21, h + th);
|
||||
CGContextAddLineToPoint(context, w - 35, h);
|
||||
CGContextAddLineToPoint(context, r, h);
|
||||
} else {
|
||||
CGContextAddLineToPoint(context, 35, h);
|
||||
CGContextAddLineToPoint(context, 21, h + th);
|
||||
CGContextAddLineToPoint(context, 21, h);
|
||||
CGContextAddLineToPoint(context, r, h);
|
||||
}
|
||||
|
||||
// Left edge
|
||||
CGContextAddArcToPoint(context, 0, h, 0, h - r, r);
|
||||
CGContextAddLineToPoint(context, 0, r);
|
||||
CGContextAddArcToPoint(context, 0, 0, r, 0, r);
|
||||
|
||||
CGContextClosePath(context);
|
||||
|
||||
// Fill path
|
||||
CGContextSetFillColor(context, _bubbleColor);
|
||||
CGContextFillPath(context);
|
||||
|
||||
// --- 2. OUTLINE PATH ---
|
||||
CGContextBeginPath(context);
|
||||
CGContextMoveToPoint(context, r, 0);
|
||||
CGContextAddLineToPoint(context, w - r, 0);
|
||||
CGContextAddArcToPoint(context, w, 0, w, r, r);
|
||||
CGContextAddLineToPoint(context, w, h - r);
|
||||
CGContextAddArcToPoint(context, w, h, w - r, h, r);
|
||||
|
||||
if (_isUser) {
|
||||
CGContextAddLineToPoint(context, w - 21, h);
|
||||
CGContextAddLineToPoint(context, w - 21, h + th);
|
||||
CGContextAddLineToPoint(context, w - 35, h);
|
||||
CGContextAddLineToPoint(context, r, h);
|
||||
} else {
|
||||
CGContextAddLineToPoint(context, 35, h);
|
||||
CGContextAddLineToPoint(context, 21, h + th);
|
||||
CGContextAddLineToPoint(context, 21, h);
|
||||
CGContextAddLineToPoint(context, r, h);
|
||||
}
|
||||
|
||||
CGContextAddArcToPoint(context, 0, h, 0, h - r, r);
|
||||
CGContextAddLineToPoint(context, 0, r);
|
||||
CGContextAddArcToPoint(context, 0, 0, r, 0, r);
|
||||
CGContextClosePath(context);
|
||||
|
||||
// Stroke outline
|
||||
CGContextSetStrokeColor(context, [CPColor colorWithWhite:0.8 alpha:1.0]);
|
||||
CGContextSetLineWidth(context, 1.0);
|
||||
CGContextStrokePath(context);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// --- MAIN CONTROLLER ---
|
||||
@implementation AppController : CPObject
|
||||
{
|
||||
CPWindow _mainWindow;
|
||||
CPScrollView _chatScrollView;
|
||||
CPView _chatDocumentView;
|
||||
CPTextField _chatInputField;
|
||||
CPButton _chatSendButton;
|
||||
CPButton _settingsButton;
|
||||
CPCheckBox _forceFallbackCheckbox;
|
||||
CPTextField _statusLabel;
|
||||
CPTextView _systemPromptTextView;
|
||||
|
||||
CPWindow _settingsWindow;
|
||||
CPPopUpButton _servicePopUp;
|
||||
CPTextField _endpointField;
|
||||
CPTextField _modelField;
|
||||
CPTextField _apiKeyField;
|
||||
|
||||
CPLanguageModelSession _session;
|
||||
float _currentChatY;
|
||||
id _currentStreamingTextView;
|
||||
CPProgressIndicator _currentSpinner;
|
||||
}
|
||||
|
||||
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
|
||||
{
|
||||
// Initialize default test settings in local user defaults
|
||||
var defaults = [CPUserDefaults standardUserDefaults];
|
||||
var defaultSettings = [CPDictionary dictionaryWithObjects:[
|
||||
@"ollama",
|
||||
@"http://localhost:11434/api/generate",
|
||||
@"gemma4:e4b",
|
||||
@""
|
||||
] forKeys:[
|
||||
@"LLMTestServiceType",
|
||||
@"LLMTestEndpoint",
|
||||
@"LLMTestModel",
|
||||
@"LLMTestAPIKey"
|
||||
]];
|
||||
[defaults registerDefaults:defaultSettings];
|
||||
|
||||
// Read values to apply fallback routing to CPLanguageModelSession
|
||||
var activeService = [defaults objectForKey:@"LLMTestServiceType"],
|
||||
endpoint = [defaults objectForKey:@"LLMTestEndpoint"],
|
||||
model = [defaults objectForKey:@"LLMTestModel"],
|
||||
apiKey = [defaults objectForKey:@"LLMTestAPIKey"];
|
||||
|
||||
[CPLanguageModelSession setFallbackServiceType:activeService
|
||||
endpoint:endpoint
|
||||
model:model
|
||||
apiKey:apiKey];
|
||||
|
||||
// Main manual test window setup
|
||||
_mainWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0, 0, 800, 650)
|
||||
styleMask:CPTitledWindowMask | CPClosableWindowMask | CPMiniaturizableWindowMask | CPResizableWindowMask];
|
||||
[_mainWindow setTitle:@"CPLanguageModel Manual Test Tool"];
|
||||
[_mainWindow center];
|
||||
|
||||
var contentView = [_mainWindow contentView];
|
||||
var bounds = [contentView bounds];
|
||||
|
||||
// --- TOP CONTROL PANEL (Height 135px) ---
|
||||
var topBar = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(bounds), 135)];
|
||||
[topBar setAutoresizingMask:CPViewWidthSizable | CPViewMaxYMargin];
|
||||
[topBar setBackgroundColor:[CPColor colorWithWhite:0.92 alpha:1.0]];
|
||||
[contentView addSubview:topBar];
|
||||
|
||||
_statusLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 12, 300, 20)];
|
||||
[_statusLabel setStringValue:@"Checking capability..."];
|
||||
[_statusLabel setFont:[CPFont systemFontOfSize:12.0]];
|
||||
[topBar addSubview:_statusLabel];
|
||||
|
||||
_forceFallbackCheckbox = [[CPCheckBox alloc] initWithFrame:CGRectMake(330, 12, 140, 20)];
|
||||
[_forceFallbackCheckbox setTitle:@"Force Fallback"];
|
||||
[_forceFallbackCheckbox setTarget:self];
|
||||
[_forceFallbackCheckbox setAction:@selector(toggleForceFallback:)];
|
||||
[topBar addSubview:_forceFallbackCheckbox];
|
||||
|
||||
_settingsButton = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth(bounds) - 210, 9, 90, 26)];
|
||||
[_settingsButton setTitle:@"Settings..."];
|
||||
[_settingsButton setAutoresizingMask:CPViewMinXMargin];
|
||||
[_settingsButton setTarget:self];
|
||||
[_settingsButton setAction:@selector(openSettingsSheet:)];
|
||||
[topBar addSubview:_settingsButton];
|
||||
|
||||
var clearButton = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth(bounds) - 110, 9, 95, 26)];
|
||||
[clearButton setTitle:@"Clear Chat"];
|
||||
[clearButton setAutoresizingMask:CPViewMinXMargin];
|
||||
[clearButton setTarget:self];
|
||||
[clearButton setAction:@selector(clearChatAction:)];
|
||||
[topBar addSubview:clearButton];
|
||||
|
||||
// System Prompt Header & Field [1]
|
||||
var promptLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 42, 300, 18)];
|
||||
[promptLabel setStringValue:@"System Instructions (Persona):"];
|
||||
[promptLabel setFont:[CPFont boldSystemFontOfSize:11.0]];
|
||||
[promptLabel setTextColor:[CPColor darkGrayColor]];
|
||||
[topBar addSubview:promptLabel];
|
||||
|
||||
var promptScroll = [[CPScrollView alloc] initWithFrame:CGRectMake(15, 62, CGRectGetWidth(bounds) - 30, 60)];
|
||||
[promptScroll setAutoresizingMask:CPViewWidthSizable];
|
||||
[promptScroll setAutohidesScrollers:YES];
|
||||
|
||||
_systemPromptTextView = [[CPTextView alloc] initWithFrame:[promptScroll bounds]];
|
||||
[_systemPromptTextView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
|
||||
[_systemPromptTextView setEditable:YES];
|
||||
[_systemPromptTextView setFont:[CPFont systemFontOfSize:11.0]];
|
||||
// Default instruction preset [1]
|
||||
[_systemPromptTextView setString:@"You are a helpful, concise testing assistant. You structure your explanations clearly using lists where appropriate."];
|
||||
|
||||
[promptScroll setDocumentView:_systemPromptTextView];
|
||||
[topBar addSubview:promptScroll];
|
||||
|
||||
// --- SCROLLING CHAT CONTAINER ---
|
||||
var scrollHeight = CGRectGetHeight(bounds) - 195;
|
||||
_chatScrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 135, CGRectGetWidth(bounds), scrollHeight)];
|
||||
[_chatScrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
|
||||
[_chatScrollView setAutohidesScrollers:YES];
|
||||
[_chatScrollView setHasHorizontalScroller:NO];
|
||||
[_chatScrollView setBackgroundColor:[CPColor whiteColor]];
|
||||
|
||||
_chatDocumentView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([_chatScrollView bounds]), scrollHeight)];
|
||||
[_chatDocumentView setAutoresizingMask:CPViewWidthSizable];
|
||||
[_chatScrollView setDocumentView:_chatDocumentView];
|
||||
[contentView addSubview:_chatScrollView];
|
||||
|
||||
// --- INPUT CONTAINER ---
|
||||
var bottomBarY = CGRectGetHeight(bounds) - 60;
|
||||
var bottomBar = [[CPView alloc] initWithFrame:CGRectMake(0, bottomBarY, CGRectGetWidth(bounds), 60)];
|
||||
[bottomBar setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin];
|
||||
[bottomBar setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]];
|
||||
[contentView addSubview:bottomBar];
|
||||
|
||||
_chatInputField = [[CPTextField alloc] initWithFrame:CGRectMake(15, 13, CGRectGetWidth(bounds) - 130, 34)];
|
||||
[_chatInputField setAutoresizingMask:CPViewWidthSizable];
|
||||
[_chatInputField setEditable:YES];
|
||||
[_chatInputField setBezeled:YES];
|
||||
[_chatInputField setPlaceholderString:@"Type a prompt and press Enter..."];
|
||||
[_chatInputField setTarget:self];
|
||||
[_chatInputField setAction:@selector(submitPromptAction:)];
|
||||
[bottomBar addSubview:_chatInputField];
|
||||
|
||||
_chatSendButton = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth(bounds) - 105, 13, 90, 34)];
|
||||
[_chatSendButton setTitle:@"Send"];
|
||||
[_chatSendButton setAutoresizingMask:CPViewMinXMargin];
|
||||
[_chatSendButton setTarget:self];
|
||||
[_chatSendButton setAction:@selector(submitPromptAction:)];
|
||||
[bottomBar addSubview:_chatSendButton];
|
||||
|
||||
[_mainWindow orderFront:self];
|
||||
|
||||
[self checkModelSupport];
|
||||
[self resetSession];
|
||||
}
|
||||
|
||||
- (void)checkModelSupport
|
||||
{
|
||||
var systemModel = [CPSystemLanguageModel defaultModel];
|
||||
[_statusLabel setStringValue:@"Checking capability..."];
|
||||
|
||||
[systemModel supportsLocaleWithCompletionHandler:function(supported) {
|
||||
|
||||
if (supported)
|
||||
[_statusLabel setStringValue:@"On-Device LLM: Supported"];
|
||||
else
|
||||
[_statusLabel setStringValue:@"On-Device LLM: Not available. Using fallback."];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)toggleForceFallback:(id)sender
|
||||
{
|
||||
var force = [sender state] === CPOnState;
|
||||
[CPLanguageModelSession setEndorsesFallback:force];
|
||||
[_statusLabel setStringValue:(force ? @"Fallback forced programmatically." : @"Prioritizing on-device model.")];
|
||||
}
|
||||
|
||||
- (void)clearChatAction:(id)sender
|
||||
{
|
||||
[self resetSession];
|
||||
}
|
||||
|
||||
- (void)resetSession
|
||||
{
|
||||
_currentChatY = 15;
|
||||
_currentStreamingTextView = nil;
|
||||
|
||||
if (_currentSpinner) {
|
||||
[_currentSpinner stopAnimation:self];
|
||||
[_currentSpinner removeFromSuperview];
|
||||
_currentSpinner = nil;
|
||||
}
|
||||
|
||||
if (_session) {
|
||||
[_session destroy];
|
||||
}
|
||||
|
||||
// Read the custom instructions from the text field upon starting a fresh session [1]
|
||||
var instructions = [_systemPromptTextView string] || @"";
|
||||
_session = [[CPLanguageModelSession alloc] initWithInstructions:instructions];
|
||||
|
||||
[[_chatDocumentView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
||||
[_chatDocumentView setFrameSize:CGSizeMake(CGRectGetWidth([_chatScrollView bounds]), CGRectGetHeight([_chatScrollView bounds]))];
|
||||
|
||||
[self appendMessage:@"Session initialized.\n\nEnter a query below to begin." isUser:NO];
|
||||
}
|
||||
|
||||
- (void)appendMessage:(CPString)text isUser:(BOOL)isUser
|
||||
{
|
||||
var docWidth = CGRectGetWidth([_chatScrollView bounds]) - 50;
|
||||
|
||||
var spinner = nil;
|
||||
var textX = 15;
|
||||
|
||||
// Erstelle den Spinner, falls die Generierung beginnt
|
||||
if (!isUser && [text isEqualToString:@"Generating response..."]) {
|
||||
spinner = [[CPProgressIndicator alloc] initWithFrame:CGRectMake(15, 12, 16, 16)];
|
||||
[spinner setStyle:CPProgressIndicatorSpinningStyle];
|
||||
[spinner setControlSize:CPSmallControlSize];
|
||||
[spinner setIndeterminate:YES];
|
||||
[spinner startAnimation:self];
|
||||
_currentSpinner = spinner;
|
||||
textX = 38; // Verschiebe den Text nach rechts, um Platz für den Spinner zu schaffen
|
||||
}
|
||||
|
||||
var textView = [[CPTextView alloc] initWithFrame:CGRectMake(textX, 10, docWidth - 15 - textX, 20)];
|
||||
[textView setEditable:YES];
|
||||
[textView setRichText:YES];
|
||||
[textView setSelectable:YES];
|
||||
[textView setBackgroundColor:[CPColor clearColor]];
|
||||
[textView setAutoresizingMask:CPViewWidthSizable];
|
||||
|
||||
[textView setVerticallyResizable:YES];
|
||||
[textView setHorizontallyResizable:NO];
|
||||
[[textView textContainer] setWidthTracksTextView:YES];
|
||||
|
||||
var textHeight = 20;
|
||||
var layoutManager = [textView layoutManager];
|
||||
var textContainer = [textView textContainer];
|
||||
|
||||
try {
|
||||
var parsedAttrStr = [CPMarkdownParser attributedStringFromMarkdown:text];
|
||||
[textView insertText:parsedAttrStr];
|
||||
|
||||
var usedRect = [layoutManager usedRectForTextContainer:textContainer];
|
||||
textHeight = CGRectGetHeight(usedRect);
|
||||
|
||||
if (textHeight < 20) {
|
||||
textHeight = 20;
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// Fallback bei Parsing-Fehler
|
||||
console.error("Markdown append failure: ", e);
|
||||
[textView setString:text];
|
||||
[textView sizeToFit];
|
||||
textHeight = CGRectGetHeight([textView frame]);
|
||||
}
|
||||
|
||||
var cardHeight = textHeight + 20;
|
||||
var bubbleHeight = cardHeight + 10;
|
||||
|
||||
var fillColor = isUser ? [CPColor colorWithRed:0.90 green:0.93 blue:1.0 alpha:1.0] : [CPColor colorWithWhite:0.96 alpha:1.0];
|
||||
var cardBox = [[SpeechBubbleBox alloc] initWithFrame:CGRectMake(15, _currentChatY, docWidth, bubbleHeight)
|
||||
isUser:isUser
|
||||
fillColor:fillColor];
|
||||
|
||||
if (spinner) {
|
||||
[cardBox addSubview:spinner];
|
||||
}
|
||||
|
||||
[cardBox addSubview:textView];
|
||||
[_chatDocumentView addSubview:cardBox];
|
||||
|
||||
if (!isUser) {
|
||||
_currentStreamingTextView = textView;
|
||||
}
|
||||
|
||||
_currentChatY += bubbleHeight + 15;
|
||||
[_chatDocumentView setFrameSize:CGSizeMake(CGRectGetWidth([_chatScrollView bounds]), _currentChatY + 20)];
|
||||
|
||||
var boundsHeight = CGRectGetHeight([_chatScrollView bounds]);
|
||||
|
||||
if (_currentChatY > boundsHeight) {
|
||||
[[_chatScrollView contentView] scrollToPoint:CGPointMake(0, _currentChatY - boundsHeight + 40)];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateMessage:(CPString)newText
|
||||
{
|
||||
if (!_currentStreamingTextView)
|
||||
return;
|
||||
|
||||
// Falls ein aktiver Spinner läuft, stoppe und entferne ihn, und richte das Textfeld wieder links aus
|
||||
if (_currentSpinner) {
|
||||
[_currentSpinner stopAnimation:self];
|
||||
[_currentSpinner removeFromSuperview];
|
||||
_currentSpinner = nil;
|
||||
|
||||
var docWidth = CGRectGetWidth([_chatScrollView bounds]) - 50;
|
||||
[_currentStreamingTextView setFrame:CGRectMake(15, 10, docWidth - 30, CGRectGetHeight([_currentStreamingTextView frame]))];
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Text über Standard-Zuweisung neu setzen
|
||||
var parsedAttrStr = [CPMarkdownParser attributedStringFromMarkdown:newText];
|
||||
|
||||
[_currentStreamingTextView setEditable:YES];
|
||||
[_currentStreamingTextView setString:@""];
|
||||
[_currentStreamingTextView insertText:parsedAttrStr];
|
||||
[_currentStreamingTextView setEditable:NO];
|
||||
|
||||
var layoutManager = [_currentStreamingTextView layoutManager];
|
||||
var textContainer = [_currentStreamingTextView textContainer];
|
||||
|
||||
var usedRect = [layoutManager usedRectForTextContainer:textContainer];
|
||||
var textHeight = CGRectGetHeight(usedRect);
|
||||
|
||||
if (textHeight < 20)
|
||||
textHeight = 20;
|
||||
|
||||
var cardHeight = textHeight + 20;
|
||||
var bubbleHeight = cardHeight + 10;
|
||||
|
||||
var container = [_currentStreamingTextView superview];
|
||||
|
||||
if (container)
|
||||
{
|
||||
var oldBubbleHeight = CGRectGetHeight([container frame]);
|
||||
|
||||
[container setFrameSize:CGSizeMake(CGRectGetWidth([container frame]), bubbleHeight)];
|
||||
[_currentStreamingTextView setFrameSize:CGSizeMake(CGRectGetWidth([_currentStreamingTextView frame]), textHeight)];
|
||||
[container setNeedsDisplay:YES];
|
||||
[_currentStreamingTextView setNeedsDisplay:YES];
|
||||
|
||||
var diffHeight = bubbleHeight - oldBubbleHeight;
|
||||
_currentChatY += diffHeight;
|
||||
}
|
||||
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.error("Markdown rendering failure: ", e);
|
||||
|
||||
[_currentStreamingTextView setEditable:YES];
|
||||
[_currentStreamingTextView setString:newText];
|
||||
[_currentStreamingTextView setEditable:NO];
|
||||
|
||||
[_currentStreamingTextView sizeToFit];
|
||||
var textHeight = CGRectGetHeight([_currentStreamingTextView frame]);
|
||||
|
||||
var container = [_currentStreamingTextView superview];
|
||||
|
||||
if (container) {
|
||||
var oldBubbleHeight = CGRectGetHeight([container frame]);
|
||||
var bubbleHeight = textHeight + 30;
|
||||
[container setFrameSize:CGSizeMake(CGRectGetWidth([container frame]), bubbleHeight)];
|
||||
[container setNeedsDisplay:YES];
|
||||
|
||||
var diffHeight = bubbleHeight - oldBubbleHeight;
|
||||
_currentChatY += diffHeight;
|
||||
}
|
||||
}
|
||||
|
||||
[_chatDocumentView setFrameSize:CGSizeMake(CGRectGetWidth([_chatScrollView bounds]), _currentChatY + 20)];
|
||||
|
||||
var boundsHeight = CGRectGetHeight([_chatScrollView bounds]);
|
||||
|
||||
if (_currentChatY > boundsHeight)
|
||||
[[_chatScrollView contentView] scrollToPoint:CGPointMake(0, _currentChatY - boundsHeight + 40)];
|
||||
}
|
||||
|
||||
- (void)submitPromptAction:(id)sender
|
||||
{
|
||||
var prompt = [_chatInputField stringValue];
|
||||
|
||||
if (!prompt || [prompt stringByTrimmingWhitespace] === @"")
|
||||
return;
|
||||
|
||||
[_chatInputField setStringValue:@""];
|
||||
[_chatInputField setEnabled:NO];
|
||||
[_chatSendButton setEnabled:NO];
|
||||
|
||||
[self appendMessage:prompt isUser:YES];
|
||||
[self appendMessage:@"Generating response..." isUser:NO];
|
||||
|
||||
[_session respondToPrompt:prompt
|
||||
options:nil
|
||||
completionHandler:function(finalText, error) {
|
||||
[_chatInputField setEnabled:YES];
|
||||
[_chatInputField becomeFirstResponder];
|
||||
[_chatSendButton setEnabled:YES];
|
||||
|
||||
if (error)
|
||||
[self updateMessage:@"Error: " + [error localizedDescription]];
|
||||
else
|
||||
[self updateMessage:finalText];
|
||||
}];
|
||||
}
|
||||
|
||||
// --- CONFIGURATION POPUP SHEETS ---
|
||||
|
||||
- (void)openSettingsSheet:(id)sender
|
||||
{
|
||||
if (!_settingsWindow)
|
||||
{
|
||||
_settingsWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0, 0, 420, 260)
|
||||
styleMask:CPTitledWindowMask | CPClosableWindowMask];
|
||||
[_settingsWindow setTitle:@"Fallback Settings"];
|
||||
|
||||
var sheetContentView = [_settingsWindow contentView];
|
||||
var sheetBounds = [sheetContentView bounds];
|
||||
|
||||
var serviceLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 25, 110, 20)];
|
||||
[serviceLabel setStringValue:@"Service Type:"];
|
||||
[serviceLabel setFont:[CPFont systemFontOfSize:12.0]];
|
||||
[serviceLabel setAlignment:CPRightTextAlignment];
|
||||
[sheetContentView addSubview:serviceLabel];
|
||||
|
||||
_servicePopUp = [[CPPopUpButton alloc] initWithFrame:CGRectMake(135, 22, 180, 26) pullsDown:NO];
|
||||
[_servicePopUp addItemWithTitle:@"Ollama (Local)"];
|
||||
[[_servicePopUp lastItem] setRepresentedObject:@"ollama"];
|
||||
[_servicePopUp addItemWithTitle:@"Groq API"];
|
||||
[[_servicePopUp lastItem] setRepresentedObject:@"groq"];
|
||||
[_servicePopUp addItemWithTitle:@"Google Gemini"];
|
||||
[[_servicePopUp lastItem] setRepresentedObject:@"gemini"];
|
||||
[_servicePopUp addItemWithTitle:@"OpenRouter"];
|
||||
[[_servicePopUp lastItem] setRepresentedObject:@"openrouter"];
|
||||
[_servicePopUp setTarget:self];
|
||||
[_servicePopUp setAction:@selector(serviceTypeDidChange:)];
|
||||
[sheetContentView addSubview:_servicePopUp];
|
||||
|
||||
var endpointLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 67, 110, 20)];
|
||||
[endpointLabel setStringValue:@"Endpoint URL:"];
|
||||
[endpointLabel setAlignment:CPRightTextAlignment];
|
||||
[sheetContentView addSubview:endpointLabel];
|
||||
|
||||
_endpointField = [[CPTextField alloc] initWithFrame:CGRectMake(135, 62, CGRectGetWidth(sheetBounds) - 155, 27)];
|
||||
[_endpointField setEditable:YES];
|
||||
[_endpointField setBezeled:YES];
|
||||
[sheetContentView addSubview:_endpointField];
|
||||
|
||||
var modelLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 107, 110, 20)];
|
||||
[modelLabel setStringValue:@"Model Name:"];
|
||||
[modelLabel setAlignment:CPRightTextAlignment];
|
||||
[sheetContentView addSubview:modelLabel];
|
||||
|
||||
_modelField = [[CPTextField alloc] initWithFrame:CGRectMake(135, 102, CGRectGetWidth(sheetBounds) - 155, 27)];
|
||||
[_modelField setEditable:YES];
|
||||
[_modelField setBezeled:YES];
|
||||
[sheetContentView addSubview:_modelField];
|
||||
|
||||
var apiKeyLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 147, 110, 20)];
|
||||
[apiKeyLabel setStringValue:@"API Key:"];
|
||||
[apiKeyLabel setAlignment:CPRightTextAlignment];
|
||||
[sheetContentView addSubview:apiKeyLabel];
|
||||
|
||||
_apiKeyField = [[CPTextField alloc] initWithFrame:CGRectMake(135, 142, CGRectGetWidth(sheetBounds) - 155, 27)];
|
||||
[_apiKeyField setEditable:YES];
|
||||
[_apiKeyField setBezeled:YES];
|
||||
[_apiKeyField setSecure:YES];
|
||||
[sheetContentView addSubview:_apiKeyField];
|
||||
|
||||
var btnY = CGRectGetHeight(sheetBounds) - 45;
|
||||
|
||||
var cancelBtn = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth(sheetBounds) - 205, btnY, 90, 26)];
|
||||
[cancelBtn setTitle:@"Cancel"];
|
||||
[cancelBtn setTarget:self];
|
||||
[cancelBtn setAction:@selector(closeSettingsSheet:)];
|
||||
[sheetContentView addSubview:cancelBtn];
|
||||
|
||||
var saveBtn = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth(sheetBounds) - 105, btnY, 90, 26)];
|
||||
[saveBtn setTitle:@"Save"];
|
||||
[saveBtn setTarget:self];
|
||||
[saveBtn setAction:@selector(saveSettings:)];
|
||||
[sheetContentView addSubview:saveBtn];
|
||||
}
|
||||
|
||||
var defaults = [CPUserDefaults standardUserDefaults];
|
||||
var activeService = [defaults objectForKey:@"LLMTestServiceType"] || @"ollama";
|
||||
|
||||
if (activeService === @"ollama") [_servicePopUp selectItemAtIndex:0];
|
||||
else if (activeService === @"groq") [_servicePopUp selectItemAtIndex:1];
|
||||
else if (activeService === @"gemini") [_servicePopUp selectItemAtIndex:2];
|
||||
else if (activeService === @"openrouter") [_servicePopUp selectItemAtIndex:3];
|
||||
|
||||
[_endpointField setStringValue:[defaults objectForKey:@"LLMTestEndpoint"] || @"http://localhost:11434/api/generate"];
|
||||
[_modelField setStringValue:[defaults objectForKey:@"LLMTestModel"] || @"gemma4:e4b"];
|
||||
[_apiKeyField setStringValue:[defaults objectForKey:@"LLMTestAPIKey"] || @""];
|
||||
|
||||
[self updateFieldsForService:activeService];
|
||||
|
||||
[CPApp beginSheet:_settingsWindow
|
||||
modalForWindow:_mainWindow
|
||||
modalDelegate:self
|
||||
didEndSelector:nil
|
||||
contextInfo:nil];
|
||||
}
|
||||
|
||||
- (void)updateFieldsForService:(CPString)serviceType
|
||||
{
|
||||
if (serviceType === @"ollama") {
|
||||
[_endpointField setEnabled:YES];
|
||||
[_apiKeyField setEnabled:NO];
|
||||
[_apiKeyField setPlaceholderString:@"Not required"];
|
||||
} else {
|
||||
[_endpointField setEnabled:NO];
|
||||
[_endpointField setPlaceholderString:@"Default platform endpoint used"];
|
||||
[_apiKeyField setEnabled:YES];
|
||||
[_apiKeyField setPlaceholderString:@"API Token values"];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)serviceTypeDidChange:(id)sender
|
||||
{
|
||||
var newService = [[_servicePopUp selectedItem] representedObject];
|
||||
[self updateFieldsForService:newService];
|
||||
}
|
||||
|
||||
- (void)closeSettingsSheet:(id)sender
|
||||
{
|
||||
[CPApp endSheet:_settingsWindow];
|
||||
[_settingsWindow orderOut:self];
|
||||
}
|
||||
|
||||
- (void)saveSettings:(id)sender
|
||||
{
|
||||
var defaults = [CPUserDefaults standardUserDefaults];
|
||||
var activeService = [[_servicePopUp selectedItem] representedObject] || @"ollama";
|
||||
var endpoint = [_endpointField stringValue];
|
||||
var model = [_modelField stringValue];
|
||||
var apiKey = [_apiKeyField stringValue];
|
||||
|
||||
[defaults setObject:activeService forKey:@"LLMTestServiceType"];
|
||||
[defaults setObject:endpoint forKey:@"LLMTestEndpoint"];
|
||||
[defaults setObject:model forKey:@"LLMTestModel"];
|
||||
[defaults setObject:apiKey forKey:@"LLMTestAPIKey"];
|
||||
|
||||
[CPLanguageModelSession setFallbackServiceType:activeService
|
||||
endpoint:endpoint
|
||||
model:model
|
||||
apiKey:apiKey];
|
||||
|
||||
[self closeSettingsSheet:sender];
|
||||
[_statusLabel setStringValue:@"Fallback settings updated."];
|
||||
}
|
||||
|
||||
@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>CPLanguageModelChatbot</string>
|
||||
<key>CPPrincipalClass</key>
|
||||
<string>CPApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Jakefile
|
||||
* CPLevelIndicator
|
||||
*
|
||||
* Created by Alexander Ljungberg on May 28, 2011.
|
||||
* Copyright 2011, WireLoad 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 ("CPLevelIndicator", function(task)
|
||||
{
|
||||
task.setBuildIntermediatesPath(FILE.join("Build", "CPLevelIndicator.build", configuration));
|
||||
task.setBuildPath(FILE.join("Build", configuration));
|
||||
|
||||
task.setProductName("CPLevelIndicator");
|
||||
task.setIdentifier("com.yourcompany.CPLevelIndicator");
|
||||
task.setVersion("1.0");
|
||||
task.setAuthor("WireLoad");
|
||||
task.setEmail("feedback @nospam@ yourcompany.com");
|
||||
task.setSummary("CPLevelIndicator");
|
||||
task.setSources((new FileList("**/*.j")).exclude(FILE.join("Build", "**")));
|
||||
task.setResources(new FileList("Resources/**"));
|
||||
task.setIndexFilePath("index.html");
|
||||
task.setInfoPlistPath("Info.plist");
|
||||
task.setNib2CibFlags("-R Resources/");
|
||||
|
||||
if (configuration === "Debug")
|
||||
task.setCompilerFlags("-DDEBUG -g");
|
||||
else
|
||||
task.setCompilerFlags("-O");
|
||||
});
|
||||
|
||||
task ("default", ["CPLevelIndicator"], 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", "CPLevelIndicator", "index.html")]);
|
||||
});
|
||||
|
||||
task ("run-release", ["release"], function()
|
||||
{
|
||||
OS.system(["open", FILE.join("Build", "Release", "CPLevelIndicator", "index.html")]);
|
||||
});
|
||||
|
||||
task ("deploy", ["release"], function()
|
||||
{
|
||||
FILE.mkdirs(FILE.join("Build", "Deployment", "CPLevelIndicator"));
|
||||
OS.system(["press", "-f", FILE.join("Build", "Release", "CPLevelIndicator"), FILE.join("Build", "Deployment", "CPLevelIndicator")]);
|
||||
printResults("Deployment")
|
||||
});
|
||||
|
||||
task ("desktop", ["release"], function()
|
||||
{
|
||||
FILE.mkdirs(FILE.join("Build", "Desktop", "CPLevelIndicator"));
|
||||
require("cappuccino/nativehost").buildNativeHost(FILE.join("Build", "Release", "CPLevelIndicator"), FILE.join("Build", "Desktop", "CPLevelIndicator", "CPLevelIndicator.app"));
|
||||
printResults("Desktop")
|
||||
});
|
||||
|
||||
task ("run-desktop", ["desktop"], function()
|
||||
{
|
||||
OS.system([FILE.join("Build", "Desktop", "CPLevelIndicator", "CPLevelIndicator.app", "Contents", "MacOS", "NativeHost"), "-i"]);
|
||||
});
|
||||
|
||||
function printResults(configuration)
|
||||
{
|
||||
print("----------------------------");
|
||||
print(configuration+" app built at path: "+FILE.join("Build", configuration, "CPLevelIndicator"));
|
||||
print("----------------------------");
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html
|
||||
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<!--
|
||||
index-debug.html
|
||||
CPLevelIndicator
|
||||
|
||||
Created by Alexander Ljungberg on May 28, 2011.
|
||||
Copyright 2011, WireLoad All rights reserved.
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1" />
|
||||
|
||||
<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>CPLevelIndicator</title>
|
||||
|
||||
<script type="text/javascript">
|
||||
OBJJ_MAIN_FILE = "main.j";
|
||||
OBJJ_INCLUDE_PATHS = ["Frameworks/Debug", "Frameworks", "SomethingElse"];
|
||||
</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:
|
||||
|
||||
// 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);
|
||||
</script>
|
||||
|
||||
<style type="text/css">
|
||||
body{margin:0; padding:0;}
|
||||
#container {position: absolute; top:50%; left:50%;}
|
||||
#content {width:800px; text-align:center; margin-left: -400px; height:50px; margin-top:-25px; line-height: 50px;}
|
||||
#content {font-family: "Helvetica", "Arial", sans-serif; font-size: 18px; color: black; text-shadow: 0px 1px 0px white; }
|
||||
#loadgraphic {margin-right: 0.2em; margin-bottom:-2px;}
|
||||
</style>
|
||||
|
||||
<!--[if lt IE 7]>
|
||||
<STYLE type="text/css">
|
||||
#container { position: relative; top: 50%; }
|
||||
#content { position: relative;}
|
||||
</STYLE>
|
||||
<![endif]-->
|
||||
|
||||
</head>
|
||||
|
||||
<body style="">
|
||||
<div id="cappuccino-body">
|
||||
<div id="loadingcontainer" style="background-color: #eeeeee; overflow:hidden; width:100%; height:100%; position: absolute; top: 0; left: 0;">
|
||||
<script type="text/javascript">
|
||||
document.write("<div id='container'><p id='content'>" +
|
||||
"<img id='loadgraphic' width='16' height='16' src='Resources/spinner.gif' /> " +
|
||||
"Loading CPLevelIndicator...</p></div>");
|
||||
</script>
|
||||
|
||||
<noscript>
|
||||
<div id="container">
|
||||
<div style="width: 440px; padding: 10px 25px 20px 25px; font-family: sans-serif; background-color: #ffffff; position: relative; left: -245px; top: -120px; text-align: center; -moz-border-radius: 20px; -webkit-border-radius: 20px; color: #555555">
|
||||
<p style="line-height: 1.4em;">JavaScript is required for this site to work correctly but is either disabled or not supported by your browser.</p>
|
||||
<p style="font-size:120%; padding:10px;"><a href="http://cappuccino-project.org/noscript">Show me how to enable JavaScript</a></p>
|
||||
<p style="font-size:80%;">You may want to upgrade to a newer browser while you're at it:</p>
|
||||
<ul style="margin:0;padding:0; text-align: center; font-size:80%;" >
|
||||
<li style="display: inline;"><a href="http://www.apple.com/safari/download/">Safari</a></li>
|
||||
<li style="display: inline;"><a href="http://www.mozilla.com/en-US/firefox/">Firefox</a></li>
|
||||
<li style="display: inline;"><a href="http://www.google.com/chrome/">Chrome</a></li>
|
||||
<li style="display: inline;"><a href="http://www.opera.com/download/">Opera</a></li>
|
||||
<li style="display: inline;"><a href="http://www.microsoft.com/windows/downloads/ie/getitnow.mspx">Internet Explorer</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
</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
|
||||
* CPLevelIndicator
|
||||
*
|
||||
* Created by Alexander Ljungberg on May 28, 2011.
|
||||
* Copyright 2011, WireLoad All rights reserved.
|
||||
*/
|
||||
|
||||
@import <Foundation/Foundation.j>
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
@import "AppController.j"
|
||||
|
||||
|
||||
function main(args, namedArgs)
|
||||
{
|
||||
CPApplicationMain(args, namedArgs);
|
||||
}
|
||||
@@ -106,7 +106,7 @@ var CPRuleEditorCustomControlClass = @"CPRuleEditorCustomControlClass";
|
||||
{
|
||||
var compound_type = [criterion objectForKey:CPRuleEditorPredicateCompoundType];
|
||||
if (compound_type != nil)
|
||||
[predicatePartsForCriterion setObject:compound_type forKey:CPRuleEditorPredicateCompoundType];
|
||||
[predicatePartsForCriterion setObject:compound_type forKey:CPRuleEditorPredicateCompoundType];
|
||||
return predicatePartsForCriterion;
|
||||
}
|
||||
|
||||
@@ -131,8 +131,8 @@ var CPRuleEditorCustomControlClass = @"CPRuleEditorCustomControlClass";
|
||||
predicatePart = [CPExpression expressionForConstantValue:transformedValue];
|
||||
}
|
||||
else if ([key isEqualToString:CPRuleEditorPredicateOperatorType] ||
|
||||
[key isEqualToString:CPRuleEditorPredicateCustomSelector] ||
|
||||
[key isEqualToString:CPRuleEditorPredicateOptions])
|
||||
[key isEqualToString:CPRuleEditorPredicateCustomSelector] ||
|
||||
[key isEqualToString:CPRuleEditorPredicateOptions])
|
||||
{
|
||||
var value = [criterion objectForKey:key];
|
||||
if (value != nil) predicatePart = value;
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
|
||||
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
|
||||
{
|
||||
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
|
||||
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0, 0, 850, 400) styleMask:CPTitledWindowMask | CPClosableWindowMask | CPMiniaturizableWindowMask],
|
||||
contentView = [theWindow contentView];
|
||||
|
||||
[theWindow setTitle:@"CPTreeController Bindings Test"];
|
||||
|
||||
// 1. Create the Data Model
|
||||
var root1 = [[Node alloc] initWithName:@"Root 1" children:[]],
|
||||
child1 = [[Node alloc] initWithName:@"Child 1.1" children:[]],
|
||||
@@ -32,36 +34,39 @@
|
||||
|
||||
// 2. Setup the Tree Controller
|
||||
treeController = [[CPTreeController alloc] init];
|
||||
[treeController setObjectClass:[Node class]];
|
||||
[treeController setChildrenKeyPath:@"children"];
|
||||
[treeController setContent:contentArray];
|
||||
|
||||
// 3. Setup the Outline View
|
||||
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(20, 20, 250, 300)];
|
||||
[scrollView setAutohidesScrollers:YES];
|
||||
// 3. Setup Outline View 1 (Left)
|
||||
var scrollView1 = [[CPScrollView alloc] initWithFrame:CGRectMake(20, 20, 240, 300)];
|
||||
[scrollView1 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];
|
||||
var outlineView1 = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 240, 300)],
|
||||
column1 = [[CPTableColumn alloc] initWithIdentifier:@"name"];
|
||||
[[column1 headerView] setStringValue:@"Outline View 1"];
|
||||
[column1 setWidth:230];
|
||||
[column1 setEditable:YES];
|
||||
|
||||
// 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];
|
||||
[outlineView1 addTableColumn:column1];
|
||||
[outlineView1 setOutlineTableColumn:column1];
|
||||
[outlineView1 setAllowsMultipleSelection:YES];
|
||||
[scrollView1 setDocumentView:outlineView1];
|
||||
[contentView addSubview:scrollView1];
|
||||
|
||||
var scrollView2 = [[CPScrollView alloc] initWithFrame:CGRectMake(300, 20, 250, 300)];
|
||||
// Bind Outline View 1 (Content & Selection)
|
||||
[outlineView1 bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
|
||||
[outlineView1 bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
|
||||
|
||||
// 4. Setup Outline View 2 (Middle - Synced)
|
||||
var scrollView2 = [[CPScrollView alloc] initWithFrame:CGRectMake(280, 20, 240, 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
|
||||
|
||||
var outlineView2 = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 240, 300)],
|
||||
column2 = [[CPTableColumn alloc] initWithIdentifier:@"name"];
|
||||
[[column2 headerView] setStringValue:@"Outline View 2 (Synced)"];
|
||||
[column2 setWidth:230];
|
||||
[column2 setEditable:YES];
|
||||
|
||||
[outlineView2 addTableColumn:column2];
|
||||
[outlineView2 setOutlineTableColumn:column2];
|
||||
@@ -69,20 +74,68 @@
|
||||
[scrollView2 setDocumentView:outlineView2];
|
||||
[contentView addSubview:scrollView2];
|
||||
|
||||
// 4. Establish Bindings for the Outline View
|
||||
// Bind Outline View 2 (Content & Selection)
|
||||
[outlineView2 bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
|
||||
[outlineView2 bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
|
||||
|
||||
// 5. Setup Inspector / Detail Controls (Right Panel)
|
||||
var panelX = 540;
|
||||
|
||||
// Label: Detail Inspector
|
||||
var detailLabel = [CPTextField labelWithTitle:@"Detail Binding (selection.name):"];
|
||||
[detailLabel setFrameOrigin:CGPointMake(panelX, 20)];
|
||||
[contentView addSubview:detailLabel];
|
||||
|
||||
// 2-Way Detail Binding TextField
|
||||
var nameField = [CPTextField textFieldWithStringValue:@"" placeholder:@"No Selection" width:260];
|
||||
[nameField setFrameOrigin:CGPointMake(panelX, 45)];
|
||||
[nameField setEditable:YES];
|
||||
[nameField bind:@"value" toObject:treeController withKeyPath:@"selection.name" options:nil];
|
||||
[contentView addSubview:nameField];
|
||||
|
||||
// Programmatic Selection Button
|
||||
var selectBtn = [CPButton buttonWithTitle:@"Select 'Child 1.2' [0, 1]"];
|
||||
[selectBtn setFrame:CGRectMake(panelX, 90, 260, 28)];
|
||||
[selectBtn setTarget:self];
|
||||
[selectBtn setAction:@selector(selectSpecificNode:)];
|
||||
[contentView addSubview:selectBtn];
|
||||
|
||||
// Add Child Button (Action + Enabled Binding)
|
||||
var addChildBtn = [CPButton buttonWithTitle:@"Add Child to Selection"];
|
||||
[addChildBtn setFrame:CGRectMake(panelX, 125, 260, 28)];
|
||||
[addChildBtn setTarget:treeController];
|
||||
[addChildBtn setAction:@selector(addChild:)];
|
||||
[addChildBtn bind:@"enabled" toObject:treeController withKeyPath:@"canAddChild" options:nil];
|
||||
[contentView addSubview:addChildBtn];
|
||||
|
||||
// Remove Selected Button
|
||||
var removeBtn = [CPButton buttonWithTitle:@"Remove Selected"];
|
||||
[removeBtn setFrame:CGRectMake(panelX, 160, 260, 28)];
|
||||
[removeBtn setTarget:treeController];
|
||||
[removeBtn setAction:@selector(remove:)];
|
||||
[contentView addSubview:removeBtn];
|
||||
|
||||
// Selection Log Label
|
||||
var logHeaderLabel = [CPTextField labelWithTitle:@"KVO selectionIndexPaths Log:"];
|
||||
[logHeaderLabel setFrameOrigin:CGPointMake(panelX, 210)];
|
||||
[contentView addSubview:logHeaderLabel];
|
||||
|
||||
logField = [CPTextField labelWithTitle:@"Selected: Root 1"];
|
||||
[logField setFrame:CGRectMake(panelX, 235, 260, 60)];
|
||||
[logField setLineBreakMode:CPLineBreakByWordWrapping];
|
||||
[contentView addSubview:logField];
|
||||
|
||||
// 6. Register Observer for Live Selection Log
|
||||
[treeController addObserver:self forKeyPath:@"selectionIndexPaths" options:CPKeyValueObservingOptionNew context:nil];
|
||||
|
||||
[theWindow center];
|
||||
[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]];
|
||||
var path = [CPIndexPath indexPathWithIndexes:[0, 1]];
|
||||
[treeController setSelectionIndexPath:path];
|
||||
}
|
||||
|
||||
@@ -94,12 +147,14 @@
|
||||
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:@", "]];
|
||||
for (var i = 0; i < [selectedObjects count]; i++)
|
||||
[names addObject:[selectedObjects[i] name]];
|
||||
|
||||
[logField setStringValue:[CPString stringWithFormat:@"Selected: %@", [names componentsJoinedByString:@", "]]];
|
||||
}
|
||||
else
|
||||
{
|
||||
[logField setStringValue:@"Nothing selected"];
|
||||
[logField setStringValue:@"Selected: (Nothing selected)"];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,16 +176,21 @@
|
||||
if (self)
|
||||
{
|
||||
name = aName;
|
||||
children = someChildren;
|
||||
children = someChildren || [];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// Explicit accessors to ensure Key-Value Observing (KVO) works flawlessly.
|
||||
- (id)init
|
||||
{
|
||||
return [self initWithName:@"New Node" children:[]];
|
||||
}
|
||||
|
||||
- (void)setName:(CPString)aName
|
||||
{
|
||||
[self willChangeValueForKey:@"name"];
|
||||
name = aName;[self didChangeValueForKey:@"name"];
|
||||
name = aName;
|
||||
[self didChangeValueForKey:@"name"];
|
||||
}
|
||||
|
||||
- (CPString)name
|
||||
@@ -150,4 +210,9 @@
|
||||
return children;
|
||||
}
|
||||
|
||||
- (CPString)description
|
||||
{
|
||||
return [CPString stringWithFormat:@"<Node %@>", [self name]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -13,10 +13,10 @@
|
||||
*/
|
||||
@implementation AppController : CPObject
|
||||
{
|
||||
CPButton unhideHtmlButton;
|
||||
CPButton unhideUrlButton;
|
||||
CPWebView webview;
|
||||
CPWebView webview2;
|
||||
CPButton unhideHtmlButton;
|
||||
CPButton unhideUrlButton;
|
||||
CPWebView webview;
|
||||
CPWebView webview2;
|
||||
}
|
||||
|
||||
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
|
||||
@@ -24,31 +24,31 @@
|
||||
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
|
||||
contentView = [theWindow contentView];
|
||||
|
||||
unhideHtmlButton = [[CPButton alloc] initWithFrame:CGRectMakeZero()];
|
||||
[unhideHtmlButton setTitle:"Unhide (loadHTMLString)"];
|
||||
[unhideHtmlButton sizeToFit];
|
||||
[unhideHtmlButton setTarget:self];
|
||||
[unhideHtmlButton setAction:@selector(unhideHtmlButtonClicked:)];
|
||||
[contentView addSubview:unhideHtmlButton];
|
||||
unhideHtmlButton = [[CPButton alloc] initWithFrame:CGRectMakeZero()];
|
||||
[unhideHtmlButton setTitle:"Unhide (loadHTMLString)"];
|
||||
[unhideHtmlButton sizeToFit];
|
||||
[unhideHtmlButton setTarget:self];
|
||||
[unhideHtmlButton setAction:@selector(unhideHtmlButtonClicked:)];
|
||||
[contentView addSubview:unhideHtmlButton];
|
||||
|
||||
unhideUrlButton = [[CPButton alloc] initWithFrame:CGRectMake([contentView frameSize].width / 2 + 10, 0, 0, 0)];
|
||||
[unhideUrlButton setTitle:"Unhide (URL)"];
|
||||
[unhideUrlButton sizeToFit];
|
||||
[unhideUrlButton setTarget:self];
|
||||
[unhideUrlButton setAction:@selector(unhideUrlButtonClicked:)];
|
||||
[contentView addSubview:unhideUrlButton];
|
||||
unhideUrlButton = [[CPButton alloc] initWithFrame:CGRectMake([contentView frameSize].width / 2 + 10, 0, 0, 0)];
|
||||
[unhideUrlButton setTitle:"Unhide (URL)"];
|
||||
[unhideUrlButton sizeToFit];
|
||||
[unhideUrlButton setTarget:self];
|
||||
[unhideUrlButton setAction:@selector(unhideUrlButtonClicked:)];
|
||||
[contentView addSubview:unhideUrlButton];
|
||||
|
||||
webview = [[CPWebView alloc] initWithFrame:CGRectMake(0, 40, [contentView frameSize].width / 2 - 10, [contentView frameSize].height - 40)];
|
||||
[webview setAutoresizingMask:CPViewWidthSizable|CPViewHeightSizable];
|
||||
[webview setHidden:YES];
|
||||
[webview loadHTMLString:"<html><body><font size='8'> Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</font></body></html>"];
|
||||
[contentView addSubview:webview];
|
||||
[webview setAutoresizingMask:CPViewWidthSizable|CPViewHeightSizable];
|
||||
[webview setHidden:YES];
|
||||
[webview loadHTMLString:"<html><body><font size='8'> Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</font></body></html>"];
|
||||
[contentView addSubview:webview];
|
||||
|
||||
webview2 = [[CPWebView alloc] initWithFrame:CGRectMake([contentView frameSize].width / 2 + 10, 40, [contentView frameSize].width / 2 - 10, [contentView frameSize].height - 40)];
|
||||
[webview2 setAutoresizingMask:CPViewWidthSizable|CPViewHeightSizable];
|
||||
[webview2 setHidden:YES];
|
||||
[webview2 setMainFrameURL:"http://www.cappuccino-project.org"];
|
||||
[contentView addSubview:webview2];
|
||||
[webview2 setAutoresizingMask:CPViewWidthSizable|CPViewHeightSizable];
|
||||
[webview2 setHidden:YES];
|
||||
[webview2 setMainFrameURL:"http://www.cappuccino-project.org"];
|
||||
[contentView addSubview:webview2];
|
||||
|
||||
[theWindow orderFront:self];
|
||||
|
||||
@@ -58,12 +58,12 @@
|
||||
|
||||
- (void)unhideHtmlButtonClicked:(id)sender
|
||||
{
|
||||
[webview setHidden:NO];
|
||||
[webview setHidden:NO];
|
||||
}
|
||||
|
||||
- (void)unhideUrlButtonClicked:(id)sender
|
||||
{
|
||||
[webview2 setHidden:NO];
|
||||
[webview2 setHidden:NO];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -15,14 +15,14 @@ END_EXPECTED
|
||||
|
||||
@implementation MySuperclass : CPObject
|
||||
{
|
||||
int myIvar;
|
||||
int myIvar;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MySubclass : MySuperclass
|
||||
{
|
||||
int myIvar;
|
||||
int myIvar;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
- (IBAction)bringWindow2Front:(id)sender
|
||||
{
|
||||
[window2 orderFront:self];
|
||||
[window2 orderFront:self];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -12,9 +12,9 @@
|
||||
@implementation AppController : CPObject
|
||||
{
|
||||
@outlet CPWindow theWindow;
|
||||
@outlet CPButton clearButton;
|
||||
@outlet CPButton escButton;
|
||||
@outlet CPTextField wasPressedLabel;
|
||||
@outlet CPButton clearButton;
|
||||
@outlet CPButton escButton;
|
||||
@outlet CPTextField wasPressedLabel;
|
||||
}
|
||||
|
||||
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
|
||||
@@ -33,11 +33,11 @@
|
||||
}
|
||||
|
||||
- (@action)clearButtonPressed:(id)sender {
|
||||
[wasPressedLabel setStringValue:@"No"];
|
||||
[wasPressedLabel setStringValue:@"No"];
|
||||
}
|
||||
|
||||
- (@action)escButtonPressed:(id)sender {
|
||||
[wasPressedLabel setStringValue:@"Yes"];
|
||||
[wasPressedLabel setStringValue:@"Yes"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
- (IBAction)closeSheet:(idf)sender
|
||||
{
|
||||
[CPApp endSheet:sheetWindow];
|
||||
[CPApp endSheet:sheetWindow];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -63,8 +63,8 @@ var rootItems = [CPArray arrayWithObject:[CPDictionary dictionaryWithObjectsAndK
|
||||
|
||||
- (void)_saveSmartFolders
|
||||
{
|
||||
var defaults = [CPUserDefaults standardUserDefaults];
|
||||
[defaults setObject:smartFolders forKey:@"SmartFolders"];
|
||||
var defaults = [CPUserDefaults standardUserDefaults];
|
||||
[defaults setObject:smartFolders forKey:@"SmartFolders"];
|
||||
}
|
||||
|
||||
- (void)awakeFromCib
|
||||
@@ -204,7 +204,7 @@ var rootItems = [CPArray arrayWithObject:[CPDictionary dictionaryWithObjectsAndK
|
||||
- (IBAction)closeSheet:(id)sender
|
||||
{
|
||||
if ([sender tag] == CPOKButton)
|
||||
[predicateEditor reloadPredicate];
|
||||
[predicateEditor reloadPredicate];
|
||||
|
||||
[CPApp endSheet:predicateSheet returnCode:[sender tag]];
|
||||
}
|
||||
@@ -218,13 +218,13 @@ var rootItems = [CPArray arrayWithObject:[CPDictionary dictionaryWithObjectsAndK
|
||||
var predicate = [predicateEditor objectValue];
|
||||
[self setFilterPredicate:predicate];
|
||||
|
||||
[folder setObject:predicate forKey:@"predicate"];
|
||||
[folder setObject:name forKey:@"name"];
|
||||
[folder setObject:predicate forKey:@"predicate"];
|
||||
[folder setObject:name forKey:@"name"];
|
||||
[folder setObject:[[tableController arrangedObjects] count] forKey:@"count"];
|
||||
[smartOutlineView reloadData];
|
||||
[smartOutlineView reloadData];
|
||||
|
||||
[self saveSmartFolders];
|
||||
[aSheet orderOut:nil];
|
||||
[self saveSmartFolders];
|
||||
[aSheet orderOut:nil];
|
||||
}
|
||||
}
|
||||
// convenience method
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
- (void)xxxx:(id)argument
|
||||
{
|
||||
[super xxxx:argument];
|
||||
[super xxxx:argument];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -9,7 +9,7 @@
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<meta name="google" content="notranslate" />
|
||||
<!--[if lte IE 8]>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
|
||||
<![endif]-->
|
||||
|
||||
Reference in new issue
Block a user