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 + + com.apple.InterfaceBuilder.CocoaPlugin + 2182 + + + YES + NSTextField + NSView + NSWindowTemplate + NSSplitView + NSTextFieldCell + NSSliderCell + NSCustomView + NSSlider + NSButtonCell + NSButton + NSCustomObject + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + + + PluginDependencyRecalculationVersion + + + + YES + + NSApplication + + + FirstResponder + + + NSApplication + + + 7 + 2 + {{335, 390}, {480, 360}} + 1946157056 + Window + NSWindow + + + + + 256 + + YES + + + 274 + + YES + + + 268 + {480, 34} + + + + _NS:3944 + YES + + 68288064 + 272630784 + Label + + LucidaGrande + 13 + 1044 + + _NS:3944 + + + 6 + System + controlColor + + 3 + MC42NjY2NjY2NjY3AA + + + + 6 + System + controlTextColor + + 3 + MAA + + + + + + + 256 + + YES + + + 268 + {{14, 24}, {142, 32}} + + + + _NS:9 + YES + + 67239424 + 134217728 + Delete Autosave + + _NS:9 + + -2038284033 + 129 + + + 200 + 25 + + + + + 301 + {{364, 30}, {96, 22}} + + + + YES + + -1804468671 + 272630784 + + + + YES + + 6 + System + textBackgroundColor + + 3 + MQA + + + + 6 + System + textColor + + + + + + {{0, 44}, {480, 85}} + + + + _NS:1113 + NSView + + + + 256 + + YES + + + 301 + {{192, 27}, {96, 21}} + + + + YES + + -2079981824 + 0 + + + Helvetica + 12 + 16 + + + 100 + 0.0 + 50 + 0.0 + 0 + 1 + NO + NO + + + + {{0, 139}, {480, 79}} + + + + _NS:1116 + NSView + + + + 256 + + YES + + + 256 + + YES + + + 274 + + YES + + + 256 + {246, 51} + + + + _NS:11 + NSView + + + + 256 + + YES + + + 256 + {{66, 18}, {114, 32}} + + + + _NS:687 + YES + + 67239424 + 134217728 + Bottom Left + + _NS:687 + + -2038284033 + 129 + + + 200 + 25 + + + + {{0, 60}, {246, 72}} + + + + _NS:13 + NSView + + + {246, 132} + + + + _NS:9 + splitViewC + + + {246, 132} + + + + _NS:1165 + NSView + + + + 256 + + YES + + + 256 + {{80, 48}, {73, 32}} + + + + _NS:687 + YES + + 67239424 + 134217728 + Right + + _NS:687 + + -2038284033 + 129 + + + 200 + 25 + + + + {{247, 0}, {233, 132}} + + + + _NS:1167 + NSView + + + {{0, 228}, {480, 132}} + + + + _NS:1163 + YES + 2 + splitViewB + + + {480, 360} + + + + _NS:1111 + 3 + splitViewA + + + {480, 360} + + + + + {{0, 0}, {2560, 1418}} + {10000000000000, 10000000000000} + YES + + + AppController + + + + + 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];