diff --git a/.gitignore b/.gitignore
index fdcc77f81..36beffcd1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,4 +25,4 @@ node_modules
/dist/objective-j/lib
/dist/cappuccino/package.json
/dist/cappuccino/lib
-
+/dist/cappuccino/bin
diff --git a/AppKit/AppKit.j b/AppKit/AppKit.j
index 99f532131..edf1dd794 100644
--- a/AppKit/AppKit.j
+++ b/AppKit/AppKit.j
@@ -115,3 +115,5 @@
@import "CPWindow.j"
@import "CPWindowController.j"
@import "CPWorkspace.j"
+@import "CPFontPanel.j"
+@import "CPTreeController.j"
diff --git a/AppKit/CPDatePicker/_CPDatePickerCalendar.j b/AppKit/CPDatePicker/_CPDatePickerCalendar.j
index af43569b3..101185add 100644
--- a/AppKit/CPDatePicker/_CPDatePickerCalendar.j
+++ b/AppKit/CPDatePicker/_CPDatePickerCalendar.j
@@ -251,7 +251,15 @@
- (void)_displayNextMonth
{
- [self setDateValue:[_monthView nextMonth]];
+ // Copy the date so we don't modify the view's state directly
+ var nextDate = [[_monthView nextMonth] copy];
+
+ // Set to the middle of the month (15th).
+ // This prevents [setDateValue:]'s timezone adjustment from
+ // shifting the date back into the previous month (e.g., Nov 1 -> Oct 31).
+ nextDate.setDate(15);
+
+ [self setDateValue:nextDate];
}
- (void)_displayPreviousMonth
diff --git a/AppKit/CPDatePicker/_CPDatePickerElementTextField.j b/AppKit/CPDatePicker/_CPDatePickerElementTextField.j
index 8bdb0b1ac..f24d0d15d 100644
--- a/AppKit/CPDatePicker/_CPDatePickerElementTextField.j
+++ b/AppKit/CPDatePicker/_CPDatePickerElementTextField.j
@@ -27,12 +27,7 @@
CPDatePickerElementTextFieldBecomeFirstResponder = @"CPDatePickerElementTextFieldBecomeFirstResponder";
CPDatePickerElementTextFieldAMPMChangedNotification = @"CPDatePickerElementTextFieldAMPMChangedNotification";
-var CPZeroKeyCode = 48,
- CPNineKeyCode = 57,
- CPMajAKeyCode = 65,
- CPMajPKeyCode = 80,
- CPAKeyCode = 97,
- CPPKeyCode = 112;
+// Removed hardcoded KeyCodes (CPZeroKeyCode, etc) as they are unreliable across browsers/layouts.
CPMonthDateType = 0;
CPDayDateType = 1;
@@ -190,23 +185,38 @@ CPAMPMDateType = 6;
*/
- (void)setValueForKeyEvent:(CPEvent)anEvent
{
- var keyCode = [anEvent keyCode];
+ var keyCode = [anEvent keyCode],
+ characters = [anEvent characters];
- if (keyCode != CPDeleteKeyCode && keyCode != CPDeleteForwardKeyCode && keyCode < CPZeroKeyCode || keyCode > CPNineKeyCode)
+ // Check if the event is a deletion
+ var isDelete = (keyCode === CPDeleteKeyCode || keyCode === CPDeleteForwardKeyCode);
+
+ // Check if the event is a numeric input.
+ // By testing the character string against a regex, we support num-pads and
+ // international keyboards correctly, rather than relying on keyCode ranges.
+ var isNumeric = (characters && [characters length] > 0 && /^[0-9]$/.test(characters));
+
+ // If it is neither a delete command nor a digit, we ignore it.
+ if (!isDelete && !isNumeric)
return;
var newValue = [self stringValue].replace(/\s/g, ''),
- length = [newValue length],
- eventKeyValue = parseInt([anEvent characters]).toString();
+ length = [newValue length];
- if (keyCode == CPDeleteKeyCode || keyCode == CPDeleteForwardKeyCode)
+ if (isDelete)
{
[_timerEdition invalidate];
_timerEdition = nil;
- newValue = [newValue substringToIndex:(length - 1)];
+
+ // Ensure we don't substring if length is 0
+ if (length > 0)
+ newValue = [newValue substringToIndex:(length - 1)];
}
else
{
+ // Since isNumeric is true, characters is a valid digit string
+ var eventKeyValue = characters;
+
if (!_timerEdition)
{
_timerEdition = [CPTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(_timerKeyEvent:) userInfo:nil repeats:NO];
@@ -227,7 +237,12 @@ CPAMPMDateType = 6;
}
}
- if (parseInt(newValue) > [self _maxNumberWithMaxDate] || ([_datePicker _isAmericanFormat] && _dateType == CPHourDateType && parseInt(newValue) > 12))
+ // Safety check for NaN before comparison
+ var numericValue = parseInt(newValue);
+ if (isNaN(numericValue))
+ numericValue = 0;
+
+ if (numericValue > [self _maxNumberWithMaxDate] || ([_datePicker _isAmericanFormat] && _dateType == CPHourDateType && numericValue > 12))
return;
_firstEvent = NO;
diff --git a/AppKit/CPDatePicker/_CPDatePickerTextField.j b/AppKit/CPDatePicker/_CPDatePickerTextField.j
index 9f53e7848..2fc284b3d 100644
--- a/AppKit/CPDatePicker/_CPDatePickerTextField.j
+++ b/AppKit/CPDatePicker/_CPDatePickerTextField.j
@@ -48,13 +48,6 @@
@global CPYearMonthDayDatePickerElementFlag
@global CPEraDatePickerElementFlag
-var CPZeroKeyCode = 48,
- CPNineKeyCode = 57,
- CPMajAKeyCode = 65,
- CPMajPKeyCode = 80,
- CPAKeyCode = 97,
- CPPKeyCode = 112;
-
// This class is used to represente the datePicker with the CPTextFieldAndStepperDatePickerStyle/CPTextFieldDatePickerStyle mode
@implementation _CPDatePickerTextField : CPControl
{
@@ -131,7 +124,7 @@ var CPZeroKeyCode = 48,
// Don't forget to unbind, otherwise several steppers will increase or decrease
[_currentTextField unbind:@"objectValue"];
[_currentTextField makeDeselectable];
- _currentTextField = nil
+ _currentTextField = nil;
// This is usefull when clicking on the stepper when the datePicker is not selected
[_stepper setObjectValue:0];
@@ -221,13 +214,30 @@ var CPZeroKeyCode = 48,
{
[_datePickerElementView _updateResponderTextField];
- // We select the firstTextField when the datePicker becomes firstResponder if _currentTextField is null. It can be null just when using tab
if (!_currentTextField)
{
+ var targetField = nil;
+
if (flags & CPShiftKeyMask)
- [self _selectTextField:_lastTextField];
+ {
+ // Try last field; if hidden, find previous visible
+ if ([_lastTextField isHidden])
+ targetField = [self _previousVisibleTextFieldFrom:_lastTextField];
+ else
+ targetField = _lastTextField;
+ }
else
- [self _selectTextField:_firstTextField];
+ {
+ // Try first field; if hidden, find next visible
+ if ([_firstTextField isHidden])
+ targetField = [self _nextVisibleTextFieldFrom:_firstTextField];
+ else
+ targetField = _firstTextField;
+ }
+
+ // Only select if we actually found a valid visible field
+ if (targetField)
+ [self _selectTextField:targetField];
}
}
@@ -334,23 +344,92 @@ var CPZeroKeyCode = 48,
return [super performKeyEquivalent:anEvent];
}
+- (_CPDatePickerElementTextField)_nextVisibleTextFieldFrom:(_CPDatePickerElementTextField)aTextField
+{
+ var runner = [aTextField nextTextField];
+
+ // If we wrapped back to the start immediately, or runner is nil, we are done.
+ if (!runner || runner == _firstTextField)
+ return nil;
+
+ // Traverse hidden fields
+ while (runner && [runner isHidden])
+ {
+ // If we hit the absolute last field and it is hidden, we've reached the end.
+ if (runner == _lastTextField)
+ return nil;
+
+ runner = [runner nextTextField];
+
+ // Safety: if we wrapped back to the start inside the loop
+ if (runner == _firstTextField)
+ return nil;
+ }
+
+ return runner;
+}
+
+- (_CPDatePickerElementTextField)_previousVisibleTextFieldFrom:(_CPDatePickerElementTextField)aTextField
+{
+ var runner = [aTextField previousTextField];
+
+ // If we wrapped back to the end immediately, or runner is nil, we are done.
+ if (!runner || runner == _lastTextField)
+ return nil;
+
+ // Traverse hidden fields
+ while (runner && [runner isHidden])
+ {
+ // If we hit the absolute first field and it is hidden, we've reached the start.
+ if (runner == _firstTextField)
+ return nil;
+
+ runner = [runner previousTextField];
+
+ // Safety: if we wrapped back to the end inside the loop
+ if (runner == _lastTextField)
+ return nil;
+ }
+
+ return runner;
+}
+
- (void)insertTab:(id)sender
{
if (!_currentTextField)
return;
- if (_currentTextField == _lastTextField)
- [[self window] selectNextKeyView:self];
+ // Ensure boundaries are up to date
+ [_datePickerElementView _updateResponderTextField];
+
+ var nextField = [self _nextVisibleTextFieldFrom:_currentTextField];
+
+ if (nextField)
+ {
+ [self _selectTextField:nextField];
+ }
else
- [self moveRight:sender];
-}
+ {
+ // We reached the visual end. Manually find the next external view.
+ // We cannot rely on [[self window] selectNextKeyView:self] because it might
+ // loop back into our own internal fields or select 'self' which refuses focus.
+ var nextView = [_currentTextField nextValidKeyView];
-- (void)moveRight:(id)sender
-{
- if (!_currentTextField)
- return;
+ // Skip any view that is part of this control (descendant)
+ while (nextView && [nextView isDescendantOf:self])
+ {
+ // If we looped back to the current field, we are trapped in a closed loop with no exit.
+ if (nextView == _currentTextField)
+ {
+ nextView = nil;
+ break;
+ }
+ nextView = [nextView nextValidKeyView];
+ }
- [self _selectTextField:[_currentTextField nextTextField]];
+ if (nextView)
+ [[self window] makeFirstResponder:nextView];
+ }
}
- (void)insertBacktab:(id)sender
@@ -358,10 +437,47 @@ var CPZeroKeyCode = 48,
if (!_currentTextField)
return;
- if (_currentTextField == _firstTextField)
- [[self window] selectPreviousKeyView:self];
+ [_datePickerElementView _updateResponderTextField];
+
+ var prevField = [self _previousVisibleTextFieldFrom:_currentTextField];
+
+ if (prevField)
+ {
+ [self _selectTextField:prevField];
+ }
else
- [self moveLeft:sender];
+ {
+ // We reached the visual start. Manually find the previous external view.
+ var prevView = [_currentTextField previousValidKeyView];
+
+ // Skip any view that is part of this control
+ while (prevView && [prevView isDescendantOf:self])
+ {
+ if (prevView == _currentTextField)
+ {
+ prevView = nil;
+ break;
+ }
+ prevView = [prevView previousValidKeyView];
+ }
+
+ if (prevView)
+ [[self window] makeFirstResponder:prevView];
+ }
+}
+
+- (void)moveRight:(id)sender
+{
+ if (!_currentTextField)
+ return;
+
+ [_datePickerElementView _updateResponderTextField];
+
+ // Use the helper to skip hidden fields
+ var nextField = [self _nextVisibleTextFieldFrom:_currentTextField];
+
+ if (nextField)
+ [self _selectTextField:nextField];
}
- (void)moveLeft:(id)sender
@@ -369,7 +485,13 @@ var CPZeroKeyCode = 48,
if (!_currentTextField)
return;
- [self _selectTextField:[_currentTextField previousTextField]];
+ [_datePickerElementView _updateResponderTextField];
+
+ // Use the helper to skip hidden fields to be safe
+ var prevField = [self _previousVisibleTextFieldFrom:_currentTextField];
+
+ if (prevField)
+ [self _selectTextField:prevField];
}
- (void)moveDown:(id)sender
@@ -401,7 +523,7 @@ var CPZeroKeyCode = 48,
}
/*! KeyDown event
- We just care care about the event A/P and every numbers
+ We just care about the event A/P and every numbers
*/
- (void)keyDown:(CPEvent)anEvent
{
@@ -410,18 +532,27 @@ var CPZeroKeyCode = 48,
[self interpretKeyEvents:[anEvent]];
- if ([_datePicker _isAmericanFormat] && [_currentTextField dateType] == CPAMPMDateType && ([anEvent keyCode] == CPAKeyCode || [anEvent keyCode] == CPPKeyCode || [anEvent keyCode] == CPMajAKeyCode || [anEvent keyCode] == CPMajPKeyCode))
+ var characters = [anEvent characters];
+
+ if ([_datePicker _isAmericanFormat] && [_currentTextField dateType] == CPAMPMDateType && [characters length] > 0)
{
- if ([anEvent keyCode] == CPAKeyCode || [anEvent keyCode] == CPMajAKeyCode)
+ var charUpper = [characters uppercaseString];
+
+ if (charUpper === "A")
+ {
[_currentTextField setStringValue:@"AM"];
- else
+ [[CPNotificationCenter defaultCenter] postNotificationName:CPDatePickerElementTextFieldAMPMChangedNotification object:_currentTextField userInfo:nil];
+ return;
+ }
+ else if (charUpper === "P")
+ {
[_currentTextField setStringValue:@"PM"];
-
- [[CPNotificationCenter defaultCenter] postNotificationName:CPDatePickerElementTextFieldAMPMChangedNotification object:_currentTextField userInfo:nil];
-
- return;
+ [[CPNotificationCenter defaultCenter] postNotificationName:CPDatePickerElementTextFieldAMPMChangedNotification object:_currentTextField userInfo:nil];
+ return;
+ }
}
+ // Pass the event down to the specific field (which handles numeric input validation via regex)
[_currentTextField setValueForKeyEvent:anEvent];
}
diff --git a/AppKit/CPDocument.j b/AppKit/CPDocument.j
index 8770d57aa..70fa6ef00 100644
--- a/AppKit/CPDocument.j
+++ b/AppKit/CPDocument.j
@@ -871,10 +871,13 @@ var CPDocumentUntitledCount = 0;
{
var theDelegate = context.delegate;
- if (aDocument === self && shouldClose)
+ // Only close the document explicitly if there is NO delegate to handle the action.
+ // If a delegate exists (e.g., the CPWindow), it is responsible for performing the close
+ // upon receiving the callback below. Calling [self close] here would cause a double-close.
+ if (aDocument === self && shouldClose && theDelegate == nil)
[self close];
- if (theDelegate != null)
+ if (theDelegate)
theDelegate.isa.objj_msgSend3(theDelegate, context.selector, aDocument, shouldClose, context.context);
}
diff --git a/AppKit/CPDragServer.j b/AppKit/CPDragServer.j
index a26189618..bd2b09a74 100644
--- a/AppKit/CPDragServer.j
+++ b/AppKit/CPDragServer.j
@@ -26,6 +26,7 @@
@import "CPPasteboard.j"
@import "CPView.j"
@import "CPWindow_Constants.j"
+@import "CPViewAnimation.j"
@class CPWindow // This file is imported by CPWindow.j
@class _CPDOMDataTransferPasteboard
@@ -132,6 +133,10 @@ var CPDraggingSource_draggedImage_movedTo_ = 1 << 0,
unsigned _dragOperation;
CPTimer _draggingUpdateTimer;
+
+ // Animation State
+ CGPoint _pendingEndLocation;
+ CPDragOperation _pendingEndOperation;
}
/*
@@ -325,6 +330,46 @@ var CPDraggingSource_draggedImage_movedTo_ = 1 << 0,
[_draggingUpdateTimer invalidate];
_draggingUpdateTimer = nil;
+ // Check if we should slide back (drag failed + slideBack requested)
+ if (![CPPlatform supportsDragAndDrop] && _shouldSlideBack && anOperation === CPDragOperationNone)
+ {
+ // Store state to finalize drag after animation completes
+ _pendingEndLocation = aLocation;
+ _pendingEndOperation = anOperation;
+
+ var currentFrame = [_draggedWindow frame],
+ targetFrame = CGRectMake(_startDragLocation.x, _startDragLocation.y, currentFrame.size.width, currentFrame.size.height);
+
+ // We use CPViewAnimation. Even though _draggedWindow is a CPWindow,
+ // CPViewAnimation supports targets that respond to setFrame: (like NSViewAnimation does for NSWindow).
+ var animation = [[CPViewAnimation alloc] initWithViewAnimations:[
+ [CPDictionary dictionaryWithObjects:[_draggedWindow, currentFrame, targetFrame]
+ forKeys:[CPViewAnimationTargetKey, CPViewAnimationStartFrameKey, CPViewAnimationEndFrameKey]]
+ ]];
+
+ [animation setAnimationCurve:CPAnimationEaseOut];
+ [animation setDuration:0.25];
+ [animation setDelegate:self];
+ [animation startAnimation];
+
+ return;
+ }
+
+ [self _performFinalCleanupWithLocation:aLocation operation:anOperation];
+}
+
+- (void)animationDidEnd:(CPAnimation)anAnimation
+{
+ [self _performFinalCleanupWithLocation:_pendingEndLocation operation:_pendingEndOperation];
+}
+
+- (void)animationDidStop:(CPAnimation)anAnimation
+{
+ [self _performFinalCleanupWithLocation:_pendingEndLocation operation:_pendingEndOperation];
+}
+
+- (void)_performFinalCleanupWithLocation:(CGPoint)aLocation operation:(CPDragOperation)anOperation
+{
[_draggedView removeFromSuperview];
if (![CPPlatform supportsDragAndDrop])
diff --git a/AppKit/CPEvent.j b/AppKit/CPEvent.j
index 7bc06d501..5cc439e08 100644
--- a/AppKit/CPEvent.j
+++ b/AppKit/CPEvent.j
@@ -70,6 +70,7 @@ var _CPEventPeriodicEventPeriod = 0,
BOOL _isARepeat;
unsigned _keyCode;
DOMEvent _DOMEvent;
+ BOOL _isActionKey;
int _data1;
int _data2;
short _subtype;
@@ -110,17 +111,28 @@ var _CPEventPeriodicEventPeriod = 0,
@param unmodCharacters the string of keys pressed without the presence of any modifiers other than Shift
@param repeatKey \c YES if this is caused by the system repeat as opposed to the user pressing the key again
@param code a number associated with the keyboard key of this event
+ @param isAnActionKey a BOOL indicating whether this key is an action key (e.g. a function key)
@throws CPInternalInconsistencyException if \c anEventType is not a CPKeyDown,
CPKeyUp or CPFlagsChanged
@return the keyboard event
*/
+ (CPEvent)keyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags
timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext
- characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code
+ characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code isActionKey:(BOOL)isAnActionKey
{
return [[self alloc] _initKeyEventWithType:anEventType location:aPoint modifierFlags:modifierFlags
timestamp:aTimestamp windowNumber:aWindowNumber context:aGraphicsContext
- characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code];
+ characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code isActionKey:isAnActionKey];
+}
+
+// for backwards compatibility only
++ (CPEvent)keyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags
+ timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext
+ characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code
+{
+ return [[self alloc] _initKeyEventWithType:anEventType location:aPoint modifierFlags:modifierFlags
+ timestamp:aTimestamp windowNumber:aWindowNumber context:aGraphicsContext
+ characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code isActionKey:NO];
}
/*!
@@ -252,7 +264,7 @@ var _CPEventPeriodicEventPeriod = 0,
/* @ignore */
- (id)_initKeyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags
timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext
- characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)isARepeat keyCode:(unsigned short)code
+ characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)isARepeat keyCode:(unsigned short)code isActionKey:(BOOL)isAnActionKey
{
if (self = [self _initWithType:anEventType])
{
@@ -264,6 +276,7 @@ var _CPEventPeriodicEventPeriod = 0,
_charactersIgnoringModifiers = unmodCharacters;
_isARepeat = isARepeat;
_keyCode = code;
+ _isActionKey = isAnActionKey;
_windowNumber = aWindowNumber;
}
@@ -571,6 +584,21 @@ var _CPEventPeriodicEventPeriod = 0,
return !firstResponderIsText;
}
+- (BOOL)_isActionOrCommandEvent
+{
+ // This method is now platform-agnostic. It checks for abstract properties
+ // of the event, including the _isActionKey flag that was set at creation time.
+ return (
+ // Is it a command shortcut?
+ (_modifierFlags & (CPCommandKeyMask | CPControlKeyMask)) ||
+
+ // Is it a key that doesn't produce a character?
+ ([_characters length] === 0) ||
+
+ // Was it identified as an action key by the platform-specific layer?
+ _isActionKey
+ );
+}
/*!
Return YES if this event is a part of processing a browser controlled cut or paste event
where the browser will go ahead and do the work of cutting or pasting within the input
diff --git a/AppKit/CPFontManager.j b/AppKit/CPFontManager.j
index f2159359d..d10a27e44 100644
--- a/AppKit/CPFontManager.j
+++ b/AppKit/CPFontManager.j
@@ -208,7 +208,11 @@ CPRemoveTraitFontAction = 7;
- (@action)addFontTrait:(id)sender
{
- var tag = [sender tag];
+ var tag = sender;
+
+ if ([sender respondsToSelector:@selector(tag)])
+ tag = [sender tag];
+
_activeChange = tag == nil ? @{} : @{ @"addTraits": tag };
_fontAction = CPAddTraitFontAction;
diff --git a/AppKit/CPMenu/CPMenu.j b/AppKit/CPMenu/CPMenu.j
index 6c23e1408..d8569ed43 100644
--- a/AppKit/CPMenu/CPMenu.j
+++ b/AppKit/CPMenu/CPMenu.j
@@ -269,7 +269,8 @@ var _CPMenuBarVisible = NO,
if (self)
{
_title = aTitle;
- _items = [];
+ // Use CPMutableArray instead of raw JS array for consistency with removeAllItems
+ _items = [CPMutableArray array];
_autoenablesItems = YES;
_showsStateColumn = YES;
@@ -374,6 +375,10 @@ var _CPMenuBarVisible = NO,
[self willChangeValueForKey:@"items"];
_items = [CPMutableArray array];
[self didChangeValueForKey:@"items"];
+
+ // Ensure the main menu updates if cleared
+ if (self === [CPApp mainMenu] && _CPMenuBarSharedWindow)
+ [_CPMenuBarSharedWindow setMenu:self];
}
/*!
@@ -390,6 +395,9 @@ var _CPMenuBarVisible = NO,
if ([aMenuItem menu] !== self || !_items)
return;
+ if (_menuWindow)
+ [[_menuWindow _menuView] tile];
+
[aMenuItem setValue:[aMenuItem valueForKey:@"changeCount"] + 1 forKey:@"changeCount"];
[[CPNotificationCenter defaultCenter]
@@ -1062,7 +1070,23 @@ var _CPMenuBarVisible = NO,
if ([anEvent _triggersKeyEquivalent:[item keyEquivalent] withModifierMask:[item keyEquivalentModifierMask]])
{
if ([item isEnabled])
+ {
+ // Flash the top-level item if this is the Main Menu
+ if (self === [CPApp mainMenu])
+ [self _flashItemAtIndex:index];
+
+ anEvent._isKeyEquivalent = YES; // prevent the menu keystroke from beeing inserted into textview
[self performActionForItemAtIndex:index];
+
+#if PLATFORM(DOM)
+ // we are done with this event in cappuccino space. do not let the browser do something weird additionally (e.g. command-o).
+ // but we must not stop copy/paste events as these can only be handled by the browser at this time even if they are in our menu
+ // (until we move to CPTextView as the fieleditor)
+
+ if (characters != "c" && characters != "x" && characters != "v")
+ _CPDOMEventStop(anEvent._DOMEvent);
+#endif
+ }
else
{
//beep?
@@ -1072,7 +1096,13 @@ var _CPMenuBarVisible = NO,
}
if ([[item submenu] performKeyEquivalent:anEvent])
+ {
+ // Flash the top-level item if a submenu handled the event
+ if (self === [CPApp mainMenu])
+ [self _flashItemAtIndex:index];
+
return YES;
+ }
}
return NO;
@@ -1152,6 +1182,25 @@ var _CPMenuBarVisible = NO,
return nil;
}
+//
+/*
+ @ignore
+*/
+- (void)_flashItemAtIndex:(int)anIndex
+{
+ // If we are using a native bridge (like a desktop wrapper), let the OS handle the visual feedback.
+ if ([CPPlatform supportsNativeMainMenu])
+ return;
+
+ [self _highlightItemAtIndex:anIndex];
+ [self performSelector:@selector(_stopFlashingItem) withObject:nil afterDelay:0.2];
+}
+
+- (void)_stopFlashingItem
+{
+ [self _highlightItemAtIndex:CPNotFound];
+}
+
@end
@@ -1223,6 +1272,11 @@ var _CPMenuBarVisible = NO,
postNotificationName:CPMenuDidAddItemNotification
object:self
userInfo:@{ @"CPMenuItemIndex": anIndex }];
+
+ // FIX #1222: If this is the main menu, force the shared menu bar window to refresh its layout.
+ // This ensures new items are positioned correctly (e.g. not pushed to the far right by previous layout states).
+ if (self === [CPApp mainMenu] && _CPMenuBarSharedWindow)
+ [_CPMenuBarSharedWindow setMenu:self];
}
- (void)removeObjectFromItemsAtIndex:(CPUInteger)anIndex
@@ -1238,6 +1292,10 @@ var _CPMenuBarVisible = NO,
postNotificationName:CPMenuDidRemoveItemNotification
object:self
userInfo:@{ @"CPMenuItemIndex": anIndex }];
+
+ // FIX #1222: Ensure the shared menu bar updates layout when items are removed.
+ if (self === [CPApp mainMenu] && _CPMenuBarSharedWindow)
+ [_CPMenuBarSharedWindow setMenu:self];
}
@end
@@ -1331,4 +1389,3 @@ var CPMenuTitleKey = @"CPMenuTitleKey",
@import "_CPMenuBarWindow.j"
@import "_CPMenuWindow.j"
-
diff --git a/AppKit/CPMenu/_CPMenuBarWindow.j b/AppKit/CPMenu/_CPMenuBarWindow.j
index 513ec2049..c39596622 100644
--- a/AppKit/CPMenu/_CPMenuBarWindow.j
+++ b/AppKit/CPMenu/_CPMenuBarWindow.j
@@ -370,12 +370,41 @@
- (void)tile
{
+ var bounds = [[self contentView] bounds],
+ height = CGRectGetHeight(bounds),
+ x = [[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-window-left-margin" forClass:_CPMenuView];
+
+ // 1. Layout the Icon (if present)
+ if (_iconImageView && ![_iconImageView isHidden])
+ {
+ var iconFrame = [_iconImageView frame];
+
+ iconFrame.origin.x = x;
+ // Vertically center
+ iconFrame.origin.y = (height - CGRectGetHeight(iconFrame)) / 2.0;
+
+ [_iconImageView setFrame:iconFrame];
+
+ x = CGRectGetMaxX(iconFrame) + 6.0; // Spacing between icon and title
+ }
+
+ // 2. Layout the Title (if present)
+ if (_titleField && [_titleField stringValue] && [[_titleField stringValue] length] > 0)
+ {
+ var titleFrame = [_titleField frame];
+
+ titleFrame.origin.x = x;
+ titleFrame.origin.y = (height - CGRectGetHeight(titleFrame)) / 2.0;
+
+ [_titleField setFrame:titleFrame];
+
+ x = CGRectGetMaxX(titleFrame) + 12.0; // Spacing between title and menu items
+ }
+
+ // 3. Layout the Menu Items
var items = [_menu itemArray],
index = 0,
- count = items.length,
-
- x = [[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-window-left-margin" forClass:_CPMenuView],
- y = 0.0,
+ count = items ? items.length : 0,
isLeftAligned = YES;
for (; index < count; ++index)
@@ -390,11 +419,18 @@
continue;
}
- if ([item isHidden])
+ // Fix for #1742: If a main menu item does not have a submenu, it should not appear in the menu bar.
+ if ([item isHidden] || ![item submenu])
+ {
+ [[item _menuItemView] setHidden:YES];
continue;
+ }
- var menuItemView = [item _menuItemView],
- frame = [menuItemView frame];
+ var menuItemView = [item _menuItemView];
+
+ [menuItemView setHidden:NO];
+
+ var frame = [menuItemView frame];
if (isLeftAligned)
{
@@ -409,21 +445,6 @@
x = CGRectGetMinX([menuItemView frame]);
}
}
-
- var bounds = [[self contentView] bounds],
- titleFrame = [_titleField frame];
-
- if ([_iconImageView isHidden])
- [_titleField setFrameOrigin:CGPointMake((CGRectGetWidth(bounds) - CGRectGetWidth(titleFrame)) / 2.0, (CGRectGetHeight(bounds) - CGRectGetHeight(titleFrame)) / 2.0)];
- else
- {
- var iconFrame = [_iconImageView frame],
- iconWidth = CGRectGetWidth(iconFrame),
- totalWidth = iconWidth + CGRectGetWidth(titleFrame);
-
- [_iconImageView setFrameOrigin:CGPointMake((CGRectGetWidth(bounds) - totalWidth) / 2.0, (CGRectGetHeight(bounds) - CGRectGetHeight(iconFrame)) / 2.0)];
- [_titleField setFrameOrigin:CGPointMake((CGRectGetWidth(bounds) - totalWidth) / 2.0 + iconWidth, (CGRectGetHeight(bounds) - CGRectGetHeight(titleFrame)) / 2.0)];
- }
}
- (void)setFrame:(CGRect)aRect display:(BOOL)shouldDisplay animate:(BOOL)shouldAnimate
@@ -464,13 +485,18 @@
{
var item = items[index];
- if ([item isHidden] || [item isSeparatorItem])
+ if ([item isHidden] || [item isSeparatorItem] || ![item submenu])
continue;
if (CGRectContainsPoint([self rectForItemAtIndex:index], aPoint))
return index;
}
+ // If the mouse is within the menu bar bounds but not over an item
+ // (e.g. dragging far left or right), force the menu to unhighlight.
+ if (CGRectContainsPoint([[self contentView] bounds], aPoint))
+ [_menu _highlightItemAtIndex:CPNotFound];
+
return CPNotFound;
}
diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j
index fde775128..f9b74d3fb 100644
--- a/AppKit/CPOutlineView.j
+++ b/AppKit/CPOutlineView.j
@@ -773,6 +773,11 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
[self reloadItem:anItem reloadChildren:NO];
}
+- (int)_numberOfRows
+{
+ return _itemsForRows ? _itemsForRows.length : 0;
+}
+
/*!
Reloads the data for a given item and optionally the children.
@@ -784,6 +789,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
_pendingItemToClean = [];
_itemAddedDuringLastLoading = [];
+ var previousRowCount = _itemsForRows.length;
+
if (!!shouldReloadChildren || !anItem)
[self _loadItemInfoForItem:anItem intermediate:NO];
else
@@ -791,6 +798,11 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
[self _cleanPendingItem];
+ // Safely update the table size and force a synchronous layout recalculation
+ // BEFORE the views are reloaded, avoiding the clipping issue.
+ if (_itemsForRows.length !== previousRowCount)
+ [self noteNumberOfRowsChanged];
+
[super _reloadDataViews];
}
@@ -837,9 +849,20 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
for (var i = [previousItems count] - 1; i >= 0; i--)
{
- var item = previousItems[i];
+ var item = previousItems[i],
+ found = NO;
- if (![children containsObject:item])
+ // Use strict identity (===) instead of containsObject: (which triggers isEqual:)
+ for (var j = 0, count = children.length; j < count; j++)
+ {
+ if (children[j] === item)
+ {
+ found = YES;
+ break;
+ }
+ }
+
+ if (!found)
[self _addPendingItem:item];
}
}
@@ -853,7 +876,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
var children = itemInfo.children;
- for (var i = [children count]; i >= 0; i--)
+ // Fixed out-of-bounds index (was previously [children count])
+ for (var i = children.length - 1; i >= 0; i--)
{
var child = children[i];
[self _addPendingItem:child];
@@ -864,7 +888,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
- (void)_cleanPendingItem
{
- for (var i = [_pendingItemToClean count]; i >= 0; i--)
+ for (var i = [_pendingItemToClean count] - 1; i >= 0; i--)
{
var item = _pendingItemToClean[i];
@@ -908,7 +932,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
var weight = itemInfo.weight,
descendants = anItem ? [anItem] : [];
- [_itemAddedDuringLastLoading addObject:anItem];
+ if (anItem)
+ [_itemAddedDuringLastLoading addObject:anItem];
if (itemInfo.isExpanded && [self _sendDataSourceShouldDeferDisplayingChildrenOfItem:anItem])
{
@@ -1085,7 +1110,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0,
var parent = itemInfo.parent;
// Check if the parent is the root item because we never return the actual root item
- if (itemInfo[[parent UID]] === _rootItemInfo)
+ if (parent && _itemInfosForItems[[parent UID]] === _rootItemInfo)
parent = nil;
return parent;
@@ -2314,3 +2339,264 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted)
? [CPColor colorWithCalibratedWhite:0.4 alpha: 1.0]
: [CPColor colorWithCalibratedWhite:0.5 alpha: 1.0]);
};
+
+@implementation CPOutlineView (CPBindings)
+
++ (void)initialize
+{
+ if (self !== [CPOutlineView class])
+ return;
+
+ [self exposeBinding:@"content"];
+ [self exposeBinding:@"selectionIndexPaths"];
+ [self exposeBinding:@"sortDescriptors"];
+}
+
+/*!
+ Returns the currently selected index paths.
+ This allows the outline view to be KVC-compliant for `selectionIndexPaths`.
+*/
+- (CPArray)selectionIndexPaths
+{
+ var indexes = [self selectedRowIndexes],
+ paths = [CPMutableArray array],
+ index = [indexes firstIndex];
+
+ while (index !== CPNotFound)
+ {
+ var item = [self itemAtRow:index];
+
+ // Check if the item is a CPTreeNode proxy (which it will be when bound to CPTreeController)
+ if ([item respondsToSelector:@selector(indexPath)])
+ [paths addObject:[item indexPath]];
+
+ index = [indexes indexGreaterThanIndex:index];
+ }
+
+ return paths;
+}
+
+@end
+
+
+@implementation CPOutlineView (CPBinder)
+
+- (id)content { return nil; }
+- (void)setContent:(id)aContent { }
+- (void)setSelectionIndexPaths:(CPArray)paths { }
+
++ (Class)_binderClassForBinding:(CPString)aBinding
+{
+ if (aBinding === @"content")
+ return [_CPOutlineViewContentBinder class];
+
+ if (aBinding === @"selectionIndexPaths")
+ return [_CPOutlineViewSelectionIndexPathsBinder class];
+
+ return [super _binderClassForBinding:aBinding];
+}
+
+@end
+
+
+// --- Content Binder ---
+
+/*!
+ _CPOutlineViewContentBinder acts as the CPOutlineViewDataSource when the outline view
+ is bound to a CPTreeController's arrangedObjects.
+*/
+@implementation _CPOutlineViewContentBinder : CPBinder
+{
+ CPTreeNode _rootNode;
+}
+
+- (void)setValueFor:(CPString)aBinding
+{
+ var destination = [_info objectForKey:CPObservedObjectKey],
+ keyPath =[_info objectForKey:CPObservedKeyPathKey],
+ value = [destination valueForKeyPath:keyPath];
+
+ if (!value || ![value isKindOfClass:[CPTreeNode class]])
+ _rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil];
+ else
+ _rootNode = value;
+
+ // Because CPBinder triggers setValueFor: synchronously during its initialization
+ // (before -bind is ever called), we must lazily assign the data source here.
+ if ([_source dataSource] !== self)
+ {
+ // Assigning the data source automatically triggers [_source reloadData]
+ // inside CPOutlineView, so we don't need to call it manually here.
+ [_source setDataSource:self];
+ }
+ else
+ {
+ // If it was already set, we just manually trigger the reload.
+ [_source reloadData];
+ }
+}
+
+- (CPTreeNode)rootNode
+{
+ return _rootNode;
+}
+
+// -- CPOutlineViewDataSource implementation --
+
+- (id)outlineView:(CPOutlineView)outlineView child:(CPInteger)index ofItem:(id)item
+{
+ var node = item || _rootNode;
+ return [[node childNodes] objectAtIndex:index];
+}
+
+- (BOOL)outlineView:(CPOutlineView)outlineView isItemExpandable:(id)item
+{
+ var node = item || _rootNode;
+ return ![node isLeaf];
+}
+
+- (int)outlineView:(CPOutlineView)outlineView numberOfChildrenOfItem:(id)item
+{
+ var node = item || _rootNode;
+ return [[node childNodes] count];
+}
+
+- (id)outlineView:(CPOutlineView)outlineView objectValueForTableColumn:(CPTableColumn)tableColumn byItem:(id)item
+{
+ if ([item respondsToSelector:@selector(representedObject)])
+ return [item representedObject];
+
+ return item;
+}
+
+@end
+
+// --- Selection Index Paths Binder ---
+
+/*!
+ _CPOutlineViewSelectionIndexPathsBinder listens for selection changes on the CPOutlineView
+ and translates the selected rows into CPIndexPaths to push to the CPTreeController.
+ It also intercepts changes from the CPTreeController and auto-expands the tree to highlight them.
+*/
+@implementation _CPOutlineViewSelectionIndexPathsBinder : CPBinder
+{
+ BOOL _isSyncingFromModel;
+}
+
+- (id)initWithBinding:(CPString)aBinding name:(CPString)aName to:(id)aDestination keyPath:(CPString)aKeyPath options:(CPDictionary)options from:(id)aSource
+{
+ self = [super initWithBinding:aBinding name:aName to:aDestination keyPath:aKeyPath options:options from:aSource];
+
+ [[CPNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(outlineViewSelectionDidChange:)
+ name:CPOutlineViewSelectionDidChangeNotification
+ object:aSource];
+}
+
++ (void)unbind:(CPString)aBinding forObject:(id)anObject
+{
+ if (aBinding === "selectionIndexPaths")
+ [[CPNotificationCenter defaultCenter]
+ removeObserver:self
+ name:CPOutlineViewSelectionDidChangeNotification
+ object:anObject];
+
+ [super unbind:aBinding forObject:anObject];
+}
+
+- (void)setValueFor:(CPString)aBinding
+{
+ // 1. SUPPRESS KVO AT THE VERY TOP to avoid circular updates when expanding parents
+ _isSyncingFromModel = YES;
+
+ var destination = [_info objectForKey:CPObservedObjectKey],
+ keyPath = [_info objectForKey:CPObservedKeyPathKey],
+ indexPaths = [destination valueForKeyPath:keyPath] || [],
+ indexes = [CPMutableIndexSet indexSet];
+
+ // 2. Fetch the root node directly from the CPTreeController (destination)
+ var rootNode = [destination respondsToSelector:@selector(arrangedObjects)] ? [destination arrangedObjects] : nil;
+
+ if (rootNode)
+ {
+ for (var i = 0, count = [indexPaths count]; i < count; i++)
+ {
+ var item = [rootNode descendantNodeAtIndexPath:[indexPaths objectAtIndex:i]];
+
+ if (item)
+ {
+ var parentsToExpand = [CPMutableArray array],
+ parent = [item parentNode];
+
+ while (parent && parent !== rootNode)
+ {
+ [parentsToExpand insertObject:parent atIndex:0];
+ parent = [parent parentNode];
+ }
+
+ for (var j = 0; j < [parentsToExpand count]; j++)
+ [_source expandItem:parentsToExpand[j]];
+
+ var row = [_source rowForItem:item];
+
+ if (row !== CPNotFound && row >= 0)
+ [indexes addIndex:row];
+ }
+ }
+ }
+
+ // Adjust the CPOutlineView selection
+ [_source selectRowIndexes:indexes byExtendingSelection:NO];
+
+ // 3. Re-enable KVO after adjustments are done
+ _isSyncingFromModel = NO;
+}
+
+- (void)outlineViewSelectionDidChange:(CPNotification)note
+{
+ // We only want to push the change back if we aren't currently syncing down from the model
+ if (_isSyncingFromModel)
+ return;
+
+ // In CPBinder, reverseSetValueFor: takes the name of the property on _source
+ // it should fetch the updated value from. Since CPOutlineView has the selectionIndexPaths method:
+ [self reverseSetValueFor:@"selectionIndexPaths"];
+}
+
+@end
+
+@implementation _CPOutlineViewContentBinder (DynamicColumns)
+
+- (id)outlineView:(CPOutlineView)outlineView objectValueForTableColumn:(CPTableColumn)tableColumn byItem:(id)item
+{
+ var rep = [item respondsToSelector:@selector(representedObject)] ? [item representedObject] : item;
+
+ // Dynamically fetch the value using the column's identifier (e.g., "name")
+ if (rep && [tableColumn identifier])
+ return [rep valueForKey:[tableColumn identifier]];
+
+ return rep;
+}
+
+// Add this to support inline bidirectional editing in the outline view
+- (void)outlineView:(CPOutlineView)outlineView setObjectValue:(id)value forTableColumn:(CPTableColumn)tableColumn byItem:(id)item
+{
+ var rep = [item respondsToSelector:@selector(representedObject)] ?[item representedObject] : item;
+
+ // Push the inline edit back to the model using the column's identifier
+ if (rep && [tableColumn identifier])
+ [rep setValue:value forKey:[tableColumn identifier]];
+}
+
+- (id)content
+{
+ // CPTableView internals probe the binder for its flat content to draw rows.
+ // For an outline view, the flat content is exactly the internally mapped items for rows.
+ if (_source && _source._itemsForRows)
+ return _source._itemsForRows;
+
+ return [];
+}
+
+@end
diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j
index f3b3e0726..1910d6153 100644
--- a/AppKit/CPRuleEditor/CPRuleEditor.j
+++ b/AppKit/CPRuleEditor/CPRuleEditor.j
@@ -1774,7 +1774,12 @@ TODO: implement
- (_CPRuleEditorViewSliceRow)_createNewSliceWithFrame:(CGRect)frame ruleEditorView:(CPRuleEditor)editor
{
- return [[_CPRuleEditorViewSliceRow alloc] initWithFrame:frame ruleEditorView:editor];
+ var slice = [[_CPRuleEditorViewSliceRow alloc] initWithFrame:frame ruleEditorView:editor];
+
+ // Ensure the slice resizes with the editor
+ [slice setAutoresizingMask:CPViewWidthSizable];
+
+ return slice;
}
- (void)_reconfigureSubviewsAnimate:(BOOL)animate
diff --git a/AppKit/CPStackView.j b/AppKit/CPStackView.j
new file mode 100644
index 000000000..dbd3a7d88
--- /dev/null
+++ b/AppKit/CPStackView.j
@@ -0,0 +1,700 @@
+/*
+ * CPStackView.j
+ * AppKit
+ *
+ * Created by Daniel Boehringer.
+ * Copyright 2025, Cappuccino Project.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "../Foundation/Foundation.h"
+
+@import "CPView.j"
+
+// Gravity Areas
+@typedef CPStackViewGravity
+ CPStackViewGravityTop = 1;
+ CPStackViewGravityLeading = 1;
+ CPStackViewGravityCenter = 2;
+ CPStackViewGravityBottom = 3;
+ CPStackViewGravityTrailing = 3;
+
+// Distribution (Deprecated in modern macOS, but kept for compatibility/logic)
+@typedef CPStackViewDistribution
+ CPStackViewDistributionGravityAreas = 0;
+ CPStackViewDistributionFill = 1;
+ CPStackViewDistributionFillEqually = 2;
+ CPStackViewDistributionFillProportionally = 3;
+ CPStackViewDistributionEqualSpacing = 4;
+ CPStackViewDistributionEqualCentering = 5;
+
+// Visibility Priority
+@typedef CPStackViewVisibilityPriority
+ CPStackViewVisibilityPriorityMustHold = 1000.0;
+ CPStackViewVisibilityPriorityNotVisible = 0.0;
+
+var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX
+
+/*!
+ @ingroup appkit
+ @class CPStackView
+
+ CPStackView arranges an array of views horizontally or vertically and updates
+ their placement and sizing when the window size changes.
+
+ Unlike a simple list, CPStackView supports "Gravity Areas" (Leading, Center, Trailing),
+ allowing you to pin groups of views to specific sections of the layout.
+*/
+@implementation CPStackView : CPView
+{
+ CPUserInterfaceLayoutOrientation _orientation;
+ CPLayoutAttribute _alignment;
+ float _spacing;
+ CPEdgeInsets _edgeInsets;
+
+ BOOL _detachesHiddenViews;
+
+ // View Storage by Gravity
+ CPMutableArray _viewsLeading;
+ CPMutableArray _viewsCenter;
+ CPMutableArray _viewsTrailing;
+
+ // Internal cache of all arranged subviews to maintain order for hittesting/iterating
+ CPMutableArray _arrangedSubviews;
+
+ // Custom Spacing storage
+ CPMapTable _customSpacings;
+
+ // Visibility Priorities
+ CPMapTable _visibilityPriorities;
+}
+
+#pragma mark -
+#pragma mark Initialization
+
++ (CPStackView)stackViewWithViews:(CPArray)views
+{
+ var stackView = [[CPStackView alloc] initWithFrame:CGRectMakeZero()];
+
+ for (var i = 0, count = [views count]; i < count; i++)
+ [stackView addView:views[i] inGravity:CPStackViewGravityLeading];
+
+ return stackView;
+}
+
+- (id)initWithFrame:(CGRect)aFrame
+{
+ if (self = [super initWithFrame:aFrame])
+ {
+ _orientation = CPUserInterfaceLayoutOrientationHorizontal;
+ _alignment = CPLayoutAttributeCenterY; // Default alignment
+ _spacing = 8.0; // Default Cocoa spacing
+ _edgeInsets = CPEdgeInsetsMake(0, 0, 0, 0);
+ _detachesHiddenViews = YES;
+
+ _viewsLeading = [[CPMutableArray alloc] init];
+ _viewsCenter = [[CPMutableArray alloc] init];
+ _viewsTrailing = [[CPMutableArray alloc] init];
+ _arrangedSubviews = [[CPMutableArray alloc] init];
+
+ _customSpacings = [[CPMapTable alloc] init];
+ _visibilityPriorities = [[CPMapTable alloc] init];
+ }
+ return self;
+}
+
+#pragma mark -
+#pragma mark Configuration
+
+/*!
+ The horizontal or vertical layout direction of the stack view.
+*/
+- (CPUserInterfaceLayoutOrientation)orientation
+{
+ return _orientation;
+}
+
+- (void)setOrientation:(CPUserInterfaceLayoutOrientation)anOrientation
+{
+ if (_orientation === anOrientation)
+ return;
+
+ _orientation = anOrientation;
+
+ // Reset default alignment based on new orientation if needed,
+ // though usually developer sets alignment explicitly.
+ // If switching to Vertical, CenterY makes less sense, usually CenterX.
+ if (_orientation === CPUserInterfaceLayoutOrientationVertical)
+ {
+ if (_alignment === CPLayoutAttributeCenterY)
+ _alignment = CPLayoutAttributeCenterX;
+ }
+ else
+ {
+ if (_alignment === CPLayoutAttributeCenterX)
+ _alignment = CPLayoutAttributeCenterY;
+ }
+
+ [self setNeedsLayout:YES];
+}
+
+/*!
+ The view alignment within the stack view.
+ Common values:
+ Horizontal: CPLayoutAttributeTop, CPLayoutAttributeBottom, CPLayoutAttributeCenterY, CPLayoutAttributeHeight (fill)
+ Vertical: CPLayoutAttributeLeading, CPLayoutAttributeTrailing, CPLayoutAttributeCenterX, CPLayoutAttributeWidth (fill)
+*/
+- (CPLayoutAttribute)alignment
+{
+ return _alignment;
+}
+
+- (void)setAlignment:(CPLayoutAttribute)anAlignment
+{
+ if (_alignment === anAlignment)
+ return;
+
+ _alignment = anAlignment;
+ [self setNeedsLayout:YES];
+}
+
+/*!
+ The minimum spacing, in points, between adjacent views in the stack view.
+*/
+- (float)spacing
+{
+ return _spacing;
+}
+
+- (void)setSpacing:(float)aSpacing
+{
+ if (_spacing === aSpacing)
+ return;
+
+ _spacing = aSpacing;
+ [self setNeedsLayout:YES];
+}
+
+/*!
+ The geometric padding, in points, inside the stack view, surrounding its views.
+*/
+- (CPEdgeInsets)edgeInsets
+{
+ return _edgeInsets;
+}
+
+- (void)setEdgeInsets:(CPEdgeInsets)insets
+{
+ if (CPEdgeInsetsEqualToEdgeInsets(_edgeInsets, insets))
+ return;
+
+ _edgeInsets = insets;
+ [self setNeedsLayout:YES];
+}
+
+/*!
+ A Boolean value that indicates whether the stack view removes hidden views from its view hierarchy.
+*/
+- (BOOL)detachesHiddenViews
+{
+ return _detachesHiddenViews;
+}
+
+- (void)setDetachesHiddenViews:(BOOL)shouldDetach
+{
+ if (_detachesHiddenViews === shouldDetach)
+ return;
+
+ _detachesHiddenViews = shouldDetach;
+ [self setNeedsLayout:YES];
+}
+
+#pragma mark -
+#pragma mark Managing Views in Gravity Areas
+
+- (CPArray)_containerForGravity:(CPStackViewGravity)gravity
+{
+ if (gravity === CPStackViewGravityCenter)
+ return _viewsCenter;
+ else if (gravity === CPStackViewGravityTrailing) // or Bottom
+ return _viewsTrailing;
+
+ return _viewsLeading; // Leading or Top
+}
+
+/*!
+ Adds a view to the end of the stack view gravity area.
+*/
+- (void)addView:(CPView)aView inGravity:(CPStackViewGravity)gravity
+{
+ var container = [self _containerForGravity:gravity];
+
+ // Check if view is already in a container
+ if ([_arrangedSubviews containsObject:aView])
+ [self removeView:aView];
+
+ [container addObject:aView];
+ [_arrangedSubviews addObject:aView];
+
+ // Add as actual subview
+ if ([aView superview] !== self)
+ [self addSubview:aView];
+
+ [self setNeedsLayout:YES];
+}
+
+/*!
+ Adds a view to a stack view gravity area at a specified index position.
+*/
+- (void)insertView:(CPView)aView atIndex:(CPInteger)index inGravity:(CPStackViewGravity)gravity
+{
+ var container = [self _containerForGravity:gravity];
+
+ if ([_arrangedSubviews containsObject:aView])
+ [self removeView:aView];
+
+ if (index >= [container count])
+ [container addObject:aView];
+ else
+ [container insertObject:aView atIndex:index];
+
+ [_arrangedSubviews addObject:aView];
+
+ if ([aView superview] !== self)
+ [self addSubview:aView];
+
+ [self setNeedsLayout:YES];
+}
+
+/*!
+ Specifies an array of views for a specified gravity area in the stack view, replacing any previous views in that area.
+*/
+- (void)setViews:(CPArray)views inGravity:(CPStackViewGravity)gravity
+{
+ var container = [self _containerForGravity:gravity];
+
+ // Remove old views from arranged list and superview
+ for (var i = 0; i < [container count]; i++)
+ {
+ var oldView = container[i];
+ [oldView removeFromSuperview];
+ [_arrangedSubviews removeObject:oldView];
+ }
+
+ [container removeAllObjects];
+
+ for (var i = 0; i < [views count]; i++)
+ {
+ var newView = views[i];
+ [container addObject:newView];
+ [_arrangedSubviews addObject:newView];
+ [self addSubview:newView];
+ }
+
+ [self setNeedsLayout:YES];
+}
+
+/*!
+ Removes a specified view from the stack view.
+*/
+- (void)removeView:(CPView)aView
+{
+ if (![_arrangedSubviews containsObject:aView])
+ return;
+
+ [_viewsLeading removeObject:aView];
+ [_viewsCenter removeObject:aView];
+ [_viewsTrailing removeObject:aView];
+ [_arrangedSubviews removeObject:aView];
+
+ [aView removeFromSuperview];
+
+ [self setNeedsLayout:YES];
+}
+
+/*!
+ Returns the array of views in the specified gravity area in the stack view.
+*/
+- (CPArray)viewsInGravity:(CPStackViewGravity)gravity
+{
+ return [[self _containerForGravity:gravity] copy];
+}
+
+/*!
+ The array of views arranged by the stack view.
+*/
+- (CPArray)arrangedSubviews
+{
+ return [_arrangedSubviews copy];
+}
+
+/*!
+ Adds the specified view to the end of the arranged subviews list.
+ (Defaults to Leading gravity if not specified).
+*/
+- (void)addArrangedSubview:(CPView)view
+{
+ [self addView:view inGravity:CPStackViewGravityLeading];
+}
+
+/*!
+ Removes the provided view from the stack’s array of arranged subviews.
+*/
+- (void)removeArrangedSubview:(CPView)view
+{
+ [self removeView:view];
+}
+
+#pragma mark -
+#pragma mark Custom Spacing
+
+- (float)customSpacingAfterView:(CPView)aView
+{
+ var val = [_customSpacings objectForKey:aView];
+ if (val)
+ return [val floatValue];
+
+ return CPStackViewSpacingUseDefault;
+}
+
+- (void)setCustomSpacing:(float)spacing afterView:(CPView)aView
+{
+ if (spacing === CPStackViewSpacingUseDefault)
+ [_customSpacings removeObjectForKey:aView];
+ else
+ [_customSpacings setObject:spacing forKey:aView];
+
+ [self setNeedsLayout:YES];
+}
+
+- (float)_spacingAfterView:(CPView)aView
+{
+ var custom = [self customSpacingAfterView:aView];
+ if (custom !== CPStackViewSpacingUseDefault)
+ return custom;
+ return _spacing;
+}
+
+#pragma mark -
+#pragma mark Visibility Priority
+
+- (void)setVisibilityPriority:(float)priority forView:(CPView)aView
+{
+ [_visibilityPriorities setObject:priority forKey:aView];
+
+ if (priority === CPStackViewVisibilityPriorityNotVisible)
+ {
+ [aView setHidden:YES];
+ }
+ else if (priority === CPStackViewVisibilityPriorityMustHold)
+ {
+ [aView setHidden:NO];
+ }
+ // Note: Intermediate priorities require complex constraint logic
+ // or a multi-pass layout system to determine fitting, which is
+ // simplified here to basic Hidden/Visible states.
+
+ [self setNeedsLayout:YES];
+}
+
+- (float)visibilityPriorityForView:(CPView)aView
+{
+ var val = [_visibilityPriorities objectForKey:aView];
+ if (val)
+ return [val floatValue];
+ return CPStackViewVisibilityPriorityMustHold;
+}
+
+#pragma mark -
+#pragma mark Layout
+
+- (void)resizeSubviewsWithOldSize:(CGSize)oldSize
+{
+ [self layoutSubviews];
+}
+
+- (void)layoutSubviews
+{
+ if (_orientation === CPUserInterfaceLayoutOrientationVertical)
+ [self _layoutVertical];
+ else
+ [self _layoutHorizontal];
+}
+
+- (void)_layoutHorizontal
+{
+ var bounds = [self bounds],
+ availWidth = CGRectGetWidth(bounds) - _edgeInsets.left - _edgeInsets.right,
+ availHeight = CGRectGetHeight(bounds) - _edgeInsets.top - _edgeInsets.bottom,
+ currentX = _edgeInsets.left;
+
+ // 1. Layout Leading Views
+ currentX = [self _layoutViews:_viewsLeading startOffset:currentX availableOrthogonalSize:availHeight direction:1];
+
+ // 2. Layout Trailing Views
+ // We layout backwards from the right
+ var startRight = CGRectGetWidth(bounds) - _edgeInsets.right;
+ [self _layoutViews:_viewsTrailing startOffset:startRight availableOrthogonalSize:availHeight direction:-1];
+
+ // 3. Layout Center Views
+ if ([_viewsCenter count] > 0)
+ {
+ // Calculate total width of center stack
+ var centerStackWidth = 0.0;
+ for (var i = 0; i < [_viewsCenter count]; i++)
+ {
+ var view = _viewsCenter[i];
+ if (_detachesHiddenViews && [view isHidden]) continue;
+
+ centerStackWidth += CGRectGetWidth([view frame]);
+ if (i < [_viewsCenter count] - 1)
+ centerStackWidth += [self _spacingAfterView:view];
+ }
+
+ var centerStart = (CGRectGetWidth(bounds) / 2.0) - (centerStackWidth / 2.0);
+
+ // Clamp to prevent overlap with Leading (simplified collision logic)
+ // ideally stack view compresses views, but here we just shift/clip
+ if (centerStart < currentX)
+ centerStart = currentX;
+
+ [self _layoutViews:_viewsCenter startOffset:centerStart availableOrthogonalSize:availHeight direction:1];
+ }
+}
+
+- (void)_layoutVertical
+{
+ var bounds = [self bounds],
+ availWidth = CGRectGetWidth(bounds) - _edgeInsets.left - _edgeInsets.right,
+ availHeight = CGRectGetHeight(bounds) - _edgeInsets.top - _edgeInsets.bottom,
+ currentY = _edgeInsets.top;
+
+ // 1. Layout Top (Leading) Views
+ currentY = [self _layoutViews:_viewsLeading startOffset:currentY availableOrthogonalSize:availWidth direction:1];
+
+ // 2. Layout Bottom (Trailing) Views
+ var startBottom = CGRectGetHeight(bounds) - _edgeInsets.bottom;
+ [self _layoutViews:_viewsTrailing startOffset:startBottom availableOrthogonalSize:availWidth direction:-1];
+
+ // 3. Layout Center Views
+ if ([_viewsCenter count] > 0)
+ {
+ var centerStackHeight = 0.0;
+ for (var i = 0; i < [_viewsCenter count]; i++)
+ {
+ var view = _viewsCenter[i];
+ if (_detachesHiddenViews && [view isHidden]) continue;
+
+ centerStackHeight += CGRectGetHeight([view frame]);
+ if (i < [_viewsCenter count] - 1)
+ centerStackHeight += [self _spacingAfterView:view];
+ }
+
+ var centerStart = (CGRectGetHeight(bounds) / 2.0) - (centerStackHeight / 2.0);
+
+ if (centerStart < currentY)
+ centerStart = currentY;
+
+ [self _layoutViews:_viewsCenter startOffset:centerStart availableOrthogonalSize:availWidth direction:1];
+ }
+}
+
+// Helper to layout a specific array of views in one direction
+// Returns the ending offset
+- (float)_layoutViews:(CPArray)views startOffset:(float)offset availableOrthogonalSize:(float)orthoSize direction:(int)dir
+{
+ var cursor = offset;
+ var isVert = (_orientation === CPUserInterfaceLayoutOrientationVertical);
+
+ // If direction is -1 (Trailing/Bottom), we iterate backwards
+ // However, the standard behavior for trailing gravity is that the *last* view added is at the *end*.
+ // Leading: [A] [B] ->
+ // Trailing: -> [C] [D] (where D is rightmost)
+ // To support Trailing logic: We start at Right Edge, move left by Width(D), place D, move left by Spacing...
+
+ var count = [views count];
+ if (count === 0) return cursor;
+
+ // If direction is negative (Trailing), we process list in reverse order to stack them from edge inwards
+ var i = (dir === 1) ? 0 : count - 1;
+ var limit = (dir === 1) ? count : -1;
+ var step = (dir === 1) ? 1 : -1;
+
+ for (; i !== limit; i += step)
+ {
+ var view = views[i];
+
+ if (_detachesHiddenViews && [view isHidden])
+ continue;
+
+ var viewFrame = [view frame];
+ var viewSizePrimary = isVert ? CGRectGetHeight(viewFrame) : CGRectGetWidth(viewFrame);
+
+ // Handle Alignment (Orthogonal Axis)
+ var orthoPos = 0.0;
+ var viewOrthoSize = isVert ? CGRectGetWidth(viewFrame) : CGRectGetHeight(viewFrame);
+
+ // Apply Stretch/Fill Alignment
+ if (isVert)
+ {
+ // Vertical Stack, dealing with Width
+ if (_alignment === CPLayoutAttributeWidth || _alignment === CPLayoutAttributeLeading || _alignment === CPLayoutAttributeTrailing)
+ {
+ // Note: CPLayoutAttributeLeading/Trailing in this context implies filling width usually,
+ // or aligning to edges. Let's assume Width/Fill for Leading/Trailing/Left/Right
+ // in this simplified implementation, or strictly left/right.
+
+ if (_alignment === CPLayoutAttributeWidth || _alignment === CPLayoutAttributeLeft || _alignment === CPLayoutAttributeLeading)
+ {
+ // Fill width if explicit, or just align left
+ if (_alignment === CPLayoutAttributeWidth) viewOrthoSize = orthoSize;
+ orthoPos = _edgeInsets.left;
+ }
+ else if (_alignment === CPLayoutAttributeRight || _alignment === CPLayoutAttributeTrailing)
+ {
+ orthoPos = _edgeInsets.left + (orthoSize - viewOrthoSize);
+ }
+ else // CenterX
+ {
+ orthoPos = _edgeInsets.left + (orthoSize - viewOrthoSize) / 2.0;
+ }
+ }
+ else // Default CenterX
+ {
+ orthoPos = _edgeInsets.left + (orthoSize - viewOrthoSize) / 2.0;
+ }
+ }
+ else
+ {
+ // Horizontal Stack, dealing with Height
+ if (_alignment === CPLayoutAttributeHeight || _alignment === CPLayoutAttributeTop || _alignment === CPLayoutAttributeBottom)
+ {
+ if (_alignment === CPLayoutAttributeHeight)
+ {
+ viewOrthoSize = orthoSize;
+ orthoPos = _edgeInsets.top;
+ }
+ else if (_alignment === CPLayoutAttributeTop)
+ {
+ orthoPos = _edgeInsets.top;
+ }
+ else if (_alignment === CPLayoutAttributeBottom)
+ {
+ orthoPos = _edgeInsets.top + (orthoSize - viewOrthoSize);
+ }
+ else // CenterY
+ {
+ orthoPos = _edgeInsets.top + (orthoSize - viewOrthoSize) / 2.0;
+ }
+ }
+ else // Default CenterY
+ {
+ orthoPos = _edgeInsets.top + (orthoSize - viewOrthoSize) / 2.0;
+ }
+ }
+
+ // Calculate Position
+ var originX = 0.0, originY = 0.0;
+ var sizeW = 0.0, sizeH = 0.0;
+
+ if (isVert)
+ {
+ // Vertical
+ sizeH = viewSizePrimary;
+ sizeW = viewOrthoSize;
+ originX = orthoPos;
+
+ if (dir === 1) {
+ originY = cursor;
+ cursor += sizeH + [self _spacingAfterView:view];
+ } else {
+ cursor -= sizeH;
+ originY = cursor;
+ cursor -= [self _spacingAfterView:view];
+ }
+ }
+ else
+ {
+ // Horizontal
+ sizeW = viewSizePrimary;
+ sizeH = viewOrthoSize;
+ originY = orthoPos;
+
+ if (dir === 1) {
+ originX = cursor;
+ cursor += sizeW + [self _spacingAfterView:view];
+ } else {
+ cursor -= sizeW;
+ originX = cursor;
+ cursor -= [self _spacingAfterView:view];
+ }
+ }
+
+ [view setFrame:CGRectMake(originX, originY, sizeW, sizeH)];
+ }
+
+ return cursor;
+}
+
+#pragma mark -
+#pragma mark CPCoding
+
+- (id)initWithCoder:(CPCoder)aCoder
+{
+ self = [super initWithCoder:aCoder];
+ if (self)
+ {
+ _orientation = [aCoder decodeIntForKey:@"CPStackViewOrientation"];
+ _alignment = [aCoder decodeIntForKey:@"CPStackViewAlignment"];
+ _spacing = [aCoder decodeFloatForKey:@"CPStackViewSpacing"];
+ _edgeInsets = [aCoder decodeObjectForKey:@"CPStackViewEdgeInsets"]; // Assuming CPEdgeInsets supports obj coding or manual decode
+ if (!_edgeInsets) _edgeInsets = CPEdgeInsetsMake(0,0,0,0);
+
+ _detachesHiddenViews = [aCoder decodeBoolForKey:@"CPStackViewDetachesHiddenViews"];
+
+ _viewsLeading = [aCoder decodeObjectForKey:@"CPStackViewViewsLeading"] || [];
+ _viewsCenter = [aCoder decodeObjectForKey:@"CPStackViewViewsCenter"] || [];
+ _viewsTrailing = [aCoder decodeObjectForKey:@"CPStackViewViewsTrailing"] || [];
+
+ // Rebuild arranged subviews cache
+ _arrangedSubviews = [[CPMutableArray alloc] init];
+ [_arrangedSubviews addObjectsFromArray:_viewsLeading];
+ [_arrangedSubviews addObjectsFromArray:_viewsCenter];
+ [_arrangedSubviews addObjectsFromArray:_viewsTrailing];
+
+ _customSpacings = [aCoder decodeObjectForKey:@"CPStackViewCustomSpacings"] || [[CPMapTable alloc] init];
+ _visibilityPriorities = [[CPMapTable alloc] init]; // usually not persisted
+ }
+ return self;
+}
+
+- (void)encodeWithCoder:(CPCoder)aCoder
+{
+ [super encodeWithCoder:aCoder];
+ [aCoder encodeInt:_orientation forKey:@"CPStackViewOrientation"];
+ [aCoder encodeInt:_alignment forKey:@"CPStackViewAlignment"];
+ [aCoder encodeFloat:_spacing forKey:@"CPStackViewSpacing"];
+ [aCoder encodeObject:_edgeInsets forKey:@"CPStackViewEdgeInsets"];
+ [aCoder encodeBool:_detachesHiddenViews forKey:@"CPStackViewDetachesHiddenViews"];
+
+ [aCoder encodeObject:_viewsLeading forKey:@"CPStackViewViewsLeading"];
+ [aCoder encodeObject:_viewsCenter forKey:@"CPStackViewViewsCenter"];
+ [aCoder encodeObject:_viewsTrailing forKey:@"CPStackViewViewsTrailing"];
+
+ [aCoder encodeObject:_customSpacings forKey:@"CPStackViewCustomSpacings"];
+}
+
+@end
diff --git a/AppKit/CPTextField.j b/AppKit/CPTextField.j
index bdf926b09..03416af31 100644
--- a/AppKit/CPTextField.j
+++ b/AppKit/CPTextField.j
@@ -332,7 +332,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
characters:nil
charactersIgnoringModifiers:nil
isARepeat:NO
- keyCode:nil];
+ keyCode:nil
+ isActionKey:NO];
[CPTextFieldInputOwner keyUp:cappEvent];
diff --git a/AppKit/CPTextView/CPFontPanel.j b/AppKit/CPTextView/CPFontPanel.j
index b3abeece7..701a0251e 100644
--- a/AppKit/CPTextView/CPFontPanel.j
+++ b/AppKit/CPTextView/CPFontPanel.j
@@ -1,34 +1,29 @@
/*
- * CPFontPanel.j
- * AppKit
- *
- * TODOs:
- * 1. make browser-width for size smaller and fix columns
- * 2. add all the missing features from the MacOS X counterpart (sampleview)
- *
- *
- * Created by Daniel Boehringer on 2/JAN/2014.
- * All modifications copyright Daniel Boehringer 2013.
- * Extensive code formatting and review by Andrew Hankinson
- * Based on original work by
- * Created by Emmanuel Maillard on 06/03/2010.
- * Copyright Emmanuel Maillard 2010.
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+ CPFontPanel.j
+ AppKit
+ Created by Daniel Boehringer on 2/JAN/2014.
+ All modifications copyright Daniel Boehringer 2013.
+ Extensive code formatting and review by Andrew Hankinson
+
+ Based on original work by
+ Created by Emmanuel Maillard on 06/03/2010.
+ Copyright Emmanuel Maillard 2010.
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
@import "CPPanel.j"
@import "CPColorWell.j"
@@ -37,7 +32,6 @@
@import "CPText.j"
@import "CPFontManager.j"
-
@class CPTextStorage
@class CPLayoutManager
@class CPTextContainer
@@ -46,27 +40,30 @@
/*
Collection indexes
*/
-var kTypefaceIndex_Normal = 0,
- kTypefaceIndex_Italic = 1,
- kTypefaceIndex_Bold = 2,
+var kTypefaceIndex_Normal = 0,
+ kTypefaceIndex_Italic = 1,
+ kTypefaceIndex_Bold = 2,
kTypefaceIndex_BoldItalic = 3,
- kToolbarHeight = 32,
- kBorderSpacing = 6,
- kInnerSpacing = 2,
- kNothingChanged = 0,
- kFontNameChanged = 1,
- kTypefaceChanged = 2,
- kSizeChanged = 3,
- kTextColorChanged = 4,
- kBackgroundColorChanged = 5,
- kUnderlineChanged = 6,
- kWeightChanged = 7,
+
+ kToolbarHeight = 32,
+ kPreviewHeight = 70,
+ kBorderSpacing = 6,
+ kInnerSpacing = 2,
+
+ kNothingChanged = 0,
+ kFontNameChanged = 1,
+ kTypefaceChanged = 2,
+ kSizeChanged = 3,
+ kTextColorChanged = 4,
+ kBackgroundColorChanged = 5,
+ kUnderlineChanged = 6,
+ kWeightChanged = 7,
+
_sharedFontPanel;
// FIXME Locale support
-var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
- _availableSizes = [@"9", @"10", @"11", @"12", @"13", @"14", @"18", @"24", @"36", @"48", @"72", @"96"];
-
+var _availableTraits = [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
+ _availableSizes = [@"9", @"10", @"11", @"12", @"13", @"14", @"18", @"24", @"36", @"48", @"64", @"72", @"96", @"144", @"288"];
/*!
@ingroup appkit
@@ -77,6 +74,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
id _fontBrowser;
id _traitBrowser;
id _sizeBrowser;
+
+ // Preview
+ _CPFontPanelPreviewView _previewView;
+
CPArray _availableFonts;
id _textColorWell;
CPColor _textColor;
@@ -85,7 +86,6 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
int _fontChanges;
}
-
#pragma mark -
#pragma mark Class methods
@@ -108,6 +108,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
return _sharedFontPanel;
}
+- (BOOL)acceptsFirstResponder
+{
+ return NO;
+}
#pragma mark -
#pragma mark Init methods
@@ -115,7 +119,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
/*! @ignore */
- (id)init
{
- if (self = [super initWithContentRect:CGRectMake(100, 90, 450, 394) styleMask:(CPTitledWindowMask | CPClosableWindowMask /*| CPResizableWindowMask*/ )])
+ if (self = [super initWithContentRect:CGRectMake(100, 90, 450, 420) styleMask:(CPTitledWindowMask | CPClosableWindowMask | CPResizableWindowMask)])
{
[[self contentView] setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]];
[self setTitle:@"Font Panel"];
@@ -152,10 +156,49 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
[aBrowser setDoubleAction:@selector(dblClicked:)];
[aBrowser setAllowsEmptySelection:NO];
[aBrowser setAllowsMultipleSelection:NO];
+
+ // Config Scrollers
+ //[aBrowser setHasHorizontalScroller:NO];
+ //[aBrowser setHasVerticalScroller:YES];
+ //[aBrowser setAutohidesScrollers:YES];
+ //[aBrowser setMaxVisibleColumns:1];
+
[aBrowser setDelegate:self];
[[self contentView] addSubview:aBrowser];
}
+- (void)_layoutBrowsers
+{
+ var contentView = [self contentView],
+ contentBounds = [contentView bounds],
+ previewY = kBorderSpacing + kToolbarHeight + kInnerSpacing,
+ browserY = previewY + kPreviewHeight + 10,
+ browserHeight = CGRectGetHeight(contentBounds) - browserY - 10,
+ availableWidth = CGRectGetWidth(contentBounds) - 20; // 10px padding L/R
+
+ // Layout Calculations
+ // Increase sizeWidth slightly to 60 to allow space for the vertical scrollbar without clipping text
+ var sizeWidth = 90,
+ spacing = 5,
+ remainingWidth = availableWidth - sizeWidth - (spacing * 2),
+ // Split remaining roughly 60% font name, 40% trait
+ fontWidth = FLOOR(remainingWidth * 0.60),
+ traitWidth = remainingWidth - fontWidth;
+
+ // Apply frames and column constraints
+ [_fontBrowser setFrame:CGRectMake(10, browserY, fontWidth, browserHeight)];
+ [_fontBrowser setDefaultColumnWidth:fontWidth];
+ [_fontBrowser setLastColumn:0];
+
+ [_traitBrowser setFrame:CGRectMake(10 + fontWidth + spacing, browserY, traitWidth, browserHeight)];
+ [_traitBrowser setDefaultColumnWidth:traitWidth];
+ [_traitBrowser setLastColumn:0];
+
+ [_sizeBrowser setFrame:CGRectMake(10 + fontWidth + traitWidth + (spacing * 2), browserY, sizeWidth, browserHeight)];
+ [_sizeBrowser setDefaultColumnWidth:sizeWidth];
+ [_sizeBrowser setLastColumn:0];
+}
+
- (void)_setupContents
{
if (_setupDone)
@@ -163,33 +206,53 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
_setupDone = YES;
+ // We set ourselves as delegate to handle resizing layout manually
+ [self setDelegate:self];
+
[self _setupToolbarView];
var contentView = [self contentView],
- label = [CPTextField labelWithTitle:@"Font name"],
- contentBounds = [contentView bounds],
- upperView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(contentBounds), CGRectGetHeight(contentBounds) - (kBorderSpacing + kToolbarHeight + kInnerSpacing))];
+ contentBounds = [contentView bounds];
[contentView addSubview:_toolbarView];
- _fontBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(10, 35, 150, 350)];
- _traitBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(155, 35, 150, 350)];
- _sizeBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(300, 35, 140, 350)];
+ // Preview View
+ var previewY = kBorderSpacing + kToolbarHeight + kInnerSpacing;
+ _previewView = [[_CPFontPanelPreviewView alloc] initWithFrame:CGRectMake(10, previewY, CGRectGetWidth(contentBounds) - 20, kPreviewHeight)];
+ [_previewView setAutoresizingMask:CPViewWidthSizable];
+ [contentView addSubview:_previewView];
+
+ // Initialize Browsers with zero rect, _layoutBrowsers will size them
+ _fontBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
+ _traitBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
+ _sizeBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
+
+ // Disable autoresizing masks because we are laying out manually in windowDidResize
+ [_fontBrowser setAutoresizingMask:CPViewNotSizable];
+ [_traitBrowser setAutoresizingMask:CPViewNotSizable];
+ [_sizeBrowser setAutoresizingMask:CPViewNotSizable];
[self _setupBrowser:_fontBrowser];
[self _setupBrowser:_traitBrowser];
[self _setupBrowser:_sizeBrowser];
+ // Perform initial layout
+ [self _layoutBrowsers];
+
[[CPNotificationCenter defaultCenter] addObserver:self
selector:@selector(textViewDidChangeSelection:)
name:CPTextViewDidChangeSelectionNotification
object:nil];
}
+- (void)windowDidResize:(CPNotification)aNotification
+{
+ [self _layoutBrowsers];
+}
+
- (void)textViewDidChangeSelection:(CPNotification)notification
{
- [self _refreshWithTextView:[notification object]];
-
+ [self _refreshWithTextView:[notification object]];
}
- (void)_refreshWithTextView:(CPTextView)textView
@@ -197,6 +260,9 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
if (![self isVisible])
return;
+ if (![textView respondsToSelector:@selector(_attributesForFontPanel)])
+ return;
+
var attribs = [textView _attributesForFontPanel],
font = [attribs objectForKey:CPFontAttributeName] || [[textView textStorage] font] || [CPFont systemFontOfSize:12.0],
color = [attribs objectForKey:CPForegroundColorAttributeName];
@@ -217,6 +283,9 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
[self setCurrentTrait:trait];
[self setCurrentSize:[font size] + ""]; //cast to string
+ // Update Preview
+ [_previewView setPreviewFont:font];
+
if (!color)
return;
@@ -253,7 +322,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
{
case kFontNameChanged:
newFont = [CPFont fontWithDescriptor:[[aFont fontDescriptor] fontDescriptorByAddingAttributes:
- [CPDictionary dictionaryWithObject:[self currentFont] forKey:CPFontNameAttribute]] size:0.0];
+ [CPDictionary dictionaryWithObject:[self currentFont] forKey:CPFontNameAttribute]] size:0.0];
break;
case kTypefaceChanged:
@@ -272,12 +341,13 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
newFont = [[CPFontManager sharedFontManager] convertFont:aFont toSize:[self currentSize]];
break;
- case kNothingChanged:
+ case kNothingChanged:
break;
default:
CPLog.trace(@"FIXME: -[" + [self className] + " " + _cmd + "] unhandled _fontChanges: " + _fontChanges);
break;
+
}
return newFont;
@@ -285,7 +355,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
- (void)setCurrentSize:(CGSize)aSize
{
- [_sizeBrowser selectRow:[_availableSizes indexOfObject:aSize] inColumn:0];
+ [_sizeBrowser selectRow:[_availableSizes indexOfObject:aSize] inColumn:0];
}
- (CPString)currentSize
@@ -295,7 +365,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
- (void)setCurrentFont:(CPFont)aFont
{
- [_fontBrowser selectRow:[_availableFonts indexOfObject:[aFont familyName]] inColumn:0];
+ [_fontBrowser selectRow:[_availableFonts indexOfObject:[aFont familyName]] inColumn:0];
}
- (CPString)currentFont
@@ -320,9 +390,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
case kTypefaceIndex_BoldItalic:
row = 3;
break;
+
}
- [_traitBrowser selectRow:row inColumn:0];
+ [_traitBrowser selectRow:row inColumn:0];
}
// FIXME Locale support
@@ -370,6 +441,8 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
if ([self currentTrait] != typefaceIndex)
[self setCurrentTrait:typefaceIndex ];
+ [_previewView setPreviewFont:font];
+
_fontChanges = kNothingChanged;
}
@@ -382,23 +455,30 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
////////////////////////////////////////////////////////////////////
// TODO: ask CPFontManager for traits //
+
- (void)browserClicked:(id)aBrowser
{
if (aBrowser === _fontBrowser)
{
_fontChanges = kFontNameChanged;
- [[CPFontManager sharedFontManager] modifyFontViaPanel:self];
}
else if (aBrowser === _traitBrowser)
{
_fontChanges = kTypefaceChanged;
- [[CPFontManager sharedFontManager] modifyFontViaPanel:self];
}
else if (aBrowser === _sizeBrowser)
{
_fontChanges = kSizeChanged;
- [[CPFontManager sharedFontManager] modifyFontViaPanel:self];
}
+
+ // Apply change immediately to manager (standard behavior)
+ [[CPFontManager sharedFontManager] modifyFontViaPanel:self];
+
+ // Update our preview manually because convertFont: calls rely on selected rows
+ // We construct a temporary font to update the preview view immediately
+ var updatedFont = [self panelConvertFont:[_previewView font]];
+ if (updatedFont)
+ [_previewView setPreviewFont:updatedFont];
}
- (void)dblClicked:(id)sender
@@ -414,7 +494,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
if (aBrowser === _traitBrowser)
return [_availableTraits count];
- return [_availableSizes count]
+ return [_availableSizes count];
}
- (id)browser:(id)aBrowser child:(int)index ofItem:(id)anItem
@@ -440,4 +520,116 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
@end
+// -----------------------------------------------------------------------------
+// _CPFontPanelPreviewView
+// A helper class to display a font sample with metrics grid
+// -----------------------------------------------------------------------------
+@implementation _CPFontPanelPreviewView : CPView
+{
+ CPTextField _sampleText;
+ CPColor _gridColor;
+ float _gridSize;
+}
+
+- (id)initWithFrame:(CGRect)aRect
+{
+ self = [super initWithFrame:aRect];
+
+ if (self)
+ {
+ [self setBackgroundColor:[CPColor whiteColor]];
+
+ _gridColor = [CPColor colorWithHexString:@"e4f4ff"];
+ _gridSize = 10.0;
+
+ _sampleText = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(aRect), CGRectGetHeight(aRect))];
+ [_sampleText setStringValue:@"AaYy-0123"];
+ [_sampleText setAlignment:CPCenterTextAlignment];
+ [_sampleText setVerticalAlignment:CPCenterVerticalTextAlignment];
+ [_sampleText setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
+ [_sampleText setTextColor:[CPColor blackColor]];
+
+ [self addSubview:_sampleText];
+ }
+ return self;
+}
+
+- (void)setPreviewFont:(CPFont)aFont
+{
+ [_sampleText setFont:aFont];
+ [self setNeedsDisplay:YES];
+}
+
+- (CPFont)font
+{
+ return [_sampleText font];
+}
+
+- (void)drawRect:(CGRect)dirtyRect
+{
+ // Draw Grid (from MetricsView inspiration)
+ var context = [[CPGraphicsContext currentContext] graphicsPort],
+ bounds = [self bounds],
+ maxX = CGRectGetMaxX(bounds),
+ maxY = CGRectGetMaxY(bounds);
+
+ CGContextSetLineWidth(context, 1.0);
+ CGContextSetStrokeColor(context, _gridColor);
+ CGContextBeginPath(context);
+
+ for (var y = 0.5; y <= maxY; y += _gridSize)
+ {
+ CGContextMoveToPoint(context, 0.0, y);
+ CGContextAddLineToPoint(context, maxX, y);
+ }
+
+ for (var x = 0.5; x <= maxX; x += _gridSize)
+ {
+ CGContextMoveToPoint(context, x, 0.0);
+ CGContextAddLineToPoint(context, x, maxY);
+ }
+ CGContextStrokePath(context);
+
+ // Draw Baseline/Ascender/Descender (from BaselineView inspiration)
+ var font = [_sampleText font];
+ if (!font) return;
+
+ var ascender = [font ascender],
+ descender = [font descender],
+ lineHeight = [font defaultLineHeightForFont];
+
+ // Calculate the baseline.
+ // CPTextField with CPCenterVerticalTextAlignment usually centers the line height.
+ // Top of line = midY - (lineHeight / 2.0)
+ // Baseline = Top of line + ascender
+ var midY = maxY / 2.0,
+ baselineY = midY - (lineHeight / 2.0) + ascender;
+
+ CGContextSetStrokeColor(context, [CPColor redColor]);
+ CGContextBeginPath(context);
+
+ // Baseline
+ CGContextMoveToPoint(context, 0, baselineY);
+ CGContextAddLineToPoint(context, maxX, baselineY);
+
+ // Ascender Line
+ CGContextMoveToPoint(context, 0, baselineY - ascender);
+ CGContextAddLineToPoint(context, maxX, baselineY - ascender);
+
+ // Descender Line
+ CGContextMoveToPoint(context, 0, baselineY - descender);
+ CGContextAddLineToPoint(context, maxX, baselineY - descender);
+
+ CGContextStrokePath(context);
+}
+
+- (void)mouseDown:(CPEvent)anEvent
+{
+ var text = prompt("Enter sample text:", [_sampleText stringValue]);
+ if (text)
+ [_sampleText setStringValue:text];
+}
+
+@end
+
[CPFontManager setFontPanelFactory:[CPFontPanel class]];
diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j
index f51a0da11..43b0a7a8b 100644
--- a/AppKit/CPTextView/CPTextView.j
+++ b/AppKit/CPTextView/CPTextView.j
@@ -449,19 +449,25 @@ var kDelegateRespondsTo_textShouldBeginEditing
{
[super copy:sender];
- if (![self isRichText])
- return;
-
var selectedRange = [self selectedRange],
pasteboard = [CPPasteboard generalPasteboard],
stringForPasting = [[self textStorage] attributedSubstringFromRange:CPMakeRangeCopy(selectedRange)],
richData = [_CPRTFProducer produceRTF:stringForPasting documentAttributes:@{}];
+ if ([self isRichText])
+ {
[pasteboard declareTypes:[CPStringPboardType, CPRTFPboardType, _CPSmartPboardType, _CPASPboardType] owner:nil];
[pasteboard setString:[stringForPasting._string stringByReplacingOccurrencesOfString:_CPAttachmentCharacterAsString withString:''] forType:CPStringPboardType];
[pasteboard setString:richData forType:CPRTFPboardType];
[pasteboard setString:_previousSelectionGranularity + '' forType:_CPSmartPboardType];
[pasteboard setString:[[CPKeyedArchiver archivedDataWithRootObject:stringForPasting] rawString] forType:_CPASPboardType];
+ }
+ else
+ {
+ [pasteboard declareTypes:[CPStringPboardType, _CPSmartPboardType] owner:nil];
+ [pasteboard setString:stringForPasting._string forType:CPStringPboardType];
+ [pasteboard setString:_previousSelectionGranularity + '' forType:_CPSmartPboardType];
+ }
}
- (void)_pasteString:(id)stringForPasting
@@ -510,8 +516,8 @@ var kDelegateRespondsTo_textShouldBeginEditing
- (void)paste:(id)sender
{
- if (![sender isKindOfClass:_CPNativeInputManager] && [[CPApp currentEvent] type] != CPAppKitDefined)
- return
+ if ([[CPApp currentEvent] type] != CPAppKitDefined)
+ return;
[self _pasteString:[self _stringForPasting]];
}
@@ -524,6 +530,11 @@ var kDelegateRespondsTo_textShouldBeginEditing
return [self isSelectable]; // editable textviews are automatically selectable
}
+- (BOOL)acceptsFirstMouse:(CPEvent)anEvent
+{
+ return YES;
+}
+
- (void)_becomeFirstResponder
{
[self updateInsertionPointStateAndRestartTimer:YES];
@@ -1012,35 +1023,6 @@ Sets the selection to a range of characters in response to user action.
}
#endif
-
-// interface to the _CPNativeInputManager
-- (void)_activateNativeInputElement:(DOMElement)aNativeField
-{
- var attributes = [[self typingAttributes] copy];
-
- // make it invisible
- [attributes setObject:[CPColor colorWithRed:1 green:1 blue:1 alpha:0] forKey:CPForegroundColorAttributeName];
-
- // FIXME: this hack to provide the visual space for the inputmanager should at least bypass the undomanager
- var placeholderString = [[CPAttributedString alloc] initWithString:aNativeField.innerHTML attributes:attributes];
- [self insertText:placeholderString];
-
- var caretOrigin = [_layoutManager boundingRectForGlyphRange:CPMakeRange(MAX(0, _selectionRange.location - 1), 1) inTextContainer:_textContainer].origin;
- caretOrigin.y += [_layoutManager _characterOffsetAtLocation:MAX(0, _selectionRange.location - 1)];
- caretOrigin.x += 2; // two pixel offset to the LHS character
- var cumulativeOffset = [self _cumulativeOffset];
-
-
-#if PLATFORM(DOM)
- aNativeField.style.left = (caretOrigin.x + cumulativeOffset.x) + "px";
- aNativeField.style.top = (caretOrigin.y + cumulativeOffset.y) + "px";
- aNativeField.style.font = [[_typingAttributes objectForKey:CPFontAttributeName] cssString];
- aNativeField.style.color = [[_typingAttributes objectForKey:CPForegroundColorAttributeName] cssString];
-#endif
-
- [_caret setVisibility:NO]; // hide our caret because now the system caret takes over
-}
-
- (CPArray)selectedRanges
{
return [_selectionRange];
@@ -1051,11 +1033,18 @@ Sets the selection to a range of characters in response to user action.
- (void)keyDown:(CPEvent)event
{
+ [[_window platformWindow] _propagateCurrentDOMEvent:YES];
- [[_window platformWindow] _propagateCurrentDOMEvent:YES]; // for the _CPNativeInputManager (necessary at least on FF and chrome)
-
- if (![_CPNativeInputManager isNativeInputFieldActive] && ![_CPNativeInputManager isDeadKey:event])
+ if ([event _isActionOrCommandEvent])
+ {
+ // This is a navigation key, action key, or command shortcut.
+ // Let the Cappuccino framework's key binding system handle it.
[self interpretKeyEvents:[event]];
+ }
+
+ // This is a normal printable character ('a', '1', '$', 'é').
+ // We do nothing, preventing the double-insertion bug. The _CPNativeInputManager
+ // will capture it from the hidden input field and insert it correctly.
[_caret setPermanentlyVisible:YES];
}
@@ -1116,6 +1105,9 @@ Sets the selection to a range of characters in response to user action.
if (![self isSelectable])
return;
+ // this is for the ipad-keyboard
+ [_CPNativeInputManager focusForClipboardOfTextView:self];
+
[_CPNativeInputManager cancelCurrentInputSessionIfNeeded];
[_caret setVisibility:NO];
@@ -1723,6 +1715,9 @@ Sets the selection to a range of characters in response to user action.
- (void)cut:(id)sender
{
+ if ([[CPApp currentEvent] type] != CPAppKitDefined)
+ return;
+
var selectedRange = [self selectedRange];
if (selectedRange.length < 1)
@@ -1872,7 +1867,11 @@ Sets the selection to a range of characters in response to user action.
}
else
{
- [_typingAttributes setObject:[sender selectedFont] forKey:CPFontAttributeName];
+ attributes = [_textStorage attributesAtIndex:_selectionRange.location
+ longestEffectiveRange:_selectionRange
+ inRange:_selectionRange];
+ oldFont = [attributes objectForKey:CPFontAttributeName] || [self font];
+ [_typingAttributes setObject:[sender convertFont:oldFont] forKey:CPFontAttributeName];
}
}
else
@@ -1929,6 +1928,87 @@ Sets the selection to a range of characters in response to user action.
[_layoutManager textStorage:_textStorage edited:0 range:CPMakeRangeCopy(range) changeInLength:0 invalidatedRange:CPMakeRangeCopy(range)];
}
+#pragma mark -
+#pragma mark Style & Alignment methods
+
+- (void)bold:(id)sender
+{
+ // This will trigger changeFont: via the FontManager
+ [[CPFontManager sharedFontManager] addFontTrait:CPBoldFontMask];
+}
+
+- (void)italic:(id)sender
+{
+ // This will trigger changeFont: via the FontManager
+ [[CPFontManager sharedFontManager] addFontTrait:CPItalicFontMask];
+}
+
+- (void)alignLeft:(id)sender
+{
+ [self _setAlignment:CPLeftTextAlignment];
+}
+
+- (void)alignCenter:(id)sender
+{
+ [self _setAlignment:CPCenterTextAlignment];
+}
+
+- (void)alignRight:(id)sender
+{
+ [self _setAlignment:CPRightTextAlignment];
+}
+
+- (void)alignJustified:(id)sender
+{
+ [self _setAlignment:CPJustifiedTextAlignment];
+}
+
+- (void)_setAlignment:(CPTextAlignment)anAlignment
+{
+ if (![self _didBeginEditing] || ![self shouldChangeTextInRange:_selectionRange replacementString:nil])
+ return;
+
+ var style = [CPParagraphStyle defaultParagraphStyle],
+ currentAttributes = _typingAttributes;
+
+ // Attempt to grab existing style from selection to preserve other paragraph settings
+ if (_selectionRange.length > 0)
+ currentAttributes = [_textStorage attributesAtIndex:_selectionRange.location effectiveRange:nil];
+
+ if ([currentAttributes objectForKey:CPParagraphStyleAttributeName])
+ style = [currentAttributes objectForKey:CPParagraphStyleAttributeName];
+
+ // Create new style with modified alignment
+ var newStyle = [style mutableCopy];
+ [newStyle setAlignment:anAlignment];
+
+ if (_selectionRange.length > 0)
+ {
+ // Add rudimentary undo support
+ var undoManager = [[self window] undoManager];
+ if (undoManager)
+ {
+ [[undoManager prepareWithInvocationTarget:self]
+ _setAlignment:[style alignment]];
+ }
+
+ [_textStorage addAttribute:CPParagraphStyleAttributeName value:newStyle range:CPMakeRangeCopy(_selectionRange)];
+
+ // Notify layout manager of changes
+ [_layoutManager textStorage:_textStorage
+ edited:0
+ range:CPMakeRangeCopy(_selectionRange)
+ changeInLength:0
+ invalidatedRange:CPMakeRangeCopy(_selectionRange)];
+ }
+ else
+ {
+ // Update typing attributes for next character
+ [_typingAttributes setObject:newStyle forKey:CPParagraphStyleAttributeName];
+ [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
+ }
+}
+
- (void)underline:(id)sender
{
if (![self _didBeginEditing] || ![self shouldChangeTextInRange:_selectionRange replacementString:nil])
@@ -2626,59 +2706,28 @@ var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey",
var _CPNativeInputField,
- _CPNativeInputFieldKeyDownCalled,
- _CPNativeInputFieldKeyUpCalled,
- _CPNativeInputFieldKeyPressedCalled,
- _CPNativeInputFieldActive;
+ _isComposing = NO; // Flag to track if an IME/dead key session is active.
var _CPCopyPlaceholder = '-';
@implementation _CPNativeInputManager : CPObject
-+ (BOOL)isNativeInputFieldActive
-{
- return _CPNativeInputFieldActive;
-}
+ (void)isDeadKey:(CPEvent)event
{
#if PLATFORM(DOM)
- return event._DOMEvent && (event._DOMEvent.key == 'Dead' || event._DOMEvent.key == 'Process');
+ return event._DOMEvent && (event._DOMEvent.key === 'Dead' || event._DOMEvent.key === 'Process');
#endif
-
return NO;
}
-+ (void)cancelCurrentNativeInputSession
-{
-
-#if PLATFORM(DOM)
- _CPNativeInputField.innerHTML = '';
-#endif
-
- [self _endInputSessionWithString:_CPNativeInputField.innerHTML];
-}
+ (void)cancelCurrentInputSessionIfNeeded
{
- if (!_CPNativeInputFieldActive)
- return;
-
- [self cancelCurrentNativeInputSession];
-}
-
-+ (void)_endInputSessionWithString:(CPString)aStr
-{
- _CPNativeInputFieldActive = NO;
-
- var currentFirstResponder = [[CPApp keyWindow] firstResponder],
- placeholderRange = CPMakeRange([currentFirstResponder selectedRange].location - 1, 1);
-
- [currentFirstResponder setSelectedRange:placeholderRange];
- [currentFirstResponder insertText:aStr];
- _CPNativeInputField.innerHTML = '';
-
-
- [self hideInputElement];
- [currentFirstResponder updateInsertionPointStateAndRestartTimer:YES];
+#if PLATFORM(DOM)
+ if (_CPNativeInputField) {
+ _CPNativeInputField.innerHTML = '';
+ }
+ _isComposing = NO;
+#endif
}
+ (void)initialize
@@ -2686,236 +2735,208 @@ var _CPCopyPlaceholder = '-';
#if PLATFORM(DOM)
_CPNativeInputField = document.createElement("div");
_CPNativeInputField.contentEditable = YES;
- _CPNativeInputField.style.width = "64px";
- _CPNativeInputField.style.zIndex = 10000;
+
+ // Style the input field to be invisible but focusable
_CPNativeInputField.style.position = "absolute";
- _CPNativeInputField.style.visibility = "visible";
- _CPNativeInputField.style.padding = "0px";
- _CPNativeInputField.style.margin = "0px";
+ _CPNativeInputField.style.top = "-1000px";
+ _CPNativeInputField.style.left = "-1000px";
+ _CPNativeInputField.style.width = "1px";
+ _CPNativeInputField.style.height = "1px";
+ _CPNativeInputField.style.opacity = "0";
+ _CPNativeInputField.style.overflow = "hidden";
_CPNativeInputField.style.whiteSpace = "pre";
- _CPNativeInputField.style.outline = "0px solid transparent";
+ _CPNativeInputField.style.zIndex = -1; // Put it behind everything
document.body.appendChild(_CPNativeInputField);
- _CPNativeInputField.addEventListener("keyup", function(e)
+ // Central function to handle inserting text into the CPTextView
+ var handleInput = function(textToInsert)
{
- _CPNativeInputFieldKeyUpCalled = YES;
-
- // filter out the shift-up, cursor keys and friends used to access the deadkeys
- // fixme: e.which is depreciated(?) -> find a better way to identify the modifier-keyups
- if (e.which < 27 || e.which == 91 || e.which == 93) // include apple command keys
- {
- if (e.which == 13)
- _CPNativeInputField.innerHTML = '';
-
- if (_CPNativeInputField.innerHTML.length == 0 || _CPNativeInputField.innerHTML.length > 2) // backspace
- [self cancelCurrentInputSessionIfNeeded];
-
- return false; // prevent the default behaviour
- }
-
- var currentFirstResponder = [[CPApp keyWindow] firstResponder];
-
- if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)])
- return false; // prevent the default behaviour
-
- // chrome-trigger: keypressed is omitted for deadkeys
- if (!_CPNativeInputFieldActive && _CPNativeInputFieldKeyPressedCalled == NO && _CPNativeInputField.innerHTML.length && _CPNativeInputField.innerHTML != _CPCopyPlaceholder && _CPNativeInputField.innerHTML.length < 3)
- {
- _CPNativeInputFieldActive = YES;
- [currentFirstResponder _activateNativeInputElement:_CPNativeInputField];
- }
- else
- {
- if (_CPNativeInputFieldActive)
- [self _endInputSessionWithString:_CPNativeInputField.innerHTML];
-
- // prevent the copy placeholder beeing removed by cursor keys
- if (_CPNativeInputFieldKeyPressedCalled)
- _CPNativeInputField.innerHTML = '';
- }
-
- _CPNativeInputFieldKeyDownCalled = NO;
-
- return false; // prevent the default behaviour
- }, true);
-
- _CPNativeInputField.addEventListener("keydown", function(e)
- {
- // this protects from heavy typing and the shift key
- if (_CPNativeInputFieldKeyDownCalled)
- return true;
-
- _CPNativeInputFieldKeyDownCalled = YES;
- _CPNativeInputFieldKeyUpCalled = NO;
- _CPNativeInputFieldKeyPressedCalled = NO;
- var currentFirstResponder = [[CPApp keyWindow] firstResponder];
-
- // webkit-browsers: cursor keys do not emit keypressed and would otherwise activate deadkey mode
- if (!CPBrowserIsEngine(CPGeckoBrowserEngine) && e.which >= 37 && e.which <= 40)
- _CPNativeInputFieldKeyPressedCalled = YES;
-
- if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)])
+ if (!textToInsert)
return;
- // FF-trigger: here the best way to detect a dead key is the missing keyup event
- if (CPBrowserIsEngine(CPGeckoBrowserEngine))
- setTimeout(function(){
- _CPNativeInputFieldKeyDownCalled = NO;
+ var currentFirstResponder = [[CPApp keyWindow] firstResponder];
- if (!_CPNativeInputFieldActive && _CPNativeInputFieldKeyUpCalled == NO && _CPNativeInputField.innerHTML.length && _CPNativeInputField.innerHTML != _CPCopyPlaceholder && _CPNativeInputField.innerHTML.length < 3 && !e.repeat)
- {
- _CPNativeInputFieldActive = YES;
- [currentFirstResponder _activateNativeInputElement:_CPNativeInputField];
- }
- else if (!_CPNativeInputFieldActive)
- [self hideInputElement];
- }, 200);
+ if (currentFirstResponder && [currentFirstResponder respondsToSelector:@selector(insertText:)])
+ var event = [CPApp currentEvent];
- return false;
- }, true); // capture mode
+ if (!event._isKeyEquivalent)
+ setTimeout(function(){
+ [currentFirstResponder insertText:textToInsert]
+ }, 20);
- _CPNativeInputField.addEventListener("keypress", function(e)
- {
- _CPNativeInputFieldKeyUpCalled = YES;
- _CPNativeInputFieldKeyPressedCalled = YES;
- return false;
-
- }, true); // capture mode
-
- _CPNativeInputField.onpaste = function(e)
- {
- var nativeClipboard = (e.originalEvent || e).clipboardData,
- richtext,
- pasteboard = [CPPasteboard generalPasteboard],
- currentFirstResponder = [[CPApp keyWindow] firstResponder],
- isPlain = NO;
-
- if ([currentFirstResponder respondsToSelector:@selector(isRichText)] && ![currentFirstResponder isRichText])
- isPlain = YES;
-
- // this is the rich chrome / FF codepath (where we can use RTF directly)
- if ((richtext = nativeClipboard.getData('text/rtf')) && !(!!(e.originalEvent || e).shiftKey) && !isPlain)
- {
- e.preventDefault();
-
- // setTimeout to prevent flickering in FF
- setTimeout(function(){
- [currentFirstResponder insertText:[[_CPRTFParser new] parseRTF:richtext]]
- }, 20);
-
- return false;
- }
-
- // plain is the same in all browsers...
-
- var data = e.clipboardData.getData('text/plain'),
- cappString = [pasteboard stringForType:CPStringPboardType];
-
- if (cappString != data)
- {
- [pasteboard declareTypes:[CPStringPboardType] owner:nil];
- [pasteboard setString:data forType:CPStringPboardType];
- }
-
- setTimeout(function(){ // prevent dom-flickering (only needed for FF)
- [currentFirstResponder paste:self];
- }, 20);
-
- return false;
+ // Clear the field immediately after grabbing its content.
+ _CPNativeInputField.innerHTML = '';
};
- if (CPBrowserIsEngine(CPGeckoBrowserEngine))
- {
- _CPNativeInputField.oncopy = function(e)
- {
- var pasteboard = [CPPasteboard generalPasteboard],
- string,
- currentFirstResponder = [[CPApp keyWindow] firstResponder];
+ // Intercept problematic keys before the browser acts.
+ _CPNativeInputField.addEventListener('keydown', function(e) {
- [currentFirstResponder copy:self];
-
- var stringForPasting = [pasteboard stringForType:CPStringPboardType];
- e.clipboardData.setData('text/plain', stringForPasting);
-
- return false;
- };
-
- _CPNativeInputField.oncut = function(e)
- {
- var pasteboard = [CPPasteboard generalPasteboard],
- string,
- currentFirstResponder = [[CPApp keyWindow] firstResponder];
-
- // prevent dom-flickering
- setTimeout(function(){
- [currentFirstResponder cut:self];
- }, 20);
-
- // this is necessary because cut will only execute in the future
- [currentFirstResponder copy:self];
-
- var stringForPasting = [pasteboard stringForType:CPStringPboardType];
-
- e.clipboardData.setData('text/plain', stringForPasting);
-
- return false;
+ if (e.key === 'Enter' || (e.key === 'Backspace' && _CPNativeInputField.innerHTML === '')) {
+ // Prevent browser default action:
+ // - 'Enter': Prevents inserting
.
+ // - 'Backspace' on empty: Prevents inserting junk characters on iPadOS.
+ e.preventDefault();
}
- }
+ });
+
+ // This listener handles all other character input.
+ _CPNativeInputField.addEventListener('input', function(e)
+ {
+ // If we are in a composition (e.g., IME), do nothing yet.
+ if (_isComposing)
+ return;
+
+ // Safety net: ignore deletion events, as they are handled by keydown.
+ if (e.inputType && e.inputType.startsWith('delete'))
+ {
+ _CPNativeInputField.innerHTML = '';
+ return;
+ }
+
+ // Robustness: Use 'textContent' instead of 'innerHTML' to strip any
+ // unexpected HTML tags the browser might have inserted.
+ var textToInsert = e.target.textContent;
+ handleInput(textToInsert);
+ });
+
+ // Fires when a composition session starts (e.g., user presses a dead key or starts an IME).
+ _CPNativeInputField.addEventListener('compositionstart', function(e) {
+ _isComposing = YES;
+ });
+
+ // Fires when the composition is finished.
+ _CPNativeInputField.addEventListener('compositionend', function(e) {
+ // The composition is over. `e.data` has the final string (e.g., "é").
+ handleInput(e.data);
+ _isComposing = NO;
+ });
+
+ // PASTE handler
+ _CPNativeInputField.onpaste = function(e)
+ {
+ e.preventDefault();
+ var nativeClipboard = (e.originalEvent || e).clipboardData;
+ var currentFirstResponder = [[CPApp keyWindow] firstResponder];
+
+ // Can we accept richtext? Then this is our preference (fixme: shift key to force plain text paste)
+ if ([currentFirstResponder isRichText])
+ {
+ var richtext = nativeClipboard.getData('text/rtf');
+
+ // prefer RTF form the outside of cappuccino
+ if (richtext)
+ richtext = [[_CPRTFParser new] parseRTF:richtext];
+ else
+ {
+ var pasteboard = [CPPasteboard generalPasteboard];
+ // If no RTF is available, try to get the internal represatation of richtext from the pasteboard
+ var richData = [pasteboard stringForType:_CPASPboardType];
+
+ if (richData)
+ richtext = [CPKeyedUnarchiver unarchiveObjectWithData:[CPData dataWithRawString:richData]];
+ }
+
+ if (richtext)
+ {
+ [currentFirstResponder _pasteString:richtext];
+
+ return;
+ }
+ // If no richtext is available, fall back to plain text
+ }
+
+ var nativeString = nativeClipboard.getData('text/plain');
+
+ // Use setTimeout to prevent flickering
+ setTimeout(function()
+ {
+ [currentFirstResponder _pasteString:nativeString || [pasteboard stringForType:CPStringPboardType] || ''];
+ }, 20);
+ };
+
+ // COPY handler
+ _CPNativeInputField.oncopy = function(e)
+ {
+ e.preventDefault();
+ var pasteboard = [CPPasteboard generalPasteboard];
+ var nativeClipboard = (e.originalEvent || e).clipboardData;
+
+ // First, copy the data to populate the CP clipboard
+ [[[CPApp keyWindow] firstResponder] copy:self];
+
+ // Now, copy the data over to the native clipboard
+ var stringForPasting = [pasteboard stringForType:CPStringPboardType] || '';
+ nativeClipboard.setData('text/plain', stringForPasting);
+
+ var rtfForPasting = [pasteboard stringForType:CPRTFPboardType];
+
+ if (rtfForPasting)
+ nativeClipboard.setData('text/rtf', rtfForPasting);
+ };
+
+ // CUT handler
+ _CPNativeInputField.oncut = function(e)
+ {
+ e.preventDefault();
+ var pasteboard = [CPPasteboard generalPasteboard];
+ var nativeClipboard = (e.originalEvent || e).clipboardData;
+ var currentFirstResponder = [[CPApp keyWindow] firstResponder];
+
+ // First, copy the data to populate the CP clipboard
+ [currentFirstResponder copy:self];
+
+ // Now, copy the data to the native clipboard
+ var stringForPasting = [pasteboard stringForType:CPStringPboardType] || '';
+ nativeClipboard.setData('text/plain', stringForPasting);
+ var rtfForPasting = [pasteboard stringForType:CPRTFPboardType];
+
+ if (rtfForPasting)
+ nativeClipboard.setData('text/rtf', rtfForPasting);
+
+ // Then, perform the delete part of the cut operation in the text view
+ // Use setTimeout to prevent flickering
+ setTimeout(function()
+ {
+ [currentFirstResponder deleteBackward:self];
+ }, 20);
+ };
#endif
}
+ (void)focusForTextView:(CPTextView)currentFirstResponder
{
- if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)])
- return;
-
- [self hideInputElement];
-
#if PLATFORM(DOM)
- _CPNativeInputField.focus();
-#endif
+ if (_CPNativeInputField && document.activeElement !== _CPNativeInputField)
+ _CPNativeInputField.focus();
+#endif
}
+ (void)focusForClipboardOfTextView:(CPTextView)textview
{
-
#if PLATFORM(DOM)
- if (!_CPNativeInputFieldActive && _CPNativeInputField.innerHTML.length == 0)
- _CPNativeInputField.innerHTML = _CPCopyPlaceholder; // make sure we have a selection to allow the native pasteboard work in safari
+ var selectedRange = [textview selectedRange];
+ if (selectedRange.length > 0) {
+ // Put the selected text into the hidden div so the browser can natively copy it.
+ var textToCopy = [[[textview textStorage] string] substringWithRange:selectedRange];
+ _CPNativeInputField.innerHTML = textToCopy;
+ } else {
+ // For paste, we just need the field to be focusable.
+ _CPNativeInputField.innerHTML = _CPCopyPlaceholder;
+ }
[self focusForTextView:textview];
- // select all in the contenteditable div (http://stackoverflow.com/questions/12243898/how-to-select-all-text-in-contenteditable-div)
- if (document.body.createTextRange)
- {
- var range = document.body.createTextRange();
-
- range.moveToElementText(_CPNativeInputField);
- range.select();
- }
- else if (window.getSelection)
- {
- var selection = window.getSelection(),
- range = document.createRange();
-
+ // Select the content of the hidden div so copy/cut works.
+ if (window.getSelection && document.createRange) {
+ var selection = window.getSelection();
+ var range = document.createRange();
range.selectNodeContents(_CPNativeInputField);
selection.removeAllRanges();
selection.addRange(range);
}
#endif
-
-}
-
-+ (void)hideInputElement
-{
-
-#if PLATFORM(DOM)
- _CPNativeInputField.style.top = "-10000px";
- _CPNativeInputField.style.left = "-10000px";
-#endif
-
}
@end
diff --git a/AppKit/CPToolbar.j b/AppKit/CPToolbar.j
index bcfec1aee..c60276b64 100644
--- a/AppKit/CPToolbar.j
+++ b/AppKit/CPToolbar.j
@@ -1014,6 +1014,7 @@ var LABEL_MARGIN = 2.0;
CPImageView _imageView;
CPView _view;
+ CPView _highlightView;
CPTextField _labelField;
@@ -1243,6 +1244,18 @@ var LABEL_MARGIN = 2.0;
if (alternateImage)
[_imageView setImage:alternateImage];
+ else
+ {
+ if (!_highlightView)
+ {
+ _highlightView = [[CPView alloc] initWithFrame:[_imageView bounds]];
+ [_highlightView setBackgroundColor:[CPColor blackColor]];
+ [_highlightView setAlphaValue:0.3];
+ [_highlightView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
+ }
+
+ [_imageView addSubview:_highlightView];
+ }
[_labelField setTextShadowOffset:CGSizeMakeZero()];
}
@@ -1253,6 +1266,8 @@ var LABEL_MARGIN = 2.0;
if (image)
[_imageView setImage:image];
+ [_highlightView removeFromSuperview];
+
[_labelField setTextShadowOffset:CGSizeMake(0.0, 1.0)];
}
diff --git a/AppKit/CPTreeController.j b/AppKit/CPTreeController.j
new file mode 100644
index 000000000..2d0e46802
--- /dev/null
+++ b/AppKit/CPTreeController.j
@@ -0,0 +1,628 @@
+/*
+ * CPTreeController.j
+ * AppKit
+ *
+ * Daniel Boehringer Mar/2026
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+@import
+@import
+@import "CPObjectController.j"
+@import "CPKeyValueBinding.j"
+@import "CPTreeNode.j"
+
+@implementation CPTreeController : CPObjectController
+{
+ BOOL _avoidsEmptySelection;
+ BOOL _preservesSelection;
+ BOOL _selectsInsertedObjects;
+ BOOL _alwaysUsesMultipleValuesMarker;
+
+ CPString _childrenKeyPath;
+ CPString _countKeyPath;
+ CPString _leafKeyPath;
+
+ CPArray _sortDescriptors;
+ id _arrangedObjects;
+
+ CPArray _selectionIndexPaths;
+ BOOL _disableSetContent;
+}
+
++ (void)initialize
+{
+ if (self !== [CPTreeController class])
+ return;
+
+ [self exposeBinding:@"contentArray"];
+ [self exposeBinding:@"sortDescriptors"];
+}
+
++ (CPSet)keyPathsForValuesAffectingContentArray
+{
+ return[CPSet setWithObjects:@"content"];
+}
+
++ (CPSet)keyPathsForValuesAffectingArrangedObjects
+{
+ return [CPSet setWithObjects:@"content", @"sortDescriptors", @"childrenKeyPath"];
+}
+
++ (CPSet)keyPathsForValuesAffectingSelectionIndexPath
+{
+ return[CPSet setWithObjects:@"selectionIndexPaths"];
+}
+
++ (CPSet)keyPathsForValuesAffectingSelectedObjects
+{
+ return [CPSet setWithObjects:@"selectionIndexPaths"];
+}
+
++ (CPSet)keyPathsForValuesAffectingSelectedNodes
+{
+ return [CPSet setWithObjects:@"selectionIndexPaths"];
+}
+
++ (CPSet)keyPathsForValuesAffectingCanAddChild
+{
+ return[CPSet setWithObjects:@"selectionIndexPaths"];
+}
+
++ (CPSet)keyPathsForValuesAffectingCanInsert
+{
+ return [CPSet setWithObjects:@"selectionIndexPaths"];
+}
+
++ (CPSet)keyPathsForValuesAffectingCanInsertChild
+{
+ return [CPSet setWithObjects:@"selectionIndexPaths"];
+}
+
+- (id)init
+{
+ if (self = [super init])
+ {
+ _preservesSelection = YES;
+ _selectsInsertedObjects = YES;
+ _avoidsEmptySelection = YES;
+ _alwaysUsesMultipleValuesMarker = NO;
+ _childrenKeyPath = @"children";
+
+ [self _init];
+ }
+ return self;
+}
+
+- (void)_init
+{
+ _sortDescriptors = [CPArray array];
+ _selectionIndexPaths = [CPArray array];
+ _arrangedObjects = [[CPTreeNode alloc] initWithRepresentedObject:nil];
+}
+
+- (void)prepareContent
+{[self _setContentArray:[CPArray arrayWithObject:[self newObject]]];
+}
+
+- (BOOL)preservesSelection { return _preservesSelection; }
+- (void)setPreservesSelection:(BOOL)value { _preservesSelection = value; }
+
+- (BOOL)selectsInsertedObjects { return _selectsInsertedObjects; }
+- (void)setSelectsInsertedObjects:(BOOL)value { _selectsInsertedObjects = value; }
+
+- (BOOL)avoidsEmptySelection { return _avoidsEmptySelection; }
+- (void)setAvoidsEmptySelection:(BOOL)value { _avoidsEmptySelection = value; }
+
+- (BOOL)alwaysUsesMultipleValuesMarker { return _alwaysUsesMultipleValuesMarker; }
+- (void)setAlwaysUsesMultipleValuesMarker:(BOOL)aFlag { _alwaysUsesMultipleValuesMarker = aFlag; }
+
+- (CPArray)sortDescriptors { return _sortDescriptors; }
+- (void)setSortDescriptors:(CPArray)value
+{
+ if (_sortDescriptors === value)
+ return;
+
+ _sortDescriptors = [value copy];
+ [self _rearrangeObjects];
+}
+
+- (CPString)childrenKeyPath { return _childrenKeyPath; }
+- (void)setChildrenKeyPath:(CPString)aKeyPath
+{
+ if (_childrenKeyPath === aKeyPath) return;
+ _childrenKeyPath = aKeyPath;[self rearrangeObjects];
+}
+
+- (CPString)countKeyPath { return _countKeyPath; }
+- (void)setCountKeyPath:(CPString)aKeyPath { _countKeyPath = aKeyPath; }
+
+- (CPString)leafKeyPath { return _leafKeyPath; }
+- (void)setLeafKeyPath:(CPString)aKeyPath { _leafKeyPath = aKeyPath; }
+
+- (CPString)childrenKeyPathForNode:(CPTreeNode)node { return [self childrenKeyPath]; }
+- (CPString)countKeyPathForNode:(CPTreeNode)node { return [self countKeyPath]; }
+- (CPString)leafKeyPathForNode:(CPTreeNode)node { return [self leafKeyPath]; }
+
+- (void)setContent:(id)value
+{
+ if (_disableSetContent) return;
+
+ if (!value)
+ value = [CPArray array];
+ if (![value isKindOfClass:[CPArray class]])
+ value = [CPArray arrayWithObject:value];
+
+ var oldSelectedObjects = nil,
+ oldSelectionIndexPaths = nil;
+
+ if ([self preservesSelection])
+ oldSelectedObjects = [self selectedObjects];
+ else
+ oldSelectionIndexPaths = [self selectionIndexPaths];
+
+ _contentObject = value;
+
+ [self _rearrangeObjects];
+
+ if ([self preservesSelection])[self __setSelectedObjects:oldSelectedObjects];
+ else[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection];
+}
+
+- (void)_setContentArray:(id)anArray {[self setContent:anArray]; }
+- (id)contentArray { return _contentObject; }
+- (id)arrangedObjects { return _arrangedObjects; }
+
+- (void)rearrangeObjects
+{
+ [self willChangeValueForKey:@"arrangedObjects"];
+ [self _rearrangeObjects];
+ [self didChangeValueForKey:@"arrangedObjects"];
+}
+
+- (void)_rearrangeObjects
+{
+ var oldSelectedObjects = nil,
+ oldSelectionIndexPaths = nil;
+
+ if ([self preservesSelection])
+ oldSelectedObjects = [self selectedObjects];
+ else
+ oldSelectionIndexPaths = [self selectionIndexPaths];
+
+ [self __rebuildArrangedObjectsTree];
+
+ if ([self preservesSelection])[self __setSelectedObjects:oldSelectedObjects];
+ else[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection];
+}
+
+- (void)__rebuildArrangedObjectsTree
+{
+ var rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil],
+ contentArray = [self contentArray];
+
+ if (contentArray && [contentArray count] > 0)
+ {
+ var children = [self _buildTreeNodesForObjects:contentArray];
+ [[rootNode mutableChildNodes] addObjectsFromArray:children];
+ }
+
+ _arrangedObjects = rootNode;
+}
+
+- (CPArray)_buildTreeNodesForObjects:(CPArray)objects
+{
+ var count = [objects count];
+
+ if (count === 0)
+ return [];
+
+ var sortedObjects = objects;
+
+ if (_sortDescriptors && [_sortDescriptors count] > 0)
+ sortedObjects = [objects sortedArrayUsingDescriptors:_sortDescriptors];
+
+ var nodes = [CPMutableArray arrayWithCapacity:count];
+
+ for (var i = 0; i < count; i++)
+ {
+ var obj = [sortedObjects objectAtIndex:i],
+ node = [[CPTreeNode alloc] initWithRepresentedObject:obj];
+
+ if (_childrenKeyPath)
+ {
+ var childObjects = [obj valueForKeyPath:_childrenKeyPath];
+
+ if (childObjects && [childObjects count] > 0)
+ {
+ var childNodes = [self _buildTreeNodesForObjects:childObjects];
+ [[node mutableChildNodes] addObjectsFromArray:childNodes];
+ }
+ }
+
+ [nodes addObject:node];
+ }
+
+ return nodes;
+}
+
+- (CPIndexPath)selectionIndexPath
+{
+ return [_selectionIndexPaths count] > 0 ? [_selectionIndexPaths objectAtIndex:0] : nil;
+}
+
+- (BOOL)setSelectionIndexPath:(CPIndexPath)indexPath
+{
+ var paths = indexPath ? [CPArray arrayWithObject:indexPath] : [CPArray array];
+ return[self setSelectionIndexPaths:paths];
+}
+
+- (CPArray)selectionIndexPaths { return _selectionIndexPaths; }
+
+- (BOOL)setSelectionIndexPaths:(CPArray)indexPaths
+{
+ [self _selectionWillChange];
+ var result = [self __setSelectionIndexPaths:indexPaths avoidEmpty:NO];
+ [self _selectionDidChange];
+
+ return result;
+}
+
+- (BOOL)__setSelectionIndexPaths:(CPArray)indexPaths avoidEmpty:(BOOL)avoidEmpty
+{
+ var newPaths = indexPaths;
+
+ if (!newPaths)
+ newPaths = [CPArray array];
+
+ if (![newPaths count] && avoidEmpty)
+ {
+ if ([[[self arrangedObjects] childNodes] count] > 0)
+ newPaths = [CPArray arrayWithObject:[CPIndexPath indexPathWithIndex:0]];
+ }
+
+ if ([_selectionIndexPaths isEqualToArray:newPaths])
+ return NO;
+
+ [self willChangeValueForKey:@"selectionIndexPaths"];
+
+ _selectionIndexPaths = [newPaths copy];
+
+ var binderClass = [[self class] _binderClassForBinding:@"selectionIndexPaths"];
+
+ if (binderClass)
+ {
+ var binding = [binderClass getBinding:@"selectionIndexPaths" forObject:self];
+
+ if (binding)
+ [binding reverseSetValueFor:@"selectionIndexPaths"];
+ }
+
+ [self didChangeValueForKey:@"selectionIndexPaths"];
+
+ return YES;
+}
+
+- (BOOL)addSelectionIndexPaths:(CPArray)indexPaths
+{
+ var newPaths = [_selectionIndexPaths mutableCopy];
+
+ [newPaths addObjectsFromArray:indexPaths];
+
+ return [self setSelectionIndexPaths:newPaths];
+}
+
+- (BOOL)removeSelectionIndexPaths:(CPArray)indexPaths
+{
+ var newPaths = [_selectionIndexPaths mutableCopy];
+ [newPaths removeObjectsInArray:indexPaths];
+ return[self setSelectionIndexPaths:newPaths];
+}
+
+- (CPArray)selectedNodes
+{
+ var nodes = [CPMutableArray array],
+ count = [_selectionIndexPaths count];
+
+ for (var i = 0; i < count; i++)
+ {
+ var node = [[self arrangedObjects] descendantNodeAtIndexPath:[_selectionIndexPaths objectAtIndex:i]];
+ if (node)
+ [nodes addObject:node];
+ }
+ return nodes;
+}
+
+- (CPArray)selectedObjects
+{
+ var objects = [CPMutableArray array],
+ nodes = [self selectedNodes],
+ count = [nodes count];
+
+ for (var i = 0; i < count; i++)
+ [objects addObject:[[nodes objectAtIndex:i] representedObject]];
+
+ return objects;
+}
+
+- (BOOL)__setSelectedObjects:(CPArray)objects
+{
+ if (!objects || [objects count] === 0)
+ return[self __setSelectionIndexPaths:[CPArray array] avoidEmpty:_avoidsEmptySelection];
+
+ var newPaths = [CPMutableArray array];
+ for (var i = 0, count = [objects count]; i < count; i++)
+ {
+ var path = [self _indexPathForObject:[objects objectAtIndex:i] inNode:[self arrangedObjects]];
+ if (path)
+ [newPaths addObject:path];
+ }
+
+ return[self __setSelectionIndexPaths:newPaths avoidEmpty:_avoidsEmptySelection];
+}
+
+- (CPIndexPath)_indexPathForObject:(id)anObject inNode:(CPTreeNode)node
+{
+ if ([node representedObject] === anObject && [node parentNode] != nil)
+ return [node indexPath];
+
+ var children = [node childNodes];
+ if (children)
+ {
+ for (var i = 0, count = [children count]; i < count; i++)
+ {
+ var found = [self _indexPathForObject:anObject inNode:[children objectAtIndex:i]];
+ if (found)
+ return found;
+ }
+ }
+ return nil;
+}
+
+- (BOOL)canInsert { return[self isEditable]; }
+- (BOOL)canInsertChild { return [self isEditable] &&[_selectionIndexPaths count] > 0; }
+- (BOOL)canAddChild { return [self canInsertChild]; }
+
+- (void)add:(id)sender
+{
+ if (![self canInsert]) return;
+
+ var newObject = [self automaticallyPreparesContent] ? [self newObject] :[self _defaultNewObject],
+ selectionPath = [self selectionIndexPath];
+
+ if (!selectionPath)
+ selectionPath = [CPIndexPath indexPathWithIndex:[[[self arrangedObjects] childNodes] count]];
+
+ var length = [selectionPath length],
+ lastIndex = [selectionPath indexAtPosition:length - 1],
+ insertPath = [selectionPath indexPathByRemovingLastIndex];
+
+ insertPath = [insertPath indexPathByAddingIndex:lastIndex + 1];
+
+ [self insertObject:newObject atArrangedObjectIndexPath:insertPath];
+}
+
+- (void)addChild:(id)sender
+{
+ if (![self canAddChild])
+ return;
+
+ var newObject = [self automaticallyPreparesContent] ?[self newObject] : [self _defaultNewObject],
+ parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:[self selectionIndexPath]],
+ childCount = [[parentNode childNodes] count],
+ insertPath = [[self selectionIndexPath] indexPathByAddingIndex:childCount];
+
+ [self insertObject:newObject atArrangedObjectIndexPath:insertPath];
+}
+
+- (void)insert:(id)sender
+{
+ if (![self canInsert]) return;
+
+ var newObject = [self automaticallyPreparesContent] ? [self newObject] :[self _defaultNewObject],
+ indexPath = [self selectionIndexPath] || [CPIndexPath indexPathWithIndex:0];
+
+ [self insertObject:newObject atArrangedObjectIndexPath:indexPath];
+}
+
+- (void)insertChild:(id)sender
+{
+ if (![self canInsertChild]) return;
+
+ var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject],
+ insertPath = [[self selectionIndexPath] indexPathByAddingIndex:0];
+
+ [self insertObject:newObject atArrangedObjectIndexPath:insertPath];
+}
+
+- (void)insertObject:(id)anObject atArrangedObjectIndexPath:(CPIndexPath)indexPath
+{
+ [self insertObjects:[CPArray arrayWithObject:anObject] atArrangedObjectIndexPaths:[CPArray arrayWithObject:indexPath]];
+}
+
+- (void)insertObjects:(CPArray)objects atArrangedObjectIndexPaths:(CPArray)indexPaths
+{
+ [self willChangeValueForKey:@"content"];
+ _disableSetContent = YES;
+
+ var count = [objects count];
+ for (var i = 0; i < count; i++)
+ {
+ var object = [objects objectAtIndex:i],
+ path = [indexPaths objectAtIndex:i],
+ length = [path length];
+
+ if (length === 1)
+ {[_contentObject insertObject:object atIndex:[path indexAtPosition:0]];
+ }
+ else
+ {
+ var parentPath = [path indexPathByRemovingLastIndex],
+ parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
+
+ if (parentNode)
+ {
+ var parentObj = [parentNode representedObject],
+ childIndex = [path indexAtPosition:length - 1];
+
+ var children = [parentObj valueForKeyPath:_childrenKeyPath];
+ if (!children)
+ {
+ children = [CPMutableArray array];
+ [parentObj setValue:children forKeyPath:_childrenKeyPath];
+ }
+
+ var mutableChildren = [parentObj mutableArrayValueForKeyPath:_childrenKeyPath];
+
+ [mutableChildren insertObject:object atIndex:childIndex];
+ }
+ }
+ }
+
+ var binding = [[self class] _binderClassForBinding:@"contentArray"];
+ if (binding)
+ [[binding getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
+
+ _disableSetContent = NO;
+ [self _rearrangeObjects];
+
+ if ([self selectsInsertedObjects])[self setSelectionIndexPaths:indexPaths];
+
+ [self didChangeValueForKey:@"content"];
+}
+
+- (void)remove:(id)sender
+{
+ [self removeObjectsAtArrangedObjectIndexPaths:_selectionIndexPaths];
+}
+
+- (void)removeObjectAtArrangedObjectIndexPath:(CPIndexPath)indexPath
+{
+ [self removeObjectsAtArrangedObjectIndexPaths:[CPArray arrayWithObject:indexPath]];
+}
+
+- (void)removeObjectsAtArrangedObjectIndexPaths:(CPArray)indexPaths
+{
+ [self willChangeValueForKey:@"content"];
+ _disableSetContent = YES;
+
+ var sortedPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)],
+ count = [sortedPaths count];
+
+ for (var i = count - 1; i >= 0; i--)
+ {
+ var path = [sortedPaths objectAtIndex:i],
+ length = [path length];
+
+ if (length === 1)
+ {
+ [_contentObject removeObjectAtIndex:[path indexAtPosition:0]];
+ }
+ else
+ {
+ var parentPath = [path indexPathByRemovingLastIndex],
+ parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath];
+
+ if (parentNode)
+ {
+ var parentObj = [parentNode representedObject],
+ childIndex = [path indexAtPosition:length - 1],
+ mutableChildren = [parentObj mutableArrayValueForKeyPath:_childrenKeyPath];
+
+ if (mutableChildren && childIndex <[mutableChildren count])
+ [mutableChildren removeObjectAtIndex:childIndex];
+ }
+ }
+ }
+
+ var binding = [[self class] _binderClassForBinding:@"contentArray"];
+ if (binding)
+ [[binding getBinding:@"contentArray" forObject:self] _contentArrayDidChange];
+
+ _disableSetContent = NO;
+ [self _rearrangeObjects];
+ [self didChangeValueForKey:@"content"];
+}
+
+- (void)moveNode:(CPTreeNode)node toIndexPath:(CPIndexPath)indexPath
+{
+ [self moveNodes:[CPArray arrayWithObject:node] toIndexPath:indexPath];
+}
+
+- (void)moveNodes:(CPArray)nodes toIndexPath:(CPIndexPath)startingIndexPath
+{[CPException raise:CPUnsupportedMethodException reason:@"moveNodes:toIndexPath: is not yet implemented in CPTreeController."];
+}
+
+@end
+
+var CPTreeControllerAvoidsEmptySelection = @"CPTreeControllerAvoidsEmptySelection",
+CPTreeControllerPreservesSelection = @"CPTreeControllerPreservesSelection",
+CPTreeControllerSelectsInsertedObjects = @"CPTreeControllerSelectsInsertedObjects",
+CPTreeControllerAlwaysUsesMultipleValuesMarker = @"CPTreeControllerAlwaysUsesMultipleValuesMarker",
+CPTreeControllerChildrenKeyPath = @"CPTreeControllerChildrenKeyPath",
+CPTreeControllerCountKeyPath = @"CPTreeControllerCountKeyPath",
+CPTreeControllerLeafKeyPath = @"CPTreeControllerLeafKeyPath";
+
+@implementation CPTreeController (CPCoding)
+
+- (id)initWithCoder:(CPCoder)aCoder
+{
+ self = [super initWithCoder:aCoder];
+
+ if (self)
+ {
+ _avoidsEmptySelection = [aCoder decodeBoolForKey:CPTreeControllerAvoidsEmptySelection];
+ _preservesSelection = [aCoder decodeBoolForKey:CPTreeControllerPreservesSelection];
+ _selectsInsertedObjects = [aCoder decodeBoolForKey:CPTreeControllerSelectsInsertedObjects];
+ _alwaysUsesMultipleValuesMarker = [aCoder decodeBoolForKey:CPTreeControllerAlwaysUsesMultipleValuesMarker];
+
+ _childrenKeyPath = [aCoder decodeObjectForKey:CPTreeControllerChildrenKeyPath] || @"children";
+ _countKeyPath = [aCoder decodeObjectForKey:CPTreeControllerCountKeyPath];
+ _leafKeyPath = [aCoder decodeObjectForKey:CPTreeControllerLeafKeyPath];
+
+ _sortDescriptors = [CPArray array];
+ _selectionIndexPaths = [CPArray array];
+ _arrangedObjects = [[CPTreeNode alloc] initWithRepresentedObject:nil];
+
+ if (![self content] && [self automaticallyPreparesContent])
+ [self prepareContent];
+ else if (![self content])
+ [self _setContentArray:[CPArray array]];
+ }
+
+ return self;
+}
+
+- (void)encodeWithCoder:(CPCoder)aCoder
+{
+ [super encodeWithCoder:aCoder];
+
+ [aCoder encodeBool:_avoidsEmptySelection forKey:CPTreeControllerAvoidsEmptySelection];
+ [aCoder encodeBool:_preservesSelection forKey:CPTreeControllerPreservesSelection];
+ [aCoder encodeBool:_selectsInsertedObjects forKey:CPTreeControllerSelectsInsertedObjects];
+ [aCoder encodeBool:_alwaysUsesMultipleValuesMarker forKey:CPTreeControllerAlwaysUsesMultipleValuesMarker];
+ [aCoder encodeObject:_childrenKeyPath forKey:CPTreeControllerChildrenKeyPath];
+ [aCoder encodeObject:_countKeyPath forKey:CPTreeControllerCountKeyPath];
+ [aCoder encodeObject:_leafKeyPath forKey:CPTreeControllerLeafKeyPath];
+}
+
+- (void)awakeFromCib
+{
+ [self _selectionWillChange];
+ [self _selectionDidChange];
+}
+
+@end
diff --git a/AppKit/CPTreeNode.j b/AppKit/CPTreeNode.j
index 0f5f88a33..f25c83649 100644
--- a/AppKit/CPTreeNode.j
+++ b/AppKit/CPTreeNode.j
@@ -22,13 +22,12 @@
@import
@import
-
+@import
@implementation CPTreeNode : CPObject
{
- id _representedObject @accessors(readonly, property=representedObject);
-
- CPTreeNode _parentNode @accessors(readonly, property=parentNode);
+ id _representedObject @accessors(property=representedObject);
+ CPTreeNode _parentNode @accessors(property=parentNode);
CPMutableArray _childNodes;
}
@@ -52,36 +51,35 @@
- (CPIndexPath)indexPath
{
- if (_parentNode != nil)
+ // If we have a parent, calculate path based on parent's path + our index
+ if (_parentNode)
{
- var path;
- var index;
-
- index = [[_parentNode childNodes] indexOfObject:self];
- path = [_parentNode indexPath];
-
- if (path != nil)
- {
- return [path indexPathByAddingIndex:index];
- }
- else
- {
- return [CPIndexPath indexPathWithIndex:index];
- }
- }
- else
- {
- return nil;
+ // Search the parent's child nodes, not our own!
+ var index = [[_parentNode childNodes] indexOfObjectIdenticalTo:self];
+
+ // If the parent is the root (and technically has no path itself in some implementations),
+ // we might get nil. Handle that gracefully.
+ var parentPath = [_parentNode indexPath];
+
+ if (parentPath)
+ return [parentPath indexPathByAddingIndex:index];
+
+ return [CPIndexPath indexPathWithIndex:index];
}
+
+ // If we are the root, we don't have an index path in the context of a tree controller usually,
+ // or we are [] (empty path). Returning nil is acceptable for the absolute root.
+ return nil;
}
- (BOOL)isLeaf
{
- return [_childNodes count] <= 0;
+ return [_childNodes count] == 0;
}
- (CPArray)childNodes
{
+ // Return a copy to prevent external modification without KVC
return [_childNodes copy];
}
@@ -90,18 +88,29 @@
return [self mutableArrayValueForKey:@"childNodes"];
}
-- (void)insertObject:(id)aTreeNode inChildNodesAtIndex:(CPInteger)anIndex
-{
- [[aTreeNode._parentNode mutableChildNodes] removeObjectIdenticalTo:aTreeNode];
+// MARK: - KVC Compliance Methods
- aTreeNode._parentNode = self;
+- (void)insertObject:(CPTreeNode)aTreeNode inChildNodesAtIndex:(CPInteger)anIndex
+{
+ // Optional: Auto-detach from old parent if strictly moving nodes
+ if ([aTreeNode isKindOfClass:[CPTreeNode class]] && aTreeNode._parentNode)
+ {
+ [[aTreeNode._parentNode mutableChildNodes] removeObjectIdenticalTo:aTreeNode];
+ }
+
+ // Direct ivar access is allowed here since we are inside the class implementation
+ if ([aTreeNode isKindOfClass:[CPTreeNode class]])
+ aTreeNode._parentNode = self;
[_childNodes insertObject:aTreeNode atIndex:anIndex];
}
- (void)removeObjectFromChildNodesAtIndex:(CPInteger)anIndex
{
- [_childNodes objectAtIndex:anIndex]._parentNode = nil;
+ var node = [_childNodes objectAtIndex:anIndex];
+
+ if ([node isKindOfClass:[CPTreeNode class]])
+ node._parentNode = nil;
[_childNodes removeObjectAtIndex:anIndex];
}
@@ -110,17 +119,34 @@
{
var oldTreeNode = [_childNodes objectAtIndex:anIndex];
- oldTreeNode._parentNode = nil;
- aTreeNode._parentNode = self;
+ if ([oldTreeNode isKindOfClass:[CPTreeNode class]])
+ oldTreeNode._parentNode = nil;
+
+ if ([aTreeNode isKindOfClass:[CPTreeNode class]])
+ aTreeNode._parentNode = self;
[_childNodes replaceObjectAtIndex:anIndex withObject:aTreeNode];
}
+// MARK: - Convenience Accessors
+
- (id)objectInChildNodesAtIndex:(CPInteger)anIndex
{
- return _childNodes[anIndex];
+ return [_childNodes objectAtIndex:anIndex];
}
+- (CPInteger)count
+{
+ return [_childNodes count];
+}
+
+- (id)objectAtIndex:(CPInteger)anIndex
+{
+ return [_childNodes objectAtIndex:anIndex];
+}
+
+// MARK: - Utility
+
- (void)sortWithSortDescriptors:(CPArray)sortDescriptors recursively:(BOOL)shouldSortRecursively
{
[_childNodes sortUsingDescriptors:sortDescriptors];
@@ -129,25 +155,39 @@
return;
var count = [_childNodes count];
-
while (count--)
- [_childNodes[count] sortWithSortDescriptors:sortDescriptors recursively:YES];
+ {
+ var child = [_childNodes objectAtIndex:count];
+ if ([child respondsToSelector:@selector(sortWithSortDescriptors:recursively:)])
+ [child sortWithSortDescriptors:sortDescriptors recursively:YES];
+ }
}
- (CPTreeNode)descendantNodeAtIndexPath:(CPIndexPath)indexPath
{
- var index = 0,
- count = [indexPath length],
- node = self;
+ if (!indexPath || [indexPath length] == 0)
+ return self;
- for (; index < count; ++index)
- node = [node objectInChildNodesAtIndex:[indexPath indexAtPosition:index]];
+ var node = self,
+ length = [indexPath length];
+
+ for (var i = 0; i < length; i++)
+ {
+ var index = [indexPath indexAtPosition:i],
+ count = [node count];
+
+ if (index >= count || index < 0)
+ return nil;
+
+ node = [node objectAtIndex:index];
+ }
return node;
}
@end
+// Coding implementation remains correct
var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey",
CPTreeNodeParentNodeKey = @"CPTreeNodeParentNodeKey",
CPTreeNodeChildNodesKey = @"CPTreeNodeChildNodesKey";
@@ -163,6 +203,10 @@ var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey",
_representedObject = [aCoder decodeObjectForKey:CPTreeNodeRepresentedObjectKey];
_parentNode = [aCoder decodeObjectForKey:CPTreeNodeParentNodeKey];
_childNodes = [aCoder decodeObjectForKey:CPTreeNodeChildNodesKey];
+
+ // Safety check to ensure decoding gave us a CPArray
+ if (!_childNodes)
+ _childNodes = [[CPMutableArray alloc] init];
}
return self;
diff --git a/AppKit/CPView.j b/AppKit/CPView.j
index adfd25a1f..31b26f017 100644
--- a/AppKit/CPView.j
+++ b/AppKit/CPView.j
@@ -662,6 +662,11 @@ var CPViewHighDPIDrawingEnabled = YES;
#endif
}
+#if PLATFORM(DOM)
+ var origin = aSubview._frame.origin;
+ CPDOMDisplayServerSetStyleLeftTop(aSubview._DOMElement, _boundsTransform, origin.x, origin.y);
+#endif
+
[aSubview setNextResponder:self];
[aSubview _scaleSizeUnitSquareToSize:[self _hierarchyScaleSize]];
diff --git a/AppKit/CPWindow/CPWindow.j b/AppKit/CPWindow/CPWindow.j
index 023193871..1b233d067 100644
--- a/AppKit/CPWindow/CPWindow.j
+++ b/AppKit/CPWindow/CPWindow.j
@@ -1266,6 +1266,10 @@ CPTexturedBackgroundWindowMask
[_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[_windowView addSubview:_contentView];
+ // The window view manages the exact layout of the content view (e.g. offsetting for the toolbar).
+ if ([_windowView respondsToSelector:@selector(tile)])
+ [_windowView tile];
+
/*
If the initial first responder has been set to something other than
the window, set it to the window because it will no longer be valid.
diff --git a/AppKit/CoreAnimation/CAAnimationGroup.j b/AppKit/CoreAnimation/CAAnimationGroup.j
new file mode 100644
index 000000000..936af3b44
--- /dev/null
+++ b/AppKit/CoreAnimation/CAAnimationGroup.j
@@ -0,0 +1,68 @@
+/*
+ * CAAnimationGroup.j
+ * AppKit
+ * Created by Daniel Boehringer.
+ * Copyright 2025.
+ *
+ * Implements grouping for Core Animation.
+ */
+
+@import
+@import "CAAnimation.j"
+
+@implementation CAAnimationGroup : CAAnimation
+{
+ CPArray _animations;
+}
+
++ (id)group
+{
+ return [[self alloc] init];
+}
+
+- (id)init
+{
+ if (self = [super init])
+ {
+ _animations = [];
+ }
+ return self;
+}
+
+- (void)setAnimations:(CPArray)anArray
+{
+ if (_animations === anArray)
+ return;
+
+ _animations = anArray;
+}
+
+- (CPArray)animations
+{
+ return _animations;
+}
+
+/*
+ Iterates through children and executes them recursively.
+ This effectively runs all grouped animations concurrently.
+*/
+- (void)runActionForKey:(CPString)aKey object:(id)anObject arguments:(CPDictionary)arguments
+{
+ var count = [_animations count],
+ i = 0;
+
+ for (; i < count; i++)
+ {
+ var animation = [_animations objectAtIndex:i];
+
+ // Recursively call runActionForKey on the child.
+ // If the child is a CABasicAnimation, it will call [anObject addAnimation:...]
+ // If the child is another Group, it will recurse here.
+ if ([animation respondsToSelector:@selector(runActionForKey:object:arguments:)])
+ {
+ [animation runActionForKey:aKey object:anObject arguments:arguments];
+ }
+ }
+}
+
+@end
diff --git a/AppKit/CoreAnimation/CALayer.j b/AppKit/CoreAnimation/CALayer.j
index 20d2dae66..4cd003635 100644
--- a/AppKit/CoreAnimation/CALayer.j
+++ b/AppKit/CoreAnimation/CALayer.j
@@ -29,6 +29,8 @@
@import "CGGeometry.j"
@import "CPColor.j"
@import "CPView.j"
+@import "CAMediaTimingFunction.j"
+
#define DOM(aLayer) aLayer._DOMElement
@@ -118,6 +120,8 @@ var CALayerRegisteredRunLoopUpdates = nil;
CGAffineTransform _transformToLayer;
CGAffineTransform _transformFromLayer;
+
+ CPMutableDictionary _activeAnimations;
}
@global document
@@ -160,6 +164,8 @@ var CALayerRegisteredRunLoopUpdates = nil;
_sublayers = [];
+ _activeAnimations = [CPMutableDictionary dictionary];
+
#if PLATFORM(DOM)
_DOMElement = document.createElement("div");
@@ -977,6 +983,244 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex)
return _delegate;
}
+/*
+ Adds an animation to the layer.
+ Supports CABasicAnimation for Numbers (opacity) and Points (position/anchorPoint).
+ The animation is exerted by means of periodically applying the keypath on the delegate
+ Only works if the delegate is set!
+*/
+- (void)addAnimation:(CAAnimation)anim forKey:(CPString)key
+{
+ if (!anim) return;
+
+ // --- 1. Handle Animation Groups ---
+ // If it's a group, we simply schedule its children individually.
+ if ([anim respondsToSelector:@selector(animations)] && [anim animations])
+ {
+ var animations = [anim animations],
+ count = [animations count],
+ i = 0;
+
+ for (; i < count; i++)
+ {
+ var child = [animations objectAtIndex:i];
+
+ // Recurse: Add the child animation.
+ // We pass 'nil' for the key so the child's own 'keyPath'
+ // is used as the storage identifier in the dictionary.
+ [self addAnimation:child forKey:nil];
+ }
+ return;
+ }
+
+ // --- 2. Determine KeyPath ---
+ var keyPath = key;
+
+ // If the animation object has an explicit keyPath (like CABasicAnimation), use it.
+ if ([anim respondsToSelector:@selector(keyPath)] && [anim keyPath])
+ keyPath = [anim keyPath];
+
+ // If we can't determine a property to animate, we must abort.
+ if (!keyPath) return;
+
+ // --- 3. Determine Values ---
+ var startValue = ([anim respondsToSelector:@selector(fromValue)]) ? [anim fromValue] : nil;
+
+ // If startValue is missing, try to read it from the layer.
+ // We wrap this in a try-catch to prevent crashes if 'keyPath' is invalid.
+ if (startValue == nil)
+ {
+ try {
+ startValue = [[self delegate] valueForKey:keyPath];
+ }
+ catch (e) {
+ // The keyPath was likely invalid (not KVC compliant), abort.
+ return;
+ }
+ }
+
+ var endValue = ([anim respondsToSelector:@selector(toValue)]) ? [anim toValue] : nil;
+
+ if (endValue == nil)
+ return;
+
+ var duration = ([anim respondsToSelector:@selector(duration)]) ? [anim duration] : 0.25;
+
+ // Default to EaseInEaseOut if not specified
+ var timingFunction = ([anim respondsToSelector:@selector(timingFunction)]) ? [anim timingFunction] : [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
+
+ // --- 4. Prepare Context ---
+ var context = {
+ "animation": anim,
+ "keyPath": keyPath,
+ "startValue": startValue,
+ "endValue": endValue,
+ "duration": duration * 1000.0, // ms
+ "timingFunction": timingFunction,
+ "startTime": null,
+ "requestId": null
+ };
+
+ // --- 5. Render Loop ---
+ var _self = self;
+
+ var renderLoop = function(timestamp) {
+ if ([_self _renderAnimationStep:context timestamp:timestamp])
+ context.requestId = window.requestAnimationFrame(renderLoop);
+ else
+ context.requestId = null;
+ };
+
+ // --- 6. Storage & Kickoff ---
+ // Use the keyPath as the identifier if no specific key was provided
+ var storageKey = (key && key.length > 0) ? key : keyPath;
+
+ // Remove any conflicting animation on this specific property/key
+ [self removeAnimationForKey:storageKey];
+
+ context.requestId = window.requestAnimationFrame(renderLoop);
+ [_activeAnimations setObject:context forKey:storageKey];
+}
+
+- (void)removeAnimationForKey:(CPString)key
+{
+ var context = [_activeAnimations objectForKey:key];
+ if (context)
+ {
+ if (context.requestId !== null)
+ window.cancelAnimationFrame(context.requestId);
+ [_activeAnimations removeObjectForKey:key];
+ }
+}
+
+- (void)removeAllAnimations
+{
+ var keys = [_activeAnimations allKeys],
+ count = [keys count];
+ while (count--)
+ [self removeAnimationForKey:[keys objectAtIndex:count]];
+}
+
+/*
+ Solves Cubic Bezier for t.
+ p1, p2 are the control points (x,y). p0 is 0,0, p3 is 1,1.
+ This is a simplified solver for standard Core Animation timing functions.
+*/
+- (float)_solveBezier:(float)t forTimingFunction:(CAMediaTimingFunction)tf
+{
+ if (!tf) return t;
+
+ // Linear optimization
+ if (tf === [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear])
+ return t;
+
+ var points = [tf controlPoints]; // [c1x, c1y, c2x, c2y]
+ var p1x = points[0], p1y = points[1],
+ p2x = points[2], p2y = points[3];
+
+ // Simple polynomial evaluation (De Casteljau's algorithm/Cubic formula subset)
+ // Since we are usually dealing with standard easing, we can approximate 1D easing on the Y axis
+ // based on linear time X, or do a full solve.
+ // For brevity/speed in JS, we often approximate basic easing:
+
+ // 3t^2 * (1-t) + t^3 ... standard bezier blending functions
+ var cx = 3.0 * p1x;
+ var bx = 3.0 * (p2x - p1x) - cx;
+ var ax = 1.0 - cx - bx;
+
+ var cy = 3.0 * p1y;
+ var by = 3.0 * (p2y - p1y) - cy;
+ var ay = 1.0 - cy - by;
+
+ // Solve for X given t (time) using Newton-Raphson
+ var sampleT = t;
+ for (var i = 0; i < 5; i++) {
+ var x = ((ax * sampleT + bx) * sampleT + cx) * sampleT - t;
+ if (Math.abs(x) < 1e-3) break;
+ var d = (3.0 * ax * sampleT + 2.0 * bx) * sampleT + cx;
+ if (Math.abs(d) < 1e-6) break;
+ sampleT = sampleT - x / d;
+ }
+
+ // Solve for Y given derived T
+ return ((ay * sampleT + by) * sampleT + cy) * sampleT;
+}
+
+- (BOOL)_renderAnimationStep:(JSObject)context timestamp:(double)timestamp
+{
+ if (context.startTime === null)
+ context.startTime = timestamp;
+
+ var elapsed = timestamp - context.startTime,
+ linearProgress = elapsed / context.duration;
+
+ if (linearProgress > 1.0) linearProgress = 1.0;
+
+ // Apply Timing Function
+ var progress = [self _solveBezier:linearProgress forTimingFunction:context.timingFunction];
+
+ var start = context.startValue,
+ end = context.endValue,
+ current = nil;
+
+ // Number
+ if (typeof start === "number")
+ {
+ current = start + (end - start) * progress;
+ }
+ // Point / Size / Rect
+ else if (start && start.x !== undefined && start.y !== undefined) // CGPoint
+ {
+ current = CGPointMake(start.x + (end.x - start.x) * progress,
+ start.y + (end.y - start.y) * progress);
+ }
+ else if (start && start.width !== undefined && start.height !== undefined) // CGSize
+ {
+ current = CGSizeMake(start.width + (end.width - start.width) * progress,
+ start.height + (end.height - start.height) * progress);
+ }
+ else if (start && start.origin !== undefined && start.size !== undefined) // CGRect
+ {
+ current = CGRectMake(
+ start.origin.x + (end.origin.x - start.origin.x) * progress,
+ start.origin.y + (end.origin.y - start.origin.y) * progress,
+ start.size.width + (end.size.width - start.size.width) * progress,
+ start.size.height + (end.size.height - start.size.height) * progress
+ );
+ }
+
+ if (current !== nil)
+ [[self delegate] setValue:current forKey:context.keyPath];
+
+ if (linearProgress >= 1.0)
+ {
+ var anim = context.animation;
+
+ // Cleanup
+ var shouldRemove = [anim respondsToSelector:@selector(isRemovedOnCompletion)] ? [anim isRemovedOnCompletion] : YES;
+
+ if (shouldRemove) {
+ // Find key by context identity to handle groups correctly
+ var keys = [_activeAnimations allKeys];
+ for (var i = 0; i < keys.length; i++) {
+ if ([_activeAnimations objectForKey:keys[i]] === context) {
+ [_activeAnimations removeObjectForKey:keys[i]];
+ break;
+ }
+ }
+ }
+
+ // Delegate
+ var delegate = [anim delegate];
+ if (delegate && [delegate respondsToSelector:@selector(animationDidStop:finished:)])
+ [delegate animationDidStop:anim finished:YES];
+
+ return NO;
+ }
+
+ return YES;
+}
+
/* @ignore */
- (void)_setOwningView:(CPView)anOwningView
{
diff --git a/AppKit/Platform/DOM/CPPlatformPasteboard.j b/AppKit/Platform/DOM/CPPlatformPasteboard.j
index 50310503f..102feb670 100644
--- a/AppKit/Platform/DOM/CPPlatformPasteboard.j
+++ b/AppKit/Platform/DOM/CPPlatformPasteboard.j
@@ -305,7 +305,7 @@ var hasEditableTarget = function(aDOMEvent)
location = [[CPApp currentEvent] locationInWindow],
anEvent = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags
timestamp:timestamp windowNumber:windowNumber context:nil
- characters:characters charactersIgnoringModifiers:characters isARepeat:NO keyCode:keyCode];
+ characters:characters charactersIgnoringModifiers:characters isARepeat:NO keyCode:keyCode isActionKey:YES];
anEvent._data1 = @{ "simulated": YES };
anEvent._DOMEvent = aDOMEvent;
diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j
index 28246375f..7c714c263 100644
--- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j
+++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j
@@ -192,6 +192,33 @@ KeyCodesToUnicodeMap[CPKeyCodes.OPEN_SQUARE_BRACKET] = "[";
KeyCodesToUnicodeMap[CPKeyCodes.BACKSLASH] = "\\";
KeyCodesToUnicodeMap[CPKeyCodes.CLOSE_SQUARE_BRACKET] = "]";
+var KeyNameToUnicodeMap = {};
+KeyNameToUnicodeMap["Backspace"] = CPDeleteCharacter;
+KeyNameToUnicodeMap["Delete"] = CPDeleteFunctionKey;
+KeyNameToUnicodeMap["Tab"] = CPTabCharacter;
+KeyNameToUnicodeMap["Enter"] = CPCarriageReturnCharacter;
+KeyNameToUnicodeMap["Escape"] = CPEscapeFunctionKey;
+KeyNameToUnicodeMap["PageUp"] = CPPageUpFunctionKey;
+KeyNameToUnicodeMap["PageDown"] = CPPageDownFunctionKey;
+KeyNameToUnicodeMap["ArrowLeft"] = CPLeftArrowFunctionKey;
+KeyNameToUnicodeMap["ArrowUp"] = CPUpArrowFunctionKey;
+KeyNameToUnicodeMap["ArrowRight"] = CPRightArrowFunctionKey;
+KeyNameToUnicodeMap["ArrowDown"] = CPDownArrowFunctionKey;
+KeyNameToUnicodeMap["Home"] = CPHomeFunctionKey;
+KeyNameToUnicodeMap["End"] = CPEndFunctionKey;
+// Add safeguards for punctuation
+KeyNameToUnicodeMap[";"] = ";";
+KeyNameToUnicodeMap["-"] = "-";
+KeyNameToUnicodeMap["="] = "=";
+KeyNameToUnicodeMap[","] = ",";
+KeyNameToUnicodeMap["."] = ".";
+KeyNameToUnicodeMap["/"] = "/";
+KeyNameToUnicodeMap["`"] = "`";
+KeyNameToUnicodeMap["'"] = "'";
+KeyNameToUnicodeMap["["] = "[";
+KeyNameToUnicodeMap["\\"] = "\\";
+KeyNameToUnicodeMap["]"] = "]";
+
var ModifierKeyCodes = [
CPKeyCodes.META,
CPKeyCodes.WEBKIT_RIGHT_META,
@@ -438,7 +465,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
theDocument.addEventListener("keyup", keyEventCallback, NO);
theDocument.addEventListener("keydown", keyEventCallback, NO);
- theDocument.addEventListener("keypress", keyEventCallback, NO);
+ // "keypress" listener removed as it's deprecated and no longer used in the new logic.
theDocument.addEventListener("touchstart", touchEventCallback, {passive: false});
theDocument.addEventListener("touchend", touchEventCallback, {passive: false});
@@ -470,7 +497,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
theDocument.removeEventListener("keyup", keyEventCallback, NO);
theDocument.removeEventListener("keydown", keyEventCallback, NO);
- theDocument.removeEventListener("keypress", keyEventCallback, NO);
theDocument.removeEventListener("touchstart", touchEventCallback, NO);
theDocument.removeEventListener("touchend", touchEventCallback, NO);
@@ -503,7 +529,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
theDocument.attachEvent("onkeyup", keyEventCallback);
theDocument.attachEvent("onkeydown", keyEventCallback);
- theDocument.attachEvent("onkeypress", keyEventCallback);
+ // "onkeypress" listener removed.
_DOMWindow.attachEvent("onresize", resizeEventCallback);
@@ -533,7 +559,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
theDocument.detachEvent("onkeyup", keyEventCallback);
theDocument.detachEvent("onkeydown", keyEventCallback);
- theDocument.detachEvent("onkeypress", keyEventCallback);
_DOMWindow.detachEvent("onresize", resizeEventCallback);
@@ -709,157 +734,138 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
(_capsLockActive ? CPAlphaShiftKeyMask : 0);
// With a few exceptions, all key events are blocked from propagating to
- // the browser. Here the following exceptions are being allowed:
- //
- // - All keys pressed along with a ctrl or cmd key _unless_ they are in
- // one of the two blacklists.
- // - Any key listed in the whitelist.
- //
- // The ctrl/cmd keys are used for browser hotkeys as are the keys listed in
- // the whitelist (F1-F12 at the time of writing).
- //
- // If a key is listed in both the blacklist and whitelist, the blacklist is
- // checked first. The key will be blocked from propagating in that case.
+ // the browser. The logic here allows browser shortcuts (Cmd/Ctrl keys)
+ // and function keys (F1-F12) to pass through, unless explicitly blacklisted.
StopDOMEventPropagation = YES;
+ var keyCodeForPropagationCheck = aDOMEvent.keyCode || 0;
+ var charForPropagationCheck = String.fromCharCode(keyCodeForPropagationCheck).toLowerCase();
- // Make sure it is not in the blacklists.
- if (!(CharacterKeysToPrevent[String.fromCharCode(aDOMEvent.keyCode || aDOMEvent.charCode).toLowerCase()] || KeyCodesToPrevent[aDOMEvent.keyCode]))
+ if (!(CharacterKeysToPrevent[charForPropagationCheck] || KeyCodesToPrevent[keyCodeForPropagationCheck]))
{
- // It is not in the blacklist, let it through if the ctrl/cmd key is
- // also down or it's in the whitelist.
- if ((modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) || KeyCodesToAllow[aDOMEvent.keyCode])
+ if ((modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) || KeyCodesToAllow[keyCodeForPropagationCheck])
StopDOMEventPropagation = NO;
}
- var overrideCharacters = nil,
+ var characters = @"",
charactersIgnoringModifiers = @"";
+ var keyCode = aDOMEvent.keyCode;
+ if (keyCode in MozKeyCodeToKeyCodeMap)
+ keyCode = MozKeyCodeToKeyCodeMap[keyCode];
+
+ var isActionKey;
+ var key = aDOMEvent.key;
+
+ if (key) {
+ isActionKey =
+ key === 'Enter' ||
+ key === 'Backspace' ||
+ key === 'Tab' ||
+ key === 'Escape' ||
+ key === 'Delete' ||
+ key.startsWith('Arrow') ||
+ key === 'Home' ||
+ key === 'End' ||
+ key === 'PageUp' ||
+ key === 'PageDown';
+ }
+ else
+ {
+ isActionKey =
+ (keyCode === 13) || (keyCode === 8) || (keyCode === 9) ||
+ (keyCode === 27) || (keyCode === 46) || (keyCode >= 37 && keyCode <= 40);
+ }
+
switch (aDOMEvent.type)
{
case "keydown":
- // Grab and store the keycode now since it is correct and consistent at this point.
- if (aDOMEvent.keyCode in MozKeyCodeToKeyCodeMap)
- _keyCode = MozKeyCodeToKeyCodeMap[aDOMEvent.keyCode];
- else
- _keyCode = aDOMEvent.keyCode;
-
- var characters;
-
- // Handle key codes for which String.fromCharCode won't work.
- // Refs #1036: In Internet Explorer, both 'which' and 'charCode' are undefined for special keys.
- if (aDOMEvent.which === 0 || aDOMEvent.charCode === 0 || (aDOMEvent.which === undefined && aDOMEvent.charCode === undefined))
- characters = KeyCodesToUnicodeMap[_keyCode];
-
- // The problem with keyCode is that this property refers to keys on the keyboard and not to characters
- // This is why String.fromCharCode does not always work in more recent versions of Firefox
- // E.g. pressing a '#' on a German keyboard gives you a charCode of 163, which refers to '£' and not '#'
- // The property key works fine, though. From there we can get the actual character more robustly.
- // Therefore we prefer key over keyCode whenever possible
-
- if (!characters)
- characters = (aDOMEvent.key && aDOMEvent.key.length == 1) ? aDOMEvent.key.toLowerCase() : String.fromCharCode(_keyCode).toLowerCase();
-
- overrideCharacters = (modifierFlags & CPShiftKeyMask || _capsLockActive) ? characters.toUpperCase() : characters;
-
- // check for caps lock state
- if (_keyCode === CPKeyCodes.CAPS_LOCK)
+ if ([ModifierKeyCodes containsObject:keyCode])
{
- _capsLockActive = YES;
-
- // Make sure the caps lock flag is set in modifierFlags
- modifierFlags |= CPAlphaShiftKeyMask;
- }
-
- if ([ModifierKeyCodes containsObject:_keyCode])
- {
- // A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break.
event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags
timestamp:timestamp windowNumber:windowNumber context:nil
- characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:_keyCode];
+ characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:keyCode isActionKey:YES];
+ break;
+ }
- break;
- }
- else if (modifierFlags & (CPControlKeyMask | CPCommandKeyMask))
+ var isARepeat = !!aDOMEvent.repeat || (_charCodes[keyCode] != nil);
+ _charCodes[keyCode] = YES;
+
+ if (aDOMEvent.key)
{
- //we are simply going to skip all keypress events that use cmd/ctrl key
- //this lets us be consistent in all browsers and send on the keydown
- //which means we can cancel the event early enough, but only if sendEvent needs to
- }
- else if (CPKeyCodes.firesKeyPressEvent(_keyCode, aDOMEvent.key, _lastKey, aDOMEvent.shiftKey, aDOMEvent.ctrlKey, aDOMEvent.altKey))
- {
- // this branch is taken by events which fire keydown, keypress, and keyup.
- // this is the only time we'll ALLOW character keys to propagate (needed for text fields)
- StopDOMEventPropagation = NO;
- break;
+ if (aDOMEvent.key.length === 1)
+ {
+ characters = aDOMEvent.key;
+ }
+ // Correctly handle dead keys to prevent inserting "Dead"
+ else if (aDOMEvent.key === "Dead" || aDOMEvent.key === "Process") {
+ characters = @"";
+ }
+ // For other named keys, map them or fall back to an empty string.
+ else
+ {
+ characters = KeyNameToUnicodeMap[aDOMEvent.key] || @"";
+ }
}
else
{
- //this branch is taken by "remedial" key events
- // In this state we continue to keypress and send the CPEvent
+ characters = KeyCodesToUnicodeMap[keyCode];
+
+ if (!characters)
+ {
+ characters = String.fromCharCode(keyCode);
+ if (modifierFlags & CPShiftKeyMask || _capsLockActive)
+ characters = characters.toUpperCase();
+ else
+ characters = characters.toLowerCase();
+ }
}
- case "keypress":
- // we unconditionally break on keypress events with modifiers,
- // because we forced the event to be sent on the keydown
- if (aDOMEvent.type === "keypress" && (modifierFlags & (CPControlKeyMask | CPCommandKeyMask)))
- break;
-
- var keyCode = _keyCode,
- charCode = aDOMEvent.keyCode || aDOMEvent.charCode,
- isARepeat = (_charCodes[keyCode] != nil);
-
- _lastKey = keyCode;
- _charCodes[keyCode] = charCode;
-
- var characters = overrideCharacters;
- // Is this a special key?
- if (!characters && (aDOMEvent.which === 0 || aDOMEvent.charCode === 0))
- characters = KeyCodesToUnicodeMap[charCode];
-
- if (!characters)
- characters = String.fromCharCode(charCode);
-
- charactersIgnoringModifiers = characters.toLowerCase(); // FIXME: This isn't correct. It SHOULD include Shift.
-
- // Safari won't send proper capitalization during cmd-key events
- if (!overrideCharacters && (modifierFlags & CPCommandKeyMask) && ((modifierFlags & CPShiftKeyMask) || _capsLockActive))
- characters = characters.toUpperCase();
+ charactersIgnoringModifiers = characters.toLowerCase();
event = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags
timestamp:timestamp windowNumber:windowNumber context:nil
- characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:charCode];
+ characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:keyCode isActionKey:isActionKey];
break;
case "keyup":
- var keyCode = aDOMEvent.keyCode,
- charCode = _charCodes[keyCode];
-
- _keyCode = -1;
- _lastKey = -1;
_charCodes[keyCode] = nil;
- // check for caps lock state
if (keyCode === CPKeyCodes.CAPS_LOCK)
{
- _capsLockActive = NO;
-
- // Make sure the caps lock flag is cleared in modifierFlags
- modifierFlags &= ~CPAlphaShiftKeyMask;
+ _capsLockActive = !_capsLockActive;
+ if (_capsLockActive)
+ modifierFlags |= CPAlphaShiftKeyMask;
+ else
+ modifierFlags &= ~CPAlphaShiftKeyMask;
}
if ([ModifierKeyCodes containsObject:keyCode])
{
- // A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break.
event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags
timestamp:timestamp windowNumber:windowNumber context:nil
- characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:_keyCode];
-
+ characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:keyCode isActionKey:YES];
break;
}
- var characters = KeyCodesToUnicodeMap[charCode] || String.fromCharCode(charCode);
+ if (aDOMEvent.key)
+ {
+ if (aDOMEvent.key.length === 1) {
+ characters = aDOMEvent.key;
+ }
+ // Ensure keyup events also don't produce "Dead"
+ else if (aDOMEvent.key === "Dead" || aDOMEvent.key === "Process") {
+ characters = @"";
+ }
+ else
+ {
+ characters = KeyNameToUnicodeMap[aDOMEvent.key] || @"";
+ }
+ }
+ else
+ characters = KeyCodesToUnicodeMap[keyCode] || String.fromCharCode(keyCode);
+
charactersIgnoringModifiers = characters.toLowerCase();
if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive)
@@ -867,7 +873,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
event = [CPEvent keyEventWithType:CPKeyUp location:location modifierFlags:modifierFlags
timestamp: timestamp windowNumber:windowNumber context:nil
- characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode];
+ characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode isActionKey:isActionKey];
break;
}
@@ -880,12 +886,11 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
if (event && ![_platformPasteboard windowShouldSuppressKeyEvent])
{
[CPApp sendEvent:event];
-
[_platformPasteboard windowDidSendKeyEvent:event];
}
var didStop = NO;
- // Platform pasteboard can overrule the decision to stop propagation either way, or it might have no opinion.
+
if ([_platformPasteboard windowShouldStopPropagation] || (StopDOMEventPropagation && ![_platformPasteboard windowShouldNotStopPropagation]))
{
didStop = YES;
@@ -1271,6 +1276,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
newEvent.type = CPDOMEventMouseUp;
break;
}
+ newEvent._isFromTouch = true; // Identify the event as touch-originated for tolerant click counting
[self mouseEvent:newEvent];
return;
}
@@ -1385,7 +1391,8 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
if (aDOMEvent.button !== _firstMouseDownButton)
return;
- event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp, location, modifierFlags, timestamp, windowNumber, nil, -1, CPDOMEventGetClickCount(_lastMouseUp, timestamp, location), 0, nil);
+ var clickCount = CPDOMEventGetClickCount(_lastMouseUp, timestamp, location, aDOMEvent._isFromTouch);
+ event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp, location, modifierFlags, timestamp, windowNumber, nil, -1, clickCount, 0, nil);
_mouseIsDown = NO;
_lastMouseUp = event;
@@ -1431,15 +1438,17 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
_DOMEventMode = YES;
_mouseIsDown = YES;
+
+ var clickCount = CPDOMEventGetClickCount(_lastMouseDown, timestamp, location, aDOMEvent._isFromTouch);
// Fake a down and up event so that event tracking mode will work correctly
[CPApp sendEvent:[CPEvent mouseEventWithType:_mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown location:location modifierFlags:modifierFlags
timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:-1
- clickCount:CPDOMEventGetClickCount(_lastMouseDown, timestamp, location) pressure:0]];
+ clickCount:clickCount pressure:0]];
[CPApp sendEvent:[CPEvent mouseEventWithType:_mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp location:location modifierFlags:modifierFlags
timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:-1
- clickCount:CPDOMEventGetClickCount(_lastMouseDown, timestamp, location) pressure:0]];
+ clickCount:clickCount pressure:0]];
return;
}
@@ -1451,7 +1460,8 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
StopContextMenuDOMEventPropagation = YES;
- event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown, location, modifierFlags, timestamp, windowNumber, nil, -1, CPDOMEventGetClickCount(_lastMouseDown, timestamp, location), 0, nil);
+ var clickCount = CPDOMEventGetClickCount(_lastMouseDown, timestamp, location, aDOMEvent._isFromTouch);
+ event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown, location, modifierFlags, timestamp, windowNumber, nil, -1, clickCount, 0, nil);
_mouseIsDown = YES;
_lastMouseDown = event;
@@ -1951,19 +1961,25 @@ var _CPEventFromNativeMouseEvent = function(aNativeEvent, anEventType, aPoint, m
return aNativeEvent;
};
-var CLICK_SPACE_DELTA = 5.0,
- CLICK_TIME_DELTA = (typeof document != "undefined" && document.addEventListener) ? 0.55 : 1.0;
+var CLICK_SPACE_DELTA = 5.0,
+ CLICK_TIME_DELTA = (typeof document != "undefined" && document.addEventListener) ? 0.55 : 1.0,
+ // Define a more generous time delta for touch events to make double-tapping easier.
+ TOUCH_CLICK_TIME_DELTA = 0.80; // Increased from 0.55s to 0.80s
-CPDOMEventGetClickCount = function(aComparisonEvent, aTimestamp, aLocation)
+CPDOMEventGetClickCount = function(aComparisonEvent, aTimestamp, aLocation, isFromTouch)
{
if (!aComparisonEvent)
return 1;
+ // For touch events, allow a larger pixel delta to accommodate "fat fingers"
+ // and a longer time delta to accommodate less precise tapping.
+ var spaceDelta = isFromTouch ? 25.0 : CLICK_SPACE_DELTA;
+ var timeDelta = isFromTouch ? TOUCH_CLICK_TIME_DELTA : CLICK_TIME_DELTA;
var comparisonLocation = [aComparisonEvent locationInWindow];
- return (aTimestamp - [aComparisonEvent timestamp] < CLICK_TIME_DELTA &&
- ABS(comparisonLocation.x - aLocation.x) < CLICK_SPACE_DELTA &&
- ABS(comparisonLocation.y - aLocation.y) < CLICK_SPACE_DELTA) ? [aComparisonEvent clickCount] + 1 : 1;
+ return (aTimestamp - [aComparisonEvent timestamp] < timeDelta &&
+ ABS(comparisonLocation.x - aLocation.x) < spaceDelta &&
+ ABS(comparisonLocation.y - aLocation.y) < spaceDelta) ? [aComparisonEvent clickCount] + 1 : 1;
};
// Global.
diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOMKeys.j b/AppKit/Platform/DOM/CPPlatformWindow+DOMKeys.j
index a24d241f3..a76bed207 100644
--- a/AppKit/Platform/DOM/CPPlatformWindow+DOMKeys.j
+++ b/AppKit/Platform/DOM/CPPlatformWindow+DOMKeys.j
@@ -132,38 +132,36 @@ CPKeyCodes = {
/*!
* Returns true if the key fires a keypress event in the current browser.
+ * The keypress event is deprecated, but this function helps manage legacy
+ * event handling by predicting its behavior.
*
- * Accoridng to MSDN [1] IE only fires keypress events for the following keys:
- * - Letters: A - Z (uppercase and lowercase)
- * - Numerals: 0 - 9
- * - Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~
- * - System: ESC, SPACEBAR, ENTER
- *
- * That's not entirely correct though, for instance there's no distinction
- * between upper and lower case letters.
- *
- * [1] http://msdn2.microsoft.com/en-us/library/ms536939(VS.85).aspx)
- *
- * Safari is similar to IE, but does not fire keypress for ESC.
- *
- * Additionally, IE6 does not fire keydown or keypress events for letters when
- * the control or alt keys are held down and the shift key is not. IE7 does
- * fire keydown in these cases, though, but not keypress.
- *
- * @param keyCode A key code.
- * @param opt_heldKeyCode Key code of a currently-held key.
- * @param opt_shiftKey Whether the shift key is held down.
- * @param opt_ctrlKey Whether the control key is held down.
- * @param opt_altKey Whether the alt key is held down.
- * @return Returns YES if it's a key that fires a keypress event.
+ * @param {number} keyCode A key code.
+ * @param {string} key The `key` property from the keyboard event.
+ * @param {number} opt_heldKeyCode Key code of a currently-held key.
+ * @param {boolean} opt_shiftKey Whether the shift key is held down.
+ * @param {boolean} opt_ctrlKey Whether the control key is held down.
+ * @param {boolean} opt_altKey Whether the alt key is held down.
+ * @return {boolean} Returns YES if it's a key that fires a keypress event.
*/
CPKeyCodes.firesKeyPressEvent = function(keyCode, key, opt_heldKeyCode, opt_shiftKey, opt_ctrlKey, opt_altKey)
{
- // The property key from event is one character wide in case of 'regular' keys (as opposed e.g. to arrow keys)
- // Regular keys all fire the keypress event
+ // Modern approach: Use event.key if available, as it is the most reliable standard.
+ if (key)
+ {
+ // Any key that produces a single, printable character fires a keypress event.
+ if (key.length === 1)
+ return true;
- if (key && key.length == 1)
- return true;
+ // "Enter" is a special non-printable key that historically fires keypress for compatibility.
+ if (key === "Enter")
+ return true;
+
+ // For all other non-printable keys (e.g., "ArrowLeft", "Escape", "F1"),
+ // modern browsers do not fire a keypress event.
+ return false;
+ }
+
+ // --- Legacy Fallback Logic (for browsers that don't support event.key) ---
if (!CPFeatureIsCompatible(CPJavaScriptRemedialKeySupport))
return true;
@@ -196,9 +194,11 @@ CPKeyCodes.firesKeyPressEvent = function(keyCode, key, opt_heldKeyCode, opt_shif
/*!
* Test for whether or not a given keyCode represents a character key.
+ * NOTE: This is a legacy function for browsers that don't support `event.key`.
+ * It is unreliable because `keyCode` represents a physical key, not the character produced.
*
- * @param keyCode A key code.
- * @return Returns YES if the keyCode is a character key.
+ * @param {number} keyCode A key code.
+ * @return {boolean} Returns YES if the keyCode is a character key.
*/
CPKeyCodes.isCharacterKey = function(keyCode)
{
diff --git a/README.markdown b/README.markdown
index b59bbc863..859570108 100644
--- a/README.markdown
+++ b/README.markdown
@@ -8,7 +8,7 @@ Cappuccino is an open-source framework that supports building powerful, desktop-
Cappuccino faithfully implements the proven design patterns of NeXTSTEP/Apple's Cocoa frameworks, enabling the creation of incredibly complex and reliable applications with a fraction of the code.
> **✨ Project Status: Active Development & Node.js Transition**
-> Cappuccino has been under continuous development since 2008 and is actively maintained. A major transition to a modern, **Node.js-based toolchain** has recently been finalized. The current release is a production-ready Release Candidate, with a formal release scheduled for 2025. It is stable, fast, and ready for new projects.
+> Cappuccino has been under continuous development since 2008 and is actively maintained. A major transition to a modern, **Node.js-based toolchain** has recently been finalized. The current release is a production-ready Release Candidate, with a formal release scheduled for 2026. It is stable, fast, and ready for new projects.
---
@@ -16,7 +16,7 @@ Cappuccino faithfully implements the proven design patterns of NeXTSTEP/Apple's
Cappuccino is not intended for building simple websites. It is for building **applications**—especially complex, data-rich, line-of-business tools where productivity and user experience are paramount.
-* **💻 True Desktop Behavior, Out-of-the-Box:** Applications built with Cappuccino behave like native desktop software by default. This includes a rich palette of UI controls, **full keyboard navigation and focus management**, and **multi-level undo/redo support** — as you can see in this [Demo application](https://ansb.uniklinik-freiburg.de/UIBuilder/index.html). Also take a look at the [Kitchen Sink demo](https://cappuccino-testbook.5apps.com/#ThemeKitchenSink).
+* **💻 True Desktop Behavior, Out-of-the-Box:** Applications built with Cappuccino behave like native desktop software by default. This includes a rich palette of UI controls, **full keyboard navigation and focus management**, and **multi-level undo/redo support** — as you can see in this [Showcase application](https://ansb.uniklinik-freiburg.de/ThemeKitchenSinkA3). Also take a look at the [Cookbook tutorial](https://cappuccino-cookbook.5apps.com/).
* **🚀 Incredible Productivity:** Less code is needed. High-level abstractions and a powerful object-oriented model mean development is focused on application logic, not browser quirks.
* **🏛️ Stable & Mature:** Built on decades of proven API design from Cocoa®, Cappuccino provides a stable foundation, free from the churn common in the JavaScript ecosystem.
* **🧱 True Object-Oriented Architecture:** Objective-J's message-passing architecture promotes loose coupling and clean design, making large-scale applications easier to build and maintain.
diff --git a/Tests/AppKit/CPButtonTest.j b/Tests/AppKit/CPButtonTest.j
index 19c0172fb..9abf42e27 100644
--- a/Tests/AppKit/CPButtonTest.j
+++ b/Tests/AppKit/CPButtonTest.j
@@ -44,11 +44,11 @@
[button setKeyEquivalent:"a"];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
- characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0]];
+ characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:wasClicked];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
- characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]];
+ characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertTrue:wasClicked];
}
@@ -60,11 +60,11 @@
[button setKeyEquivalentModifierMask:CPAlternateKeyMask];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
- characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]];
+ characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:wasClicked];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:CPAlternateKeyMask
timestamp:0 windowNumber:0 context:nil
- characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]];
+ characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertTrue:wasClicked];
}
@@ -76,12 +76,12 @@
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
- characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]];
+ characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:wasClicked];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:CPShiftKeyMask
timestamp:0 windowNumber:0 context:nil
- characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]];
+ characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertTrue:wasClicked];
}
@@ -92,15 +92,15 @@
[button setKeyEquivalent:CPEscapeFunctionKey];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
- characters:CPDeleteCharacter charactersIgnoringModifiers:CPDeleteCharacter isARepeat:NO keyCode:0]];
+ characters:CPDeleteCharacter charactersIgnoringModifiers:CPDeleteCharacter isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:wasClicked];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
- characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]];
+ characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:wasClicked];
[button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
- characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]];
+ characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertTrue:wasClicked];
}
diff --git a/Tests/AppKit/CPEventTest.j b/Tests/AppKit/CPEventTest.j
index 7570583dc..feef12694 100644
--- a/Tests/AppKit/CPEventTest.j
+++ b/Tests/AppKit/CPEventTest.j
@@ -31,7 +31,7 @@
timestamp:400.5 windowNumber:300 context:nil eventNumber:0 clickCount:2 pressure:0.5];
[self assert:@"CPEvent: type=2 loc={50, 50} time=400.5 flags=0x20000 win=undefined winNum=0 ctxt=null evNum=0 click=2 buttonNumber=0 pressure=0.5" equals:[anEvent description]];
- anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask | CPCommandKeyMask timestamp:12345.6 windowNumber:10 context:nil characters:"X" charactersIgnoringModifiers:"x" isARepeat:NO keyCode:10];
+ anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask | CPCommandKeyMask timestamp:12345.6 windowNumber:10 context:nil characters:"X" charactersIgnoringModifiers:"x" isARepeat:NO keyCode:10 isActionKey:NO];
[self assert:@"CPEvent: type=10 loc={0, 0} time=12345.6 flags=0x120000 win=null winNum=10 ctxt=null chars=\"X\" unmodchars=\"x\" repeat=0 keyCode=10" equals:[anEvent description]];
@@ -44,13 +44,13 @@
{
[self assert:0 equals:[CPEvent modifierFlags] message:@"no modifier flags active in a newly started app"];
- var anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0];
+ var anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO];
[CPApp sendEvent:anEvent];
[self assert:CPShiftKeyMask equals:[CPEvent modifierFlags] message:@"shift key pressed"];
// When the key up event is sent the modifier flags are cleared.
- anEvent = [CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0];
+ anEvent = [CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO];
[CPApp sendEvent:anEvent];
[self assert:0 equals:[CPEvent modifierFlags] message:@"shift key released"];
diff --git a/Tests/AppKit/CPMenuTest.j b/Tests/AppKit/CPMenuTest.j
index 6449b65aa..b50afdea3 100644
--- a/Tests/AppKit/CPMenuTest.j
+++ b/Tests/AppKit/CPMenuTest.j
@@ -149,24 +149,24 @@
// Don't match anything.
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
timestamp:0 windowNumber:0 context:nil
- characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0]];
+ characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled];
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
- characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0]];
+ characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled];
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
timestamp:0 windowNumber:0 context:nil
- characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0]];
+ characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || undoWasCalled];
[self assertTrue:openDocumentWasCalled message:"expect openDocumentWasCalled"];
openDocumentWasCalled = NO;
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
timestamp:0 windowNumber:0 context:nil
- characters:CPUndoKeyEquivalent charactersIgnoringModifiers:CPUndoKeyEquivalent isARepeat:NO keyCode:0]];
+ characters:CPUndoKeyEquivalent charactersIgnoringModifiers:CPUndoKeyEquivalent isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled];
[self assertTrue:undoWasCalled];
}
@@ -177,7 +177,7 @@
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
- characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]];
+ characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || openDocumentWasCalled || undoWasCalled];
[self assertTrue:escapeNoModifierWasCalled];
@@ -185,7 +185,7 @@
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
timestamp:0 windowNumber:0 context:nil
- characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]];
+ characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled];
[self assertTrue:escapeWasCalled];
}
@@ -196,7 +196,7 @@
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
timestamp:0 windowNumber:0 context:nil
- characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0]];
+ characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || saveDocumentAsWasCalled || undoWasCalled];
[self assertTrue:saveDocumentWasCalled message:"saveDocumentWasCalled"];
@@ -204,7 +204,7 @@
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask | CPShiftKeyMask
timestamp:0 windowNumber:0 context:nil
- characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0]];
+ characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0 isActionKey:NO]];
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || saveDocumentWasCalled || undoWasCalled];
[self assertTrue:saveDocumentAsWasCalled message:"saveDocumentAsWasCalled"];
}
diff --git a/Tests/AppKit/CPResponderTest.j b/Tests/AppKit/CPResponderTest.j
index 4c3159ab3..2aba5adc3 100644
--- a/Tests/AppKit/CPResponderTest.j
+++ b/Tests/AppKit/CPResponderTest.j
@@ -45,7 +45,7 @@
var keyEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0
timestamp:0 windowNumber:0 context:nil
- characters:character charactersIgnoringModifiers:character isARepeat:NO keyCode:keyCode];
+ characters:character charactersIgnoringModifiers:character isARepeat:NO keyCode:keyCode isActionKey:YES];
[responder interpretKeyEvents:[keyEvent]];
[self assert:[selector] equals:responder.doCommandCalls];
}
@@ -57,7 +57,7 @@
var keyEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask
timestamp:0 windowNumber:0 context:nil
- characters:CPLeftArrowFunctionKey charactersIgnoringModifiers:CPLeftArrowFunctionKey isARepeat:NO keyCode:CPKeyCodes.LEFT];
+ characters:CPLeftArrowFunctionKey charactersIgnoringModifiers:CPLeftArrowFunctionKey isARepeat:NO keyCode:CPKeyCodes.LEFT isActionKey:YES];
[responder interpretKeyEvents:[keyEvent]];
[self assert:[@selector(moveLeftAndModifySelection:)] equals:responder.doCommandCalls];
}
diff --git a/Tests/AppKit/CPTreeControllerTest.j b/Tests/AppKit/CPTreeControllerTest.j
new file mode 100644
index 000000000..9487e4e46
--- /dev/null
+++ b/Tests/AppKit/CPTreeControllerTest.j
@@ -0,0 +1,266 @@
+/*
+ * CPTreeControllerTest.j
+ *
+ * Test suite for CPTreeController
+ */
+
+@import
+@import
+@import
+@import
+@import
+
+@class OrgNode
+
+@implementation CPTreeControllerTest : OJTestCase
+{
+ CPTreeController _treeController @accessors(property=treeController);
+ CPArray _contentArray @accessors(property=contentArray);
+
+ CPArray observations;
+ int aCount @accessors;
+}
+
+- (CPArray)makeTestTree
+{
+ var engineering = [OrgNode nodeWithName:@"Engineering"],
+ marketing = [OrgNode nodeWithName:@"Marketing"];
+
+ var webTeam = [OrgNode nodeWithName:@"Web Team"],
+ backendTeam = [OrgNode nodeWithName:@"Backend Team"];
+
+ [engineering setChildren:[CPMutableArray arrayWithObjects:webTeam, backendTeam]];
+
+ var dev1 = [OrgNode nodeWithName:@"Francisco"],
+ dev2 = [OrgNode nodeWithName:@"Ross"];
+
+ [webTeam setChildren:[CPMutableArray arrayWithObjects:dev1, dev2]];
+
+ return [CPMutableArray arrayWithObjects:engineering, marketing];
+}
+
+- (void)setUp
+{
+ [[CPApplication alloc] init];
+
+ _contentArray = [self makeTestTree];
+ _treeController = [[CPTreeController alloc] init];
+ [_treeController setChildrenKeyPath:@"children"];
+ [_treeController setContent:[_contentArray copy]];
+}
+
+- (void)testInitWithContent
+{
+ [self assert:[_contentArray count] equals:[[_treeController contentArray] count]];
+ [self assert:[CPTreeNode class] equals:[[[self treeController] arrangedObjects] class] message:@"arranged objects should be a proxy CPTreeNode root"];
+}
+
+- (void)testInitWithoutContent
+{
+ var emptyController = [[CPTreeController alloc] init];
+ [self assert:[CPArray array] equals:[emptyController contentArray]];
+ [self assert:0 equals:[[[emptyController arrangedObjects] childNodes] count]];
+}
+
+- (void)testSetContent
+{
+ var newTree = [CPMutableArray arrayWithObject:[OrgNode nodeWithName:@"Solo Department"]];
+ [[self treeController] setContent:newTree];
+
+ [self assert:newTree equals:[[self treeController] contentArray]];
+ [self assert:1 equals:[[[[self treeController] arrangedObjects] childNodes] count]];
+}
+
+- (void)testSelectionPaths
+{
+ var controller = [self treeController];
+
+ var path = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
+ [controller setSelectionIndexPath:path];
+
+ var selectedPath = [controller selectionIndexPath];
+ [self assert:path equals:selectedPath];
+
+ var selectedNodes = [controller selectedNodes];
+ [self assert:1 equals:[selectedNodes count]];
+ [self assert:@"Web Team" equals:[[[selectedNodes objectAtIndex:0] representedObject] name]];
+}
+
+- (void)testAddChild
+{
+ var controller = [self treeController];
+ [controller setObjectClass:[OrgNode class]];
+
+ var parentPath = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:1];
+ [controller setSelectionIndexPath:parentPath];
+
+ var newDev = [OrgNode nodeWithName:@"Tom"];
+ var insertPath = [parentPath indexPathByAddingIndex:0];
+ [controller insertObject:newDev atArrangedObjectIndexPath:insertPath];
+
+ var engineering = [[controller contentArray] objectAtIndex:0],
+ backendTeam = [[engineering children] objectAtIndex:1];
+ [self assert:1 equals:[[backendTeam children] count] message:@"Child should be added to the model object's children array"];
+ [self assert:@"Tom" equals:[[[backendTeam children] objectAtIndex:0] name]];
+}
+
+- (void)testInsertObjectAtArrangedObjectIndexPath
+{
+ var controller = [self treeController];
+
+ var path = [CPIndexPath indexPathWithIndex:1];
+ var hrDept = [OrgNode nodeWithName:@"Human Resources"];
+
+ [controller insertObject:hrDept atArrangedObjectIndexPath:path];
+ [self assert:3 equals:[[controller contentArray] count]];
+ [self assert:hrDept equals:[[controller contentArray] objectAtIndex:1]];
+
+ var nestedPath = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
+ var subDept = [OrgNode nodeWithName:@"Sub Dept"];
+
+ [controller insertObject:subDept atArrangedObjectIndexPath:nestedPath];
+ var engChildren = [[[controller contentArray] objectAtIndex:0] children];
+ [self assert:subDept equals:[engChildren objectAtIndex:0] message:@"Object should be inserted at the correct nested index path"];
+}
+
+- (void)testRemoveObjectAtArrangedObjectIndexPath
+{
+ var controller = [self treeController];
+
+ var path = [[[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0] indexPathByAddingIndex:0];
+
+ [controller removeObjectAtArrangedObjectIndexPath:path];
+
+ var engineering = [[controller contentArray] objectAtIndex:0],
+ webTeam = [[engineering children] objectAtIndex:0];
+
+ [self assert:1 equals:[[webTeam children] count] message:@"Francisco should be removed, leaving only Ross"];
+ [self assert:@"Ross" equals:[[[webTeam children] objectAtIndex:0] name]];
+}
+
+- (void)testRemoveObjectsAtArrangedObjectIndexPaths
+{
+ var controller = [self treeController];
+
+ var paths = [CPArray arrayWithObjects:[CPIndexPath indexPathWithIndex:0], [CPIndexPath indexPathWithIndex:1]];
+
+ [controller removeObjectsAtArrangedObjectIndexPaths:paths];
+
+ [self assert:0 equals:[[controller contentArray] count] message:@"All root nodes should be removed"];
+}
+
+- (void)testSelectingEmptyIndexPathsExplicitlyWithAvoidsEmptySelection
+{
+ var controller = [self treeController];
+
+ [controller setAvoidsEmptySelection:YES];
+
+ [controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
+ [controller setSelectionIndexPaths:[CPArray array]];
+
+ [self assertTrue:([[controller selectionIndexPaths] count] == 0) message:@"Selection should be empty when unselecting explicitly, even with avoidsEmptySelection"];
+}
+
+- (void)testAvoidsEmptySelectionWhenRemoving
+{
+ var controller = [self treeController];
+ [controller setAvoidsEmptySelection:YES];
+
+ var path = [CPIndexPath indexPathWithIndex:0];
+ [controller setSelectionIndexPath:path];
+
+ // Remove "Engineering"
+ [controller removeObjectAtArrangedObjectIndexPath:path];
+
+ [self assertTrue:([[controller selectionIndexPaths] count] == 1) message:@"Selection should fallback to the first item when avoidsEmptySelection is YES"];
+ // "Marketing" is now at index 0
+ [self assert:[CPIndexPath indexPathWithIndex:0] equals:[controller selectionIndexPath]];
+
+ // Test behavior when AvoidsEmptySelection is NO[controller insertObject:[OrgNode nodeWithName:@"New Dept"] atArrangedObjectIndexPath:[CPIndexPath indexPathWithIndex:1]];
+ [controller setAvoidsEmptySelection:NO];
+
+ // Reselect "Marketing" at index 0
+ [controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
+
+ // Remove "Marketing"
+ [controller removeObjectAtArrangedObjectIndexPath:[CPIndexPath indexPathWithIndex:0]];
+
+ [self assertTrue:([[controller selectionIndexPaths] count] == 0) message:@"Selection should be allowed to be empty when avoidsEmptySelection is NO"];
+}
+
+- (void)testChildrenKeyPathOverride
+{
+ var controller = [[CPTreeController alloc] init];
+ [controller setChildrenKeyPath:@"subItems"];
+
+ var data = [OrgNode nodeWithName:@"Root"];
+
+ [data setValue:[CPMutableArray arrayWithObject:[OrgNode nodeWithName:@"Sub"]] forKey:@"subItems"];
+ [controller setContent:[CPMutableArray arrayWithObject:data]];
+
+ var path = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
+ var newItem = [OrgNode nodeWithName:@"New Sub"];
+ [controller insertObject:newItem atArrangedObjectIndexPath:path];
+
+ var subs = [data valueForKey:@"subItems"];
+ [self assert:2 equals:[subs count]];
+ [self assert:newItem equals:[subs objectAtIndex:0] message:@"Object should be inserted using the custom childrenKeyPath"];
+}
+
+- (void)testContentBinding
+{
+ var controller = [[CPTreeController alloc] init];
+
+ [controller bind:@"contentArray" toObject:self withKeyPath:@"contentArray" options:nil];
+ [self assert:[self contentArray] equals:[controller contentArray]];
+ [self assert:2 equals:[[[controller arrangedObjects] childNodes] count]];
+}
+
+- (void)testSelectedObjects
+{
+ var controller = [self treeController];
+
+ var path = [CPIndexPath indexPathWithIndex:1];
+ [controller setSelectionIndexPath:path];
+
+ var selectedObjects = [controller selectedObjects];
+
+ [self assert:1 equals:[selectedObjects count]];
+ [self assert:@"Marketing" equals:[[selectedObjects objectAtIndex:0] name]];
+}
+
+@end
+
+/*
+ * Dummy Model Class for Testing
+ */
+@implementation OrgNode : CPObject
+{
+ CPString _name @accessors(property=name);
+ CPMutableArray _children @accessors(property=children);
+ CPMutableArray _subItems @accessors(property=subItems);
+}
+
++ (id)nodeWithName:(CPString)aName
+{
+ return [[self alloc] initWithName:aName];
+}
+
+- (id)initWithName:(CPString)aName
+{
+ if (self = [super init])
+ {
+ _name = aName;
+ _children = [CPMutableArray array];
+ _subItems = [CPMutableArray array];
+ }
+
+ return self;
+}
+
+- (CPString)description
+{
+ return [CPString stringWithFormat:@"", [self name]];
+}
+
+@end
diff --git a/Tests/AppKit/CPTreeNodeTest.j b/Tests/AppKit/CPTreeNodeTest.j
index 4e40b5f98..a9c8c9c19 100644
--- a/Tests/AppKit/CPTreeNodeTest.j
+++ b/Tests/AppKit/CPTreeNodeTest.j
@@ -25,7 +25,7 @@
indexPath = [CPIndexPath indexPathWithIndex:1];
- [self assert:undefined equals:[treeNode descendantNodeAtIndexPath:indexPath]];
+ [self assert:nil equals:[treeNode descendantNodeAtIndexPath:indexPath]];
}
@end
diff --git a/Tests/Manual/CPAnimationContextTest/AppController.j b/Tests/Manual/CPAnimationContextTest/AppController.j
index 7ae29e0eb..a9d657859 100644
--- a/Tests/Manual/CPAnimationContextTest/AppController.j
+++ b/Tests/Manual/CPAnimationContextTest/AppController.j
@@ -9,6 +9,7 @@
@import
@import
@import
+@import
#define UIAssert(a) [self markTest:_cmd didPass:a];
@@ -403,6 +404,231 @@
[CPAnimationContext endGrouping];
}
+- (void)testGroupAnimation:(id)sender
+{
+ // Reset state
+ [_testView setFrame:_initialTestViewFrame];
+ [_testView setAlphaValue:1.0];
+ [_pathView setPath:nil]; // Clear the path view as we aren't using it here
+
+ // Ensure the view is layer-backed.
+ // Without this, [_testView layer] returns nil.
+ [_testView setWantsLayer:YES];
+
+ var layer = [_testView layer];
+
+ // 1. Define the start and end positions
+ // CALayer 'position' corresponds to the center of the view (anchorPoint 0.5,0.5)
+ var startPos = [layer position];
+
+ // Safety check in case layer creation failed (though setWantsLayer:YES should ensure it)
+ if (!startPos) startPos = CGPointMake(0,0);
+
+ var endPos = CGPointMake(startPos.x + 150, startPos.y + 50);
+
+ // 2. Create a Position Animation
+ var moveAnim = [CABasicAnimation animationWithKeyPath:@"position"];
+ [moveAnim setFromValue:startPos];
+ [moveAnim setToValue:endPos];
+ [moveAnim setDuration:1.0];
+
+ // 3. Create an Opacity Animation
+ var fadeAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
+ [fadeAnim setFromValue:1.0];
+ [fadeAnim setToValue:0.25];
+ [fadeAnim setDuration:1.0];
+
+ // 4. Group them
+ // This tests the recursive logic in CAAnimationGroup and the timer logic in CALayer
+ var group = [CAAnimationGroup group];
+ [group setAnimations:[moveAnim, fadeAnim]];
+ [group setDuration:1.0];
+
+ // 5. Run the animation on the layer
+ [layer addAnimation:group forKey:@"groupTest"];
+
+ // 6. Verify results after the animation completes (1.0s duration + 0.1s buffer)
+ [self performSelector:@selector(_verifyGroupAnimation:) withObject:endPos afterDelay:1.1];
+}
+
+- (void)_verifyGroupAnimation:(CGPoint)expectedPos
+{
+ var layer = [_testView layer],
+ currentPos = [layer position],
+ currentOpacity = [layer opacity];
+
+ // Allow for small floating point differences
+ var posPassed = (Math.abs(currentPos.x - expectedPos.x) < 1.0 && Math.abs(currentPos.y - expectedPos.y) < 1.0);
+ var opacityPassed = (Math.abs(currentOpacity - 0.25) < 0.05);
+
+ [self markTest:@selector(testGroupAnimation:) didPass:(posPassed && opacityPassed)];
+
+ // Reset for next test
+ [self performSelector:@selector(cleanupAfterAnimation) withObject:nil afterDelay:0.5];
+}
+
+- (void)testManualRotation:(id)sender
+{
+ // 1. Cleanup previous test view
+ if (_testView)
+ [_testView removeFromSuperview];
+
+ // 2. Setup the RotatableView
+ // View is 100x100, but the blue box drawn inside is 70x70 to allow room to spin.
+ var frame = CGRectMake(390, 450, 100, 100);
+ _testView = [[RotatableView alloc] initWithFrame:frame];
+ [[theWindow contentView] addSubview:_testView];
+
+ // Ensure layer-backed so we have a layer to animate
+ [_testView setWantsLayer:YES];
+
+ var layer = [_testView layer];
+
+ // 3. Define the Animation
+ // Animation is performed on _testView
+ [layer setDelegate:_testView];
+ var rotationAnim = [CABasicAnimation animationWithKeyPath:@"angle"];
+
+ // Rotate 360 degrees (2 * PI)
+ [rotationAnim setFromValue:0.0];
+ [rotationAnim setToValue:2 * PI];
+ [rotationAnim setDuration:2.0];
+
+ // Use an easing function for smooth start/stop
+ [rotationAnim setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
+
+ // 4. Add to Layer
+ [layer addAnimation:rotationAnim forKey:@"rotateTest"];
+
+ // 5. Verify results after animation
+ [self performSelector:@selector(_verifyRotation:) withObject:nil afterDelay:2.1];
+}
+
+- (void)_verifyRotation:(id)sender
+{
+ var layer = [_testView layer];
+ var endAngle = [layer angle];
+
+ // Check if we reached approx 2*PI (6.28)
+ var passed = (Math.abs(endAngle - (2 * PI)) < 0.1);
+
+ [self markTest:@selector(testManualRotation:) didPass:passed];
+
+ // Reset view
+ [self performSelector:@selector(cleanupAfterAnimation) withObject:nil afterDelay:0.5];
+}
+
+@end
+
+/*
+ A custom view that draws a box with a line in it.
+ We draw the box smaller than the view bounds to prevent clipping during rotation.
+*/
+@implementation RotatableView : CPView
+{
+ float _angle;
+}
+
+- (void)initWithFrame:(CGRect)aFrame
+{
+ self = [super initWithFrame:aFrame];
+ _angle = 0;
+
+ return self;
+}
+
+- (void)setAngle:(float)anAngle
+{
+ _angle = anAngle;
+ [self display];
+}
+
+- (float)angle
+{
+ return _angle;
+}
+
+- (void)drawRect:(CGRect)aRect
+{
+ var context = [[CPGraphicsContext currentContext] graphicsPort],
+ bounds = [self bounds],
+ cx = CGRectGetWidth(bounds) / 2.0,
+ cy = CGRectGetHeight(bounds) / 2.0;
+
+ // 1. Clear Context
+ CGContextClearRect(context, bounds);
+
+ // 2. Precompute Trig
+ var cosA = Math.cos(_angle),
+ sinA = Math.sin(_angle);
+
+ /*
+ Helper closure to transform a local point (x,y) relative to center
+ into global view coordinates.
+ */
+ var getPoint = function(localX, localY)
+ {
+ // Rotation Matrix:
+ // x' = x*cos - y*sin
+ // y' = x*sin + y*cos
+ var rotX = localX * cosA - localY * sinA;
+ var rotY = localX * sinA + localY * cosA;
+
+ // Translate back to view center
+ return CGPointMake(cx + rotX, cy + rotY);
+ };
+
+ // --- DRAW BLUE SQUARE (70x70) ---
+ var s = 35.0; // half size
+
+ // Calculate the 4 corners manually
+ var p1 = getPoint(-s, -s); // Top-Left
+ var p2 = getPoint( s, -s); // Top-Right
+ var p3 = getPoint( s, s); // Bottom-Right
+ var p4 = getPoint(-s, s); // Bottom-Left
+
+ CGContextBeginPath(context);
+ CGContextMoveToPoint(context, p1.x, p1.y);
+ CGContextAddLineToPoint(context, p2.x, p2.y);
+ CGContextAddLineToPoint(context, p3.x, p3.y);
+ CGContextAddLineToPoint(context, p4.x, p4.y);
+ CGContextClosePath(context);
+
+ [[CPColor greenColor] setFill];
+ CGContextFillPath(context);
+
+ // --- DRAW RED MARKER (Top-Left Corner) ---
+ // A 20x20 square in the top-left of the blue box
+ // Local coords relative to center: x from -35 to -15, y from -35 to -15
+ var r1 = getPoint(-35, -35);
+ var r2 = getPoint(-15, -35);
+ var r3 = getPoint(-15, -15);
+ var r4 = getPoint(-35, -15);
+
+ CGContextBeginPath(context);
+ CGContextMoveToPoint(context, r1.x, r1.y);
+ CGContextAddLineToPoint(context, r2.x, r2.y);
+ CGContextAddLineToPoint(context, r3.x, r3.y);
+ CGContextAddLineToPoint(context, r4.x, r4.y);
+ CGContextClosePath(context);
+
+ [[CPColor redColor] setFill];
+ CGContextFillPath(context);
+
+ // --- DRAW WHITE POINTER LINE ---
+ // Line from Center (0,0) to Right Edge (35, 0)
+ var lineStart = getPoint(0, 0);
+ var lineEnd = getPoint(35, 0);
+
+ CGContextBeginPath(context);
+ CGContextMoveToPoint(context, lineStart.x, lineStart.y);
+ CGContextAddLineToPoint(context, lineEnd.x, lineEnd.y);
+
+ [[CPColor whiteColor] setStroke];
+ CGContextSetLineWidth(context, 3.0);
+ CGContextStrokePath(context);
+}
+
@end
var unCamelCase = function(aString)
diff --git a/Tests/Manual/CPMenuSubmenuPlacementConstraints/AppController.j b/Tests/Manual/CPMenuSubmenuPlacementConstraints/AppController.j
index 3261959e8..ca282ee48 100644
--- a/Tests/Manual/CPMenuSubmenuPlacementConstraints/AppController.j
+++ b/Tests/Manual/CPMenuSubmenuPlacementConstraints/AppController.j
@@ -3,6 +3,8 @@
* CPMenuTest
*
* Created by Daniel Boehringer 2025 for submenu constraints on the rightmost end of the screen.
+ * Updated for Issue #3149 (Immediate menu updates).
+ * Updated for Issue #3153 (Hide main menu items without submenus).
*/
@@ -12,6 +14,19 @@
{
CPWindow theWindow;
BOOL _isEnabled;
+
+ // Ivars for Live Update Test (#3149)
+ CPMenuItem _changeTitleItem;
+ CPMenuItem _changeStateItem;
+ CPMenuItem _changeEnabledItem;
+
+ // Ivars for Hidden Menu Test (#3153)
+ CPMenuItem _ghostMenuItem;
+ CPMenu _ghostMenu;
+ // Ivars for Live Update Test
+ CPMenuItem _changeTitleItem;
+ CPMenuItem _changeStateItem;
+ CPMenuItem _changeEnabledItem;
}
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
@@ -21,13 +36,13 @@
mainMenu = [[CPMenu alloc] initWithTitle:@"MainMenu"],
appMenu = [[CPMenu alloc] initWithTitle:@"App"],
- fileMenu = [[CPMenu alloc] initWithTitle:@"File"],
- bindingsMenu = [[CPMenu alloc] initWithTitle:@"Bindings Test"];
+ fileMenu = [[CPMenu alloc] initWithTitle:@"File"];
_isEnabled = YES;
[CPApp setMainMenu:mainMenu];
+ // Standard App Menu
[mainMenu addItemWithTitle:@"App" action:nil keyEquivalent:@""];
[mainMenu setSubmenu:appMenu forItem:[mainMenu itemWithTitle:@"App"]];
@@ -35,6 +50,7 @@
[appMenu addItem:[CPMenuItem separatorItem]];
[appMenu addItemWithTitle:@"Quit" action:@selector(terminate:) keyEquivalent:@"q"];
+ // Standard File Menu
[mainMenu addItemWithTitle:@"File" action:nil keyEquivalent:@""];
[mainMenu setSubmenu:fileMenu forItem:[mainMenu itemWithTitle:@"File"]];
@@ -42,53 +58,95 @@
[fileMenu addItemWithTitle:@"Open" action:@selector(openDocument:) keyEquivalent:@"o"];
[fileMenu addItemWithTitle:@"Close" action:@selector(newDocument:) keyEquivalent:@"w"];
- // 1. Add some dummy menus to push the test menu further to the right.
- var dummyMenu1 = [[CPMenu alloc] initWithTitle:@"Dummy 1"],
- dummyMenu2 = [[CPMenu alloc] initWithTitle:@"Dummy 2"];
+ // -------------------------------------------------------------------------
+ // TEST ADDITION FOR ISSUE #3149: Immediate Updates
+ // -------------------------------------------------------------------------
+ var liveUpdateMenu = [[CPMenu alloc] initWithTitle:@"Live Update"],
+ liveUpdateMenuItem = [mainMenu addItemWithTitle:@"Live Update" action:nil keyEquivalent:@""];
+
+ [liveUpdateMenu setAutoenablesItems:NO];
+ [mainMenu setSubmenu:liveUpdateMenu forItem:liveUpdateMenuItem];
+ [liveUpdateMenu addItemWithTitle:@"1. Click 'Start Timer' below" action:nil keyEquivalent:@""];
+ [liveUpdateMenu addItemWithTitle:@"2. Keep this menu OPEN" action:nil keyEquivalent:@""];
+ [liveUpdateMenu addItem:[CPMenuItem separatorItem]];
+
+ _changeTitleItem = [liveUpdateMenu addItemWithTitle:@"Title will change in 3s" action:nil keyEquivalent:@""];
+ _changeStateItem = [liveUpdateMenu addItemWithTitle:@"State will change in 3s" action:nil keyEquivalent:@""];
+ _changeEnabledItem = [liveUpdateMenu addItemWithTitle:@"Enabled will change in 3s" action:nil keyEquivalent:@""];
+
+ [liveUpdateMenu addItem:[CPMenuItem separatorItem]];
+ [liveUpdateMenu addItemWithTitle:@"Start 3s Timer" action:@selector(startUpdateTimer:) keyEquivalent:@""];
+
+ // -------------------------------------------------------------------------
+ // TEST ADDITION FOR ISSUE #3153: Hide main menu items with no submenus
+ // -------------------------------------------------------------------------
+
+ // 1. Create a "Ghost" item in the main menu bar.
+ // We intentionally DO NOT set a submenu for it yet.
+ // EXPECTATION: "Ghost Item" should NOT be visible in the menu bar.
+ _ghostMenuItem = [mainMenu addItemWithTitle:@"Ghost Item" action:nil keyEquivalent:@""];
+
+ // Prepare the menu that we will attach later
+ _ghostMenu = [[CPMenu alloc] initWithTitle:@"Ghost Menu"];
+ [_ghostMenu addItemWithTitle:@"I was hidden!" action:nil keyEquivalent:@""];
+
+ // 2. Create a control menu to toggle the submenu
+ var visibilityMenu = [[CPMenu alloc] initWithTitle:@"Visibility Test"],
+ visibilityMenuItem = [mainMenu addItemWithTitle:@"Visibility Test" action:nil keyEquivalent:@""];
+
+ [mainMenu setSubmenu:visibilityMenu forItem:visibilityMenuItem];
+ [visibilityMenu addItemWithTitle:@"Toggle 'Ghost Item' Submenu" action:@selector(toggleGhost:) keyEquivalent:@""];
+ [visibilityMenu addItemWithTitle:@"(If 'Ghost Item' is visible in bar now, bug is present)" action:nil keyEquivalent:@""];
+
+
+ // -------------------------------------------------------------------------
+ // Layout Testing (Right-side constraints)
+ // -------------------------------------------------------------------------
+
+ // Add some dummy menus to push the test menu further to the right.
+ var dummyMenu1 = [[CPMenu alloc] initWithTitle:@"Dummy 1"];
[dummyMenu1 addItemWithTitle:@"Dummy Action A" action:nil keyEquivalent:@""];
- [dummyMenu1 addItemWithTitle:@"Dummy Action B" action:nil keyEquivalent:@""];
-
- [dummyMenu2 addItemWithTitle:@"Another Dummy Action" action:nil keyEquivalent:@""];
-
- var dummyMenuItem1 = [mainMenu addItemWithTitle:@"Dummy Menu 1" action:nil keyEquivalent:@""];
+
+ var dummyMenuItem1 = [mainMenu addItemWithTitle:@"Dummy 1" action:nil keyEquivalent:@""];
[mainMenu setSubmenu:dummyMenu1 forItem:dummyMenuItem1];
+ // TEST ADDITION FOR ISSUE #3149: Immediate Updates
+ // -------------------------------------------------------------------------
+ var liveUpdateMenu = [[CPMenu alloc] initWithTitle:@"Live Update"],
+ liveUpdateMenuItem = [mainMenu addItemWithTitle:@"Live Update" action:nil keyEquivalent:@""];
+
+ // Disable auto-enable so we can manually test setEnabled: on items without actions
+ [liveUpdateMenu setAutoenablesItems:NO];
+ [mainMenu setSubmenu:liveUpdateMenu forItem:liveUpdateMenuItem];
- var dummyMenuItem2 = [mainMenu addItemWithTitle:@"Dummy Menu 2" action:nil keyEquivalent:@""];
- [mainMenu setSubmenu:dummyMenu2 forItem:dummyMenuItem2];
+ [liveUpdateMenu addItemWithTitle:@"1. Click 'Start Timer' below" action:nil keyEquivalent:@""];
+ [liveUpdateMenu addItemWithTitle:@"2. Keep this menu OPEN" action:nil keyEquivalent:@""];
+ [liveUpdateMenu addItem:[CPMenuItem separatorItem]];
+
+ _changeTitleItem = [liveUpdateMenu addItemWithTitle:@"Title will change in 3s" action:nil keyEquivalent:@""];
+ _changeStateItem = [liveUpdateMenu addItemWithTitle:@"State will change in 3s" action:nil keyEquivalent:@""];
+ _changeEnabledItem = [liveUpdateMenu addItemWithTitle:@"Enabled will change in 3s" action:nil keyEquivalent:@""];
+
+ [liveUpdateMenu addItem:[CPMenuItem separatorItem]];
+ [liveUpdateMenu addItemWithTitle:@"Start 3s Timer" action:@selector(startUpdateTimer:) keyEquivalent:@""];
+ // -------------------------------------------------------------------------
- // 2. Create the right-most menu with submenus for testing.
+ // Create the right-most menu with submenus for testing layout.
var rightTestMenu = [[CPMenu alloc] initWithTitle:@"Right-Side Test"],
rightTestMenuItem = [mainMenu addItemWithTitle:@"Right-Side Test" action:nil keyEquivalent:@""];
[mainMenu setSubmenu:rightTestMenu forItem:rightTestMenuItem];
- // Add some simple items
- [rightTestMenu addItemWithTitle:@"Simple Item (No Submenu)" action:nil keyEquivalent:@""];
+ [rightTestMenu addItemWithTitle:@"Simple Item" action:nil keyEquivalent:@""];
[rightTestMenu addItem:[CPMenuItem separatorItem]];
- // Create the first level submenu
var submenu1 = [[CPMenu alloc] initWithTitle:@"Submenu 1"],
submenu1Item = [rightTestMenu addItemWithTitle:@"Test First Submenu" action:nil keyEquivalent:@""];
[submenu1 addItemWithTitle:@"Sub-item A" action:nil keyEquivalent:@""];
- [submenu1 addItemWithTitle:@"Sub-item B" action:nil keyEquivalent:@""];
[rightTestMenu setSubmenu:submenu1 forItem:submenu1Item];
- // Create a nested submenu for deeper testing
- var submenu2 = [[CPMenu alloc] initWithTitle:@"Submenu 2"],
- submenu2Item = [rightTestMenu addItemWithTitle:@"Test Nested Submenu" action:nil keyEquivalent:@""],
- deeperSubmenu = [[CPMenu alloc] initWithTitle:@"Deeper"],
- deeperSubmenuItem = [submenu2 addItemWithTitle:@"Deeper Submenu..." action:nil keyEquivalent:@""];
-
- [submenu2 addItemWithTitle:@"Another Sub-item" action:nil keyEquivalent:@""];
- [deeperSubmenu addItemWithTitle:@"Deep Item X" action:nil keyEquivalent:@""];
- [deeperSubmenu addItemWithTitle:@"Deep Item Y" action:nil keyEquivalent:@""];
-
- [submenu2 setSubmenu:deeperSubmenu forItem:deeperSubmenuItem];
- [rightTestMenu setSubmenu:submenu2 forItem:submenu2Item];
-
[CPMenu setMenuBarVisible:YES];
}
@@ -106,4 +164,50 @@
return YES;
}
+// -------------------------------------------------------------------------
+// Live Update Test Actions (#3149)
+// -------------------------------------------------------------------------
+
+- (void)startUpdateTimer:(id)sender
+{
+ // Reset state
+ [_changeTitleItem setTitle:@"Title will change in 3s"];
+ [_changeStateItem setState:CPOffState];
+ [_changeStateItem setTitle:@"State will change in 3s"];
+ [_changeEnabledItem setEnabled:YES];
+ [_changeEnabledItem setTitle:@"Enabled will change in 3s"];
+
+ // Trigger update
+ [self performSelector:@selector(performLiveUpdate) withObject:nil afterDelay:3.0];
+}
+
+- (void)performLiveUpdate
+{
+ [_changeTitleItem setTitle:@"Title Changed!"];
+
+ [_changeStateItem setState:CPOnState];
+ [_changeStateItem setTitle:@"State Changed! (Checked)"];
+
+ [_changeEnabledItem setEnabled:NO];
+ [_changeEnabledItem setTitle:@"Enabled Changed! (Disabled)"];
+}
+
+// -------------------------------------------------------------------------
+// Visibility Test Actions (#3153)
+// -------------------------------------------------------------------------
+
+- (void)toggleGhost:(id)sender
+{
+ if ([_ghostMenuItem submenu])
+ {
+ // Remove submenu -> Item should disappear from the bar
+ [mainMenu setSubmenu:nil forItem:_ghostMenuItem];
+ }
+ else
+ {
+ // Add submenu -> Item should appear in the bar
+ [mainMenu setSubmenu:_ghostMenu forItem:_ghostMenuItem];
+ }
+}
+
@end
diff --git a/Tests/Manual/CPStackViewTest/AppController.j b/Tests/Manual/CPStackViewTest/AppController.j
new file mode 100644
index 000000000..05db03974
--- /dev/null
+++ b/Tests/Manual/CPStackViewTest/AppController.j
@@ -0,0 +1,207 @@
+/*
+ * AppController.j
+ * CPStackViewTest
+ *
+ * Created by Daniel Boehringer.
+ * Copyright 2025, Cappuccino Project.
+ */
+
+@import
+@import
+
+// We import the class to be tested.
+// Assuming CPStackView.j is in the same directory or properly included in the build.
+
+@implementation AppController : CPObject
+{
+ CPWindow theWindow;
+
+ // We will construct these programmatically for the test
+ // to avoid needing a .cib file for a new class.
+ CPStackView stackViewHorizontal;
+ CPStackView stackViewVertical;
+
+ // Control references
+ CPCheckBox detachHiddenCheckbox;
+ CPView toggleTargetView;
+}
+
+- (void)applicationDidFinishLaunching:(CPNotification)aNotification
+{
+ theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask];
+ var contentView = [theWindow contentView];
+ [contentView setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]];
+
+ // 1. Create a Label
+ var label = [CPTextField labelWithTitle:@"CPStackView Manual Test"];
+ [label setFont:[CPFont boldSystemFontOfSize:18]];
+ [label setFrameOrigin:CGPointMake(20, 20)];
+ [contentView addSubview:label];
+
+ // 2. Create Horizontal Stack View (The primary test subject)
+ // We frame it in the top half
+ stackViewHorizontal = [[CPStackView alloc] initWithFrame:CGRectMake(20, 60, 600, 150)];
+ [stackViewHorizontal setBackgroundColor:[CPColor whiteColor]];
+ [stackViewHorizontal setOrientation:CPUserInterfaceLayoutOrientationHorizontal];
+ [stackViewHorizontal setEdgeInsets:CPEdgeInsetsMake(10, 10, 10, 10)];
+
+ // Add Views to Horizontal Stack
+ // Leading
+ [stackViewHorizontal addView:[self _createBoxColor:[CPColor redColor] size:CGSizeMake(40, 40) label:@"L1"] inGravity:CPStackViewGravityLeading];
+ [stackViewHorizontal addView:[self _createBoxColor:[CPColor redColor] size:CGSizeMake(60, 80) label:@"L2"] inGravity:CPStackViewGravityLeading]; // Taller to test alignment
+
+ // Center
+ toggleTargetView = [self _createBoxColor:[CPColor greenColor] size:CGSizeMake(50, 50) label:@"C1\n(Toggle)"];
+ [stackViewHorizontal addView:toggleTargetView inGravity:CPStackViewGravityCenter];
+ [stackViewHorizontal addView:[self _createBoxColor:[CPColor greenColor] size:CGSizeMake(50, 50) label:@"C2"] inGravity:CPStackViewGravityCenter];
+
+ // Trailing
+ [stackViewHorizontal addView:[self _createBoxColor:[CPColor blueColor] size:CGSizeMake(40, 40) label:@"T1"] inGravity:CPStackViewGravityTrailing];
+ [stackViewHorizontal addView:[self _createBoxColor:[CPColor blueColor] size:CGSizeMake(40, 40) label:@"T2"] inGravity:CPStackViewGravityTrailing];
+
+ // Set Autoresizing to stick to width
+ [stackViewHorizontal setAutoresizingMask:CPViewWidthSizable];
+
+ // Visual border for the stack view itself
+ var borderView = [[CPView alloc] initWithFrame:CGRectInset([stackViewHorizontal frame], -1, -1)];
+ [borderView setBackgroundColor:[CPColor grayColor]];
+ [contentView addSubview:borderView];
+ [contentView addSubview:stackViewHorizontal];
+
+
+ // 3. Create Vertical Stack View (Secondary test)
+ stackViewVertical = [[CPStackView alloc] initWithFrame:CGRectMake(20, 230, 200, 300)];
+ [stackViewVertical setBackgroundColor:[CPColor whiteColor]];
+ [stackViewVertical setOrientation:CPUserInterfaceLayoutOrientationVertical];
+ [stackViewVertical setEdgeInsets:CPEdgeInsetsMake(5, 5, 5, 5)];
+ [stackViewVertical setAlignment:CPLayoutAttributeCenterX]; // Center items horizontally
+
+ [stackViewVertical addView:[self _createBoxColor:[CPColor orangeColor] size:CGSizeMake(40, 30) label:@"Top"] inGravity:CPStackViewGravityTop];
+ [stackViewVertical addView:[self _createBoxColor:[CPColor purpleColor] size:CGSizeMake(80, 40) label:@"Mid"] inGravity:CPStackViewGravityCenter];
+ [stackViewVertical addView:[self _createBoxColor:[CPColor brownColor] size:CGSizeMake(40, 30) label:@"Bot"] inGravity:CPStackViewGravityBottom];
+
+ var borderViewVert = [[CPView alloc] initWithFrame:CGRectInset([stackViewVertical frame], -1, -1)];
+ [borderViewVert setBackgroundColor:[CPColor grayColor]];
+ [contentView addSubview:borderViewVert];
+ [contentView addSubview:stackViewVertical];
+
+
+ // 4. Controls Area
+ var controlsY = 230;
+ var controlsX = 250;
+
+ var btnToggleHide = [CPButton buttonWithTitle:@"Toggle Center View Hidden"];
+ [btnToggleHide setFrameOrigin:CGPointMake(controlsX, controlsY)];
+ [btnToggleHide setTarget:self];
+ [btnToggleHide setAction:@selector(toggleHidden:)];
+ [contentView addSubview:btnToggleHide];
+
+ controlsY += 40;
+ detachHiddenCheckbox = [CPCheckBox checkBoxWithTitle:@"Detaches Hidden Views"];
+ [detachHiddenCheckbox setFrameOrigin:CGPointMake(controlsX, controlsY)];
+ [detachHiddenCheckbox setState:CPOnState];
+ [detachHiddenCheckbox setTarget:self];
+ [detachHiddenCheckbox setAction:@selector(toggleDetaches:)];
+ [contentView addSubview:detachHiddenCheckbox];
+
+ controlsY += 40;
+ var btnAlignTop = [CPButton buttonWithTitle:@"Align Horizontal: Top"];
+ [btnAlignTop setFrameOrigin:CGPointMake(controlsX, controlsY)];
+ [btnAlignTop setTarget:self];
+ [btnAlignTop setAction:@selector(setAlignmentTop:)];
+ [contentView addSubview:btnAlignTop];
+
+ controlsY += 30;
+ var btnAlignCenter = [CPButton buttonWithTitle:@"Align Horizontal: CenterY"];
+ [btnAlignCenter setFrameOrigin:CGPointMake(controlsX, controlsY)];
+ [btnAlignCenter setTarget:self];
+ [btnAlignCenter setAction:@selector(setAlignmentCenter:)];
+ [contentView addSubview:btnAlignCenter];
+
+ controlsY += 30;
+ var btnAlignFill = [CPButton buttonWithTitle:@"Align Horizontal: Height (Fill)"];
+ [btnAlignFill setFrameOrigin:CGPointMake(controlsX, controlsY)];
+ [btnAlignFill setTarget:self];
+ [btnAlignFill setAction:@selector(setAlignmentHeight:)];
+ [contentView addSubview:btnAlignFill];
+
+ controlsY += 40;
+ var btnSpacing = [CPButton buttonWithTitle:@"Increase Spacing"];
+ [btnSpacing setFrameOrigin:CGPointMake(controlsX, controlsY)];
+ [btnSpacing setTarget:self];
+ [btnSpacing setAction:@selector(changeSpacing:)];
+ [contentView addSubview:btnSpacing];
+
+ [theWindow setFullPlatformWindow:YES];
+ [theWindow orderFront:self];
+}
+
+- (void)awakeFromCib
+{
+ // If we were using a Cib, initialization would happen here.
+}
+
+#pragma mark -
+#pragma mark Actions
+
+- (@action)toggleHidden:(id)sender
+{
+ var isHidden = [toggleTargetView isHidden];
+ [toggleTargetView setHidden:!isHidden];
+
+ // In standard Cocoa, hiding a view triggers layout if stackView is observing,
+ // but in this manual implementation, we might need to nudge it or ensure
+ // setHidden triggers needsLayout on superview.
+ // The CPStackView provided relies on 'layoutSubviews' being called.
+
+ [stackViewHorizontal setNeedsLayout:YES];
+}
+
+- (@action)toggleDetaches:(id)sender
+{
+ [stackViewHorizontal setDetachesHiddenViews:([sender state] === CPOnState)];
+}
+
+- (@action)setAlignmentTop:(id)sender
+{
+ [stackViewHorizontal setAlignment:CPLayoutAttributeTop];
+}
+
+- (@action)setAlignmentCenter:(id)sender
+{
+ [stackViewHorizontal setAlignment:CPLayoutAttributeCenterY];
+}
+
+- (@action)setAlignmentHeight:(id)sender
+{
+ [stackViewHorizontal setAlignment:CPLayoutAttributeHeight];
+}
+
+- (@action)changeSpacing:(id)sender
+{
+ var current = [stackViewHorizontal spacing];
+ [stackViewHorizontal setSpacing:(current >= 20.0 ? 8.0 : current + 4.0)];
+}
+
+#pragma mark -
+#pragma mark Helpers
+
+- (CPView)_createBoxColor:(CPColor)aColor size:(CGSize)aSize label:(CPString)text
+{
+ var view = [[CPView alloc] initWithFrame:CGRectMake(0, 0, aSize.width, aSize.height)];
+ [view setBackgroundColor:aColor];
+
+ var label = [[CPTextField alloc] initWithFrame:CGRectInset([view bounds], 2, 2)];
+ [label setStringValue:text];
+ [label setTextColor:[CPColor whiteColor]];
+ [label setAlignment:CPCenterTextAlignment];
+ [label setVerticalAlignment:CPCenterVerticalTextAlignment];
+ [label setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
+ [label setLineBreakMode:CPLineBreakByWordWrapping];
+
+ [view addSubview:label];
+
+ return view;
+}
+
+@end
diff --git a/Tests/Manual/CPStackViewTest/Info.plist b/Tests/Manual/CPStackViewTest/Info.plist
new file mode 100644
index 000000000..7f51d542f
--- /dev/null
+++ b/Tests/Manual/CPStackViewTest/Info.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ Main cib file base name
+ MainMenu.cib
+ CPBundleName
+ CPStackViewTest
+
+
diff --git a/Tests/Manual/CPStackViewTest/Jakefile b/Tests/Manual/CPStackViewTest/Jakefile
new file mode 100644
index 000000000..a4174055a
--- /dev/null
+++ b/Tests/Manual/CPStackViewTest/Jakefile
@@ -0,0 +1,94 @@
+/*
+ * Jakefile
+ * CPSplitViewTest
+ *
+ * Created by Alexander Ljungberg on January 27, 2012.
+ * Copyright 2012, 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 ("CPSplitViewTest", function(task)
+{
+ task.setBuildIntermediatesPath(FILE.join("Build", "CPSplitViewTest.build", configuration));
+ task.setBuildPath(FILE.join("Build", configuration));
+
+ task.setProductName("CPSplitViewTest");
+ task.setIdentifier("com.yourcompany.CPSplitViewTest");
+ task.setVersion("1.0");
+ task.setAuthor("WireLoad");
+ task.setEmail("feedback @nospam@ yourcompany.com");
+ task.setSummary("CPSplitViewTest");
+ 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", ["CPSplitViewTest"], 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", "CPSplitViewTest", "index.html")]);
+});
+
+task ("run-release", ["release"], function()
+{
+ OS.system(["open", FILE.join("Build", "Release", "CPSplitViewTest", "index.html")]);
+});
+
+task ("deploy", ["release"], function()
+{
+ FILE.mkdirs(FILE.join("Build", "Deployment", "CPSplitViewTest"));
+ OS.system(["press", "-f", FILE.join("Build", "Release", "CPSplitViewTest"), FILE.join("Build", "Deployment", "CPSplitViewTest")]);
+ printResults("Deployment")
+});
+
+task ("desktop", ["release"], function()
+{
+ FILE.mkdirs(FILE.join("Build", "Desktop", "CPSplitViewTest"));
+ require("cappuccino/nativehost").buildNativeHost(FILE.join("Build", "Release", "CPSplitViewTest"), FILE.join("Build", "Desktop", "CPSplitViewTest", "CPSplitViewTest.app"));
+ printResults("Desktop")
+});
+
+task ("run-desktop", ["desktop"], function()
+{
+ OS.system([FILE.join("Build", "Desktop", "CPSplitViewTest", "CPSplitViewTest.app", "Contents", "MacOS", "NativeHost"), "-i"]);
+});
+
+function printResults(configuration)
+{
+ print("----------------------------");
+ print(configuration+" app built at path: "+FILE.join("Build", configuration, "CPSplitViewTest"));
+ print("----------------------------");
+}
diff --git a/Tests/Manual/CPStackViewTest/Resources/MainMenu.cib b/Tests/Manual/CPStackViewTest/Resources/MainMenu.cib
new file mode 100644
index 000000000..21dc1cbf9
--- /dev/null
+++ b/Tests/Manual/CPStackViewTest/Resources/MainMenu.cib
@@ -0,0 +1 @@
+280NPLIST;1.0;D;K;4;$topD;K;18;CPCibObjectDataKeyD;K;6;CP$UIDd;1;2E;E;K;8;$objectsA;S;5;$nullD;K;10;$classnameS;16;_CPCibObjectDataK;8;$classesA;S;16;_CPCibObjectDataS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;1E;K;28;_CPCibObjectDataNamesKeysKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataNamesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataClassesKeysKeyD;K;6;CP$UIDd;1;0E;K;32;_CPCibObjectDataClassesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataConnectionsKeyD;K;6;CP$UIDd;1;4E;K;28;_CPCibObjectDataFrameworkKeyD;K;6;CP$UIDd;1;0E;K;26;_CPCibObjectDataNextOidKeyD;K;6;CP$UIDd;1;5E;K;30;_CPCibObjectDataObjectsKeysKeyD;K;6;CP$UIDd;1;6E;K;32;_CPCibObjectDataObjectsValuesKeyD;K;6;CP$UIDd;1;7E;K;26;_CPCibObjectDataOidKeysKeyD;K;6;CP$UIDd;1;8E;K;28;_CPCibObjectDataOidValuesKeyD;K;6;CP$UIDd;1;9E;K;28;_CPCibObjectDataFileOwnerKeyD;K;6;CP$UIDd;2;11E;K;33;_CPCibObjectDataVisibleWindowsKeyD;K;6;CP$UIDd;2;13E;E;D;K;10;$classnameS;7;CPArrayK;8;$classesA;S;7;CPArrayS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;15E;D;K;6;CP$UIDd;2;16E;D;K;6;CP$UIDd;2;17E;D;K;6;CP$UIDd;2;18E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;20E;D;K;6;CP$UIDd;2;22E;D;K;6;CP$UIDd;2;23E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;24E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;28E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;34E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;36E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;39E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;40E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;41E;D;K;6;CP$UIDd;2;42E;D;K;6;CP$UIDd;2;43E;D;K;6;CP$UIDd;2;44E;D;K;6;CP$UIDd;2;45E;D;K;6;CP$UIDd;2;46E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;47E;D;K;6;CP$UIDd;2;48E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;49E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;28E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;34E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;36E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;39E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;40E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;41E;D;K;6;CP$UIDd;2;42E;D;K;6;CP$UIDd;2;43E;D;K;6;CP$UIDd;2;43E;D;K;6;CP$UIDd;2;45E;D;K;6;CP$UIDd;2;46E;D;K;6;CP$UIDd;2;41E;D;K;6;CP$UIDd;2;47E;D;K;6;CP$UIDd;2;48E;D;K;6;CP$UIDd;2;11E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;10;$classnameS;18;_CPCibCustomObjectK;8;$classesA;S;18;_CPCibCustomObjectS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;50E;E;D;K;10;$classnameS;5;CPSetK;8;$classesA;S;5;CPSetS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;12E;K;15;CPSetObjectsKeyD;K;6;CP$UIDd;2;51E;E;D;K;10;$classnameS;20;CPCibOutletConnectorK;8;$classesA;S;20;CPCibOutletConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;49E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;52E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;49E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;30E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;53E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;49E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;41E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;54E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;49E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;43E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;55E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;49E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;26E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;56E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;30E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;49E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;52E;E;D;K;10;$classnameS;21;CPCibControlConnectorK;8;$classesA;S;21;CPCibControlConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;21E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;39E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;36E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;57E;E;D;K;6;$classD;K;6;CP$UIDd;2;21E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;34E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;49E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;58E;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;50E;E;D;K;10;$classnameS;20;_CPCibWindowTemplateK;8;$classesA;S;20;_CPCibWindowTemplateS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;25E;K;30;_CPCibWindowTemplateMaxSizeKeyD;K;6;CP$UIDd;2;59E;K;32;_CPCibWindowTemplateViewClassKeyD;K;6;CP$UIDd;1;0E;K;34;_CPCibWindowTemplateWindowClassKeyD;K;6;CP$UIDd;2;60E;K;30;_CPCibWindowTemplateWTFlagsKeyD;K;6;CP$UIDd;2;61E;K;33;_CPCibWindowTemplateWindowRectKeyD;K;6;CP$UIDd;2;62E;K;33;_CPCibWindowTemplateScreenRectKeyD;K;6;CP$UIDd;2;63E;K;30;_CPCibWindowTempatStyleMaskKeyD;K;6;CP$UIDd;2;64E;K;34;_CPCibWindowTemplateWindowTitleKeyD;K;6;CP$UIDd;2;65E;K;33;_CPCibWindowTemplateWindowViewKeyD;K;6;CP$UIDd;2;28E;E;D;K;10;$classnameS;6;CPViewK;8;$classesA;S;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;27E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;67E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;67E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;2;68E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;73E;E;D;K;10;$classnameS;11;CPSplitViewK;8;$classesA;S;11;CPSplitViewS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;29E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;28E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;67E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;67E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;2;74E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;28E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;75E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;76E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;77E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;78E;K;22;CPSplitViewDelegateKeyD;K;6;CP$UIDd;1;0E;K;24;CPSplitViewIsVerticalKeyD;K;6;CP$UIDd;2;72E;K;26;CPSplitViewDividerStyleKeyD;K;6;CP$UIDd;2;79E;K;29;CPSplitViewDividerSubviewsKeyD;K;6;CP$UIDd;2;80E;K;30;CPSplitViewArrangedSubviewsKeyD;K;6;CP$UIDd;2;81E;K;26;CPSplitViewRealSubviewsKeyD;K;6;CP$UIDd;2;82E;K;33;CPSplitViewArrangesAllSubviewsKeyD;K;6;CP$UIDd;2;83E;K;26;CPSplitViewAutosaveNameKeyD;K;6;CP$UIDd;2;53E;E;D;K;10;$classnameS;16;_CPCibCustomViewK;8;$classesA;S;16;_CPCibCustomViewS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;31E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;30E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;84E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;85E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;2;86E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;30E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;88E;K;28;_CPCibCustomViewClassNameKeyD;K;6;CP$UIDd;2;89E;E;D;K;10;$classnameS;8;CPButtonK;8;$classesA;S;8;CPButtonS;9;CPControlS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;33E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;32E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;90E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;91E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;32E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;92E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;93E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;94E;K;15;$aimage-scalingD;K;6;CP$UIDd;2;66E;K;16;$aimage-positionD;K;6;CP$UIDd;2;66E;K;6;$afontD;K;6;CP$UIDd;2;96E;K;17;$aline-break-modeD;K;6;CP$UIDd;2;66E;K;11;$aalignmentD;K;6;CP$UIDd;2;97E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;98E;K;17;CPControlValueKeyD;K;6;CP$UIDd;2;66E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;2;99E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;2;66E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;3;100E;K;16;CPButtonTitleKeyD;K;6;CP$UIDd;3;101E;K;25;CPButtonAlternateTitleKeyD;K;6;CP$UIDd;3;102E;K;27;CPButtonAllowsMixedStateKeyD;K;6;CP$UIDd;2;72E;K;23;CPButtonHighlightsByKeyD;K;6;CP$UIDd;3;103E;K;23;CPButtonShowsStateByKeyD;K;6;CP$UIDd;2;66E;K;32;CPButtonImageDimsWhenDisabledKeyD;K;6;CP$UIDd;2;83E;K;24;CPButtonImagePositionKeyD;K;6;CP$UIDd;2;66E;K;28;CPButtonKeyEquivalentMaskKeyD;K;6;CP$UIDd;2;66E;K;24;CPButtonPeriodicDelayKeyD;K;6;CP$UIDd;1;0E;K;27;CPButtonPeriodicIntervalKeyD;K;6;CP$UIDd;1;0E;K;21;CPButtonIsBorderedKeyD;K;6;CP$UIDd;2;83E;K;21;CPButtonBezelStyleKeyD;K;6;CP$UIDd;3;104E;E;D;K;10;$classnameS;11;CPTextFieldK;8;$classesA;S;11;CPTextFieldS;9;CPControlS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;35E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;32E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;105E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;106E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;32E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;3;107E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;108E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;3;109E;K;6;$afontD;K;6;CP$UIDd;3;111E;K;17;$aline-break-modeD;K;6;CP$UIDd;3;112E;K;11;$aalignmentD;K;6;CP$UIDd;3;113E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;114E;K;35;CPControlSendsActionOnEndEditingKeyD;K;6;CP$UIDd;2;83E;K;17;CPControlValueKeyD;K;6;CP$UIDd;3;102E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;2;99E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;2;66E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;3;100E;K;24;CPTextFieldIsEditableKeyD;K;6;CP$UIDd;2;83E;K;26;CPTextFieldIsSelectableKeyD;K;6;CP$UIDd;2;83E;K;29;CPTextFieldDrawsBackgroundKeyD;K;6;CP$UIDd;2;83E;K;29;CPTextFieldBackgroundColorKeyD;K;6;CP$UIDd;3;116E;K;27;CPTextFieldLineBreakModeKeyD;K;6;CP$UIDd;2;97E;K;23;CPTextFieldAlignmentKeyD;K;6;CP$UIDd;2;99E;K;31;CPTextFieldPlaceholderStringKeyD;K;6;CP$UIDd;1;0E;K;29;CPTextFieldUsesSingleLineModeD;K;6;CP$UIDd;2;72E;K;16;CPTextFieldWrapsD;K;6;CP$UIDd;2;72E;K;18;CPTextFieldScrollsD;K;6;CP$UIDd;2;83E;E;D;K;6;$classD;K;6;CP$UIDd;2;31E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;30E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;117E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;118E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;3;119E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;30E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;120E;K;28;_CPCibCustomViewClassNameKeyD;K;6;CP$UIDd;2;89E;E;D;K;10;$classnameS;8;CPSliderK;8;$classesA;S;8;CPSliderS;9;CPControlS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;38E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;37E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;121E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;122E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;37E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;3;107E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;123E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;3;124E;K;6;$afontD;K;6;CP$UIDd;3;125E;K;17;$aline-break-modeD;K;6;CP$UIDd;2;66E;K;11;$aalignmentD;K;6;CP$UIDd;2;66E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;126E;K;17;CPControlValueKeyD;K;6;CP$UIDd;3;127E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;3;128E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;2;66E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;3;100E;K;19;CPSliderMinValueKeyD;K;6;CP$UIDd;2;66E;K;19;CPSliderMaxValueKeyD;K;6;CP$UIDd;3;129E;K;23;CPSliderAltIncrValueKeyD;K;6;CP$UIDd;2;66E;K;15;CPSliderTypeKeyD;K;6;CP$UIDd;2;66E;K;35;CPSliderAllowsTickMarkValuesOnlyKeyD;K;6;CP$UIDd;2;72E;K;27;CPSliderTickMarkPositionKeyD;K;6;CP$UIDd;3;104E;K;28;CPSliderNumberOfTickMarksKeyD;K;6;CP$UIDd;2;66E;E;D;K;6;$classD;K;6;CP$UIDd;2;35E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;30E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;130E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;131E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;30E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;92E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;108E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;3;124E;K;6;$afontD;K;6;CP$UIDd;3;132E;K;17;$aline-break-modeD;K;6;CP$UIDd;3;133E;K;11;$aalignmentD;K;6;CP$UIDd;3;134E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;135E;K;35;CPControlSendsActionOnEndEditingKeyD;K;6;CP$UIDd;2;83E;K;17;CPControlValueKeyD;K;6;CP$UIDd;3;136E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;2;99E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;2;66E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;3;100E;K;24;CPTextFieldIsEditableKeyD;K;6;CP$UIDd;2;72E;K;26;CPTextFieldIsSelectableKeyD;K;6;CP$UIDd;2;72E;K;29;CPTextFieldDrawsBackgroundKeyD;K;6;CP$UIDd;2;72E;K;29;CPTextFieldBackgroundColorKeyD;K;6;CP$UIDd;3;137E;K;27;CPTextFieldLineBreakModeKeyD;K;6;CP$UIDd;2;97E;K;23;CPTextFieldAlignmentKeyD;K;6;CP$UIDd;2;99E;K;31;CPTextFieldPlaceholderStringKeyD;K;6;CP$UIDd;1;0E;K;29;CPTextFieldUsesSingleLineModeD;K;6;CP$UIDd;2;72E;K;16;CPTextFieldWrapsD;K;6;CP$UIDd;2;72E;K;18;CPTextFieldScrollsD;K;6;CP$UIDd;2;83E;E;D;K;6;$classD;K;6;CP$UIDd;2;29E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;30E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;138E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;138E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;3;139E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;30E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;76E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;3;140E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;141E;K;22;CPSplitViewDelegateKeyD;K;6;CP$UIDd;1;0E;K;24;CPSplitViewIsVerticalKeyD;K;6;CP$UIDd;2;83E;K;26;CPSplitViewDividerStyleKeyD;K;6;CP$UIDd;2;97E;K;29;CPSplitViewDividerSubviewsKeyD;K;6;CP$UIDd;3;142E;K;30;CPSplitViewArrangedSubviewsKeyD;K;6;CP$UIDd;3;143E;K;26;CPSplitViewRealSubviewsKeyD;K;6;CP$UIDd;3;144E;K;33;CPSplitViewArrangesAllSubviewsKeyD;K;6;CP$UIDd;2;83E;K;26;CPSplitViewAutosaveNameKeyD;K;6;CP$UIDd;2;54E;E;D;K;6;$classD;K;6;CP$UIDd;2;31E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;41E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;145E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;145E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;3;146E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;41E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;147E;K;28;_CPCibCustomViewClassNameKeyD;K;6;CP$UIDd;2;89E;E;D;K;6;$classD;K;6;CP$UIDd;2;29E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;42E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;145E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;145E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;3;148E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;42E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;75E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;76E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;3;149E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;150E;K;22;CPSplitViewDelegateKeyD;K;6;CP$UIDd;1;0E;K;24;CPSplitViewIsVerticalKeyD;K;6;CP$UIDd;2;72E;K;26;CPSplitViewDividerStyleKeyD;K;6;CP$UIDd;3;104E;K;29;CPSplitViewDividerSubviewsKeyD;K;6;CP$UIDd;3;151E;K;30;CPSplitViewArrangedSubviewsKeyD;K;6;CP$UIDd;3;152E;K;26;CPSplitViewRealSubviewsKeyD;K;6;CP$UIDd;3;153E;K;33;CPSplitViewArrangesAllSubviewsKeyD;K;6;CP$UIDd;2;83E;K;26;CPSplitViewAutosaveNameKeyD;K;6;CP$UIDd;2;55E;E;D;K;6;$classD;K;6;CP$UIDd;2;31E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;43E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;154E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;155E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;43E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;156E;K;28;_CPCibCustomViewClassNameKeyD;K;6;CP$UIDd;2;89E;E;D;K;6;$classD;K;6;CP$UIDd;2;31E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;43E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;157E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;157E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;3;158E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;43E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;159E;K;28;_CPCibCustomViewClassNameKeyD;K;6;CP$UIDd;2;89E;E;D;K;6;$classD;K;6;CP$UIDd;2;33E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;45E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;160E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;161E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;45E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;93E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;94E;K;15;$aimage-scalingD;K;6;CP$UIDd;2;66E;K;16;$aimage-positionD;K;6;CP$UIDd;2;66E;K;6;$afontD;K;6;CP$UIDd;2;96E;K;17;$aline-break-modeD;K;6;CP$UIDd;2;66E;K;11;$aalignmentD;K;6;CP$UIDd;2;97E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;162E;K;17;CPControlValueKeyD;K;6;CP$UIDd;2;66E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;2;99E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;2;66E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;3;100E;K;16;CPButtonTitleKeyD;K;6;CP$UIDd;3;163E;K;25;CPButtonAlternateTitleKeyD;K;6;CP$UIDd;3;102E;K;27;CPButtonAllowsMixedStateKeyD;K;6;CP$UIDd;2;72E;K;23;CPButtonHighlightsByKeyD;K;6;CP$UIDd;3;103E;K;23;CPButtonShowsStateByKeyD;K;6;CP$UIDd;2;66E;K;32;CPButtonImageDimsWhenDisabledKeyD;K;6;CP$UIDd;2;83E;K;24;CPButtonImagePositionKeyD;K;6;CP$UIDd;2;66E;K;28;CPButtonKeyEquivalentMaskKeyD;K;6;CP$UIDd;2;66E;K;24;CPButtonPeriodicDelayKeyD;K;6;CP$UIDd;1;0E;K;27;CPButtonPeriodicIntervalKeyD;K;6;CP$UIDd;1;0E;K;21;CPButtonIsBorderedKeyD;K;6;CP$UIDd;2;83E;K;21;CPButtonBezelStyleKeyD;K;6;CP$UIDd;3;104E;E;D;K;6;$classD;K;6;CP$UIDd;2;31E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;41E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;164E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;165E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;3;166E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;41E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;167E;K;28;_CPCibCustomViewClassNameKeyD;K;6;CP$UIDd;2;89E;E;D;K;6;$classD;K;6;CP$UIDd;2;33E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;47E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;168E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;169E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;47E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;93E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;94E;K;15;$aimage-scalingD;K;6;CP$UIDd;2;66E;K;16;$aimage-positionD;K;6;CP$UIDd;2;66E;K;6;$afontD;K;6;CP$UIDd;2;96E;K;17;$aline-break-modeD;K;6;CP$UIDd;2;66E;K;11;$aalignmentD;K;6;CP$UIDd;2;97E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;170E;K;17;CPControlValueKeyD;K;6;CP$UIDd;2;66E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;2;99E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;2;66E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;3;100E;K;16;CPButtonTitleKeyD;K;6;CP$UIDd;3;171E;K;25;CPButtonAlternateTitleKeyD;K;6;CP$UIDd;3;102E;K;27;CPButtonAllowsMixedStateKeyD;K;6;CP$UIDd;2;72E;K;23;CPButtonHighlightsByKeyD;K;6;CP$UIDd;3;103E;K;23;CPButtonShowsStateByKeyD;K;6;CP$UIDd;2;66E;K;32;CPButtonImageDimsWhenDisabledKeyD;K;6;CP$UIDd;2;83E;K;24;CPButtonImagePositionKeyD;K;6;CP$UIDd;2;66E;K;28;CPButtonKeyEquivalentMaskKeyD;K;6;CP$UIDd;2;66E;K;24;CPButtonPeriodicDelayKeyD;K;6;CP$UIDd;1;0E;K;27;CPButtonPeriodicIntervalKeyD;K;6;CP$UIDd;1;0E;K;21;CPButtonIsBorderedKeyD;K;6;CP$UIDd;2;83E;K;21;CPButtonBezelStyleKeyD;K;6;CP$UIDd;3;104E;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;3;172E;E;S;13;CPApplicationD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;26E;E;E;S;8;delegateS;10;splitViewAS;10;splitViewBS;10;splitViewCS;9;theWindowS;20;takeDoubleValueFrom:S;15;deleteAutosave:S;32;{10000000000000, 10000000000000}S;8;CPWindowd;10;1946157056S;24;{{335, 668}, {480, 360}}S;22;{{0, 0}, {2560, 1418}}d;1;7S;6;Windowd;1;0S;20;{{0, 0}, {480, 360}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;30E;E;E;S;4;viewS;6;normalS;6;{1, 1}F;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;40E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;41E;E;E;d;2;18S;9;splitviewS;31;splitview-divider-pane-splitterD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;d;1;3D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;T;S;21;{{0, 231}, {480, 85}}S;19;{{0, 0}, {480, 85}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;36E;D;K;6;CP$UIDd;2;34E;E;E;d;1;8D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;6;CPViewS;21;{{20, 33}, {130, 25}}S;19;{{0, 0}, {130, 25}}d;2;36S;6;buttonS;35;bordered+controlSizeRegular+roundedD;K;10;$classnameS;6;CPFontK;8;$classesA;S;6;CPFontS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;95E;K;13;CPFontNameKeyD;K;6;CP$UIDd;3;173E;K;13;CPFontSizeKeyD;K;6;CP$UIDd;3;100E;K;15;CPFontIsBoldKeyD;K;6;CP$UIDd;2;83E;K;17;CPFontIsItalicKeyD;K;6;CP$UIDd;2;72E;K;17;CPFontIsSystemKeyD;K;6;CP$UIDd;2;83E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;174E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;E;d;1;2D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;d;1;4d;2;-1S;15;Delete AutosaveS;0;d;2;14d;1;1S;22;{{360, 30}, {104, 22}}S;19;{{0, 0}, {104, 22}}d;2;45S;9;textfieldS;47;bezeled+controlSizeRegular+editable+placeholderD;K;10;$classnameS;17;_CPThemeAttributeK;8;$classesA;S;17;_CPThemeAttributeS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;110E;K;4;nameD;K;6;CP$UIDd;3;174E;K;12;defaultValueD;K;6;CP$UIDd;3;176E;K;6;valuesD;K;6;CP$UIDd;3;178E;E;D;K;6;$classD;K;6;CP$UIDd;3;110E;K;4;nameD;K;6;CP$UIDd;3;179E;K;12;defaultValueD;K;6;CP$UIDd;2;97E;K;6;valuesD;K;6;CP$UIDd;3;180E;E;D;K;6;$classD;K;6;CP$UIDd;3;110E;K;4;nameD;K;6;CP$UIDd;3;181E;K;12;defaultValueD;K;6;CP$UIDd;2;66E;K;6;valuesD;K;6;CP$UIDd;3;182E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;184E;D;K;6;CP$UIDd;3;185E;E;E;D;K;10;$classnameS;7;CPColorK;8;$classesA;S;7;CPColorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;115E;K;20;CPColorComponentsKeyD;K;6;CP$UIDd;3;186E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;187E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;E;S;21;{{0, 142}, {480, 79}}S;19;{{0, 0}, {480, 79}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;39E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;21;{{194, 31}, {92, 21}}S;18;{{0, 0}, {92, 21}}S;6;sliderS;18;controlSizeRegularD;K;6;$classD;K;6;CP$UIDd;2;95E;K;13;CPFontNameKeyD;K;6;CP$UIDd;3;188E;K;13;CPFontSizeKeyD;K;6;CP$UIDd;3;189E;K;15;CPFontIsBoldKeyD;K;6;CP$UIDd;2;72E;K;17;CPFontIsItalicKeyD;K;6;CP$UIDd;2;72E;K;17;CPFontIsSystemKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;174E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;d;2;50d;2;68d;3;100S;21;{{2, 326}, {476, 34}}S;19;{{0, 0}, {476, 34}}D;K;6;$classD;K;6;CP$UIDd;3;110E;K;4;nameD;K;6;CP$UIDd;3;174E;K;12;defaultValueD;K;6;CP$UIDd;3;176E;K;6;valuesD;K;6;CP$UIDd;3;190E;E;D;K;6;$classD;K;6;CP$UIDd;3;110E;K;4;nameD;K;6;CP$UIDd;3;179E;K;12;defaultValueD;K;6;CP$UIDd;2;97E;K;6;valuesD;K;6;CP$UIDd;3;191E;E;D;K;6;$classD;K;6;CP$UIDd;3;110E;K;4;nameD;K;6;CP$UIDd;3;181E;K;12;defaultValueD;K;6;CP$UIDd;2;66E;K;6;valuesD;K;6;CP$UIDd;3;192E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;193E;E;E;S;5;LabelD;K;6;$classD;K;6;CP$UIDd;3;115E;K;20;CPColorComponentsKeyD;K;6;CP$UIDd;3;194E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;187E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;E;S;20;{{0, 0}, {480, 132}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;42E;D;K;6;CP$UIDd;2;47E;E;E;S;31;splitview-divider-thin+verticalD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;20;{{0, 0}, {246, 132}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;43E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;44E;D;K;6;CP$UIDd;2;45E;E;E;S;78;dummy one as CPSplitViewDividerStyle is not zero-based+splitview-divider-thickD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;20;{{0, 81}, {246, 51}}S;19;{{0, 0}, {246, 51}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;19;{{0, 0}, {246, 72}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;46E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;21;{{72, 26}, {102, 25}}S;19;{{0, 0}, {102, 25}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;11;Bottom LeftS;22;{{247, 0}, {233, 132}}S;20;{{0, 0}, {233, 132}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;48E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;20;{{86, 56}, {61, 25}}S;18;{{0, 0}, {61, 25}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;5;RightS;13;AppControllerS;28;_CPFontSystemFacePlaceholderS;4;fontD;K;10;$classnameS;6;CPNullK;8;$classesA;S;6;CPNullS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;175E;E;D;K;10;$classnameS;12;CPDictionaryK;8;$classesA;S;12;CPDictionaryS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;177E;K;10;CP.objectsD;K;13;tableDataViewD;K;6;CP$UIDd;3;195E;K;6;normalD;K;6;CP$UIDd;3;195E;E;E;S;15;line-break-modeD;K;6;$classD;K;6;CP$UIDd;3;177E;K;10;CP.objectsD;K;13;tableDataViewD;K;6;CP$UIDd;2;97E;K;6;normalD;K;6;CP$UIDd;2;97E;E;E;S;9;alignmentD;K;6;$classD;K;6;CP$UIDd;3;177E;K;10;CP.objectsD;K;13;tableDataViewD;K;6;CP$UIDd;2;99E;K;6;normalD;K;6;CP$UIDd;2;99E;E;E;D;K;10;$classnameS;14;CPTrackingAreaK;8;$classesA;S;14;CPTrackingAreaS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;183E;K;25;CPTrackinkAreaViewRectKeyD;K;6;CP$UIDd;3;197E;K;24;CPTrackingAreaOptionsKeyD;K;6;CP$UIDd;3;198E;K;22;CPTrackingAreaOwnerKeyD;K;6;CP$UIDd;2;36E;K;25;CPTrackingAreaUserInfoKeyD;K;6;CP$UIDd;1;0E;K;32;CPTrackingAreaReferencingViewKeyD;K;6;CP$UIDd;2;36E;K;24;CPTrackingAreaWindowRectD;K;6;CP$UIDd;3;199E;E;D;K;6;$classD;K;6;CP$UIDd;3;183E;K;25;CPTrackinkAreaViewRectKeyD;K;6;CP$UIDd;3;200E;K;24;CPTrackingAreaOptionsKeyD;K;6;CP$UIDd;3;201E;K;22;CPTrackingAreaOwnerKeyD;K;6;CP$UIDd;2;36E;K;25;CPTrackingAreaUserInfoKeyD;K;6;CP$UIDd;1;0E;K;32;CPTrackingAreaReferencingViewKeyD;K;6;CP$UIDd;2;36E;K;24;CPTrackingAreaWindowRectD;K;6;CP$UIDd;3;202E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;104E;D;K;6;CP$UIDd;3;104E;D;K;6;CP$UIDd;3;104E;D;K;6;CP$UIDd;3;104E;E;E;S;5;colorS;9;Helveticad;2;12D;K;6;$classD;K;6;CP$UIDd;3;177E;K;10;CP.objectsD;K;13;tableDataViewD;K;6;CP$UIDd;3;195E;K;6;normalD;K;6;CP$UIDd;3;195E;E;E;D;K;6;$classD;K;6;CP$UIDd;3;177E;K;10;CP.objectsD;K;13;tableDataViewD;K;6;CP$UIDd;2;97E;K;6;normalD;K;6;CP$UIDd;2;97E;E;E;D;K;6;$classD;K;6;CP$UIDd;3;177E;K;10;CP.objectsD;K;13;tableDataViewD;K;6;CP$UIDd;2;99E;K;6;normalD;K;6;CP$UIDd;2;99E;E;E;D;K;6;$classD;K;6;CP$UIDd;3;183E;K;25;CPTrackinkAreaViewRectKeyD;K;6;CP$UIDd;3;203E;K;24;CPTrackingAreaOptionsKeyD;K;6;CP$UIDd;3;201E;K;22;CPTrackingAreaOwnerKeyD;K;6;CP$UIDd;2;40E;K;25;CPTrackingAreaUserInfoKeyD;K;6;CP$UIDd;1;0E;K;32;CPTrackingAreaReferencingViewKeyD;K;6;CP$UIDd;2;40E;K;24;CPTrackingAreaWindowRectD;K;6;CP$UIDd;3;204E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;205E;D;K;6;CP$UIDd;3;205E;D;K;6;CP$UIDd;3;205E;D;K;6;CP$UIDd;3;104E;E;E;D;K;6;$classD;K;6;CP$UIDd;2;95E;K;13;CPFontNameKeyD;K;6;CP$UIDd;3;173E;K;13;CPFontSizeKeyD;K;6;CP$UIDd;3;100E;K;15;CPFontIsBoldKeyD;K;6;CP$UIDd;2;72E;K;17;CPFontIsItalicKeyD;K;6;CP$UIDd;2;72E;K;17;CPFontIsSystemKeyD;K;6;CP$UIDd;2;83E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;174E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;E;D;K;10;$classnameS;21;_CPKeyedArchiverValueK;8;$classesA;S;21;_CPKeyedArchiverValueS;7;CPValueS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;196E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;206E;E;d;2;40D;K;6;$classD;K;6;CP$UIDd;3;196E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;207E;E;D;K;6;$classD;K;6;CP$UIDd;3;196E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;208E;E;d;3;546D;K;6;$classD;K;6;CP$UIDd;3;196E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;209E;E;D;K;6;$classD;K;6;CP$UIDd;3;196E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;208E;E;D;K;6;$classD;K;6;CP$UIDd;3;196E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;210E;E;f;18;0.6862745098039216S;56;{"origin":{"x":0,"y":1},"size":{"width":96,"height":21}}S;59;{"origin":{"x":364,"y":75},"size":{"width":96,"height":21}}S;54;{"origin":{"x":0,"y":0},"size":{"width":0,"height":0}}S;59;{"origin":{"x":364,"y":74},"size":{"width":96,"height":22}}S;57;{"origin":{"x":0,"y":0},"size":{"width":480,"height":34}}E;K;9;$archiverS;15;CPKeyedArchiverK;8;$versionS;6;100000E;
\ No newline at end of file
diff --git a/Tests/Manual/CPStackViewTest/Resources/MainMenu.xib b/Tests/Manual/CPStackViewTest/Resources/MainMenu.xib
new file mode 100644
index 000000000..d497e2df5
--- /dev/null
+++ b/Tests/Manual/CPStackViewTest/Resources/MainMenu.xib
@@ -0,0 +1,809 @@
+
+
+
+ 1050
+ 11E53
+ 2182
+ 1138.47
+ 569.00
+
+
+
+
+
+
+
+ YES
+
+
+ delegate
+
+
+
+ 451
+
+
+
+ theWindow
+
+
+
+ 459
+
+
+
+ splitViewA
+
+
+
+ 479
+
+
+
+ splitViewB
+
+
+
+ 480
+
+
+
+ splitViewC
+
+
+
+ 481
+
+
+
+ deleteAutosave:
+
+
+
+ 482
+
+
+
+ takeDoubleValueFrom:
+
+
+
+ 458
+
+
+
+ delegate
+
+
+
+ 473
+
+
+
+
+ YES
+
+ 0
+
+ YES
+
+
+
+
+
+ -2
+
+
+ File's Owner
+
+
+ -1
+
+
+ First Responder
+
+
+ -3
+
+
+ Application
+
+
+ 371
+
+
+ YES
+
+
+
+
+
+ 372
+
+
+ YES
+
+
+
+
+
+ 450
+
+
+
+
+ 460
+
+
+ YES
+
+
+
+
+
+
+
+
+ 461
+
+
+ YES
+
+
+
+
+
+
+ 462
+
+
+ YES
+
+
+
+
+
+ 456
+
+
+ YES
+
+
+
+
+
+ 457
+
+
+
+
+ 452
+
+
+ YES
+
+
+
+
+
+ 453
+
+
+
+
+ 463
+
+
+ YES
+
+
+
+
+
+ 464
+
+
+
+
+ 465
+
+
+ YES
+
+
+
+
+
+
+ 466
+
+
+ YES
+
+
+
+
+
+ 467
+
+
+ YES
+
+
+
+
+
+ 470
+
+
+ YES
+
+
+
+
+
+ 471
+
+
+
+
+ 474
+
+
+ YES
+
+
+
+
+
+
+ 475
+
+
+
+
+ 476
+
+
+ YES
+
+
+
+
+
+ 468
+
+
+ YES
+
+
+
+
+
+ 469
+
+
+
+
+ 477
+
+
+ YES
+
+
+
+
+
+ 478
+
+
+
+
+
+
+ YES
+
+ YES
+ -1.IBPluginDependency
+ -2.IBPluginDependency
+ -3.IBPluginDependency
+ 371.IBPluginDependency
+ 371.IBWindowTemplateEditedContentRect
+ 371.NSWindowTemplate.visibleAtLaunch
+ 372.IBPluginDependency
+ 450.IBPluginDependency
+ 452.IBPluginDependency
+ 453.IBPluginDependency
+ 456.IBPluginDependency
+ 457.IBPluginDependency
+ 460.IBPluginDependency
+ 461.IBPluginDependency
+ 462.IBPluginDependency
+ 463.IBPluginDependency
+ 464.IBPluginDependency
+ 465.IBPluginDependency
+ 466.IBPluginDependency
+ 467.IBPluginDependency
+ 468.IBPluginDependency
+ 469.IBPluginDependency
+ 470.IBPluginDependency
+ 471.IBPluginDependency
+ 474.IBPluginDependency
+ 475.IBPluginDependency
+ 476.IBPluginDependency
+ 477.IBPluginDependency
+ 478.IBPluginDependency
+
+
+ YES
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ {{303, 221}, {480, 360}}
+
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+ com.apple.InterfaceBuilder.CocoaPlugin
+
+
+
+ YES
+
+
+
+
+
+ YES
+
+
+
+
+ 482
+
+
+
+ YES
+
+ AppController
+ NSObject
+
+ deleteAutosave:
+ id
+
+
+ deleteAutosave:
+
+ deleteAutosave:
+ id
+
+
+
+ YES
+
+ YES
+ splitViewA
+ splitViewB
+ splitViewC
+
+
+ YES
+ NSSplitView
+ NSSplitView
+ NSSplitView
+
+
+
+ YES
+
+ YES
+ splitViewA
+ splitViewB
+ splitViewC
+
+
+ YES
+
+ splitViewA
+ NSSplitView
+
+
+ splitViewB
+ NSSplitView
+
+
+ splitViewC
+ NSSplitView
+
+
+
+
+ IBProjectSource
+ ./Classes/AppController.h
+
+
+
+
+ 0
+ IBCocoaFramework
+
+ com.apple.InterfaceBuilder.CocoaPlugin.macosx
+
+
+
+ com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3
+
+
+ YES
+ 3
+
+
diff --git a/Tests/Manual/CPStackViewTest/Resources/spinner.gif b/Tests/Manual/CPStackViewTest/Resources/spinner.gif
new file mode 100644
index 000000000..a5e705f6c
Binary files /dev/null and b/Tests/Manual/CPStackViewTest/Resources/spinner.gif differ
diff --git a/Tests/Manual/CPStackViewTest/index-debug.html b/Tests/Manual/CPStackViewTest/index-debug.html
new file mode 100644
index 000000000..a36b1d3b9
--- /dev/null
+++ b/Tests/Manual/CPStackViewTest/index-debug.html
@@ -0,0 +1,204 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ __project.name__
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/Manual/CPStackViewTest/index.html b/Tests/Manual/CPStackViewTest/index.html
new file mode 100644
index 000000000..ac42c98a7
--- /dev/null
+++ b/Tests/Manual/CPStackViewTest/index.html
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ __project.name__
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/Manual/CPStackViewTest/main.j b/Tests/Manual/CPStackViewTest/main.j
new file mode 100644
index 000000000..bceec5f14
--- /dev/null
+++ b/Tests/Manual/CPStackViewTest/main.j
@@ -0,0 +1,18 @@
+/*
+ * AppController.j
+ * CPStackViewTest
+ *
+ * Created by Daniel Böhringer on December 26, 2025.
+ * Copyright 2025, All rights reserved.
+ */
+
+@import
+@import
+
+@import "AppController.j"
+
+
+function main(args, namedArgs)
+{
+ CPApplicationMain(args, namedArgs);
+}
diff --git a/Tests/Manual/CPTreeControllerTest/AppController.j b/Tests/Manual/CPTreeControllerTest/AppController.j
new file mode 100644
index 000000000..15e24c0e4
--- /dev/null
+++ b/Tests/Manual/CPTreeControllerTest/AppController.j
@@ -0,0 +1,153 @@
+/*
+ * AppController.j
+ * TreeControllerBindingsTest
+ *
+ * Created for testing CPOutlineView and CPTreeController bindings.
+ */
+
+@import
+@import
+
+@implementation AppController : CPObject
+{
+ CPTreeController treeController;
+ CPTextField logField;
+}
+
+- (void)applicationDidFinishLaunching:(CPNotification)aNotification
+{
+ var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
+ contentView = [theWindow contentView];
+
+ // 1. Create the Data Model
+ var root1 = [[Node alloc] initWithName:@"Root 1" children:[]],
+ child1 = [[Node alloc] initWithName:@"Child 1.1" children:[]],
+ child2 = [[Node alloc] initWithName:@"Child 1.2" children:[]],
+ root2 = [[Node alloc] initWithName:@"Root 2" children:[]],
+ child3 = [[Node alloc] initWithName:@"Child 2.1" children:[]];
+
+ [root1 setChildren:[child1, child2]];
+ [root2 setChildren:[child3]];
+ var contentArray = [root1, root2];
+
+ // 2. Setup the Tree Controller
+ treeController = [[CPTreeController alloc] init];
+ [treeController setChildrenKeyPath:@"children"];
+ [treeController setContent:contentArray];
+
+ // 3. Setup the Outline View
+ var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(20, 20, 250, 300)];
+ [scrollView setAutohidesScrollers:YES];
+
+ var outlineView = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 250, 300)];
+ var column = [[CPTableColumn alloc] initWithIdentifier:@"name"];
+ [[column headerView] setStringValue:@"Node Name"];
+ [column setWidth:240];
+ [column setEditable:YES]; // Editable to test bidirectional bindings in the tree
+
+ [outlineView addTableColumn:column];
+ [outlineView setOutlineTableColumn:column];
+ [outlineView setAllowsMultipleSelection:YES];
+ [scrollView setDocumentView:outlineView];
+ [contentView addSubview:scrollView];
+
+ // 4. Establish Bindings for the Outline View
+ [outlineView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
+ [outlineView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
+
+ var scrollView2 = [[CPScrollView alloc] initWithFrame:CGRectMake(300, 20, 250, 300)];
+ [scrollView2 setAutohidesScrollers:YES];
+ var outlineView2 = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 250, 300)];
+ var column2 = [[CPTableColumn alloc] initWithIdentifier:@"name"];
+ [[column2 headerView] setStringValue:@"Node Name"];
+ [column2 setWidth:240];
+ [column2 setEditable:YES]; // Editable to test bidirectional bindings in the tree
+
+ [outlineView2 addTableColumn:column2];
+ [outlineView2 setOutlineTableColumn:column2];
+ [outlineView2 setAllowsMultipleSelection:YES];
+ [scrollView2 setDocumentView:outlineView2];
+ [contentView addSubview:scrollView2];
+
+ // 4. Establish Bindings for the Outline View
+ [outlineView2 bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
+ [outlineView2 bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
+
+
+
+ [theWindow orderFront:self];
+}
+
+- (void)selectSpecificNode:(id)sender
+{
+ // Programmatically select index path [0, 1] which is "Child 1.2"
+ // This tests the `_CPOutlineViewSelectionIndexPathsBinder` auto-expand logic.
+ var path =[CPIndexPath indexPathWithIndexes:[0, 1]];
+ [treeController setSelectionIndexPath:path];
+}
+
+- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context
+{
+ if (keyPath === @"selectionIndexPaths")
+ {
+ var selectedObjects = [treeController selectedObjects];
+ if ([selectedObjects count] > 0)
+ {
+ var names = [CPMutableArray array];
+ for (var i = 0; i <[selectedObjects count]; i++)
+ [names addObject:[selectedObjects[i] name]];[logField setStringValue:[names componentsJoinedByString:@", "]];
+ }
+ else
+ {
+ [logField setStringValue:@"Nothing selected"];
+ }
+ }
+}
+
+@end
+
+
+// --- Custom Data Model ---
+
+@implementation Node : CPObject
+{
+ CPString name;
+ CPArray children;
+}
+
+- (id)initWithName:(CPString)aName children:(CPArray)someChildren
+{
+ self = [super init];
+ if (self)
+ {
+ name = aName;
+ children = someChildren;
+ }
+ return self;
+}
+
+// Explicit accessors to ensure Key-Value Observing (KVO) works flawlessly.
+- (void)setName:(CPString)aName
+{
+ [self willChangeValueForKey:@"name"];
+ name = aName;[self didChangeValueForKey:@"name"];
+}
+
+- (CPString)name
+{
+ return name;
+}
+
+- (void)setChildren:(CPArray)someChildren
+{
+ [self willChangeValueForKey:@"children"];
+ children = someChildren;
+ [self didChangeValueForKey:@"children"];
+}
+
+- (CPArray)children
+{
+ return children;
+}
+
+@end
diff --git a/Tests/Manual/CPTreeControllerTest/Info.plist b/Tests/Manual/CPTreeControllerTest/Info.plist
new file mode 100644
index 000000000..3af283991
--- /dev/null
+++ b/Tests/Manual/CPTreeControllerTest/Info.plist
@@ -0,0 +1,12 @@
+
+
+
+
+ CPApplicationDelegateClass
+ AppController
+ CPBundleName
+ treecontroller
+ CPPrincipalClass
+ CPApplication
+
+
diff --git a/Tests/Manual/CPTreeControllerTest/Jakefile b/Tests/Manual/CPTreeControllerTest/Jakefile
new file mode 100644
index 000000000..8f62ca5cd
--- /dev/null
+++ b/Tests/Manual/CPTreeControllerTest/Jakefile
@@ -0,0 +1,93 @@
+/*
+ * Jakefile
+ * tooltips
+ *
+ * Created by You on April 26, 2011.
+ * Copyright 2011, Your Company All rights reserved.
+ */
+
+var ENV = require("system").env,
+ FILE = require("file"),
+ JAKE = require("jake"),
+ task = JAKE.task,
+ FileList = JAKE.FileList,
+ app = require("cappuccino/jake").app,
+ configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug",
+ OS = require("os");
+
+app ("tooltips", function(task)
+{
+ task.setBuildIntermediatesPath(FILE.join("Build", "tooltips.build", configuration));
+ task.setBuildPath(FILE.join("Build", configuration));
+
+ task.setProductName("tooltips");
+ task.setIdentifier("com.yourcompany.tooltips");
+ task.setVersion("1.0");
+ task.setAuthor("Your Company");
+ task.setEmail("feedback @nospam@ yourcompany.com");
+ task.setSummary("tooltips");
+ task.setSources((new FileList("**/*.j")).exclude(FILE.join("Build", "**")));
+ task.setResources(new FileList("Resources/**"));
+ task.setIndexFilePath("index.html");
+ task.setInfoPlistPath("Info.plist");
+
+ if (configuration === "Debug")
+ task.setCompilerFlags("-DDEBUG -g");
+ else
+ task.setCompilerFlags("-O");
+});
+
+task ("default", ["tooltips"], function()
+{
+ printResults(configuration);
+});
+
+task ("build", ["default"]);
+
+task ("debug", function()
+{
+ ENV["CONFIGURATION"] = "Debug";
+ JAKE.subjake(["."], "build", ENV);
+});
+
+task ("release", function()
+{
+ ENV["CONFIGURATION"] = "Release";
+ JAKE.subjake(["."], "build", ENV);
+});
+
+task ("run", ["debug"], function()
+{
+ OS.system(["open", FILE.join("Build", "Debug", "tooltips", "index.html")]);
+});
+
+task ("run-release", ["release"], function()
+{
+ OS.system(["open", FILE.join("Build", "Release", "tooltips", "index.html")]);
+});
+
+task ("deploy", ["release"], function()
+{
+ FILE.mkdirs(FILE.join("Build", "Deployment", "tooltips"));
+ OS.system(["press", "-f", FILE.join("Build", "Release", "tooltips"), FILE.join("Build", "Deployment", "tooltips")]);
+ printResults("Deployment")
+});
+
+task ("desktop", ["release"], function()
+{
+ FILE.mkdirs(FILE.join("Build", "Desktop", "tooltips"));
+ require("cappuccino/nativehost").buildNativeHost(FILE.join("Build", "Release", "tooltips"), FILE.join("Build", "Desktop", "tooltips", "tooltips.app"));
+ printResults("Desktop")
+});
+
+task ("run-desktop", ["desktop"], function()
+{
+ OS.system([FILE.join("Build", "Desktop", "tooltips", "tooltips.app", "Contents", "MacOS", "NativeHost"), "-i"]);
+});
+
+function printResults(configuration)
+{
+ print("----------------------------");
+ print(configuration+" app built at path: "+FILE.join("Build", configuration, "tooltips"));
+ print("----------------------------");
+}
diff --git a/Tests/Manual/CPTreeControllerTest/index-debug.html b/Tests/Manual/CPTreeControllerTest/index-debug.html
new file mode 100644
index 000000000..a36b1d3b9
--- /dev/null
+++ b/Tests/Manual/CPTreeControllerTest/index-debug.html
@@ -0,0 +1,204 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ __project.name__
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/Manual/CPTreeControllerTest/index.html b/Tests/Manual/CPTreeControllerTest/index.html
new file mode 100644
index 000000000..ac42c98a7
--- /dev/null
+++ b/Tests/Manual/CPTreeControllerTest/index.html
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ __project.name__
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/Manual/CPTreeControllerTest/main.j b/Tests/Manual/CPTreeControllerTest/main.j
new file mode 100644
index 000000000..9e6a15286
--- /dev/null
+++ b/Tests/Manual/CPTreeControllerTest/main.j
@@ -0,0 +1,18 @@
+/*
+ * AppController.j
+ * tooltips
+ *
+ * Created by You on April 26, 2011.
+ * Copyright 2011, Your Company All rights reserved.
+ */
+
+@import
+@import
+
+@import "AppController.j"
+
+
+function main(args, namedArgs)
+{
+ CPApplicationMain(args, namedArgs);
+}
diff --git a/Tools/imagesize/imagesize.m b/Tools/imagesize/imagesize.m
index 3d427ec65..6ca2eeeb4 100644
--- a/Tools/imagesize/imagesize.m
+++ b/Tools/imagesize/imagesize.m
@@ -30,6 +30,13 @@ int getImageSize(const char* utf8Path, BOOL appendLineFeed)
NSImageRep *representation = representations[0];
NSInteger width = [representation pixelsWide];
NSInteger height = [representation pixelsHigh];
+ NSString *extension = [[path pathExtension] lowercaseString];
+
+ if ([extension isEqualToString:@"svg"])
+ {
+ width = [image size].width;
+ height = [image size].height;
+ }
NSMutableString* result = [NSMutableString stringWithFormat:@"{\"width\":%ld, \"height\":%ld}", (long)width, (long)height];