mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-25 21:17:03 +00:00
Merge remote-tracking branch 'upstream/main' into cpoutlineview-delegate-fix
This commit is contained in:
+1
-1
@@ -25,4 +25,4 @@ node_modules
|
||||
/dist/objective-j/lib
|
||||
/dist/cappuccino/package.json
|
||||
/dist/cappuccino/lib
|
||||
|
||||
/dist/cappuccino/bin
|
||||
|
||||
@@ -115,3 +115,5 @@
|
||||
@import "CPWindow.j"
|
||||
@import "CPWindowController.j"
|
||||
@import "CPWorkspace.j"
|
||||
@import "CPFontPanel.j"
|
||||
@import "CPTreeController.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
|
||||
|
||||
@@ -27,12 +27,7 @@
|
||||
CPDatePickerElementTextFieldBecomeFirstResponder = @"CPDatePickerElementTextFieldBecomeFirstResponder";
|
||||
CPDatePickerElementTextFieldAMPMChangedNotification = @"CPDatePickerElementTextFieldAMPMChangedNotification";
|
||||
|
||||
var CPZeroKeyCode = 48,
|
||||
CPNineKeyCode = 57,
|
||||
CPMajAKeyCode = 65,
|
||||
CPMajPKeyCode = 80,
|
||||
CPAKeyCode = 97,
|
||||
CPPKeyCode = 112;
|
||||
// Removed hardcoded KeyCodes (CPZeroKeyCode, etc) as they are unreliable across browsers/layouts.
|
||||
|
||||
CPMonthDateType = 0;
|
||||
CPDayDateType = 1;
|
||||
@@ -190,23 +185,38 @@ CPAMPMDateType = 6;
|
||||
*/
|
||||
- (void)setValueForKeyEvent:(CPEvent)anEvent
|
||||
{
|
||||
var keyCode = [anEvent keyCode];
|
||||
var keyCode = [anEvent keyCode],
|
||||
characters = [anEvent characters];
|
||||
|
||||
if (keyCode != CPDeleteKeyCode && keyCode != CPDeleteForwardKeyCode && keyCode < CPZeroKeyCode || keyCode > CPNineKeyCode)
|
||||
// Check if the event is a deletion
|
||||
var isDelete = (keyCode === CPDeleteKeyCode || keyCode === CPDeleteForwardKeyCode);
|
||||
|
||||
// Check if the event is a numeric input.
|
||||
// By testing the character string against a regex, we support num-pads and
|
||||
// international keyboards correctly, rather than relying on keyCode ranges.
|
||||
var isNumeric = (characters && [characters length] > 0 && /^[0-9]$/.test(characters));
|
||||
|
||||
// If it is neither a delete command nor a digit, we ignore it.
|
||||
if (!isDelete && !isNumeric)
|
||||
return;
|
||||
|
||||
var newValue = [self stringValue].replace(/\s/g, ''),
|
||||
length = [newValue length],
|
||||
eventKeyValue = parseInt([anEvent characters]).toString();
|
||||
length = [newValue length];
|
||||
|
||||
if (keyCode == CPDeleteKeyCode || keyCode == CPDeleteForwardKeyCode)
|
||||
if (isDelete)
|
||||
{
|
||||
[_timerEdition invalidate];
|
||||
_timerEdition = nil;
|
||||
newValue = [newValue substringToIndex:(length - 1)];
|
||||
|
||||
// Ensure we don't substring if length is 0
|
||||
if (length > 0)
|
||||
newValue = [newValue substringToIndex:(length - 1)];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Since isNumeric is true, characters is a valid digit string
|
||||
var eventKeyValue = characters;
|
||||
|
||||
if (!_timerEdition)
|
||||
{
|
||||
_timerEdition = [CPTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(_timerKeyEvent:) userInfo:nil repeats:NO];
|
||||
@@ -227,7 +237,12 @@ CPAMPMDateType = 6;
|
||||
}
|
||||
}
|
||||
|
||||
if (parseInt(newValue) > [self _maxNumberWithMaxDate] || ([_datePicker _isAmericanFormat] && _dateType == CPHourDateType && parseInt(newValue) > 12))
|
||||
// Safety check for NaN before comparison
|
||||
var numericValue = parseInt(newValue);
|
||||
if (isNaN(numericValue))
|
||||
numericValue = 0;
|
||||
|
||||
if (numericValue > [self _maxNumberWithMaxDate] || ([_datePicker _isAmericanFormat] && _dateType == CPHourDateType && numericValue > 12))
|
||||
return;
|
||||
|
||||
_firstEvent = NO;
|
||||
|
||||
@@ -48,13 +48,6 @@
|
||||
@global CPYearMonthDayDatePickerElementFlag
|
||||
@global CPEraDatePickerElementFlag
|
||||
|
||||
var CPZeroKeyCode = 48,
|
||||
CPNineKeyCode = 57,
|
||||
CPMajAKeyCode = 65,
|
||||
CPMajPKeyCode = 80,
|
||||
CPAKeyCode = 97,
|
||||
CPPKeyCode = 112;
|
||||
|
||||
// This class is used to represente the datePicker with the CPTextFieldAndStepperDatePickerStyle/CPTextFieldDatePickerStyle mode
|
||||
@implementation _CPDatePickerTextField : CPControl
|
||||
{
|
||||
@@ -131,7 +124,7 @@ var CPZeroKeyCode = 48,
|
||||
// Don't forget to unbind, otherwise several steppers will increase or decrease
|
||||
[_currentTextField unbind:@"objectValue"];
|
||||
[_currentTextField makeDeselectable];
|
||||
_currentTextField = nil
|
||||
_currentTextField = nil;
|
||||
|
||||
// This is usefull when clicking on the stepper when the datePicker is not selected
|
||||
[_stepper setObjectValue:0];
|
||||
@@ -221,13 +214,30 @@ var CPZeroKeyCode = 48,
|
||||
{
|
||||
[_datePickerElementView _updateResponderTextField];
|
||||
|
||||
// We select the firstTextField when the datePicker becomes firstResponder if _currentTextField is null. It can be null just when using tab
|
||||
if (!_currentTextField)
|
||||
{
|
||||
var targetField = nil;
|
||||
|
||||
if (flags & CPShiftKeyMask)
|
||||
[self _selectTextField:_lastTextField];
|
||||
{
|
||||
// Try last field; if hidden, find previous visible
|
||||
if ([_lastTextField isHidden])
|
||||
targetField = [self _previousVisibleTextFieldFrom:_lastTextField];
|
||||
else
|
||||
targetField = _lastTextField;
|
||||
}
|
||||
else
|
||||
[self _selectTextField:_firstTextField];
|
||||
{
|
||||
// Try first field; if hidden, find next visible
|
||||
if ([_firstTextField isHidden])
|
||||
targetField = [self _nextVisibleTextFieldFrom:_firstTextField];
|
||||
else
|
||||
targetField = _firstTextField;
|
||||
}
|
||||
|
||||
// Only select if we actually found a valid visible field
|
||||
if (targetField)
|
||||
[self _selectTextField:targetField];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,23 +344,92 @@ var CPZeroKeyCode = 48,
|
||||
return [super performKeyEquivalent:anEvent];
|
||||
}
|
||||
|
||||
- (_CPDatePickerElementTextField)_nextVisibleTextFieldFrom:(_CPDatePickerElementTextField)aTextField
|
||||
{
|
||||
var runner = [aTextField nextTextField];
|
||||
|
||||
// If we wrapped back to the start immediately, or runner is nil, we are done.
|
||||
if (!runner || runner == _firstTextField)
|
||||
return nil;
|
||||
|
||||
// Traverse hidden fields
|
||||
while (runner && [runner isHidden])
|
||||
{
|
||||
// If we hit the absolute last field and it is hidden, we've reached the end.
|
||||
if (runner == _lastTextField)
|
||||
return nil;
|
||||
|
||||
runner = [runner nextTextField];
|
||||
|
||||
// Safety: if we wrapped back to the start inside the loop
|
||||
if (runner == _firstTextField)
|
||||
return nil;
|
||||
}
|
||||
|
||||
return runner;
|
||||
}
|
||||
|
||||
- (_CPDatePickerElementTextField)_previousVisibleTextFieldFrom:(_CPDatePickerElementTextField)aTextField
|
||||
{
|
||||
var runner = [aTextField previousTextField];
|
||||
|
||||
// If we wrapped back to the end immediately, or runner is nil, we are done.
|
||||
if (!runner || runner == _lastTextField)
|
||||
return nil;
|
||||
|
||||
// Traverse hidden fields
|
||||
while (runner && [runner isHidden])
|
||||
{
|
||||
// If we hit the absolute first field and it is hidden, we've reached the start.
|
||||
if (runner == _firstTextField)
|
||||
return nil;
|
||||
|
||||
runner = [runner previousTextField];
|
||||
|
||||
// Safety: if we wrapped back to the end inside the loop
|
||||
if (runner == _lastTextField)
|
||||
return nil;
|
||||
}
|
||||
|
||||
return runner;
|
||||
}
|
||||
|
||||
- (void)insertTab:(id)sender
|
||||
{
|
||||
if (!_currentTextField)
|
||||
return;
|
||||
|
||||
if (_currentTextField == _lastTextField)
|
||||
[[self window] selectNextKeyView:self];
|
||||
// Ensure boundaries are up to date
|
||||
[_datePickerElementView _updateResponderTextField];
|
||||
|
||||
var nextField = [self _nextVisibleTextFieldFrom:_currentTextField];
|
||||
|
||||
if (nextField)
|
||||
{
|
||||
[self _selectTextField:nextField];
|
||||
}
|
||||
else
|
||||
[self moveRight:sender];
|
||||
}
|
||||
{
|
||||
// We reached the visual end. Manually find the next external view.
|
||||
// We cannot rely on [[self window] selectNextKeyView:self] because it might
|
||||
// loop back into our own internal fields or select 'self' which refuses focus.
|
||||
var nextView = [_currentTextField nextValidKeyView];
|
||||
|
||||
- (void)moveRight:(id)sender
|
||||
{
|
||||
if (!_currentTextField)
|
||||
return;
|
||||
// Skip any view that is part of this control (descendant)
|
||||
while (nextView && [nextView isDescendantOf:self])
|
||||
{
|
||||
// If we looped back to the current field, we are trapped in a closed loop with no exit.
|
||||
if (nextView == _currentTextField)
|
||||
{
|
||||
nextView = nil;
|
||||
break;
|
||||
}
|
||||
nextView = [nextView nextValidKeyView];
|
||||
}
|
||||
|
||||
[self _selectTextField:[_currentTextField nextTextField]];
|
||||
if (nextView)
|
||||
[[self window] makeFirstResponder:nextView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)insertBacktab:(id)sender
|
||||
@@ -358,10 +437,47 @@ var CPZeroKeyCode = 48,
|
||||
if (!_currentTextField)
|
||||
return;
|
||||
|
||||
if (_currentTextField == _firstTextField)
|
||||
[[self window] selectPreviousKeyView:self];
|
||||
[_datePickerElementView _updateResponderTextField];
|
||||
|
||||
var prevField = [self _previousVisibleTextFieldFrom:_currentTextField];
|
||||
|
||||
if (prevField)
|
||||
{
|
||||
[self _selectTextField:prevField];
|
||||
}
|
||||
else
|
||||
[self moveLeft:sender];
|
||||
{
|
||||
// We reached the visual start. Manually find the previous external view.
|
||||
var prevView = [_currentTextField previousValidKeyView];
|
||||
|
||||
// Skip any view that is part of this control
|
||||
while (prevView && [prevView isDescendantOf:self])
|
||||
{
|
||||
if (prevView == _currentTextField)
|
||||
{
|
||||
prevView = nil;
|
||||
break;
|
||||
}
|
||||
prevView = [prevView previousValidKeyView];
|
||||
}
|
||||
|
||||
if (prevView)
|
||||
[[self window] makeFirstResponder:prevView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)moveRight:(id)sender
|
||||
{
|
||||
if (!_currentTextField)
|
||||
return;
|
||||
|
||||
[_datePickerElementView _updateResponderTextField];
|
||||
|
||||
// Use the helper to skip hidden fields
|
||||
var nextField = [self _nextVisibleTextFieldFrom:_currentTextField];
|
||||
|
||||
if (nextField)
|
||||
[self _selectTextField:nextField];
|
||||
}
|
||||
|
||||
- (void)moveLeft:(id)sender
|
||||
@@ -369,7 +485,13 @@ var CPZeroKeyCode = 48,
|
||||
if (!_currentTextField)
|
||||
return;
|
||||
|
||||
[self _selectTextField:[_currentTextField previousTextField]];
|
||||
[_datePickerElementView _updateResponderTextField];
|
||||
|
||||
// Use the helper to skip hidden fields to be safe
|
||||
var prevField = [self _previousVisibleTextFieldFrom:_currentTextField];
|
||||
|
||||
if (prevField)
|
||||
[self _selectTextField:prevField];
|
||||
}
|
||||
|
||||
- (void)moveDown:(id)sender
|
||||
@@ -401,7 +523,7 @@ var CPZeroKeyCode = 48,
|
||||
}
|
||||
|
||||
/*! KeyDown event
|
||||
We just care care about the event A/P and every numbers
|
||||
We just care about the event A/P and every numbers
|
||||
*/
|
||||
- (void)keyDown:(CPEvent)anEvent
|
||||
{
|
||||
@@ -410,18 +532,27 @@ var CPZeroKeyCode = 48,
|
||||
|
||||
[self interpretKeyEvents:[anEvent]];
|
||||
|
||||
if ([_datePicker _isAmericanFormat] && [_currentTextField dateType] == CPAMPMDateType && ([anEvent keyCode] == CPAKeyCode || [anEvent keyCode] == CPPKeyCode || [anEvent keyCode] == CPMajAKeyCode || [anEvent keyCode] == CPMajPKeyCode))
|
||||
var characters = [anEvent characters];
|
||||
|
||||
if ([_datePicker _isAmericanFormat] && [_currentTextField dateType] == CPAMPMDateType && [characters length] > 0)
|
||||
{
|
||||
if ([anEvent keyCode] == CPAKeyCode || [anEvent keyCode] == CPMajAKeyCode)
|
||||
var charUpper = [characters uppercaseString];
|
||||
|
||||
if (charUpper === "A")
|
||||
{
|
||||
[_currentTextField setStringValue:@"AM"];
|
||||
else
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPDatePickerElementTextFieldAMPMChangedNotification object:_currentTextField userInfo:nil];
|
||||
return;
|
||||
}
|
||||
else if (charUpper === "P")
|
||||
{
|
||||
[_currentTextField setStringValue:@"PM"];
|
||||
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPDatePickerElementTextFieldAMPMChangedNotification object:_currentTextField userInfo:nil];
|
||||
|
||||
return;
|
||||
[[CPNotificationCenter defaultCenter] postNotificationName:CPDatePickerElementTextFieldAMPMChangedNotification object:_currentTextField userInfo:nil];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass the event down to the specific field (which handles numeric input validation via regex)
|
||||
[_currentTextField setValueForKeyEvent:anEvent];
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
+31
-3
@@ -70,6 +70,7 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
BOOL _isARepeat;
|
||||
unsigned _keyCode;
|
||||
DOMEvent _DOMEvent;
|
||||
BOOL _isActionKey;
|
||||
int _data1;
|
||||
int _data2;
|
||||
short _subtype;
|
||||
@@ -110,17 +111,28 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
@param unmodCharacters the string of keys pressed without the presence of any modifiers other than Shift
|
||||
@param repeatKey \c YES if this is caused by the system repeat as opposed to the user pressing the key again
|
||||
@param code a number associated with the keyboard key of this event
|
||||
@param isAnActionKey a BOOL indicating whether this key is an action key (e.g. a function key)
|
||||
@throws CPInternalInconsistencyException if \c anEventType is not a CPKeyDown,
|
||||
CPKeyUp or CPFlagsChanged
|
||||
@return the keyboard event
|
||||
*/
|
||||
+ (CPEvent)keyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags
|
||||
timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext
|
||||
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code
|
||||
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code isActionKey:(BOOL)isAnActionKey
|
||||
{
|
||||
return [[self alloc] _initKeyEventWithType:anEventType location:aPoint modifierFlags:modifierFlags
|
||||
timestamp:aTimestamp windowNumber:aWindowNumber context:aGraphicsContext
|
||||
characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code];
|
||||
characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code isActionKey:isAnActionKey];
|
||||
}
|
||||
|
||||
// for backwards compatibility only
|
||||
+ (CPEvent)keyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags
|
||||
timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext
|
||||
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code
|
||||
{
|
||||
return [[self alloc] _initKeyEventWithType:anEventType location:aPoint modifierFlags:modifierFlags
|
||||
timestamp:aTimestamp windowNumber:aWindowNumber context:aGraphicsContext
|
||||
characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code isActionKey:NO];
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -252,7 +264,7 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
/* @ignore */
|
||||
- (id)_initKeyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags
|
||||
timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext
|
||||
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)isARepeat keyCode:(unsigned short)code
|
||||
characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)isARepeat keyCode:(unsigned short)code isActionKey:(BOOL)isAnActionKey
|
||||
{
|
||||
if (self = [self _initWithType:anEventType])
|
||||
{
|
||||
@@ -264,6 +276,7 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
_charactersIgnoringModifiers = unmodCharacters;
|
||||
_isARepeat = isARepeat;
|
||||
_keyCode = code;
|
||||
_isActionKey = isAnActionKey;
|
||||
_windowNumber = aWindowNumber;
|
||||
}
|
||||
|
||||
@@ -571,6 +584,21 @@ var _CPEventPeriodicEventPeriod = 0,
|
||||
return !firstResponderIsText;
|
||||
}
|
||||
|
||||
- (BOOL)_isActionOrCommandEvent
|
||||
{
|
||||
// This method is now platform-agnostic. It checks for abstract properties
|
||||
// of the event, including the _isActionKey flag that was set at creation time.
|
||||
return (
|
||||
// Is it a command shortcut?
|
||||
(_modifierFlags & (CPCommandKeyMask | CPControlKeyMask)) ||
|
||||
|
||||
// Is it a key that doesn't produce a character?
|
||||
([_characters length] === 0) ||
|
||||
|
||||
// Was it identified as an action key by the platform-specific layer?
|
||||
_isActionKey
|
||||
);
|
||||
}
|
||||
/*!
|
||||
Return YES if this event is a part of processing a browser controlled cut or paste event
|
||||
where the browser will go ahead and do the work of cutting or pasting within the input
|
||||
|
||||
@@ -208,7 +208,11 @@ CPRemoveTraitFontAction = 7;
|
||||
|
||||
- (@action)addFontTrait:(id)sender
|
||||
{
|
||||
var tag = [sender tag];
|
||||
var tag = sender;
|
||||
|
||||
if ([sender respondsToSelector:@selector(tag)])
|
||||
tag = [sender tag];
|
||||
|
||||
_activeChange = tag == nil ? @{} : @{ @"addTraits": tag };
|
||||
_fontAction = CPAddTraitFontAction;
|
||||
|
||||
|
||||
+59
-2
@@ -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"
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+292
-6
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -332,7 +332,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
|
||||
characters:nil
|
||||
charactersIgnoringModifiers:nil
|
||||
isARepeat:NO
|
||||
keyCode:nil];
|
||||
keyCode:nil
|
||||
isActionKey:NO];
|
||||
|
||||
[CPTextFieldInputOwner keyUp:cappEvent];
|
||||
|
||||
|
||||
+258
-66
@@ -1,34 +1,29 @@
|
||||
/*
|
||||
* CPFontPanel.j
|
||||
* AppKit
|
||||
*
|
||||
* TODOs:
|
||||
* 1. make browser-width for size smaller and fix columns
|
||||
* 2. add all the missing features from the MacOS X counterpart (sampleview)
|
||||
*
|
||||
*
|
||||
* Created by Daniel Boehringer on 2/JAN/2014.
|
||||
* All modifications copyright Daniel Boehringer 2013.
|
||||
* Extensive code formatting and review by Andrew Hankinson
|
||||
* Based on original work by
|
||||
* Created by Emmanuel Maillard on 06/03/2010.
|
||||
* Copyright Emmanuel Maillard 2010.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
CPFontPanel.j
|
||||
AppKit
|
||||
|
||||
Created by Daniel Boehringer on 2/JAN/2014.
|
||||
All modifications copyright Daniel Boehringer 2013.
|
||||
Extensive code formatting and review by Andrew Hankinson
|
||||
|
||||
Based on original work by
|
||||
Created by Emmanuel Maillard on 06/03/2010.
|
||||
Copyright Emmanuel Maillard 2010.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
@import "CPPanel.j"
|
||||
@import "CPColorWell.j"
|
||||
@@ -37,7 +32,6 @@
|
||||
@import "CPText.j"
|
||||
@import "CPFontManager.j"
|
||||
|
||||
|
||||
@class CPTextStorage
|
||||
@class CPLayoutManager
|
||||
@class CPTextContainer
|
||||
@@ -46,27 +40,30 @@
|
||||
/*
|
||||
Collection indexes
|
||||
*/
|
||||
var kTypefaceIndex_Normal = 0,
|
||||
kTypefaceIndex_Italic = 1,
|
||||
kTypefaceIndex_Bold = 2,
|
||||
var kTypefaceIndex_Normal = 0,
|
||||
kTypefaceIndex_Italic = 1,
|
||||
kTypefaceIndex_Bold = 2,
|
||||
kTypefaceIndex_BoldItalic = 3,
|
||||
kToolbarHeight = 32,
|
||||
kBorderSpacing = 6,
|
||||
kInnerSpacing = 2,
|
||||
kNothingChanged = 0,
|
||||
kFontNameChanged = 1,
|
||||
kTypefaceChanged = 2,
|
||||
kSizeChanged = 3,
|
||||
kTextColorChanged = 4,
|
||||
kBackgroundColorChanged = 5,
|
||||
kUnderlineChanged = 6,
|
||||
kWeightChanged = 7,
|
||||
|
||||
kToolbarHeight = 32,
|
||||
kPreviewHeight = 70,
|
||||
kBorderSpacing = 6,
|
||||
kInnerSpacing = 2,
|
||||
|
||||
kNothingChanged = 0,
|
||||
kFontNameChanged = 1,
|
||||
kTypefaceChanged = 2,
|
||||
kSizeChanged = 3,
|
||||
kTextColorChanged = 4,
|
||||
kBackgroundColorChanged = 5,
|
||||
kUnderlineChanged = 6,
|
||||
kWeightChanged = 7,
|
||||
|
||||
_sharedFontPanel;
|
||||
|
||||
// FIXME<!> Locale support
|
||||
var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
_availableSizes = [@"9", @"10", @"11", @"12", @"13", @"14", @"18", @"24", @"36", @"48", @"72", @"96"];
|
||||
|
||||
var _availableTraits = [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
_availableSizes = [@"9", @"10", @"11", @"12", @"13", @"14", @"18", @"24", @"36", @"48", @"64", @"72", @"96", @"144", @"288"];
|
||||
|
||||
/*!
|
||||
@ingroup appkit
|
||||
@@ -77,6 +74,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
id _fontBrowser;
|
||||
id _traitBrowser;
|
||||
id _sizeBrowser;
|
||||
|
||||
// Preview
|
||||
_CPFontPanelPreviewView _previewView;
|
||||
|
||||
CPArray _availableFonts;
|
||||
id _textColorWell;
|
||||
CPColor _textColor;
|
||||
@@ -85,7 +86,6 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
int _fontChanges;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Class methods
|
||||
|
||||
@@ -108,6 +108,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
return _sharedFontPanel;
|
||||
}
|
||||
|
||||
- (BOOL)acceptsFirstResponder
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Init methods
|
||||
@@ -115,7 +119,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
/*! @ignore */
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super initWithContentRect:CGRectMake(100, 90, 450, 394) styleMask:(CPTitledWindowMask | CPClosableWindowMask /*| CPResizableWindowMask*/ )])
|
||||
if (self = [super initWithContentRect:CGRectMake(100, 90, 450, 420) styleMask:(CPTitledWindowMask | CPClosableWindowMask | CPResizableWindowMask)])
|
||||
{
|
||||
[[self contentView] setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]];
|
||||
[self setTitle:@"Font Panel"];
|
||||
@@ -152,10 +156,49 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
[aBrowser setDoubleAction:@selector(dblClicked:)];
|
||||
[aBrowser setAllowsEmptySelection:NO];
|
||||
[aBrowser setAllowsMultipleSelection:NO];
|
||||
|
||||
// Config Scrollers
|
||||
//[aBrowser setHasHorizontalScroller:NO];
|
||||
//[aBrowser setHasVerticalScroller:YES];
|
||||
//[aBrowser setAutohidesScrollers:YES];
|
||||
//[aBrowser setMaxVisibleColumns:1];
|
||||
|
||||
[aBrowser setDelegate:self];
|
||||
[[self contentView] addSubview:aBrowser];
|
||||
}
|
||||
|
||||
- (void)_layoutBrowsers
|
||||
{
|
||||
var contentView = [self contentView],
|
||||
contentBounds = [contentView bounds],
|
||||
previewY = kBorderSpacing + kToolbarHeight + kInnerSpacing,
|
||||
browserY = previewY + kPreviewHeight + 10,
|
||||
browserHeight = CGRectGetHeight(contentBounds) - browserY - 10,
|
||||
availableWidth = CGRectGetWidth(contentBounds) - 20; // 10px padding L/R
|
||||
|
||||
// Layout Calculations
|
||||
// Increase sizeWidth slightly to 60 to allow space for the vertical scrollbar without clipping text
|
||||
var sizeWidth = 90,
|
||||
spacing = 5,
|
||||
remainingWidth = availableWidth - sizeWidth - (spacing * 2),
|
||||
// Split remaining roughly 60% font name, 40% trait
|
||||
fontWidth = FLOOR(remainingWidth * 0.60),
|
||||
traitWidth = remainingWidth - fontWidth;
|
||||
|
||||
// Apply frames and column constraints
|
||||
[_fontBrowser setFrame:CGRectMake(10, browserY, fontWidth, browserHeight)];
|
||||
[_fontBrowser setDefaultColumnWidth:fontWidth];
|
||||
[_fontBrowser setLastColumn:0];
|
||||
|
||||
[_traitBrowser setFrame:CGRectMake(10 + fontWidth + spacing, browserY, traitWidth, browserHeight)];
|
||||
[_traitBrowser setDefaultColumnWidth:traitWidth];
|
||||
[_traitBrowser setLastColumn:0];
|
||||
|
||||
[_sizeBrowser setFrame:CGRectMake(10 + fontWidth + traitWidth + (spacing * 2), browserY, sizeWidth, browserHeight)];
|
||||
[_sizeBrowser setDefaultColumnWidth:sizeWidth];
|
||||
[_sizeBrowser setLastColumn:0];
|
||||
}
|
||||
|
||||
- (void)_setupContents
|
||||
{
|
||||
if (_setupDone)
|
||||
@@ -163,33 +206,53 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
|
||||
_setupDone = YES;
|
||||
|
||||
// We set ourselves as delegate to handle resizing layout manually
|
||||
[self setDelegate:self];
|
||||
|
||||
[self _setupToolbarView];
|
||||
|
||||
var contentView = [self contentView],
|
||||
label = [CPTextField labelWithTitle:@"Font name"],
|
||||
contentBounds = [contentView bounds],
|
||||
upperView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(contentBounds), CGRectGetHeight(contentBounds) - (kBorderSpacing + kToolbarHeight + kInnerSpacing))];
|
||||
contentBounds = [contentView bounds];
|
||||
|
||||
[contentView addSubview:_toolbarView];
|
||||
|
||||
_fontBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(10, 35, 150, 350)];
|
||||
_traitBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(155, 35, 150, 350)];
|
||||
_sizeBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(300, 35, 140, 350)];
|
||||
// Preview View
|
||||
var previewY = kBorderSpacing + kToolbarHeight + kInnerSpacing;
|
||||
_previewView = [[_CPFontPanelPreviewView alloc] initWithFrame:CGRectMake(10, previewY, CGRectGetWidth(contentBounds) - 20, kPreviewHeight)];
|
||||
[_previewView setAutoresizingMask:CPViewWidthSizable];
|
||||
[contentView addSubview:_previewView];
|
||||
|
||||
// Initialize Browsers with zero rect, _layoutBrowsers will size them
|
||||
_fontBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
|
||||
_traitBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
|
||||
_sizeBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()];
|
||||
|
||||
// Disable autoresizing masks because we are laying out manually in windowDidResize
|
||||
[_fontBrowser setAutoresizingMask:CPViewNotSizable];
|
||||
[_traitBrowser setAutoresizingMask:CPViewNotSizable];
|
||||
[_sizeBrowser setAutoresizingMask:CPViewNotSizable];
|
||||
|
||||
[self _setupBrowser:_fontBrowser];
|
||||
[self _setupBrowser:_traitBrowser];
|
||||
[self _setupBrowser:_sizeBrowser];
|
||||
|
||||
// Perform initial layout
|
||||
[self _layoutBrowsers];
|
||||
|
||||
[[CPNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(textViewDidChangeSelection:)
|
||||
name:CPTextViewDidChangeSelectionNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)windowDidResize:(CPNotification)aNotification
|
||||
{
|
||||
[self _layoutBrowsers];
|
||||
}
|
||||
|
||||
- (void)textViewDidChangeSelection:(CPNotification)notification
|
||||
{
|
||||
[self _refreshWithTextView:[notification object]];
|
||||
|
||||
[self _refreshWithTextView:[notification object]];
|
||||
}
|
||||
|
||||
- (void)_refreshWithTextView:(CPTextView)textView
|
||||
@@ -197,6 +260,9 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
if (![self isVisible])
|
||||
return;
|
||||
|
||||
if (![textView respondsToSelector:@selector(_attributesForFontPanel)])
|
||||
return;
|
||||
|
||||
var attribs = [textView _attributesForFontPanel],
|
||||
font = [attribs objectForKey:CPFontAttributeName] || [[textView textStorage] font] || [CPFont systemFontOfSize:12.0],
|
||||
color = [attribs objectForKey:CPForegroundColorAttributeName];
|
||||
@@ -217,6 +283,9 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
[self setCurrentTrait:trait];
|
||||
[self setCurrentSize:[font size] + ""]; //cast to string
|
||||
|
||||
// Update Preview
|
||||
[_previewView setPreviewFont:font];
|
||||
|
||||
if (!color)
|
||||
return;
|
||||
|
||||
@@ -253,7 +322,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
{
|
||||
case kFontNameChanged:
|
||||
newFont = [CPFont fontWithDescriptor:[[aFont fontDescriptor] fontDescriptorByAddingAttributes:
|
||||
[CPDictionary dictionaryWithObject:[self currentFont] forKey:CPFontNameAttribute]] size:0.0];
|
||||
[CPDictionary dictionaryWithObject:[self currentFont] forKey:CPFontNameAttribute]] size:0.0];
|
||||
break;
|
||||
|
||||
case kTypefaceChanged:
|
||||
@@ -272,12 +341,13 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
newFont = [[CPFontManager sharedFontManager] convertFont:aFont toSize:[self currentSize]];
|
||||
break;
|
||||
|
||||
case kNothingChanged:
|
||||
case kNothingChanged:
|
||||
break;
|
||||
|
||||
default:
|
||||
CPLog.trace(@"FIXME: -[" + [self className] + " " + _cmd + "] unhandled _fontChanges: " + _fontChanges);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return newFont;
|
||||
@@ -285,7 +355,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
|
||||
- (void)setCurrentSize:(CGSize)aSize
|
||||
{
|
||||
[_sizeBrowser selectRow:[_availableSizes indexOfObject:aSize] inColumn:0];
|
||||
[_sizeBrowser selectRow:[_availableSizes indexOfObject:aSize] inColumn:0];
|
||||
}
|
||||
|
||||
- (CPString)currentSize
|
||||
@@ -295,7 +365,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
|
||||
- (void)setCurrentFont:(CPFont)aFont
|
||||
{
|
||||
[_fontBrowser selectRow:[_availableFonts indexOfObject:[aFont familyName]] inColumn:0];
|
||||
[_fontBrowser selectRow:[_availableFonts indexOfObject:[aFont familyName]] inColumn:0];
|
||||
}
|
||||
|
||||
- (CPString)currentFont
|
||||
@@ -320,9 +390,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
case kTypefaceIndex_BoldItalic:
|
||||
row = 3;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
[_traitBrowser selectRow:row inColumn:0];
|
||||
[_traitBrowser selectRow:row inColumn:0];
|
||||
}
|
||||
|
||||
// FIXME<!> Locale support
|
||||
@@ -370,6 +441,8 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
if ([self currentTrait] != typefaceIndex)
|
||||
[self setCurrentTrait:typefaceIndex ];
|
||||
|
||||
[_previewView setPreviewFont:font];
|
||||
|
||||
_fontChanges = kNothingChanged;
|
||||
}
|
||||
|
||||
@@ -382,23 +455,30 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// TODO: ask CPFontManager for traits //
|
||||
|
||||
- (void)browserClicked:(id)aBrowser
|
||||
{
|
||||
if (aBrowser === _fontBrowser)
|
||||
{
|
||||
_fontChanges = kFontNameChanged;
|
||||
[[CPFontManager sharedFontManager] modifyFontViaPanel:self];
|
||||
}
|
||||
else if (aBrowser === _traitBrowser)
|
||||
{
|
||||
_fontChanges = kTypefaceChanged;
|
||||
[[CPFontManager sharedFontManager] modifyFontViaPanel:self];
|
||||
}
|
||||
else if (aBrowser === _sizeBrowser)
|
||||
{
|
||||
_fontChanges = kSizeChanged;
|
||||
[[CPFontManager sharedFontManager] modifyFontViaPanel:self];
|
||||
}
|
||||
|
||||
// Apply change immediately to manager (standard behavior)
|
||||
[[CPFontManager sharedFontManager] modifyFontViaPanel:self];
|
||||
|
||||
// Update our preview manually because convertFont: calls rely on selected rows
|
||||
// We construct a temporary font to update the preview view immediately
|
||||
var updatedFont = [self panelConvertFont:[_previewView font]];
|
||||
if (updatedFont)
|
||||
[_previewView setPreviewFont:updatedFont];
|
||||
}
|
||||
|
||||
- (void)dblClicked:(id)sender
|
||||
@@ -414,7 +494,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
if (aBrowser === _traitBrowser)
|
||||
return [_availableTraits count];
|
||||
|
||||
return [_availableSizes count]
|
||||
return [_availableSizes count];
|
||||
}
|
||||
|
||||
- (id)browser:(id)aBrowser child:(int)index ofItem:(id)anItem
|
||||
@@ -440,4 +520,116 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"],
|
||||
|
||||
@end
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// _CPFontPanelPreviewView
|
||||
// A helper class to display a font sample with metrics grid
|
||||
// -----------------------------------------------------------------------------
|
||||
@implementation _CPFontPanelPreviewView : CPView
|
||||
{
|
||||
CPTextField _sampleText;
|
||||
CPColor _gridColor;
|
||||
float _gridSize;
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)aRect
|
||||
{
|
||||
self = [super initWithFrame:aRect];
|
||||
|
||||
if (self)
|
||||
{
|
||||
[self setBackgroundColor:[CPColor whiteColor]];
|
||||
|
||||
_gridColor = [CPColor colorWithHexString:@"e4f4ff"];
|
||||
_gridSize = 10.0;
|
||||
|
||||
_sampleText = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(aRect), CGRectGetHeight(aRect))];
|
||||
[_sampleText setStringValue:@"AaYy-0123"];
|
||||
[_sampleText setAlignment:CPCenterTextAlignment];
|
||||
[_sampleText setVerticalAlignment:CPCenterVerticalTextAlignment];
|
||||
[_sampleText setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
|
||||
[_sampleText setTextColor:[CPColor blackColor]];
|
||||
|
||||
[self addSubview:_sampleText];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setPreviewFont:(CPFont)aFont
|
||||
{
|
||||
[_sampleText setFont:aFont];
|
||||
[self setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
- (CPFont)font
|
||||
{
|
||||
return [_sampleText font];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)dirtyRect
|
||||
{
|
||||
// Draw Grid (from MetricsView inspiration)
|
||||
var context = [[CPGraphicsContext currentContext] graphicsPort],
|
||||
bounds = [self bounds],
|
||||
maxX = CGRectGetMaxX(bounds),
|
||||
maxY = CGRectGetMaxY(bounds);
|
||||
|
||||
CGContextSetLineWidth(context, 1.0);
|
||||
CGContextSetStrokeColor(context, _gridColor);
|
||||
CGContextBeginPath(context);
|
||||
|
||||
for (var y = 0.5; y <= maxY; y += _gridSize)
|
||||
{
|
||||
CGContextMoveToPoint(context, 0.0, y);
|
||||
CGContextAddLineToPoint(context, maxX, y);
|
||||
}
|
||||
|
||||
for (var x = 0.5; x <= maxX; x += _gridSize)
|
||||
{
|
||||
CGContextMoveToPoint(context, x, 0.0);
|
||||
CGContextAddLineToPoint(context, x, maxY);
|
||||
}
|
||||
CGContextStrokePath(context);
|
||||
|
||||
// Draw Baseline/Ascender/Descender (from BaselineView inspiration)
|
||||
var font = [_sampleText font];
|
||||
if (!font) return;
|
||||
|
||||
var ascender = [font ascender],
|
||||
descender = [font descender],
|
||||
lineHeight = [font defaultLineHeightForFont];
|
||||
|
||||
// Calculate the baseline.
|
||||
// CPTextField with CPCenterVerticalTextAlignment usually centers the line height.
|
||||
// Top of line = midY - (lineHeight / 2.0)
|
||||
// Baseline = Top of line + ascender
|
||||
var midY = maxY / 2.0,
|
||||
baselineY = midY - (lineHeight / 2.0) + ascender;
|
||||
|
||||
CGContextSetStrokeColor(context, [CPColor redColor]);
|
||||
CGContextBeginPath(context);
|
||||
|
||||
// Baseline
|
||||
CGContextMoveToPoint(context, 0, baselineY);
|
||||
CGContextAddLineToPoint(context, maxX, baselineY);
|
||||
|
||||
// Ascender Line
|
||||
CGContextMoveToPoint(context, 0, baselineY - ascender);
|
||||
CGContextAddLineToPoint(context, maxX, baselineY - ascender);
|
||||
|
||||
// Descender Line
|
||||
CGContextMoveToPoint(context, 0, baselineY - descender);
|
||||
CGContextAddLineToPoint(context, maxX, baselineY - descender);
|
||||
|
||||
CGContextStrokePath(context);
|
||||
}
|
||||
|
||||
- (void)mouseDown:(CPEvent)anEvent
|
||||
{
|
||||
var text = prompt("Enter sample text:", [_sampleText stringValue]);
|
||||
if (text)
|
||||
[_sampleText setStringValue:text];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
[CPFontManager setFontPanelFactory:[CPFontPanel class]];
|
||||
|
||||
+293
-272
@@ -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 <div><br></div>.
|
||||
// - 'Backspace' on empty: Prevents inserting junk characters on iPadOS.
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// This listener handles all other character input.
|
||||
_CPNativeInputField.addEventListener('input', function(e)
|
||||
{
|
||||
// If we are in a composition (e.g., IME), do nothing yet.
|
||||
if (_isComposing)
|
||||
return;
|
||||
|
||||
// Safety net: ignore deletion events, as they are handled by keydown.
|
||||
if (e.inputType && e.inputType.startsWith('delete'))
|
||||
{
|
||||
_CPNativeInputField.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Robustness: Use 'textContent' instead of 'innerHTML' to strip any
|
||||
// unexpected HTML tags the browser might have inserted.
|
||||
var textToInsert = e.target.textContent;
|
||||
handleInput(textToInsert);
|
||||
});
|
||||
|
||||
// Fires when a composition session starts (e.g., user presses a dead key or starts an IME).
|
||||
_CPNativeInputField.addEventListener('compositionstart', function(e) {
|
||||
_isComposing = YES;
|
||||
});
|
||||
|
||||
// Fires when the composition is finished.
|
||||
_CPNativeInputField.addEventListener('compositionend', function(e) {
|
||||
// The composition is over. `e.data` has the final string (e.g., "é").
|
||||
handleInput(e.data);
|
||||
_isComposing = NO;
|
||||
});
|
||||
|
||||
// PASTE handler
|
||||
_CPNativeInputField.onpaste = function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
var nativeClipboard = (e.originalEvent || e).clipboardData;
|
||||
var currentFirstResponder = [[CPApp keyWindow] firstResponder];
|
||||
|
||||
// Can we accept richtext? Then this is our preference (fixme: shift key to force plain text paste)
|
||||
if ([currentFirstResponder isRichText])
|
||||
{
|
||||
var richtext = nativeClipboard.getData('text/rtf');
|
||||
|
||||
// prefer RTF form the outside of cappuccino
|
||||
if (richtext)
|
||||
richtext = [[_CPRTFParser new] parseRTF:richtext];
|
||||
else
|
||||
{
|
||||
var pasteboard = [CPPasteboard generalPasteboard];
|
||||
// If no RTF is available, try to get the internal represatation of richtext from the pasteboard
|
||||
var richData = [pasteboard stringForType:_CPASPboardType];
|
||||
|
||||
if (richData)
|
||||
richtext = [CPKeyedUnarchiver unarchiveObjectWithData:[CPData dataWithRawString:richData]];
|
||||
}
|
||||
|
||||
if (richtext)
|
||||
{
|
||||
[currentFirstResponder _pasteString:richtext];
|
||||
|
||||
return;
|
||||
}
|
||||
// If no richtext is available, fall back to plain text
|
||||
}
|
||||
|
||||
var nativeString = nativeClipboard.getData('text/plain');
|
||||
|
||||
// Use setTimeout to prevent flickering
|
||||
setTimeout(function()
|
||||
{
|
||||
[currentFirstResponder _pasteString:nativeString || [pasteboard stringForType:CPStringPboardType] || ''];
|
||||
}, 20);
|
||||
};
|
||||
|
||||
// COPY handler
|
||||
_CPNativeInputField.oncopy = function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
var pasteboard = [CPPasteboard generalPasteboard];
|
||||
var nativeClipboard = (e.originalEvent || e).clipboardData;
|
||||
|
||||
// First, copy the data to populate the CP clipboard
|
||||
[[[CPApp keyWindow] firstResponder] copy:self];
|
||||
|
||||
// Now, copy the data over to the native clipboard
|
||||
var stringForPasting = [pasteboard stringForType:CPStringPboardType] || '';
|
||||
nativeClipboard.setData('text/plain', stringForPasting);
|
||||
|
||||
var rtfForPasting = [pasteboard stringForType:CPRTFPboardType];
|
||||
|
||||
if (rtfForPasting)
|
||||
nativeClipboard.setData('text/rtf', rtfForPasting);
|
||||
};
|
||||
|
||||
// CUT handler
|
||||
_CPNativeInputField.oncut = function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
var pasteboard = [CPPasteboard generalPasteboard];
|
||||
var nativeClipboard = (e.originalEvent || e).clipboardData;
|
||||
var currentFirstResponder = [[CPApp keyWindow] firstResponder];
|
||||
|
||||
// First, copy the data to populate the CP clipboard
|
||||
[currentFirstResponder copy:self];
|
||||
|
||||
// Now, copy the data to the native clipboard
|
||||
var stringForPasting = [pasteboard stringForType:CPStringPboardType] || '';
|
||||
nativeClipboard.setData('text/plain', stringForPasting);
|
||||
var rtfForPasting = [pasteboard stringForType:CPRTFPboardType];
|
||||
|
||||
if (rtfForPasting)
|
||||
nativeClipboard.setData('text/rtf', rtfForPasting);
|
||||
|
||||
// Then, perform the delete part of the cut operation in the text view
|
||||
// Use setTimeout to prevent flickering
|
||||
setTimeout(function()
|
||||
{
|
||||
[currentFirstResponder deleteBackward:self];
|
||||
}, 20);
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (void)focusForTextView:(CPTextView)currentFirstResponder
|
||||
{
|
||||
if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)])
|
||||
return;
|
||||
|
||||
[self hideInputElement];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_CPNativeInputField.focus();
|
||||
#endif
|
||||
|
||||
if (_CPNativeInputField && document.activeElement !== _CPNativeInputField)
|
||||
_CPNativeInputField.focus();
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (void)focusForClipboardOfTextView:(CPTextView)textview
|
||||
{
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
if (!_CPNativeInputFieldActive && _CPNativeInputField.innerHTML.length == 0)
|
||||
_CPNativeInputField.innerHTML = _CPCopyPlaceholder; // make sure we have a selection to allow the native pasteboard work in safari
|
||||
var selectedRange = [textview selectedRange];
|
||||
if (selectedRange.length > 0) {
|
||||
// Put the selected text into the hidden div so the browser can natively copy it.
|
||||
var textToCopy = [[[textview textStorage] string] substringWithRange:selectedRange];
|
||||
_CPNativeInputField.innerHTML = textToCopy;
|
||||
} else {
|
||||
// For paste, we just need the field to be focusable.
|
||||
_CPNativeInputField.innerHTML = _CPCopyPlaceholder;
|
||||
}
|
||||
|
||||
[self focusForTextView:textview];
|
||||
|
||||
// select all in the contenteditable div (http://stackoverflow.com/questions/12243898/how-to-select-all-text-in-contenteditable-div)
|
||||
if (document.body.createTextRange)
|
||||
{
|
||||
var range = document.body.createTextRange();
|
||||
|
||||
range.moveToElementText(_CPNativeInputField);
|
||||
range.select();
|
||||
}
|
||||
else if (window.getSelection)
|
||||
{
|
||||
var selection = window.getSelection(),
|
||||
range = document.createRange();
|
||||
|
||||
// Select the content of the hidden div so copy/cut works.
|
||||
if (window.getSelection && document.createRange) {
|
||||
var selection = window.getSelection();
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(_CPNativeInputField);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
+ (void)hideInputElement
|
||||
{
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_CPNativeInputField.style.top = "-10000px";
|
||||
_CPNativeInputField.style.left = "-10000px";
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <Foundation/CPArray.j>
|
||||
@import <Foundation/CPIndexPath.j>
|
||||
@import "CPObjectController.j"
|
||||
@import "CPKeyValueBinding.j"
|
||||
@import "CPTreeNode.j"
|
||||
|
||||
@implementation CPTreeController : CPObjectController
|
||||
{
|
||||
BOOL _avoidsEmptySelection;
|
||||
BOOL _preservesSelection;
|
||||
BOOL _selectsInsertedObjects;
|
||||
BOOL _alwaysUsesMultipleValuesMarker;
|
||||
|
||||
CPString _childrenKeyPath;
|
||||
CPString _countKeyPath;
|
||||
CPString _leafKeyPath;
|
||||
|
||||
CPArray _sortDescriptors;
|
||||
id _arrangedObjects;
|
||||
|
||||
CPArray _selectionIndexPaths;
|
||||
BOOL _disableSetContent;
|
||||
}
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
if (self !== [CPTreeController class])
|
||||
return;
|
||||
|
||||
[self exposeBinding:@"contentArray"];
|
||||
[self exposeBinding:@"sortDescriptors"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingContentArray
|
||||
{
|
||||
return[CPSet setWithObjects:@"content"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingArrangedObjects
|
||||
{
|
||||
return [CPSet setWithObjects:@"content", @"sortDescriptors", @"childrenKeyPath"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectionIndexPath
|
||||
{
|
||||
return[CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectedObjects
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingSelectedNodes
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanAddChild
|
||||
{
|
||||
return[CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanInsert
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
+ (CPSet)keyPathsForValuesAffectingCanInsertChild
|
||||
{
|
||||
return [CPSet setWithObjects:@"selectionIndexPaths"];
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_preservesSelection = YES;
|
||||
_selectsInsertedObjects = YES;
|
||||
_avoidsEmptySelection = YES;
|
||||
_alwaysUsesMultipleValuesMarker = NO;
|
||||
_childrenKeyPath = @"children";
|
||||
|
||||
[self _init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)_init
|
||||
{
|
||||
_sortDescriptors = [CPArray array];
|
||||
_selectionIndexPaths = [CPArray array];
|
||||
_arrangedObjects = [[CPTreeNode alloc] initWithRepresentedObject:nil];
|
||||
}
|
||||
|
||||
- (void)prepareContent
|
||||
{[self _setContentArray:[CPArray arrayWithObject:[self newObject]]];
|
||||
}
|
||||
|
||||
- (BOOL)preservesSelection { return _preservesSelection; }
|
||||
- (void)setPreservesSelection:(BOOL)value { _preservesSelection = value; }
|
||||
|
||||
- (BOOL)selectsInsertedObjects { return _selectsInsertedObjects; }
|
||||
- (void)setSelectsInsertedObjects:(BOOL)value { _selectsInsertedObjects = value; }
|
||||
|
||||
- (BOOL)avoidsEmptySelection { return _avoidsEmptySelection; }
|
||||
- (void)setAvoidsEmptySelection:(BOOL)value { _avoidsEmptySelection = value; }
|
||||
|
||||
- (BOOL)alwaysUsesMultipleValuesMarker { return _alwaysUsesMultipleValuesMarker; }
|
||||
- (void)setAlwaysUsesMultipleValuesMarker:(BOOL)aFlag { _alwaysUsesMultipleValuesMarker = aFlag; }
|
||||
|
||||
- (CPArray)sortDescriptors { return _sortDescriptors; }
|
||||
- (void)setSortDescriptors:(CPArray)value
|
||||
{
|
||||
if (_sortDescriptors === value)
|
||||
return;
|
||||
|
||||
_sortDescriptors = [value copy];
|
||||
[self _rearrangeObjects];
|
||||
}
|
||||
|
||||
- (CPString)childrenKeyPath { return _childrenKeyPath; }
|
||||
- (void)setChildrenKeyPath:(CPString)aKeyPath
|
||||
{
|
||||
if (_childrenKeyPath === aKeyPath) return;
|
||||
_childrenKeyPath = aKeyPath;[self rearrangeObjects];
|
||||
}
|
||||
|
||||
- (CPString)countKeyPath { return _countKeyPath; }
|
||||
- (void)setCountKeyPath:(CPString)aKeyPath { _countKeyPath = aKeyPath; }
|
||||
|
||||
- (CPString)leafKeyPath { return _leafKeyPath; }
|
||||
- (void)setLeafKeyPath:(CPString)aKeyPath { _leafKeyPath = aKeyPath; }
|
||||
|
||||
- (CPString)childrenKeyPathForNode:(CPTreeNode)node { return [self childrenKeyPath]; }
|
||||
- (CPString)countKeyPathForNode:(CPTreeNode)node { return [self countKeyPath]; }
|
||||
- (CPString)leafKeyPathForNode:(CPTreeNode)node { return [self leafKeyPath]; }
|
||||
|
||||
- (void)setContent:(id)value
|
||||
{
|
||||
if (_disableSetContent) return;
|
||||
|
||||
if (!value)
|
||||
value = [CPArray array];
|
||||
if (![value isKindOfClass:[CPArray class]])
|
||||
value = [CPArray arrayWithObject:value];
|
||||
|
||||
var oldSelectedObjects = nil,
|
||||
oldSelectionIndexPaths = nil;
|
||||
|
||||
if ([self preservesSelection])
|
||||
oldSelectedObjects = [self selectedObjects];
|
||||
else
|
||||
oldSelectionIndexPaths = [self selectionIndexPaths];
|
||||
|
||||
_contentObject = value;
|
||||
|
||||
[self _rearrangeObjects];
|
||||
|
||||
if ([self preservesSelection])[self __setSelectedObjects:oldSelectedObjects];
|
||||
else[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection];
|
||||
}
|
||||
|
||||
- (void)_setContentArray:(id)anArray {[self setContent:anArray]; }
|
||||
- (id)contentArray { return _contentObject; }
|
||||
- (id)arrangedObjects { return _arrangedObjects; }
|
||||
|
||||
- (void)rearrangeObjects
|
||||
{
|
||||
[self willChangeValueForKey:@"arrangedObjects"];
|
||||
[self _rearrangeObjects];
|
||||
[self didChangeValueForKey:@"arrangedObjects"];
|
||||
}
|
||||
|
||||
- (void)_rearrangeObjects
|
||||
{
|
||||
var oldSelectedObjects = nil,
|
||||
oldSelectionIndexPaths = nil;
|
||||
|
||||
if ([self preservesSelection])
|
||||
oldSelectedObjects = [self selectedObjects];
|
||||
else
|
||||
oldSelectionIndexPaths = [self selectionIndexPaths];
|
||||
|
||||
[self __rebuildArrangedObjectsTree];
|
||||
|
||||
if ([self preservesSelection])[self __setSelectedObjects:oldSelectedObjects];
|
||||
else[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection];
|
||||
}
|
||||
|
||||
- (void)__rebuildArrangedObjectsTree
|
||||
{
|
||||
var rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil],
|
||||
contentArray = [self contentArray];
|
||||
|
||||
if (contentArray && [contentArray count] > 0)
|
||||
{
|
||||
var children = [self _buildTreeNodesForObjects:contentArray];
|
||||
[[rootNode mutableChildNodes] addObjectsFromArray:children];
|
||||
}
|
||||
|
||||
_arrangedObjects = rootNode;
|
||||
}
|
||||
|
||||
- (CPArray)_buildTreeNodesForObjects:(CPArray)objects
|
||||
{
|
||||
var count = [objects count];
|
||||
|
||||
if (count === 0)
|
||||
return [];
|
||||
|
||||
var sortedObjects = objects;
|
||||
|
||||
if (_sortDescriptors && [_sortDescriptors count] > 0)
|
||||
sortedObjects = [objects sortedArrayUsingDescriptors:_sortDescriptors];
|
||||
|
||||
var nodes = [CPMutableArray arrayWithCapacity:count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var obj = [sortedObjects objectAtIndex:i],
|
||||
node = [[CPTreeNode alloc] initWithRepresentedObject:obj];
|
||||
|
||||
if (_childrenKeyPath)
|
||||
{
|
||||
var childObjects = [obj valueForKeyPath:_childrenKeyPath];
|
||||
|
||||
if (childObjects && [childObjects count] > 0)
|
||||
{
|
||||
var childNodes = [self _buildTreeNodesForObjects:childObjects];
|
||||
[[node mutableChildNodes] addObjectsFromArray:childNodes];
|
||||
}
|
||||
}
|
||||
|
||||
[nodes addObject:node];
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
- (CPIndexPath)selectionIndexPath
|
||||
{
|
||||
return [_selectionIndexPaths count] > 0 ? [_selectionIndexPaths objectAtIndex:0] : nil;
|
||||
}
|
||||
|
||||
- (BOOL)setSelectionIndexPath:(CPIndexPath)indexPath
|
||||
{
|
||||
var paths = indexPath ? [CPArray arrayWithObject:indexPath] : [CPArray array];
|
||||
return[self setSelectionIndexPaths:paths];
|
||||
}
|
||||
|
||||
- (CPArray)selectionIndexPaths { return _selectionIndexPaths; }
|
||||
|
||||
- (BOOL)setSelectionIndexPaths:(CPArray)indexPaths
|
||||
{
|
||||
[self _selectionWillChange];
|
||||
var result = [self __setSelectionIndexPaths:indexPaths avoidEmpty:NO];
|
||||
[self _selectionDidChange];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (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
|
||||
+83
-39
@@ -22,13 +22,12 @@
|
||||
|
||||
@import <Foundation/CPObject.j>
|
||||
@import <Foundation/CPIndexPath.j>
|
||||
|
||||
@import <Foundation/CPArray.j>
|
||||
|
||||
@implementation CPTreeNode : CPObject
|
||||
{
|
||||
id _representedObject @accessors(readonly, property=representedObject);
|
||||
|
||||
CPTreeNode _parentNode @accessors(readonly, property=parentNode);
|
||||
id _representedObject @accessors(property=representedObject);
|
||||
CPTreeNode _parentNode @accessors(property=parentNode);
|
||||
CPMutableArray _childNodes;
|
||||
}
|
||||
|
||||
@@ -52,36 +51,35 @@
|
||||
|
||||
- (CPIndexPath)indexPath
|
||||
{
|
||||
if (_parentNode != nil)
|
||||
// If we have a parent, calculate path based on parent's path + our index
|
||||
if (_parentNode)
|
||||
{
|
||||
var path;
|
||||
var index;
|
||||
|
||||
index = [[_parentNode childNodes] indexOfObject:self];
|
||||
path = [_parentNode indexPath];
|
||||
|
||||
if (path != nil)
|
||||
{
|
||||
return [path indexPathByAddingIndex:index];
|
||||
}
|
||||
else
|
||||
{
|
||||
return [CPIndexPath indexPathWithIndex:index];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return nil;
|
||||
// Search the parent's child nodes, not our own!
|
||||
var index = [[_parentNode childNodes] indexOfObjectIdenticalTo:self];
|
||||
|
||||
// If the parent is the root (and technically has no path itself in some implementations),
|
||||
// we might get nil. Handle that gracefully.
|
||||
var parentPath = [_parentNode indexPath];
|
||||
|
||||
if (parentPath)
|
||||
return [parentPath indexPathByAddingIndex:index];
|
||||
|
||||
return [CPIndexPath indexPathWithIndex:index];
|
||||
}
|
||||
|
||||
// If we are the root, we don't have an index path in the context of a tree controller usually,
|
||||
// or we are [] (empty path). Returning nil is acceptable for the absolute root.
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)isLeaf
|
||||
{
|
||||
return [_childNodes count] <= 0;
|
||||
return [_childNodes count] == 0;
|
||||
}
|
||||
|
||||
- (CPArray)childNodes
|
||||
{
|
||||
// Return a copy to prevent external modification without KVC
|
||||
return [_childNodes copy];
|
||||
}
|
||||
|
||||
@@ -90,18 +88,29 @@
|
||||
return [self mutableArrayValueForKey:@"childNodes"];
|
||||
}
|
||||
|
||||
- (void)insertObject:(id)aTreeNode inChildNodesAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
[[aTreeNode._parentNode mutableChildNodes] removeObjectIdenticalTo:aTreeNode];
|
||||
// MARK: - KVC Compliance Methods
|
||||
|
||||
aTreeNode._parentNode = self;
|
||||
- (void)insertObject:(CPTreeNode)aTreeNode inChildNodesAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
// Optional: Auto-detach from old parent if strictly moving nodes
|
||||
if ([aTreeNode isKindOfClass:[CPTreeNode class]] && aTreeNode._parentNode)
|
||||
{
|
||||
[[aTreeNode._parentNode mutableChildNodes] removeObjectIdenticalTo:aTreeNode];
|
||||
}
|
||||
|
||||
// Direct ivar access is allowed here since we are inside the class implementation
|
||||
if ([aTreeNode isKindOfClass:[CPTreeNode class]])
|
||||
aTreeNode._parentNode = self;
|
||||
|
||||
[_childNodes insertObject:aTreeNode atIndex:anIndex];
|
||||
}
|
||||
|
||||
- (void)removeObjectFromChildNodesAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
[_childNodes objectAtIndex:anIndex]._parentNode = nil;
|
||||
var node = [_childNodes objectAtIndex:anIndex];
|
||||
|
||||
if ([node isKindOfClass:[CPTreeNode class]])
|
||||
node._parentNode = nil;
|
||||
|
||||
[_childNodes removeObjectAtIndex:anIndex];
|
||||
}
|
||||
@@ -110,17 +119,34 @@
|
||||
{
|
||||
var oldTreeNode = [_childNodes objectAtIndex:anIndex];
|
||||
|
||||
oldTreeNode._parentNode = nil;
|
||||
aTreeNode._parentNode = self;
|
||||
if ([oldTreeNode isKindOfClass:[CPTreeNode class]])
|
||||
oldTreeNode._parentNode = nil;
|
||||
|
||||
if ([aTreeNode isKindOfClass:[CPTreeNode class]])
|
||||
aTreeNode._parentNode = self;
|
||||
|
||||
[_childNodes replaceObjectAtIndex:anIndex withObject:aTreeNode];
|
||||
}
|
||||
|
||||
// MARK: - Convenience Accessors
|
||||
|
||||
- (id)objectInChildNodesAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
return _childNodes[anIndex];
|
||||
return [_childNodes objectAtIndex:anIndex];
|
||||
}
|
||||
|
||||
- (CPInteger)count
|
||||
{
|
||||
return [_childNodes count];
|
||||
}
|
||||
|
||||
- (id)objectAtIndex:(CPInteger)anIndex
|
||||
{
|
||||
return [_childNodes objectAtIndex:anIndex];
|
||||
}
|
||||
|
||||
// MARK: - Utility
|
||||
|
||||
- (void)sortWithSortDescriptors:(CPArray)sortDescriptors recursively:(BOOL)shouldSortRecursively
|
||||
{
|
||||
[_childNodes sortUsingDescriptors:sortDescriptors];
|
||||
@@ -129,25 +155,39 @@
|
||||
return;
|
||||
|
||||
var count = [_childNodes count];
|
||||
|
||||
while (count--)
|
||||
[_childNodes[count] sortWithSortDescriptors:sortDescriptors recursively:YES];
|
||||
{
|
||||
var child = [_childNodes objectAtIndex:count];
|
||||
if ([child respondsToSelector:@selector(sortWithSortDescriptors:recursively:)])
|
||||
[child sortWithSortDescriptors:sortDescriptors recursively:YES];
|
||||
}
|
||||
}
|
||||
|
||||
- (CPTreeNode)descendantNodeAtIndexPath:(CPIndexPath)indexPath
|
||||
{
|
||||
var index = 0,
|
||||
count = [indexPath length],
|
||||
node = self;
|
||||
if (!indexPath || [indexPath length] == 0)
|
||||
return self;
|
||||
|
||||
for (; index < count; ++index)
|
||||
node = [node objectInChildNodesAtIndex:[indexPath indexAtPosition:index]];
|
||||
var node = self,
|
||||
length = [indexPath length];
|
||||
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
var index = [indexPath indexAtPosition:i],
|
||||
count = [node count];
|
||||
|
||||
if (index >= count || index < 0)
|
||||
return nil;
|
||||
|
||||
node = [node objectAtIndex:index];
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// Coding implementation remains correct
|
||||
var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey",
|
||||
CPTreeNodeParentNodeKey = @"CPTreeNodeParentNodeKey",
|
||||
CPTreeNodeChildNodesKey = @"CPTreeNodeChildNodesKey";
|
||||
@@ -163,6 +203,10 @@ var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey",
|
||||
_representedObject = [aCoder decodeObjectForKey:CPTreeNodeRepresentedObjectKey];
|
||||
_parentNode = [aCoder decodeObjectForKey:CPTreeNodeParentNodeKey];
|
||||
_childNodes = [aCoder decodeObjectForKey:CPTreeNodeChildNodesKey];
|
||||
|
||||
// Safety check to ensure decoding gave us a CPArray
|
||||
if (!_childNodes)
|
||||
_childNodes = [[CPMutableArray alloc] init];
|
||||
}
|
||||
|
||||
return self;
|
||||
|
||||
@@ -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]];
|
||||
|
||||
|
||||
@@ -1266,6 +1266,10 @@ CPTexturedBackgroundWindowMask
|
||||
[_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
|
||||
[_windowView addSubview:_contentView];
|
||||
|
||||
// The window view manages the exact layout of the content view (e.g. offsetting for the toolbar).
|
||||
if ([_windowView respondsToSelector:@selector(tile)])
|
||||
[_windowView tile];
|
||||
|
||||
/*
|
||||
If the initial first responder has been set to something other than
|
||||
the window, set it to the window because it will no longer be valid.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* CAAnimationGroup.j
|
||||
* AppKit
|
||||
* Created by Daniel Boehringer.
|
||||
* Copyright 2025.
|
||||
*
|
||||
* Implements grouping for Core Animation.
|
||||
*/
|
||||
|
||||
@import <Foundation/CPArray.j>
|
||||
@import "CAAnimation.j"
|
||||
|
||||
@implementation CAAnimationGroup : CAAnimation
|
||||
{
|
||||
CPArray _animations;
|
||||
}
|
||||
|
||||
+ (id)group
|
||||
{
|
||||
return [[self alloc] init];
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_animations = [];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setAnimations:(CPArray)anArray
|
||||
{
|
||||
if (_animations === anArray)
|
||||
return;
|
||||
|
||||
_animations = anArray;
|
||||
}
|
||||
|
||||
- (CPArray)animations
|
||||
{
|
||||
return _animations;
|
||||
}
|
||||
|
||||
/*
|
||||
Iterates through children and executes them recursively.
|
||||
This effectively runs all grouped animations concurrently.
|
||||
*/
|
||||
- (void)runActionForKey:(CPString)aKey object:(id)anObject arguments:(CPDictionary)arguments
|
||||
{
|
||||
var count = [_animations count],
|
||||
i = 0;
|
||||
|
||||
for (; i < count; i++)
|
||||
{
|
||||
var animation = [_animations objectAtIndex:i];
|
||||
|
||||
// Recursively call runActionForKey on the child.
|
||||
// If the child is a CABasicAnimation, it will call [anObject addAnimation:...]
|
||||
// If the child is another Group, it will recurse here.
|
||||
if ([animation respondsToSelector:@selector(runActionForKey:object:arguments:)])
|
||||
{
|
||||
[animation runActionForKey:aKey object:anObject arguments:arguments];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -29,6 +29,8 @@
|
||||
@import "CGGeometry.j"
|
||||
@import "CPColor.j"
|
||||
@import "CPView.j"
|
||||
@import "CAMediaTimingFunction.j"
|
||||
|
||||
|
||||
#define DOM(aLayer) aLayer._DOMElement
|
||||
|
||||
@@ -118,6 +120,8 @@ var CALayerRegisteredRunLoopUpdates = nil;
|
||||
|
||||
CGAffineTransform _transformToLayer;
|
||||
CGAffineTransform _transformFromLayer;
|
||||
|
||||
CPMutableDictionary _activeAnimations;
|
||||
}
|
||||
|
||||
@global document
|
||||
@@ -160,6 +164,8 @@ var CALayerRegisteredRunLoopUpdates = nil;
|
||||
|
||||
_sublayers = [];
|
||||
|
||||
_activeAnimations = [CPMutableDictionary dictionary];
|
||||
|
||||
#if PLATFORM(DOM)
|
||||
_DOMElement = document.createElement("div");
|
||||
|
||||
@@ -977,6 +983,244 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex)
|
||||
return _delegate;
|
||||
}
|
||||
|
||||
/*
|
||||
Adds an animation to the layer.
|
||||
Supports CABasicAnimation for Numbers (opacity) and Points (position/anchorPoint).
|
||||
The animation is exerted by means of periodically applying the keypath on the delegate
|
||||
Only works if the delegate is set!
|
||||
*/
|
||||
- (void)addAnimation:(CAAnimation)anim forKey:(CPString)key
|
||||
{
|
||||
if (!anim) return;
|
||||
|
||||
// --- 1. Handle Animation Groups ---
|
||||
// If it's a group, we simply schedule its children individually.
|
||||
if ([anim respondsToSelector:@selector(animations)] && [anim animations])
|
||||
{
|
||||
var animations = [anim animations],
|
||||
count = [animations count],
|
||||
i = 0;
|
||||
|
||||
for (; i < count; i++)
|
||||
{
|
||||
var child = [animations objectAtIndex:i];
|
||||
|
||||
// Recurse: Add the child animation.
|
||||
// We pass 'nil' for the key so the child's own 'keyPath'
|
||||
// is used as the storage identifier in the dictionary.
|
||||
[self addAnimation:child forKey:nil];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 2. Determine KeyPath ---
|
||||
var keyPath = key;
|
||||
|
||||
// If the animation object has an explicit keyPath (like CABasicAnimation), use it.
|
||||
if ([anim respondsToSelector:@selector(keyPath)] && [anim keyPath])
|
||||
keyPath = [anim keyPath];
|
||||
|
||||
// If we can't determine a property to animate, we must abort.
|
||||
if (!keyPath) return;
|
||||
|
||||
// --- 3. Determine Values ---
|
||||
var startValue = ([anim respondsToSelector:@selector(fromValue)]) ? [anim fromValue] : nil;
|
||||
|
||||
// If startValue is missing, try to read it from the layer.
|
||||
// We wrap this in a try-catch to prevent crashes if 'keyPath' is invalid.
|
||||
if (startValue == nil)
|
||||
{
|
||||
try {
|
||||
startValue = [[self delegate] valueForKey:keyPath];
|
||||
}
|
||||
catch (e) {
|
||||
// The keyPath was likely invalid (not KVC compliant), abort.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var endValue = ([anim respondsToSelector:@selector(toValue)]) ? [anim toValue] : nil;
|
||||
|
||||
if (endValue == nil)
|
||||
return;
|
||||
|
||||
var duration = ([anim respondsToSelector:@selector(duration)]) ? [anim duration] : 0.25;
|
||||
|
||||
// Default to EaseInEaseOut if not specified
|
||||
var timingFunction = ([anim respondsToSelector:@selector(timingFunction)]) ? [anim timingFunction] : [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
|
||||
|
||||
// --- 4. Prepare Context ---
|
||||
var context = {
|
||||
"animation": anim,
|
||||
"keyPath": keyPath,
|
||||
"startValue": startValue,
|
||||
"endValue": endValue,
|
||||
"duration": duration * 1000.0, // ms
|
||||
"timingFunction": timingFunction,
|
||||
"startTime": null,
|
||||
"requestId": null
|
||||
};
|
||||
|
||||
// --- 5. Render Loop ---
|
||||
var _self = self;
|
||||
|
||||
var renderLoop = function(timestamp) {
|
||||
if ([_self _renderAnimationStep:context timestamp:timestamp])
|
||||
context.requestId = window.requestAnimationFrame(renderLoop);
|
||||
else
|
||||
context.requestId = null;
|
||||
};
|
||||
|
||||
// --- 6. Storage & Kickoff ---
|
||||
// Use the keyPath as the identifier if no specific key was provided
|
||||
var storageKey = (key && key.length > 0) ? key : keyPath;
|
||||
|
||||
// Remove any conflicting animation on this specific property/key
|
||||
[self removeAnimationForKey:storageKey];
|
||||
|
||||
context.requestId = window.requestAnimationFrame(renderLoop);
|
||||
[_activeAnimations setObject:context forKey:storageKey];
|
||||
}
|
||||
|
||||
- (void)removeAnimationForKey:(CPString)key
|
||||
{
|
||||
var context = [_activeAnimations objectForKey:key];
|
||||
if (context)
|
||||
{
|
||||
if (context.requestId !== null)
|
||||
window.cancelAnimationFrame(context.requestId);
|
||||
[_activeAnimations removeObjectForKey:key];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeAllAnimations
|
||||
{
|
||||
var keys = [_activeAnimations allKeys],
|
||||
count = [keys count];
|
||||
while (count--)
|
||||
[self removeAnimationForKey:[keys objectAtIndex:count]];
|
||||
}
|
||||
|
||||
/*
|
||||
Solves Cubic Bezier for t.
|
||||
p1, p2 are the control points (x,y). p0 is 0,0, p3 is 1,1.
|
||||
This is a simplified solver for standard Core Animation timing functions.
|
||||
*/
|
||||
- (float)_solveBezier:(float)t forTimingFunction:(CAMediaTimingFunction)tf
|
||||
{
|
||||
if (!tf) return t;
|
||||
|
||||
// Linear optimization
|
||||
if (tf === [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear])
|
||||
return t;
|
||||
|
||||
var points = [tf controlPoints]; // [c1x, c1y, c2x, c2y]
|
||||
var p1x = points[0], p1y = points[1],
|
||||
p2x = points[2], p2y = points[3];
|
||||
|
||||
// Simple polynomial evaluation (De Casteljau's algorithm/Cubic formula subset)
|
||||
// Since we are usually dealing with standard easing, we can approximate 1D easing on the Y axis
|
||||
// based on linear time X, or do a full solve.
|
||||
// For brevity/speed in JS, we often approximate basic easing:
|
||||
|
||||
// 3t^2 * (1-t) + t^3 ... standard bezier blending functions
|
||||
var cx = 3.0 * p1x;
|
||||
var bx = 3.0 * (p2x - p1x) - cx;
|
||||
var ax = 1.0 - cx - bx;
|
||||
|
||||
var cy = 3.0 * p1y;
|
||||
var by = 3.0 * (p2y - p1y) - cy;
|
||||
var ay = 1.0 - cy - by;
|
||||
|
||||
// Solve for X given t (time) using Newton-Raphson
|
||||
var sampleT = t;
|
||||
for (var i = 0; i < 5; i++) {
|
||||
var x = ((ax * sampleT + bx) * sampleT + cx) * sampleT - t;
|
||||
if (Math.abs(x) < 1e-3) break;
|
||||
var d = (3.0 * ax * sampleT + 2.0 * bx) * sampleT + cx;
|
||||
if (Math.abs(d) < 1e-6) break;
|
||||
sampleT = sampleT - x / d;
|
||||
}
|
||||
|
||||
// Solve for Y given derived T
|
||||
return ((ay * sampleT + by) * sampleT + cy) * sampleT;
|
||||
}
|
||||
|
||||
- (BOOL)_renderAnimationStep:(JSObject)context timestamp:(double)timestamp
|
||||
{
|
||||
if (context.startTime === null)
|
||||
context.startTime = timestamp;
|
||||
|
||||
var elapsed = timestamp - context.startTime,
|
||||
linearProgress = elapsed / context.duration;
|
||||
|
||||
if (linearProgress > 1.0) linearProgress = 1.0;
|
||||
|
||||
// Apply Timing Function
|
||||
var progress = [self _solveBezier:linearProgress forTimingFunction:context.timingFunction];
|
||||
|
||||
var start = context.startValue,
|
||||
end = context.endValue,
|
||||
current = nil;
|
||||
|
||||
// Number
|
||||
if (typeof start === "number")
|
||||
{
|
||||
current = start + (end - start) * progress;
|
||||
}
|
||||
// Point / Size / Rect
|
||||
else if (start && start.x !== undefined && start.y !== undefined) // CGPoint
|
||||
{
|
||||
current = CGPointMake(start.x + (end.x - start.x) * progress,
|
||||
start.y + (end.y - start.y) * progress);
|
||||
}
|
||||
else if (start && start.width !== undefined && start.height !== undefined) // CGSize
|
||||
{
|
||||
current = CGSizeMake(start.width + (end.width - start.width) * progress,
|
||||
start.height + (end.height - start.height) * progress);
|
||||
}
|
||||
else if (start && start.origin !== undefined && start.size !== undefined) // CGRect
|
||||
{
|
||||
current = CGRectMake(
|
||||
start.origin.x + (end.origin.x - start.origin.x) * progress,
|
||||
start.origin.y + (end.origin.y - start.origin.y) * progress,
|
||||
start.size.width + (end.size.width - start.size.width) * progress,
|
||||
start.size.height + (end.size.height - start.size.height) * progress
|
||||
);
|
||||
}
|
||||
|
||||
if (current !== nil)
|
||||
[[self delegate] setValue:current forKey:context.keyPath];
|
||||
|
||||
if (linearProgress >= 1.0)
|
||||
{
|
||||
var anim = context.animation;
|
||||
|
||||
// Cleanup
|
||||
var shouldRemove = [anim respondsToSelector:@selector(isRemovedOnCompletion)] ? [anim isRemovedOnCompletion] : YES;
|
||||
|
||||
if (shouldRemove) {
|
||||
// Find key by context identity to handle groups correctly
|
||||
var keys = [_activeAnimations allKeys];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if ([_activeAnimations objectForKey:keys[i]] === context) {
|
||||
[_activeAnimations removeObjectForKey:keys[i]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate
|
||||
var delegate = [anim delegate];
|
||||
if (delegate && [delegate respondsToSelector:@selector(animationDidStop:finished:)])
|
||||
[delegate animationDidStop:anim finished:YES];
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
/* @ignore */
|
||||
- (void)_setOwningView:(CPView)anOwningView
|
||||
{
|
||||
|
||||
@@ -305,7 +305,7 @@ var hasEditableTarget = function(aDOMEvent)
|
||||
location = [[CPApp currentEvent] locationInWindow],
|
||||
anEvent = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags
|
||||
timestamp:timestamp windowNumber:windowNumber context:nil
|
||||
characters:characters charactersIgnoringModifiers:characters isARepeat:NO keyCode:keyCode];
|
||||
characters:characters charactersIgnoringModifiers:characters isARepeat:NO keyCode:keyCode isActionKey:YES];
|
||||
|
||||
anEvent._data1 = @{ "simulated": YES };
|
||||
anEvent._DOMEvent = aDOMEvent;
|
||||
|
||||
@@ -192,6 +192,33 @@ KeyCodesToUnicodeMap[CPKeyCodes.OPEN_SQUARE_BRACKET] = "[";
|
||||
KeyCodesToUnicodeMap[CPKeyCodes.BACKSLASH] = "\\";
|
||||
KeyCodesToUnicodeMap[CPKeyCodes.CLOSE_SQUARE_BRACKET] = "]";
|
||||
|
||||
var KeyNameToUnicodeMap = {};
|
||||
KeyNameToUnicodeMap["Backspace"] = CPDeleteCharacter;
|
||||
KeyNameToUnicodeMap["Delete"] = CPDeleteFunctionKey;
|
||||
KeyNameToUnicodeMap["Tab"] = CPTabCharacter;
|
||||
KeyNameToUnicodeMap["Enter"] = CPCarriageReturnCharacter;
|
||||
KeyNameToUnicodeMap["Escape"] = CPEscapeFunctionKey;
|
||||
KeyNameToUnicodeMap["PageUp"] = CPPageUpFunctionKey;
|
||||
KeyNameToUnicodeMap["PageDown"] = CPPageDownFunctionKey;
|
||||
KeyNameToUnicodeMap["ArrowLeft"] = CPLeftArrowFunctionKey;
|
||||
KeyNameToUnicodeMap["ArrowUp"] = CPUpArrowFunctionKey;
|
||||
KeyNameToUnicodeMap["ArrowRight"] = CPRightArrowFunctionKey;
|
||||
KeyNameToUnicodeMap["ArrowDown"] = CPDownArrowFunctionKey;
|
||||
KeyNameToUnicodeMap["Home"] = CPHomeFunctionKey;
|
||||
KeyNameToUnicodeMap["End"] = CPEndFunctionKey;
|
||||
// Add safeguards for punctuation
|
||||
KeyNameToUnicodeMap[";"] = ";";
|
||||
KeyNameToUnicodeMap["-"] = "-";
|
||||
KeyNameToUnicodeMap["="] = "=";
|
||||
KeyNameToUnicodeMap[","] = ",";
|
||||
KeyNameToUnicodeMap["."] = ".";
|
||||
KeyNameToUnicodeMap["/"] = "/";
|
||||
KeyNameToUnicodeMap["`"] = "`";
|
||||
KeyNameToUnicodeMap["'"] = "'";
|
||||
KeyNameToUnicodeMap["["] = "[";
|
||||
KeyNameToUnicodeMap["\\"] = "\\";
|
||||
KeyNameToUnicodeMap["]"] = "]";
|
||||
|
||||
var ModifierKeyCodes = [
|
||||
CPKeyCodes.META,
|
||||
CPKeyCodes.WEBKIT_RIGHT_META,
|
||||
@@ -438,7 +465,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
theDocument.addEventListener("keyup", keyEventCallback, NO);
|
||||
theDocument.addEventListener("keydown", keyEventCallback, NO);
|
||||
theDocument.addEventListener("keypress", keyEventCallback, NO);
|
||||
// "keypress" listener removed as it's deprecated and no longer used in the new logic.
|
||||
|
||||
theDocument.addEventListener("touchstart", touchEventCallback, {passive: false});
|
||||
theDocument.addEventListener("touchend", touchEventCallback, {passive: false});
|
||||
@@ -470,7 +497,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
theDocument.removeEventListener("keyup", keyEventCallback, NO);
|
||||
theDocument.removeEventListener("keydown", keyEventCallback, NO);
|
||||
theDocument.removeEventListener("keypress", keyEventCallback, NO);
|
||||
|
||||
theDocument.removeEventListener("touchstart", touchEventCallback, NO);
|
||||
theDocument.removeEventListener("touchend", touchEventCallback, NO);
|
||||
@@ -503,7 +529,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
theDocument.attachEvent("onkeyup", keyEventCallback);
|
||||
theDocument.attachEvent("onkeydown", keyEventCallback);
|
||||
theDocument.attachEvent("onkeypress", keyEventCallback);
|
||||
// "onkeypress" listener removed.
|
||||
|
||||
_DOMWindow.attachEvent("onresize", resizeEventCallback);
|
||||
|
||||
@@ -533,7 +559,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
theDocument.detachEvent("onkeyup", keyEventCallback);
|
||||
theDocument.detachEvent("onkeydown", keyEventCallback);
|
||||
theDocument.detachEvent("onkeypress", keyEventCallback);
|
||||
|
||||
_DOMWindow.detachEvent("onresize", resizeEventCallback);
|
||||
|
||||
@@ -709,157 +734,138 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
(_capsLockActive ? CPAlphaShiftKeyMask : 0);
|
||||
|
||||
// With a few exceptions, all key events are blocked from propagating to
|
||||
// the browser. Here the following exceptions are being allowed:
|
||||
//
|
||||
// - All keys pressed along with a ctrl or cmd key _unless_ they are in
|
||||
// one of the two blacklists.
|
||||
// - Any key listed in the whitelist.
|
||||
//
|
||||
// The ctrl/cmd keys are used for browser hotkeys as are the keys listed in
|
||||
// the whitelist (F1-F12 at the time of writing).
|
||||
//
|
||||
// If a key is listed in both the blacklist and whitelist, the blacklist is
|
||||
// checked first. The key will be blocked from propagating in that case.
|
||||
// the browser. The logic here allows browser shortcuts (Cmd/Ctrl keys)
|
||||
// and function keys (F1-F12) to pass through, unless explicitly blacklisted.
|
||||
|
||||
StopDOMEventPropagation = YES;
|
||||
var keyCodeForPropagationCheck = aDOMEvent.keyCode || 0;
|
||||
var charForPropagationCheck = String.fromCharCode(keyCodeForPropagationCheck).toLowerCase();
|
||||
|
||||
// Make sure it is not in the blacklists.
|
||||
if (!(CharacterKeysToPrevent[String.fromCharCode(aDOMEvent.keyCode || aDOMEvent.charCode).toLowerCase()] || KeyCodesToPrevent[aDOMEvent.keyCode]))
|
||||
if (!(CharacterKeysToPrevent[charForPropagationCheck] || KeyCodesToPrevent[keyCodeForPropagationCheck]))
|
||||
{
|
||||
// It is not in the blacklist, let it through if the ctrl/cmd key is
|
||||
// also down or it's in the whitelist.
|
||||
if ((modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) || KeyCodesToAllow[aDOMEvent.keyCode])
|
||||
if ((modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) || KeyCodesToAllow[keyCodeForPropagationCheck])
|
||||
StopDOMEventPropagation = NO;
|
||||
}
|
||||
|
||||
var overrideCharacters = nil,
|
||||
var characters = @"",
|
||||
charactersIgnoringModifiers = @"";
|
||||
|
||||
var keyCode = aDOMEvent.keyCode;
|
||||
if (keyCode in MozKeyCodeToKeyCodeMap)
|
||||
keyCode = MozKeyCodeToKeyCodeMap[keyCode];
|
||||
|
||||
var isActionKey;
|
||||
var key = aDOMEvent.key;
|
||||
|
||||
if (key) {
|
||||
isActionKey =
|
||||
key === 'Enter' ||
|
||||
key === 'Backspace' ||
|
||||
key === 'Tab' ||
|
||||
key === 'Escape' ||
|
||||
key === 'Delete' ||
|
||||
key.startsWith('Arrow') ||
|
||||
key === 'Home' ||
|
||||
key === 'End' ||
|
||||
key === 'PageUp' ||
|
||||
key === 'PageDown';
|
||||
}
|
||||
else
|
||||
{
|
||||
isActionKey =
|
||||
(keyCode === 13) || (keyCode === 8) || (keyCode === 9) ||
|
||||
(keyCode === 27) || (keyCode === 46) || (keyCode >= 37 && keyCode <= 40);
|
||||
}
|
||||
|
||||
switch (aDOMEvent.type)
|
||||
{
|
||||
case "keydown":
|
||||
// Grab and store the keycode now since it is correct and consistent at this point.
|
||||
if (aDOMEvent.keyCode in MozKeyCodeToKeyCodeMap)
|
||||
_keyCode = MozKeyCodeToKeyCodeMap[aDOMEvent.keyCode];
|
||||
else
|
||||
_keyCode = aDOMEvent.keyCode;
|
||||
|
||||
var characters;
|
||||
|
||||
// Handle key codes for which String.fromCharCode won't work.
|
||||
// Refs #1036: In Internet Explorer, both 'which' and 'charCode' are undefined for special keys.
|
||||
if (aDOMEvent.which === 0 || aDOMEvent.charCode === 0 || (aDOMEvent.which === undefined && aDOMEvent.charCode === undefined))
|
||||
characters = KeyCodesToUnicodeMap[_keyCode];
|
||||
|
||||
// The problem with keyCode is that this property refers to keys on the keyboard and not to characters
|
||||
// This is why String.fromCharCode does not always work in more recent versions of Firefox
|
||||
// E.g. pressing a '#' on a German keyboard gives you a charCode of 163, which refers to '£' and not '#'
|
||||
// The property key works fine, though. From there we can get the actual character more robustly.
|
||||
// Therefore we prefer key over keyCode whenever possible
|
||||
|
||||
if (!characters)
|
||||
characters = (aDOMEvent.key && aDOMEvent.key.length == 1) ? aDOMEvent.key.toLowerCase() : String.fromCharCode(_keyCode).toLowerCase();
|
||||
|
||||
overrideCharacters = (modifierFlags & CPShiftKeyMask || _capsLockActive) ? characters.toUpperCase() : characters;
|
||||
|
||||
// check for caps lock state
|
||||
if (_keyCode === CPKeyCodes.CAPS_LOCK)
|
||||
if ([ModifierKeyCodes containsObject:keyCode])
|
||||
{
|
||||
_capsLockActive = YES;
|
||||
|
||||
// Make sure the caps lock flag is set in modifierFlags
|
||||
modifierFlags |= CPAlphaShiftKeyMask;
|
||||
}
|
||||
|
||||
if ([ModifierKeyCodes containsObject:_keyCode])
|
||||
{
|
||||
// A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break.
|
||||
event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags
|
||||
timestamp:timestamp windowNumber:windowNumber context:nil
|
||||
characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:_keyCode];
|
||||
characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:keyCode isActionKey:YES];
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
else if (modifierFlags & (CPControlKeyMask | CPCommandKeyMask))
|
||||
var isARepeat = !!aDOMEvent.repeat || (_charCodes[keyCode] != nil);
|
||||
_charCodes[keyCode] = YES;
|
||||
|
||||
if (aDOMEvent.key)
|
||||
{
|
||||
//we are simply going to skip all keypress events that use cmd/ctrl key
|
||||
//this lets us be consistent in all browsers and send on the keydown
|
||||
//which means we can cancel the event early enough, but only if sendEvent needs to
|
||||
}
|
||||
else if (CPKeyCodes.firesKeyPressEvent(_keyCode, aDOMEvent.key, _lastKey, aDOMEvent.shiftKey, aDOMEvent.ctrlKey, aDOMEvent.altKey))
|
||||
{
|
||||
// this branch is taken by events which fire keydown, keypress, and keyup.
|
||||
// this is the only time we'll ALLOW character keys to propagate (needed for text fields)
|
||||
StopDOMEventPropagation = NO;
|
||||
break;
|
||||
if (aDOMEvent.key.length === 1)
|
||||
{
|
||||
characters = aDOMEvent.key;
|
||||
}
|
||||
// Correctly handle dead keys to prevent inserting "Dead"
|
||||
else if (aDOMEvent.key === "Dead" || aDOMEvent.key === "Process") {
|
||||
characters = @"";
|
||||
}
|
||||
// For other named keys, map them or fall back to an empty string.
|
||||
else
|
||||
{
|
||||
characters = KeyNameToUnicodeMap[aDOMEvent.key] || @"";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//this branch is taken by "remedial" key events
|
||||
// In this state we continue to keypress and send the CPEvent
|
||||
characters = KeyCodesToUnicodeMap[keyCode];
|
||||
|
||||
if (!characters)
|
||||
{
|
||||
characters = String.fromCharCode(keyCode);
|
||||
if (modifierFlags & CPShiftKeyMask || _capsLockActive)
|
||||
characters = characters.toUpperCase();
|
||||
else
|
||||
characters = characters.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
case "keypress":
|
||||
// we unconditionally break on keypress events with modifiers,
|
||||
// because we forced the event to be sent on the keydown
|
||||
if (aDOMEvent.type === "keypress" && (modifierFlags & (CPControlKeyMask | CPCommandKeyMask)))
|
||||
break;
|
||||
|
||||
var keyCode = _keyCode,
|
||||
charCode = aDOMEvent.keyCode || aDOMEvent.charCode,
|
||||
isARepeat = (_charCodes[keyCode] != nil);
|
||||
|
||||
_lastKey = keyCode;
|
||||
_charCodes[keyCode] = charCode;
|
||||
|
||||
var characters = overrideCharacters;
|
||||
// Is this a special key?
|
||||
if (!characters && (aDOMEvent.which === 0 || aDOMEvent.charCode === 0))
|
||||
characters = KeyCodesToUnicodeMap[charCode];
|
||||
|
||||
if (!characters)
|
||||
characters = String.fromCharCode(charCode);
|
||||
|
||||
charactersIgnoringModifiers = characters.toLowerCase(); // FIXME: This isn't correct. It SHOULD include Shift.
|
||||
|
||||
// Safari won't send proper capitalization during cmd-key events
|
||||
if (!overrideCharacters && (modifierFlags & CPCommandKeyMask) && ((modifierFlags & CPShiftKeyMask) || _capsLockActive))
|
||||
characters = characters.toUpperCase();
|
||||
charactersIgnoringModifiers = characters.toLowerCase();
|
||||
|
||||
event = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags
|
||||
timestamp:timestamp windowNumber:windowNumber context:nil
|
||||
characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:charCode];
|
||||
characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:keyCode isActionKey:isActionKey];
|
||||
|
||||
break;
|
||||
|
||||
case "keyup":
|
||||
var keyCode = aDOMEvent.keyCode,
|
||||
charCode = _charCodes[keyCode];
|
||||
|
||||
_keyCode = -1;
|
||||
_lastKey = -1;
|
||||
_charCodes[keyCode] = nil;
|
||||
|
||||
// check for caps lock state
|
||||
if (keyCode === CPKeyCodes.CAPS_LOCK)
|
||||
{
|
||||
_capsLockActive = NO;
|
||||
|
||||
// Make sure the caps lock flag is cleared in modifierFlags
|
||||
modifierFlags &= ~CPAlphaShiftKeyMask;
|
||||
_capsLockActive = !_capsLockActive;
|
||||
if (_capsLockActive)
|
||||
modifierFlags |= CPAlphaShiftKeyMask;
|
||||
else
|
||||
modifierFlags &= ~CPAlphaShiftKeyMask;
|
||||
}
|
||||
|
||||
if ([ModifierKeyCodes containsObject:keyCode])
|
||||
{
|
||||
// A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break.
|
||||
event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags
|
||||
timestamp:timestamp windowNumber:windowNumber context:nil
|
||||
characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:_keyCode];
|
||||
|
||||
characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:keyCode isActionKey:YES];
|
||||
break;
|
||||
}
|
||||
|
||||
var characters = KeyCodesToUnicodeMap[charCode] || String.fromCharCode(charCode);
|
||||
if (aDOMEvent.key)
|
||||
{
|
||||
if (aDOMEvent.key.length === 1) {
|
||||
characters = aDOMEvent.key;
|
||||
}
|
||||
// Ensure keyup events also don't produce "Dead"
|
||||
else if (aDOMEvent.key === "Dead" || aDOMEvent.key === "Process") {
|
||||
characters = @"";
|
||||
}
|
||||
else
|
||||
{
|
||||
characters = KeyNameToUnicodeMap[aDOMEvent.key] || @"";
|
||||
}
|
||||
}
|
||||
else
|
||||
characters = KeyCodesToUnicodeMap[keyCode] || String.fromCharCode(keyCode);
|
||||
|
||||
charactersIgnoringModifiers = characters.toLowerCase();
|
||||
|
||||
if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive)
|
||||
@@ -867,7 +873,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
event = [CPEvent keyEventWithType:CPKeyUp location:location modifierFlags:modifierFlags
|
||||
timestamp: timestamp windowNumber:windowNumber context:nil
|
||||
characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode];
|
||||
characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode isActionKey:isActionKey];
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -880,12 +886,11 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
if (event && ![_platformPasteboard windowShouldSuppressKeyEvent])
|
||||
{
|
||||
[CPApp sendEvent:event];
|
||||
|
||||
[_platformPasteboard windowDidSendKeyEvent:event];
|
||||
}
|
||||
|
||||
var didStop = NO;
|
||||
// Platform pasteboard can overrule the decision to stop propagation either way, or it might have no opinion.
|
||||
|
||||
if ([_platformPasteboard windowShouldStopPropagation] || (StopDOMEventPropagation && ![_platformPasteboard windowShouldNotStopPropagation]))
|
||||
{
|
||||
didStop = YES;
|
||||
@@ -1271,6 +1276,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
newEvent.type = CPDOMEventMouseUp;
|
||||
break;
|
||||
}
|
||||
newEvent._isFromTouch = true; // Identify the event as touch-originated for tolerant click counting
|
||||
[self mouseEvent:newEvent];
|
||||
return;
|
||||
}
|
||||
@@ -1385,7 +1391,8 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
if (aDOMEvent.button !== _firstMouseDownButton)
|
||||
return;
|
||||
|
||||
event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp, location, modifierFlags, timestamp, windowNumber, nil, -1, CPDOMEventGetClickCount(_lastMouseUp, timestamp, location), 0, nil);
|
||||
var clickCount = CPDOMEventGetClickCount(_lastMouseUp, timestamp, location, aDOMEvent._isFromTouch);
|
||||
event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp, location, modifierFlags, timestamp, windowNumber, nil, -1, clickCount, 0, nil);
|
||||
|
||||
_mouseIsDown = NO;
|
||||
_lastMouseUp = event;
|
||||
@@ -1431,15 +1438,17 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
_DOMEventMode = YES;
|
||||
_mouseIsDown = YES;
|
||||
|
||||
var clickCount = CPDOMEventGetClickCount(_lastMouseDown, timestamp, location, aDOMEvent._isFromTouch);
|
||||
|
||||
// Fake a down and up event so that event tracking mode will work correctly
|
||||
[CPApp sendEvent:[CPEvent mouseEventWithType:_mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown location:location modifierFlags:modifierFlags
|
||||
timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:-1
|
||||
clickCount:CPDOMEventGetClickCount(_lastMouseDown, timestamp, location) pressure:0]];
|
||||
clickCount:clickCount pressure:0]];
|
||||
|
||||
[CPApp sendEvent:[CPEvent mouseEventWithType:_mouseDownIsRightClick ? CPRightMouseUp : CPLeftMouseUp location:location modifierFlags:modifierFlags
|
||||
timestamp:timestamp windowNumber:windowNumber context:nil eventNumber:-1
|
||||
clickCount:CPDOMEventGetClickCount(_lastMouseDown, timestamp, location) pressure:0]];
|
||||
clickCount:clickCount pressure:0]];
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -1451,7 +1460,8 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
|
||||
|
||||
StopContextMenuDOMEventPropagation = YES;
|
||||
|
||||
event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown, location, modifierFlags, timestamp, windowNumber, nil, -1, CPDOMEventGetClickCount(_lastMouseDown, timestamp, location), 0, nil);
|
||||
var clickCount = CPDOMEventGetClickCount(_lastMouseDown, timestamp, location, aDOMEvent._isFromTouch);
|
||||
event = _CPEventFromNativeMouseEvent(aDOMEvent, _mouseDownIsRightClick ? CPRightMouseDown : CPLeftMouseDown, location, modifierFlags, timestamp, windowNumber, nil, -1, clickCount, 0, nil);
|
||||
|
||||
_mouseIsDown = YES;
|
||||
_lastMouseDown = event;
|
||||
@@ -1951,19 +1961,25 @@ var _CPEventFromNativeMouseEvent = function(aNativeEvent, anEventType, aPoint, m
|
||||
return aNativeEvent;
|
||||
};
|
||||
|
||||
var CLICK_SPACE_DELTA = 5.0,
|
||||
CLICK_TIME_DELTA = (typeof document != "undefined" && document.addEventListener) ? 0.55 : 1.0;
|
||||
var CLICK_SPACE_DELTA = 5.0,
|
||||
CLICK_TIME_DELTA = (typeof document != "undefined" && document.addEventListener) ? 0.55 : 1.0,
|
||||
// Define a more generous time delta for touch events to make double-tapping easier.
|
||||
TOUCH_CLICK_TIME_DELTA = 0.80; // Increased from 0.55s to 0.80s
|
||||
|
||||
CPDOMEventGetClickCount = function(aComparisonEvent, aTimestamp, aLocation)
|
||||
CPDOMEventGetClickCount = function(aComparisonEvent, aTimestamp, aLocation, isFromTouch)
|
||||
{
|
||||
if (!aComparisonEvent)
|
||||
return 1;
|
||||
|
||||
// For touch events, allow a larger pixel delta to accommodate "fat fingers"
|
||||
// and a longer time delta to accommodate less precise tapping.
|
||||
var spaceDelta = isFromTouch ? 25.0 : CLICK_SPACE_DELTA;
|
||||
var timeDelta = isFromTouch ? TOUCH_CLICK_TIME_DELTA : CLICK_TIME_DELTA;
|
||||
var comparisonLocation = [aComparisonEvent locationInWindow];
|
||||
|
||||
return (aTimestamp - [aComparisonEvent timestamp] < CLICK_TIME_DELTA &&
|
||||
ABS(comparisonLocation.x - aLocation.x) < CLICK_SPACE_DELTA &&
|
||||
ABS(comparisonLocation.y - aLocation.y) < CLICK_SPACE_DELTA) ? [aComparisonEvent clickCount] + 1 : 1;
|
||||
return (aTimestamp - [aComparisonEvent timestamp] < timeDelta &&
|
||||
ABS(comparisonLocation.x - aLocation.x) < spaceDelta &&
|
||||
ABS(comparisonLocation.y - aLocation.y) < spaceDelta) ? [aComparisonEvent clickCount] + 1 : 1;
|
||||
};
|
||||
|
||||
// Global.
|
||||
|
||||
@@ -132,38 +132,36 @@ CPKeyCodes = {
|
||||
|
||||
/*!
|
||||
* Returns true if the key fires a keypress event in the current browser.
|
||||
* The keypress event is deprecated, but this function helps manage legacy
|
||||
* event handling by predicting its behavior.
|
||||
*
|
||||
* Accoridng to MSDN [1] IE only fires keypress events for the following keys:
|
||||
* - Letters: A - Z (uppercase and lowercase)
|
||||
* - Numerals: 0 - 9
|
||||
* - Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~
|
||||
* - System: ESC, SPACEBAR, ENTER
|
||||
*
|
||||
* That's not entirely correct though, for instance there's no distinction
|
||||
* between upper and lower case letters.
|
||||
*
|
||||
* [1] http://msdn2.microsoft.com/en-us/library/ms536939(VS.85).aspx)
|
||||
*
|
||||
* Safari is similar to IE, but does not fire keypress for ESC.
|
||||
*
|
||||
* Additionally, IE6 does not fire keydown or keypress events for letters when
|
||||
* the control or alt keys are held down and the shift key is not. IE7 does
|
||||
* fire keydown in these cases, though, but not keypress.
|
||||
*
|
||||
* @param keyCode A key code.
|
||||
* @param opt_heldKeyCode Key code of a currently-held key.
|
||||
* @param opt_shiftKey Whether the shift key is held down.
|
||||
* @param opt_ctrlKey Whether the control key is held down.
|
||||
* @param opt_altKey Whether the alt key is held down.
|
||||
* @return Returns YES if it's a key that fires a keypress event.
|
||||
* @param {number} keyCode A key code.
|
||||
* @param {string} key The `key` property from the keyboard event.
|
||||
* @param {number} opt_heldKeyCode Key code of a currently-held key.
|
||||
* @param {boolean} opt_shiftKey Whether the shift key is held down.
|
||||
* @param {boolean} opt_ctrlKey Whether the control key is held down.
|
||||
* @param {boolean} opt_altKey Whether the alt key is held down.
|
||||
* @return {boolean} Returns YES if it's a key that fires a keypress event.
|
||||
*/
|
||||
CPKeyCodes.firesKeyPressEvent = function(keyCode, key, opt_heldKeyCode, opt_shiftKey, opt_ctrlKey, opt_altKey)
|
||||
{
|
||||
// The property key from event is one character wide in case of 'regular' keys (as opposed e.g. to arrow keys)
|
||||
// Regular keys all fire the keypress event
|
||||
// Modern approach: Use event.key if available, as it is the most reliable standard.
|
||||
if (key)
|
||||
{
|
||||
// Any key that produces a single, printable character fires a keypress event.
|
||||
if (key.length === 1)
|
||||
return true;
|
||||
|
||||
if (key && key.length == 1)
|
||||
return true;
|
||||
// "Enter" is a special non-printable key that historically fires keypress for compatibility.
|
||||
if (key === "Enter")
|
||||
return true;
|
||||
|
||||
// For all other non-printable keys (e.g., "ArrowLeft", "Escape", "F1"),
|
||||
// modern browsers do not fire a keypress event.
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Legacy Fallback Logic (for browsers that don't support event.key) ---
|
||||
|
||||
if (!CPFeatureIsCompatible(CPJavaScriptRemedialKeySupport))
|
||||
return true;
|
||||
@@ -196,9 +194,11 @@ CPKeyCodes.firesKeyPressEvent = function(keyCode, key, opt_heldKeyCode, opt_shif
|
||||
|
||||
/*!
|
||||
* Test for whether or not a given keyCode represents a character key.
|
||||
* NOTE: This is a legacy function for browsers that don't support `event.key`.
|
||||
* It is unreliable because `keyCode` represents a physical key, not the character produced.
|
||||
*
|
||||
* @param keyCode A key code.
|
||||
* @return Returns YES if the keyCode is a character key.
|
||||
* @param {number} keyCode A key code.
|
||||
* @return {boolean} Returns YES if the keyCode is a character key.
|
||||
*/
|
||||
CPKeyCodes.isCharacterKey = function(keyCode)
|
||||
{
|
||||
|
||||
+2
-2
@@ -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.
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
timestamp:400.5 windowNumber:300 context:nil eventNumber:0 clickCount:2 pressure:0.5];
|
||||
[self assert:@"CPEvent: type=2 loc={50, 50} time=400.5 flags=0x20000 win=undefined winNum=0 ctxt=null evNum=0 click=2 buttonNumber=0 pressure=0.5" equals:[anEvent description]];
|
||||
|
||||
anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask | CPCommandKeyMask timestamp:12345.6 windowNumber:10 context:nil characters:"X" charactersIgnoringModifiers:"x" isARepeat:NO keyCode:10];
|
||||
anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask | CPCommandKeyMask timestamp:12345.6 windowNumber:10 context:nil characters:"X" charactersIgnoringModifiers:"x" isARepeat:NO keyCode:10 isActionKey:NO];
|
||||
|
||||
[self assert:@"CPEvent: type=10 loc={0, 0} time=12345.6 flags=0x120000 win=null winNum=10 ctxt=null chars=\"X\" unmodchars=\"x\" repeat=0 keyCode=10" equals:[anEvent description]];
|
||||
|
||||
@@ -44,13 +44,13 @@
|
||||
{
|
||||
[self assert:0 equals:[CPEvent modifierFlags] message:@"no modifier flags active in a newly started app"];
|
||||
|
||||
var anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0];
|
||||
var anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO];
|
||||
[CPApp sendEvent:anEvent];
|
||||
|
||||
[self assert:CPShiftKeyMask equals:[CPEvent modifierFlags] message:@"shift key pressed"];
|
||||
|
||||
// When the key up event is sent the modifier flags are cleared.
|
||||
anEvent = [CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0];
|
||||
anEvent = [CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO];
|
||||
[CPApp sendEvent:anEvent];
|
||||
|
||||
[self assert:0 equals:[CPEvent modifierFlags] message:@"shift key released"];
|
||||
|
||||
@@ -149,24 +149,24 @@
|
||||
// Don't match anything.
|
||||
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
|
||||
timestamp:0 windowNumber:0 context:nil
|
||||
characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0]];
|
||||
characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0 isActionKey:NO]];
|
||||
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled];
|
||||
|
||||
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0
|
||||
timestamp:0 windowNumber:0 context:nil
|
||||
characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0]];
|
||||
characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0 isActionKey:NO]];
|
||||
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled];
|
||||
|
||||
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
|
||||
timestamp:0 windowNumber:0 context:nil
|
||||
characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0]];
|
||||
characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0 isActionKey:NO]];
|
||||
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || undoWasCalled];
|
||||
[self assertTrue:openDocumentWasCalled message:"expect openDocumentWasCalled"];
|
||||
|
||||
openDocumentWasCalled = NO;
|
||||
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
|
||||
timestamp:0 windowNumber:0 context:nil
|
||||
characters:CPUndoKeyEquivalent charactersIgnoringModifiers:CPUndoKeyEquivalent isARepeat:NO keyCode:0]];
|
||||
characters:CPUndoKeyEquivalent charactersIgnoringModifiers:CPUndoKeyEquivalent isARepeat:NO keyCode:0 isActionKey:NO]];
|
||||
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled];
|
||||
[self assertTrue:undoWasCalled];
|
||||
}
|
||||
@@ -177,7 +177,7 @@
|
||||
|
||||
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0
|
||||
timestamp:0 windowNumber:0 context:nil
|
||||
characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]];
|
||||
characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]];
|
||||
[self assertFalse:escapeWasCalled || openDocumentWasCalled || undoWasCalled];
|
||||
[self assertTrue:escapeNoModifierWasCalled];
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
|
||||
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
|
||||
timestamp:0 windowNumber:0 context:nil
|
||||
characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]];
|
||||
characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]];
|
||||
[self assertFalse:escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled];
|
||||
[self assertTrue:escapeWasCalled];
|
||||
}
|
||||
@@ -196,7 +196,7 @@
|
||||
|
||||
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask
|
||||
timestamp:0 windowNumber:0 context:nil
|
||||
characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0]];
|
||||
characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0 isActionKey:NO]];
|
||||
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || saveDocumentAsWasCalled || undoWasCalled];
|
||||
[self assertTrue:saveDocumentWasCalled message:"saveDocumentWasCalled"];
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
|
||||
[menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask | CPShiftKeyMask
|
||||
timestamp:0 windowNumber:0 context:nil
|
||||
characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0]];
|
||||
characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0 isActionKey:NO]];
|
||||
[self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || saveDocumentWasCalled || undoWasCalled];
|
||||
[self assertTrue:saveDocumentAsWasCalled message:"saveDocumentAsWasCalled"];
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
var keyEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0
|
||||
timestamp:0 windowNumber:0 context:nil
|
||||
characters:character charactersIgnoringModifiers:character isARepeat:NO keyCode:keyCode];
|
||||
characters:character charactersIgnoringModifiers:character isARepeat:NO keyCode:keyCode isActionKey:YES];
|
||||
[responder interpretKeyEvents:[keyEvent]];
|
||||
[self assert:[selector] equals:responder.doCommandCalls];
|
||||
}
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
var keyEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask
|
||||
timestamp:0 windowNumber:0 context:nil
|
||||
characters:CPLeftArrowFunctionKey charactersIgnoringModifiers:CPLeftArrowFunctionKey isARepeat:NO keyCode:CPKeyCodes.LEFT];
|
||||
characters:CPLeftArrowFunctionKey charactersIgnoringModifiers:CPLeftArrowFunctionKey isARepeat:NO keyCode:CPKeyCodes.LEFT isActionKey:YES];
|
||||
[responder interpretKeyEvents:[keyEvent]];
|
||||
[self assert:[@selector(moveLeftAndModifySelection:)] equals:responder.doCommandCalls];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* CPTreeControllerTest.j
|
||||
*
|
||||
* Test suite for CPTreeController
|
||||
*/
|
||||
|
||||
@import <Foundation/CPArray.j>
|
||||
@import <Foundation/CPIndexPath.j>
|
||||
@import <AppKit/CPTreeController.j>
|
||||
@import <AppKit/CPTreeNode.j>
|
||||
@import <AppKit/CPTextField.j>
|
||||
|
||||
@class OrgNode
|
||||
|
||||
@implementation CPTreeControllerTest : OJTestCase
|
||||
{
|
||||
CPTreeController _treeController @accessors(property=treeController);
|
||||
CPArray _contentArray @accessors(property=contentArray);
|
||||
|
||||
CPArray observations;
|
||||
int aCount @accessors;
|
||||
}
|
||||
|
||||
- (CPArray)makeTestTree
|
||||
{
|
||||
var engineering = [OrgNode nodeWithName:@"Engineering"],
|
||||
marketing = [OrgNode nodeWithName:@"Marketing"];
|
||||
|
||||
var webTeam = [OrgNode nodeWithName:@"Web Team"],
|
||||
backendTeam = [OrgNode nodeWithName:@"Backend Team"];
|
||||
|
||||
[engineering setChildren:[CPMutableArray arrayWithObjects:webTeam, backendTeam]];
|
||||
|
||||
var dev1 = [OrgNode nodeWithName:@"Francisco"],
|
||||
dev2 = [OrgNode nodeWithName:@"Ross"];
|
||||
|
||||
[webTeam setChildren:[CPMutableArray arrayWithObjects:dev1, dev2]];
|
||||
|
||||
return [CPMutableArray arrayWithObjects:engineering, marketing];
|
||||
}
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
[[CPApplication alloc] init];
|
||||
|
||||
_contentArray = [self makeTestTree];
|
||||
_treeController = [[CPTreeController alloc] init];
|
||||
[_treeController setChildrenKeyPath:@"children"];
|
||||
[_treeController setContent:[_contentArray copy]];
|
||||
}
|
||||
|
||||
- (void)testInitWithContent
|
||||
{
|
||||
[self assert:[_contentArray count] equals:[[_treeController contentArray] count]];
|
||||
[self assert:[CPTreeNode class] equals:[[[self treeController] arrangedObjects] class] message:@"arranged objects should be a proxy CPTreeNode root"];
|
||||
}
|
||||
|
||||
- (void)testInitWithoutContent
|
||||
{
|
||||
var emptyController = [[CPTreeController alloc] init];
|
||||
[self assert:[CPArray array] equals:[emptyController contentArray]];
|
||||
[self assert:0 equals:[[[emptyController arrangedObjects] childNodes] count]];
|
||||
}
|
||||
|
||||
- (void)testSetContent
|
||||
{
|
||||
var newTree = [CPMutableArray arrayWithObject:[OrgNode nodeWithName:@"Solo Department"]];
|
||||
[[self treeController] setContent:newTree];
|
||||
|
||||
[self assert:newTree equals:[[self treeController] contentArray]];
|
||||
[self assert:1 equals:[[[[self treeController] arrangedObjects] childNodes] count]];
|
||||
}
|
||||
|
||||
- (void)testSelectionPaths
|
||||
{
|
||||
var controller = [self treeController];
|
||||
|
||||
var path = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
|
||||
[controller setSelectionIndexPath:path];
|
||||
|
||||
var selectedPath = [controller selectionIndexPath];
|
||||
[self assert:path equals:selectedPath];
|
||||
|
||||
var selectedNodes = [controller selectedNodes];
|
||||
[self assert:1 equals:[selectedNodes count]];
|
||||
[self assert:@"Web Team" equals:[[[selectedNodes objectAtIndex:0] representedObject] name]];
|
||||
}
|
||||
|
||||
- (void)testAddChild
|
||||
{
|
||||
var controller = [self treeController];
|
||||
[controller setObjectClass:[OrgNode class]];
|
||||
|
||||
var parentPath = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:1];
|
||||
[controller setSelectionIndexPath:parentPath];
|
||||
|
||||
var newDev = [OrgNode nodeWithName:@"Tom"];
|
||||
var insertPath = [parentPath indexPathByAddingIndex:0];
|
||||
[controller insertObject:newDev atArrangedObjectIndexPath:insertPath];
|
||||
|
||||
var engineering = [[controller contentArray] objectAtIndex:0],
|
||||
backendTeam = [[engineering children] objectAtIndex:1];
|
||||
[self assert:1 equals:[[backendTeam children] count] message:@"Child should be added to the model object's children array"];
|
||||
[self assert:@"Tom" equals:[[[backendTeam children] objectAtIndex:0] name]];
|
||||
}
|
||||
|
||||
- (void)testInsertObjectAtArrangedObjectIndexPath
|
||||
{
|
||||
var controller = [self treeController];
|
||||
|
||||
var path = [CPIndexPath indexPathWithIndex:1];
|
||||
var hrDept = [OrgNode nodeWithName:@"Human Resources"];
|
||||
|
||||
[controller insertObject:hrDept atArrangedObjectIndexPath:path];
|
||||
[self assert:3 equals:[[controller contentArray] count]];
|
||||
[self assert:hrDept equals:[[controller contentArray] objectAtIndex:1]];
|
||||
|
||||
var nestedPath = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
|
||||
var subDept = [OrgNode nodeWithName:@"Sub Dept"];
|
||||
|
||||
[controller insertObject:subDept atArrangedObjectIndexPath:nestedPath];
|
||||
var engChildren = [[[controller contentArray] objectAtIndex:0] children];
|
||||
[self assert:subDept equals:[engChildren objectAtIndex:0] message:@"Object should be inserted at the correct nested index path"];
|
||||
}
|
||||
|
||||
- (void)testRemoveObjectAtArrangedObjectIndexPath
|
||||
{
|
||||
var controller = [self treeController];
|
||||
|
||||
var path = [[[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0] indexPathByAddingIndex:0];
|
||||
|
||||
[controller removeObjectAtArrangedObjectIndexPath:path];
|
||||
|
||||
var engineering = [[controller contentArray] objectAtIndex:0],
|
||||
webTeam = [[engineering children] objectAtIndex:0];
|
||||
|
||||
[self assert:1 equals:[[webTeam children] count] message:@"Francisco should be removed, leaving only Ross"];
|
||||
[self assert:@"Ross" equals:[[[webTeam children] objectAtIndex:0] name]];
|
||||
}
|
||||
|
||||
- (void)testRemoveObjectsAtArrangedObjectIndexPaths
|
||||
{
|
||||
var controller = [self treeController];
|
||||
|
||||
var paths = [CPArray arrayWithObjects:[CPIndexPath indexPathWithIndex:0], [CPIndexPath indexPathWithIndex:1]];
|
||||
|
||||
[controller removeObjectsAtArrangedObjectIndexPaths:paths];
|
||||
|
||||
[self assert:0 equals:[[controller contentArray] count] message:@"All root nodes should be removed"];
|
||||
}
|
||||
|
||||
- (void)testSelectingEmptyIndexPathsExplicitlyWithAvoidsEmptySelection
|
||||
{
|
||||
var controller = [self treeController];
|
||||
|
||||
[controller setAvoidsEmptySelection:YES];
|
||||
|
||||
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
|
||||
[controller setSelectionIndexPaths:[CPArray array]];
|
||||
|
||||
[self assertTrue:([[controller selectionIndexPaths] count] == 0) message:@"Selection should be empty when unselecting explicitly, even with avoidsEmptySelection"];
|
||||
}
|
||||
|
||||
- (void)testAvoidsEmptySelectionWhenRemoving
|
||||
{
|
||||
var controller = [self treeController];
|
||||
[controller setAvoidsEmptySelection:YES];
|
||||
|
||||
var path = [CPIndexPath indexPathWithIndex:0];
|
||||
[controller setSelectionIndexPath:path];
|
||||
|
||||
// Remove "Engineering"
|
||||
[controller removeObjectAtArrangedObjectIndexPath:path];
|
||||
|
||||
[self assertTrue:([[controller selectionIndexPaths] count] == 1) message:@"Selection should fallback to the first item when avoidsEmptySelection is YES"];
|
||||
// "Marketing" is now at index 0
|
||||
[self assert:[CPIndexPath indexPathWithIndex:0] equals:[controller selectionIndexPath]];
|
||||
|
||||
// Test behavior when AvoidsEmptySelection is NO[controller insertObject:[OrgNode nodeWithName:@"New Dept"] atArrangedObjectIndexPath:[CPIndexPath indexPathWithIndex:1]];
|
||||
[controller setAvoidsEmptySelection:NO];
|
||||
|
||||
// Reselect "Marketing" at index 0
|
||||
[controller setSelectionIndexPath:[CPIndexPath indexPathWithIndex:0]];
|
||||
|
||||
// Remove "Marketing"
|
||||
[controller removeObjectAtArrangedObjectIndexPath:[CPIndexPath indexPathWithIndex:0]];
|
||||
|
||||
[self assertTrue:([[controller selectionIndexPaths] count] == 0) message:@"Selection should be allowed to be empty when avoidsEmptySelection is NO"];
|
||||
}
|
||||
|
||||
- (void)testChildrenKeyPathOverride
|
||||
{
|
||||
var controller = [[CPTreeController alloc] init];
|
||||
[controller setChildrenKeyPath:@"subItems"];
|
||||
|
||||
var data = [OrgNode nodeWithName:@"Root"];
|
||||
|
||||
[data setValue:[CPMutableArray arrayWithObject:[OrgNode nodeWithName:@"Sub"]] forKey:@"subItems"];
|
||||
[controller setContent:[CPMutableArray arrayWithObject:data]];
|
||||
|
||||
var path = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];
|
||||
var newItem = [OrgNode nodeWithName:@"New Sub"];
|
||||
[controller insertObject:newItem atArrangedObjectIndexPath:path];
|
||||
|
||||
var subs = [data valueForKey:@"subItems"];
|
||||
[self assert:2 equals:[subs count]];
|
||||
[self assert:newItem equals:[subs objectAtIndex:0] message:@"Object should be inserted using the custom childrenKeyPath"];
|
||||
}
|
||||
|
||||
- (void)testContentBinding
|
||||
{
|
||||
var controller = [[CPTreeController alloc] init];
|
||||
|
||||
[controller bind:@"contentArray" toObject:self withKeyPath:@"contentArray" options:nil];
|
||||
[self assert:[self contentArray] equals:[controller contentArray]];
|
||||
[self assert:2 equals:[[[controller arrangedObjects] childNodes] count]];
|
||||
}
|
||||
|
||||
- (void)testSelectedObjects
|
||||
{
|
||||
var controller = [self treeController];
|
||||
|
||||
var path = [CPIndexPath indexPathWithIndex:1];
|
||||
[controller setSelectionIndexPath:path];
|
||||
|
||||
var selectedObjects = [controller selectedObjects];
|
||||
|
||||
[self assert:1 equals:[selectedObjects count]];
|
||||
[self assert:@"Marketing" equals:[[selectedObjects objectAtIndex:0] name]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
* Dummy Model Class for Testing
|
||||
*/
|
||||
@implementation OrgNode : CPObject
|
||||
{
|
||||
CPString _name @accessors(property=name);
|
||||
CPMutableArray _children @accessors(property=children);
|
||||
CPMutableArray _subItems @accessors(property=subItems);
|
||||
}
|
||||
|
||||
+ (id)nodeWithName:(CPString)aName
|
||||
{
|
||||
return [[self alloc] initWithName:aName];
|
||||
}
|
||||
|
||||
- (id)initWithName:(CPString)aName
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_name = aName;
|
||||
_children = [CPMutableArray array];
|
||||
_subItems = [CPMutableArray array];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CPString)description
|
||||
{
|
||||
return [CPString stringWithFormat:@"<OrgNode %@>", [self name]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
indexPath = [CPIndexPath indexPathWithIndex:1];
|
||||
|
||||
[self assert:undefined equals:[treeNode descendantNodeAtIndexPath:indexPath]];
|
||||
[self assert:nil equals:[treeNode descendantNodeAtIndexPath:indexPath]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
@import <Foundation/Foundation.j>
|
||||
@import <AppKit/AppKit.j>
|
||||
@import <AppKit/CAKeyframeAnimation.j>
|
||||
@import <AppKit/CAAnimationGroup.j>
|
||||
|
||||
#define UIAssert(a) [self markTest:_cmd didPass:a];
|
||||
|
||||
@@ -403,6 +404,231 @@
|
||||
[CPAnimationContext endGrouping];
|
||||
}
|
||||
|
||||
- (void)testGroupAnimation:(id)sender
|
||||
{
|
||||
// Reset state
|
||||
[_testView setFrame:_initialTestViewFrame];
|
||||
[_testView setAlphaValue:1.0];
|
||||
[_pathView setPath:nil]; // Clear the path view as we aren't using it here
|
||||
|
||||
// Ensure the view is layer-backed.
|
||||
// Without this, [_testView layer] returns nil.
|
||||
[_testView setWantsLayer:YES];
|
||||
|
||||
var layer = [_testView layer];
|
||||
|
||||
// 1. Define the start and end positions
|
||||
// CALayer 'position' corresponds to the center of the view (anchorPoint 0.5,0.5)
|
||||
var startPos = [layer position];
|
||||
|
||||
// Safety check in case layer creation failed (though setWantsLayer:YES should ensure it)
|
||||
if (!startPos) startPos = CGPointMake(0,0);
|
||||
|
||||
var endPos = CGPointMake(startPos.x + 150, startPos.y + 50);
|
||||
|
||||
// 2. Create a Position Animation
|
||||
var moveAnim = [CABasicAnimation animationWithKeyPath:@"position"];
|
||||
[moveAnim setFromValue:startPos];
|
||||
[moveAnim setToValue:endPos];
|
||||
[moveAnim setDuration:1.0];
|
||||
|
||||
// 3. Create an Opacity Animation
|
||||
var fadeAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
|
||||
[fadeAnim setFromValue:1.0];
|
||||
[fadeAnim setToValue:0.25];
|
||||
[fadeAnim setDuration:1.0];
|
||||
|
||||
// 4. Group them
|
||||
// This tests the recursive logic in CAAnimationGroup and the timer logic in CALayer
|
||||
var group = [CAAnimationGroup group];
|
||||
[group setAnimations:[moveAnim, fadeAnim]];
|
||||
[group setDuration:1.0];
|
||||
|
||||
// 5. Run the animation on the layer
|
||||
[layer addAnimation:group forKey:@"groupTest"];
|
||||
|
||||
// 6. Verify results after the animation completes (1.0s duration + 0.1s buffer)
|
||||
[self performSelector:@selector(_verifyGroupAnimation:) withObject:endPos afterDelay:1.1];
|
||||
}
|
||||
|
||||
- (void)_verifyGroupAnimation:(CGPoint)expectedPos
|
||||
{
|
||||
var layer = [_testView layer],
|
||||
currentPos = [layer position],
|
||||
currentOpacity = [layer opacity];
|
||||
|
||||
// Allow for small floating point differences
|
||||
var posPassed = (Math.abs(currentPos.x - expectedPos.x) < 1.0 && Math.abs(currentPos.y - expectedPos.y) < 1.0);
|
||||
var opacityPassed = (Math.abs(currentOpacity - 0.25) < 0.05);
|
||||
|
||||
[self markTest:@selector(testGroupAnimation:) didPass:(posPassed && opacityPassed)];
|
||||
|
||||
// Reset for next test
|
||||
[self performSelector:@selector(cleanupAfterAnimation) withObject:nil afterDelay:0.5];
|
||||
}
|
||||
|
||||
- (void)testManualRotation:(id)sender
|
||||
{
|
||||
// 1. Cleanup previous test view
|
||||
if (_testView)
|
||||
[_testView removeFromSuperview];
|
||||
|
||||
// 2. Setup the RotatableView
|
||||
// View is 100x100, but the blue box drawn inside is 70x70 to allow room to spin.
|
||||
var frame = CGRectMake(390, 450, 100, 100);
|
||||
_testView = [[RotatableView alloc] initWithFrame:frame];
|
||||
[[theWindow contentView] addSubview:_testView];
|
||||
|
||||
// Ensure layer-backed so we have a layer to animate
|
||||
[_testView setWantsLayer:YES];
|
||||
|
||||
var layer = [_testView layer];
|
||||
|
||||
// 3. Define the Animation
|
||||
// Animation is performed on _testView
|
||||
[layer setDelegate:_testView];
|
||||
var rotationAnim = [CABasicAnimation animationWithKeyPath:@"angle"];
|
||||
|
||||
// Rotate 360 degrees (2 * PI)
|
||||
[rotationAnim setFromValue:0.0];
|
||||
[rotationAnim setToValue:2 * PI];
|
||||
[rotationAnim setDuration:2.0];
|
||||
|
||||
// Use an easing function for smooth start/stop
|
||||
[rotationAnim setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
|
||||
|
||||
// 4. Add to Layer
|
||||
[layer addAnimation:rotationAnim forKey:@"rotateTest"];
|
||||
|
||||
// 5. Verify results after animation
|
||||
[self performSelector:@selector(_verifyRotation:) withObject:nil afterDelay:2.1];
|
||||
}
|
||||
|
||||
- (void)_verifyRotation:(id)sender
|
||||
{
|
||||
var layer = [_testView layer];
|
||||
var endAngle = [layer angle];
|
||||
|
||||
// Check if we reached approx 2*PI (6.28)
|
||||
var passed = (Math.abs(endAngle - (2 * PI)) < 0.1);
|
||||
|
||||
[self markTest:@selector(testManualRotation:) didPass:passed];
|
||||
|
||||
// Reset view
|
||||
[self performSelector:@selector(cleanupAfterAnimation) withObject:nil afterDelay:0.5];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
A custom view that draws a box with a line in it.
|
||||
We draw the box smaller than the view bounds to prevent clipping during rotation.
|
||||
*/
|
||||
@implementation RotatableView : CPView
|
||||
{
|
||||
float _angle;
|
||||
}
|
||||
|
||||
- (void)initWithFrame:(CGRect)aFrame
|
||||
{
|
||||
self = [super initWithFrame:aFrame];
|
||||
_angle = 0;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setAngle:(float)anAngle
|
||||
{
|
||||
_angle = anAngle;
|
||||
[self display];
|
||||
}
|
||||
|
||||
- (float)angle
|
||||
{
|
||||
return _angle;
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)aRect
|
||||
{
|
||||
var context = [[CPGraphicsContext currentContext] graphicsPort],
|
||||
bounds = [self bounds],
|
||||
cx = CGRectGetWidth(bounds) / 2.0,
|
||||
cy = CGRectGetHeight(bounds) / 2.0;
|
||||
|
||||
// 1. Clear Context
|
||||
CGContextClearRect(context, bounds);
|
||||
|
||||
// 2. Precompute Trig
|
||||
var cosA = Math.cos(_angle),
|
||||
sinA = Math.sin(_angle);
|
||||
|
||||
/*
|
||||
Helper closure to transform a local point (x,y) relative to center
|
||||
into global view coordinates.
|
||||
*/
|
||||
var getPoint = function(localX, localY)
|
||||
{
|
||||
// Rotation Matrix:
|
||||
// x' = x*cos - y*sin
|
||||
// y' = x*sin + y*cos
|
||||
var rotX = localX * cosA - localY * sinA;
|
||||
var rotY = localX * sinA + localY * cosA;
|
||||
|
||||
// Translate back to view center
|
||||
return CGPointMake(cx + rotX, cy + rotY);
|
||||
};
|
||||
|
||||
// --- DRAW BLUE SQUARE (70x70) ---
|
||||
var s = 35.0; // half size
|
||||
|
||||
// Calculate the 4 corners manually
|
||||
var p1 = getPoint(-s, -s); // Top-Left
|
||||
var p2 = getPoint( s, -s); // Top-Right
|
||||
var p3 = getPoint( s, s); // Bottom-Right
|
||||
var p4 = getPoint(-s, s); // Bottom-Left
|
||||
|
||||
CGContextBeginPath(context);
|
||||
CGContextMoveToPoint(context, p1.x, p1.y);
|
||||
CGContextAddLineToPoint(context, p2.x, p2.y);
|
||||
CGContextAddLineToPoint(context, p3.x, p3.y);
|
||||
CGContextAddLineToPoint(context, p4.x, p4.y);
|
||||
CGContextClosePath(context);
|
||||
|
||||
[[CPColor greenColor] setFill];
|
||||
CGContextFillPath(context);
|
||||
|
||||
// --- DRAW RED MARKER (Top-Left Corner) ---
|
||||
// A 20x20 square in the top-left of the blue box
|
||||
// Local coords relative to center: x from -35 to -15, y from -35 to -15
|
||||
var r1 = getPoint(-35, -35);
|
||||
var r2 = getPoint(-15, -35);
|
||||
var r3 = getPoint(-15, -15);
|
||||
var r4 = getPoint(-35, -15);
|
||||
|
||||
CGContextBeginPath(context);
|
||||
CGContextMoveToPoint(context, r1.x, r1.y);
|
||||
CGContextAddLineToPoint(context, r2.x, r2.y);
|
||||
CGContextAddLineToPoint(context, r3.x, r3.y);
|
||||
CGContextAddLineToPoint(context, r4.x, r4.y);
|
||||
CGContextClosePath(context);
|
||||
|
||||
[[CPColor redColor] setFill];
|
||||
CGContextFillPath(context);
|
||||
|
||||
// --- DRAW WHITE POINTER LINE ---
|
||||
// Line from Center (0,0) to Right Edge (35, 0)
|
||||
var lineStart = getPoint(0, 0);
|
||||
var lineEnd = getPoint(35, 0);
|
||||
|
||||
CGContextBeginPath(context);
|
||||
CGContextMoveToPoint(context, lineStart.x, lineStart.y);
|
||||
CGContextAddLineToPoint(context, lineEnd.x, lineEnd.y);
|
||||
|
||||
[[CPColor whiteColor] setStroke];
|
||||
CGContextSetLineWidth(context, 3.0);
|
||||
CGContextStrokePath(context);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
var unCamelCase = function(aString)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* AppController.j
|
||||
* CPStackViewTest
|
||||
*
|
||||
* Created by Daniel Boehringer.
|
||||
* Copyright 2025, Cappuccino Project.
|
||||
*/
|
||||
|
||||
@import <Foundation/CPObject.j>
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
// 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
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Main cib file base name</key>
|
||||
<string>MainMenu.cib</string>
|
||||
<key>CPBundleName</key>
|
||||
<string>CPStackViewTest</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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("----------------------------");
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,809 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
|
||||
<data>
|
||||
<int key="IBDocument.SystemTarget">1050</int>
|
||||
<string key="IBDocument.SystemVersion">11E53</string>
|
||||
<string key="IBDocument.InterfaceBuilderVersion">2182</string>
|
||||
<string key="IBDocument.AppKitVersion">1138.47</string>
|
||||
<string key="IBDocument.HIToolboxVersion">569.00</string>
|
||||
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
|
||||
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string key="NS.object.0">2182</string>
|
||||
</object>
|
||||
<object class="NSArray" key="IBDocument.IntegratedClassDependencies">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>NSTextField</string>
|
||||
<string>NSView</string>
|
||||
<string>NSWindowTemplate</string>
|
||||
<string>NSSplitView</string>
|
||||
<string>NSTextFieldCell</string>
|
||||
<string>NSSliderCell</string>
|
||||
<string>NSCustomView</string>
|
||||
<string>NSSlider</string>
|
||||
<string>NSButtonCell</string>
|
||||
<string>NSButton</string>
|
||||
<string>NSCustomObject</string>
|
||||
</object>
|
||||
<object class="NSArray" key="IBDocument.PluginDependencies">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="IBDocument.Metadata">
|
||||
<string key="NS.key.0">PluginDependencyRecalculationVersion</string>
|
||||
<integer value="1" key="NS.object.0"/>
|
||||
</object>
|
||||
<object class="NSMutableArray" key="IBDocument.RootObjects" id="1048">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSCustomObject" id="1021">
|
||||
<string key="NSClassName">NSApplication</string>
|
||||
</object>
|
||||
<object class="NSCustomObject" id="1014">
|
||||
<string key="NSClassName">FirstResponder</string>
|
||||
</object>
|
||||
<object class="NSCustomObject" id="1050">
|
||||
<string key="NSClassName">NSApplication</string>
|
||||
</object>
|
||||
<object class="NSWindowTemplate" id="972006081">
|
||||
<int key="NSWindowStyleMask">7</int>
|
||||
<int key="NSWindowBacking">2</int>
|
||||
<string key="NSWindowRect">{{335, 390}, {480, 360}}</string>
|
||||
<int key="NSWTFlags">1946157056</int>
|
||||
<string key="NSWindowTitle">Window</string>
|
||||
<string key="NSWindowClass">NSWindow</string>
|
||||
<nil key="NSViewClass"/>
|
||||
<nil key="NSUserInterfaceItemIdentifier"/>
|
||||
<object class="NSView" key="NSWindowView" id="439893737">
|
||||
<reference key="NSNextResponder"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSSplitView" id="980028635">
|
||||
<reference key="NSNextResponder" ref="439893737"/>
|
||||
<int key="NSvFlags">274</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSTextField" id="655520722">
|
||||
<reference key="NSNextResponder" ref="980028635"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<string key="NSFrameSize">{480, 34}</string>
|
||||
<reference key="NSSuperview" ref="980028635"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="871265355"/>
|
||||
<string key="NSReuseIdentifierKey">_NS:3944</string>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSTextFieldCell" key="NSCell" id="295214252">
|
||||
<int key="NSCellFlags">68288064</int>
|
||||
<int key="NSCellFlags2">272630784</int>
|
||||
<string key="NSContents">Label</string>
|
||||
<object class="NSFont" key="NSSupport" id="611476865">
|
||||
<string key="NSName">LucidaGrande</string>
|
||||
<double key="NSSize">13</double>
|
||||
<int key="NSfFlags">1044</int>
|
||||
</object>
|
||||
<string key="NSCellIdentifier">_NS:3944</string>
|
||||
<reference key="NSControlView" ref="655520722"/>
|
||||
<object class="NSColor" key="NSBackgroundColor">
|
||||
<int key="NSColorSpace">6</int>
|
||||
<string key="NSCatalogName">System</string>
|
||||
<string key="NSColorName">controlColor</string>
|
||||
<object class="NSColor" key="NSColor">
|
||||
<int key="NSColorSpace">3</int>
|
||||
<bytes key="NSWhite">MC42NjY2NjY2NjY3AA</bytes>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSColor" key="NSTextColor">
|
||||
<int key="NSColorSpace">6</int>
|
||||
<string key="NSCatalogName">System</string>
|
||||
<string key="NSColorName">controlTextColor</string>
|
||||
<object class="NSColor" key="NSColor" id="6106584">
|
||||
<int key="NSColorSpace">3</int>
|
||||
<bytes key="NSWhite">MAA</bytes>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSCustomView" id="871265355">
|
||||
<reference key="NSNextResponder" ref="980028635"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSButton" id="688798207">
|
||||
<reference key="NSNextResponder" ref="871265355"/>
|
||||
<int key="NSvFlags">268</int>
|
||||
<string key="NSFrame">{{14, 24}, {142, 32}}</string>
|
||||
<reference key="NSSuperview" ref="871265355"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="160623561"/>
|
||||
<string key="NSReuseIdentifierKey">_NS:9</string>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSButtonCell" key="NSCell" id="676112794">
|
||||
<int key="NSCellFlags">67239424</int>
|
||||
<int key="NSCellFlags2">134217728</int>
|
||||
<string key="NSContents">Delete Autosave</string>
|
||||
<reference key="NSSupport" ref="611476865"/>
|
||||
<string key="NSCellIdentifier">_NS:9</string>
|
||||
<reference key="NSControlView" ref="688798207"/>
|
||||
<int key="NSButtonFlags">-2038284033</int>
|
||||
<int key="NSButtonFlags2">129</int>
|
||||
<string key="NSAlternateContents"/>
|
||||
<string key="NSKeyEquivalent"/>
|
||||
<int key="NSPeriodicDelay">200</int>
|
||||
<int key="NSPeriodicInterval">25</int>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSTextField" id="160623561">
|
||||
<reference key="NSNextResponder" ref="871265355"/>
|
||||
<int key="NSvFlags">301</int>
|
||||
<string key="NSFrame">{{364, 30}, {96, 22}}</string>
|
||||
<reference key="NSSuperview" ref="871265355"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="877119126"/>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSTextFieldCell" key="NSCell" id="46991665">
|
||||
<int key="NSCellFlags">-1804468671</int>
|
||||
<int key="NSCellFlags2">272630784</int>
|
||||
<string key="NSContents"/>
|
||||
<reference key="NSSupport" ref="611476865"/>
|
||||
<reference key="NSControlView" ref="160623561"/>
|
||||
<bool key="NSDrawsBackground">YES</bool>
|
||||
<object class="NSColor" key="NSBackgroundColor">
|
||||
<int key="NSColorSpace">6</int>
|
||||
<string key="NSCatalogName">System</string>
|
||||
<string key="NSColorName">textBackgroundColor</string>
|
||||
<object class="NSColor" key="NSColor">
|
||||
<int key="NSColorSpace">3</int>
|
||||
<bytes key="NSWhite">MQA</bytes>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSColor" key="NSTextColor">
|
||||
<int key="NSColorSpace">6</int>
|
||||
<string key="NSCatalogName">System</string>
|
||||
<string key="NSColorName">textColor</string>
|
||||
<reference key="NSColor" ref="6106584"/>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{0, 44}, {480, 85}}</string>
|
||||
<reference key="NSSuperview" ref="980028635"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="688798207"/>
|
||||
<string key="NSReuseIdentifierKey">_NS:1113</string>
|
||||
<string key="NSClassName">NSView</string>
|
||||
</object>
|
||||
<object class="NSCustomView" id="877119126">
|
||||
<reference key="NSNextResponder" ref="980028635"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSSlider" id="974764906">
|
||||
<reference key="NSNextResponder" ref="877119126"/>
|
||||
<int key="NSvFlags">301</int>
|
||||
<string key="NSFrame">{{192, 27}, {96, 21}}</string>
|
||||
<reference key="NSSuperview" ref="877119126"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="105337172"/>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSSliderCell" key="NSCell" id="600801425">
|
||||
<int key="NSCellFlags">-2079981824</int>
|
||||
<int key="NSCellFlags2">0</int>
|
||||
<string key="NSContents"/>
|
||||
<object class="NSFont" key="NSSupport">
|
||||
<string key="NSName">Helvetica</string>
|
||||
<double key="NSSize">12</double>
|
||||
<int key="NSfFlags">16</int>
|
||||
</object>
|
||||
<reference key="NSControlView" ref="974764906"/>
|
||||
<double key="NSMaxValue">100</double>
|
||||
<double key="NSMinValue">0.0</double>
|
||||
<double key="NSValue">50</double>
|
||||
<double key="NSAltIncValue">0.0</double>
|
||||
<int key="NSNumberOfTickMarks">0</int>
|
||||
<int key="NSTickMarkPosition">1</int>
|
||||
<bool key="NSAllowsTickMarkValuesOnly">NO</bool>
|
||||
<bool key="NSVertical">NO</bool>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{0, 139}, {480, 79}}</string>
|
||||
<reference key="NSSuperview" ref="980028635"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="974764906"/>
|
||||
<string key="NSReuseIdentifierKey">_NS:1116</string>
|
||||
<string key="NSClassName">NSView</string>
|
||||
</object>
|
||||
<object class="NSSplitView" id="105337172">
|
||||
<reference key="NSNextResponder" ref="980028635"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSCustomView" id="600528701">
|
||||
<reference key="NSNextResponder" ref="105337172"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSSplitView" id="241459165">
|
||||
<reference key="NSNextResponder" ref="600528701"/>
|
||||
<int key="NSvFlags">274</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSCustomView" id="309483115">
|
||||
<reference key="NSNextResponder" ref="241459165"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<string key="NSFrameSize">{246, 51}</string>
|
||||
<reference key="NSSuperview" ref="241459165"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="858498729"/>
|
||||
<string key="NSReuseIdentifierKey">_NS:11</string>
|
||||
<string key="NSClassName">NSView</string>
|
||||
</object>
|
||||
<object class="NSCustomView" id="858498729">
|
||||
<reference key="NSNextResponder" ref="241459165"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSButton" id="290967118">
|
||||
<reference key="NSNextResponder" ref="858498729"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<string key="NSFrame">{{66, 18}, {114, 32}}</string>
|
||||
<reference key="NSSuperview" ref="858498729"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="1031367026"/>
|
||||
<string key="NSReuseIdentifierKey">_NS:687</string>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSButtonCell" key="NSCell" id="825192224">
|
||||
<int key="NSCellFlags">67239424</int>
|
||||
<int key="NSCellFlags2">134217728</int>
|
||||
<string key="NSContents">Bottom Left</string>
|
||||
<reference key="NSSupport" ref="611476865"/>
|
||||
<string key="NSCellIdentifier">_NS:687</string>
|
||||
<reference key="NSControlView" ref="290967118"/>
|
||||
<int key="NSButtonFlags">-2038284033</int>
|
||||
<int key="NSButtonFlags2">129</int>
|
||||
<string key="NSAlternateContents"/>
|
||||
<string key="NSKeyEquivalent"/>
|
||||
<int key="NSPeriodicDelay">200</int>
|
||||
<int key="NSPeriodicInterval">25</int>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{0, 60}, {246, 72}}</string>
|
||||
<reference key="NSSuperview" ref="241459165"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="290967118"/>
|
||||
<string key="NSReuseIdentifierKey">_NS:13</string>
|
||||
<string key="NSClassName">NSView</string>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrameSize">{246, 132}</string>
|
||||
<reference key="NSSuperview" ref="600528701"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="309483115"/>
|
||||
<string key="NSReuseIdentifierKey">_NS:9</string>
|
||||
<string key="NSAutosaveName">splitViewC</string>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrameSize">{246, 132}</string>
|
||||
<reference key="NSSuperview" ref="105337172"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="241459165"/>
|
||||
<string key="NSReuseIdentifierKey">_NS:1165</string>
|
||||
<string key="NSClassName">NSView</string>
|
||||
</object>
|
||||
<object class="NSCustomView" id="1031367026">
|
||||
<reference key="NSNextResponder" ref="105337172"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<object class="NSMutableArray" key="NSSubviews">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSButton" id="165016153">
|
||||
<reference key="NSNextResponder" ref="1031367026"/>
|
||||
<int key="NSvFlags">256</int>
|
||||
<string key="NSFrame">{{80, 48}, {73, 32}}</string>
|
||||
<reference key="NSSuperview" ref="1031367026"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView"/>
|
||||
<string key="NSReuseIdentifierKey">_NS:687</string>
|
||||
<bool key="NSEnabled">YES</bool>
|
||||
<object class="NSButtonCell" key="NSCell" id="1015466174">
|
||||
<int key="NSCellFlags">67239424</int>
|
||||
<int key="NSCellFlags2">134217728</int>
|
||||
<string key="NSContents">Right</string>
|
||||
<reference key="NSSupport" ref="611476865"/>
|
||||
<string key="NSCellIdentifier">_NS:687</string>
|
||||
<reference key="NSControlView" ref="165016153"/>
|
||||
<int key="NSButtonFlags">-2038284033</int>
|
||||
<int key="NSButtonFlags2">129</int>
|
||||
<string key="NSAlternateContents"/>
|
||||
<string key="NSKeyEquivalent"/>
|
||||
<int key="NSPeriodicDelay">200</int>
|
||||
<int key="NSPeriodicInterval">25</int>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{247, 0}, {233, 132}}</string>
|
||||
<reference key="NSSuperview" ref="105337172"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="165016153"/>
|
||||
<string key="NSReuseIdentifierKey">_NS:1167</string>
|
||||
<string key="NSClassName">NSView</string>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrame">{{0, 228}, {480, 132}}</string>
|
||||
<reference key="NSSuperview" ref="980028635"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="600528701"/>
|
||||
<string key="NSReuseIdentifierKey">_NS:1163</string>
|
||||
<bool key="NSIsVertical">YES</bool>
|
||||
<int key="NSDividerStyle">2</int>
|
||||
<string key="NSAutosaveName">splitViewB</string>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrameSize">{480, 360}</string>
|
||||
<reference key="NSSuperview" ref="439893737"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="655520722"/>
|
||||
<string key="NSReuseIdentifierKey">_NS:1111</string>
|
||||
<int key="NSDividerStyle">3</int>
|
||||
<string key="NSAutosaveName">splitViewA</string>
|
||||
</object>
|
||||
</object>
|
||||
<string key="NSFrameSize">{480, 360}</string>
|
||||
<reference key="NSSuperview"/>
|
||||
<reference key="NSWindow"/>
|
||||
<reference key="NSNextKeyView" ref="980028635"/>
|
||||
</object>
|
||||
<string key="NSScreenRect">{{0, 0}, {2560, 1418}}</string>
|
||||
<string key="NSMaxSize">{10000000000000, 10000000000000}</string>
|
||||
<bool key="NSWindowIsRestorable">YES</bool>
|
||||
</object>
|
||||
<object class="NSCustomObject" id="635946545">
|
||||
<string key="NSClassName">AppController</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="IBObjectContainer" key="IBDocument.Objects">
|
||||
<object class="NSMutableArray" key="connectionRecords">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">delegate</string>
|
||||
<reference key="source" ref="1021"/>
|
||||
<reference key="destination" ref="635946545"/>
|
||||
</object>
|
||||
<int key="connectionID">451</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">theWindow</string>
|
||||
<reference key="source" ref="635946545"/>
|
||||
<reference key="destination" ref="972006081"/>
|
||||
</object>
|
||||
<int key="connectionID">459</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">splitViewA</string>
|
||||
<reference key="source" ref="635946545"/>
|
||||
<reference key="destination" ref="980028635"/>
|
||||
</object>
|
||||
<int key="connectionID">479</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">splitViewB</string>
|
||||
<reference key="source" ref="635946545"/>
|
||||
<reference key="destination" ref="105337172"/>
|
||||
</object>
|
||||
<int key="connectionID">480</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">splitViewC</string>
|
||||
<reference key="source" ref="635946545"/>
|
||||
<reference key="destination" ref="241459165"/>
|
||||
</object>
|
||||
<int key="connectionID">481</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">deleteAutosave:</string>
|
||||
<reference key="source" ref="635946545"/>
|
||||
<reference key="destination" ref="688798207"/>
|
||||
</object>
|
||||
<int key="connectionID">482</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBActionConnection" key="connection">
|
||||
<string key="label">takeDoubleValueFrom:</string>
|
||||
<reference key="source" ref="160623561"/>
|
||||
<reference key="destination" ref="974764906"/>
|
||||
</object>
|
||||
<int key="connectionID">458</int>
|
||||
</object>
|
||||
<object class="IBConnectionRecord">
|
||||
<object class="IBOutletConnection" key="connection">
|
||||
<string key="label">delegate</string>
|
||||
<reference key="source" ref="980028635"/>
|
||||
<reference key="destination" ref="635946545"/>
|
||||
</object>
|
||||
<int key="connectionID">473</int>
|
||||
</object>
|
||||
</object>
|
||||
<object class="IBMutableOrderedSet" key="objectRecords">
|
||||
<object class="NSArray" key="orderedObjects">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">0</int>
|
||||
<object class="NSArray" key="object" id="0">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
</object>
|
||||
<reference key="children" ref="1048"/>
|
||||
<nil key="parent"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-2</int>
|
||||
<reference key="object" ref="1021"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">File's Owner</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-1</int>
|
||||
<reference key="object" ref="1014"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">First Responder</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">-3</int>
|
||||
<reference key="object" ref="1050"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
<string key="objectName">Application</string>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">371</int>
|
||||
<reference key="object" ref="972006081"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="439893737"/>
|
||||
</object>
|
||||
<reference key="parent" ref="0"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">372</int>
|
||||
<reference key="object" ref="439893737"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="980028635"/>
|
||||
</object>
|
||||
<reference key="parent" ref="972006081"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">450</int>
|
||||
<reference key="object" ref="635946545"/>
|
||||
<reference key="parent" ref="0"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">460</int>
|
||||
<reference key="object" ref="980028635"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="871265355"/>
|
||||
<reference ref="877119126"/>
|
||||
<reference ref="655520722"/>
|
||||
<reference ref="105337172"/>
|
||||
</object>
|
||||
<reference key="parent" ref="439893737"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">461</int>
|
||||
<reference key="object" ref="871265355"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="688798207"/>
|
||||
<reference ref="160623561"/>
|
||||
</object>
|
||||
<reference key="parent" ref="980028635"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">462</int>
|
||||
<reference key="object" ref="877119126"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="974764906"/>
|
||||
</object>
|
||||
<reference key="parent" ref="980028635"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">456</int>
|
||||
<reference key="object" ref="160623561"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="46991665"/>
|
||||
</object>
|
||||
<reference key="parent" ref="871265355"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">457</int>
|
||||
<reference key="object" ref="46991665"/>
|
||||
<reference key="parent" ref="160623561"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">452</int>
|
||||
<reference key="object" ref="974764906"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="600801425"/>
|
||||
</object>
|
||||
<reference key="parent" ref="877119126"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">453</int>
|
||||
<reference key="object" ref="600801425"/>
|
||||
<reference key="parent" ref="974764906"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">463</int>
|
||||
<reference key="object" ref="655520722"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="295214252"/>
|
||||
</object>
|
||||
<reference key="parent" ref="980028635"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">464</int>
|
||||
<reference key="object" ref="295214252"/>
|
||||
<reference key="parent" ref="655520722"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">465</int>
|
||||
<reference key="object" ref="105337172"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="600528701"/>
|
||||
<reference ref="1031367026"/>
|
||||
</object>
|
||||
<reference key="parent" ref="980028635"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">466</int>
|
||||
<reference key="object" ref="600528701"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="241459165"/>
|
||||
</object>
|
||||
<reference key="parent" ref="105337172"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">467</int>
|
||||
<reference key="object" ref="1031367026"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="165016153"/>
|
||||
</object>
|
||||
<reference key="parent" ref="105337172"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">470</int>
|
||||
<reference key="object" ref="165016153"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="1015466174"/>
|
||||
</object>
|
||||
<reference key="parent" ref="1031367026"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">471</int>
|
||||
<reference key="object" ref="1015466174"/>
|
||||
<reference key="parent" ref="165016153"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">474</int>
|
||||
<reference key="object" ref="241459165"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="309483115"/>
|
||||
<reference ref="858498729"/>
|
||||
</object>
|
||||
<reference key="parent" ref="600528701"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">475</int>
|
||||
<reference key="object" ref="309483115"/>
|
||||
<reference key="parent" ref="241459165"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">476</int>
|
||||
<reference key="object" ref="858498729"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="290967118"/>
|
||||
</object>
|
||||
<reference key="parent" ref="241459165"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">468</int>
|
||||
<reference key="object" ref="290967118"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="825192224"/>
|
||||
</object>
|
||||
<reference key="parent" ref="858498729"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">469</int>
|
||||
<reference key="object" ref="825192224"/>
|
||||
<reference key="parent" ref="290967118"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">477</int>
|
||||
<reference key="object" ref="688798207"/>
|
||||
<object class="NSMutableArray" key="children">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference ref="676112794"/>
|
||||
</object>
|
||||
<reference key="parent" ref="871265355"/>
|
||||
</object>
|
||||
<object class="IBObjectRecord">
|
||||
<int key="objectID">478</int>
|
||||
<reference key="object" ref="676112794"/>
|
||||
<reference key="parent" ref="688798207"/>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="flattenedProperties">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>-1.IBPluginDependency</string>
|
||||
<string>-2.IBPluginDependency</string>
|
||||
<string>-3.IBPluginDependency</string>
|
||||
<string>371.IBPluginDependency</string>
|
||||
<string>371.IBWindowTemplateEditedContentRect</string>
|
||||
<string>371.NSWindowTemplate.visibleAtLaunch</string>
|
||||
<string>372.IBPluginDependency</string>
|
||||
<string>450.IBPluginDependency</string>
|
||||
<string>452.IBPluginDependency</string>
|
||||
<string>453.IBPluginDependency</string>
|
||||
<string>456.IBPluginDependency</string>
|
||||
<string>457.IBPluginDependency</string>
|
||||
<string>460.IBPluginDependency</string>
|
||||
<string>461.IBPluginDependency</string>
|
||||
<string>462.IBPluginDependency</string>
|
||||
<string>463.IBPluginDependency</string>
|
||||
<string>464.IBPluginDependency</string>
|
||||
<string>465.IBPluginDependency</string>
|
||||
<string>466.IBPluginDependency</string>
|
||||
<string>467.IBPluginDependency</string>
|
||||
<string>468.IBPluginDependency</string>
|
||||
<string>469.IBPluginDependency</string>
|
||||
<string>470.IBPluginDependency</string>
|
||||
<string>471.IBPluginDependency</string>
|
||||
<string>474.IBPluginDependency</string>
|
||||
<string>475.IBPluginDependency</string>
|
||||
<string>476.IBPluginDependency</string>
|
||||
<string>477.IBPluginDependency</string>
|
||||
<string>478.IBPluginDependency</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>{{303, 221}, {480, 360}}</string>
|
||||
<integer value="1"/>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="unlocalizedProperties">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference key="dict.sortedKeys" ref="0"/>
|
||||
<reference key="dict.values" ref="0"/>
|
||||
</object>
|
||||
<nil key="activeLocalization"/>
|
||||
<object class="NSMutableDictionary" key="localizations">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<reference key="dict.sortedKeys" ref="0"/>
|
||||
<reference key="dict.values" ref="0"/>
|
||||
</object>
|
||||
<nil key="sourceID"/>
|
||||
<int key="maxID">482</int>
|
||||
</object>
|
||||
<object class="IBClassDescriber" key="IBDocument.Classes">
|
||||
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="IBPartialClassDescription">
|
||||
<string key="className">AppController</string>
|
||||
<string key="superclassName">NSObject</string>
|
||||
<object class="NSMutableDictionary" key="actions">
|
||||
<string key="NS.key.0">deleteAutosave:</string>
|
||||
<string key="NS.object.0">id</string>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="actionInfosByName">
|
||||
<string key="NS.key.0">deleteAutosave:</string>
|
||||
<object class="IBActionInfo" key="NS.object.0">
|
||||
<string key="name">deleteAutosave:</string>
|
||||
<string key="candidateClassName">id</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="outlets">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>splitViewA</string>
|
||||
<string>splitViewB</string>
|
||||
<string>splitViewC</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>NSSplitView</string>
|
||||
<string>NSSplitView</string>
|
||||
<string>NSSplitView</string>
|
||||
</object>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="NSArray" key="dict.sortedKeys">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<string>splitViewA</string>
|
||||
<string>splitViewB</string>
|
||||
<string>splitViewC</string>
|
||||
</object>
|
||||
<object class="NSArray" key="dict.values">
|
||||
<bool key="EncodedWithXMLCoder">YES</bool>
|
||||
<object class="IBToOneOutletInfo">
|
||||
<string key="name">splitViewA</string>
|
||||
<string key="candidateClassName">NSSplitView</string>
|
||||
</object>
|
||||
<object class="IBToOneOutletInfo">
|
||||
<string key="name">splitViewB</string>
|
||||
<string key="candidateClassName">NSSplitView</string>
|
||||
</object>
|
||||
<object class="IBToOneOutletInfo">
|
||||
<string key="name">splitViewC</string>
|
||||
<string key="candidateClassName">NSSplitView</string>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<object class="IBClassDescriptionSource" key="sourceIdentifier">
|
||||
<string key="majorKey">IBProjectSource</string>
|
||||
<string key="minorKey">./Classes/AppController.h</string>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
<int key="IBDocument.localizationMode">0</int>
|
||||
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
|
||||
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
|
||||
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
|
||||
<integer value="1050" key="NS.object.0"/>
|
||||
</object>
|
||||
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies">
|
||||
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3</string>
|
||||
<integer value="3000" key="NS.object.0"/>
|
||||
</object>
|
||||
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
|
||||
<int key="IBDocument.defaultPropertyAccessControl">3</int>
|
||||
</data>
|
||||
</archive>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,204 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!--
|
||||
index-debug.html
|
||||
__project.name__
|
||||
|
||||
Created by __user.name__ on __project.date__.
|
||||
Copyright __project.year__, __organization.name__ All rights reserved.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<!--[if lte IE 8]>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
|
||||
<![endif]-->
|
||||
<!--[if gte IE 9]>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
|
||||
<![endif]-->
|
||||
|
||||
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
|
||||
<link rel="apple-touch-icon" href="Resources/icon.png">
|
||||
<link rel="apple-touch-startup-image" href="Resources/default.png">
|
||||
|
||||
<title>__project.name__</title>
|
||||
|
||||
<!-- Custom javascript goes here -->
|
||||
<!-- End custom javascript -->
|
||||
|
||||
<script type="text/javascript">
|
||||
OBJJ_MAIN_FILE = "main.j";
|
||||
OBJJ_INCLUDE_PATHS = ["Frameworks/Debug", "Frameworks"];
|
||||
// The below will tell the compiler to generate debug symbols, type signatures and not inline objj_msgSend functions.
|
||||
// This will affect only Objective-J code that is compiled when loading the application. It will not affect precompiled
|
||||
// code like the Cappuccino frameworks.
|
||||
//
|
||||
// Debug symbols will give each Objective-J method a Javascript function name. Without a name it will be very hard to find
|
||||
// the methods in the debugger.
|
||||
// Type Signatures will give type information to the Objective-J runtime for instance variables and methods.
|
||||
// Inline objj_msgSend will give a speed increase but decorators will not work. Check below on debug options for
|
||||
// more information on decorators.
|
||||
//
|
||||
// Uncomment or comment on the line below to change the flags
|
||||
OBJJ_COMPILER_FLAGS = ["IncludeDebugSymbols", "IncludeTypeSignatures", "SourceMap", "InlineMsgSend"];
|
||||
|
||||
var progressBar = null;
|
||||
|
||||
OBJJ_PROGRESS_CALLBACK = function(percent, appSize, path)
|
||||
{
|
||||
percent = percent * 100;
|
||||
|
||||
if (!progressBar)
|
||||
progressBar = document.getElementById("progress-bar");
|
||||
|
||||
if (progressBar)
|
||||
progressBar.style.width = Math.min(percent, 100) + "%";
|
||||
}
|
||||
|
||||
var loadingHTML =
|
||||
'<div id="loading">' +
|
||||
' <div id="loading-text">Loading...</div>' +
|
||||
' <div id="progress-indicator">' +
|
||||
' <span id="progress-bar" style="width:0%"></span>' +
|
||||
' </div>' +
|
||||
'</div>';
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="Frameworks/Debug/Objective-J/Objective-J.js" charset="UTF-8"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
objj_msgSend_reset();
|
||||
|
||||
// DEBUG OPTIONS:
|
||||
// Decorators will only work when the code is compiled without the compiler option 'InlineMsgSend'. Check above.
|
||||
|
||||
// Uncomment to enable printing of backtraces on exceptions:
|
||||
//objj_msgSend_decorate(objj_backtrace_decorator);
|
||||
|
||||
// Uncomment to supress exceptions that take place inside a message
|
||||
//objj_msgSend_decorate(objj_supress_exceptions_decorator)
|
||||
|
||||
// Uncomment to enable runtime type checking:
|
||||
//objj_msgSend_decorate(objj_typecheck_decorator);
|
||||
|
||||
// Uncomment (along with both above) to print backtraces on type check errors:
|
||||
//objj_typecheck_prints_backtrace = true;
|
||||
|
||||
// Uncomment to disable the default logger (CPLogConsole if window.console exists, CPLogPopup otherwise):
|
||||
//CPLogUnregister(CPLogDefault);
|
||||
|
||||
// Uncomment to enable a specific logger:
|
||||
//CPLogRegister(CPLogConsole);
|
||||
//CPLogRegister(CPLogPopup);
|
||||
|
||||
// Tag view DOM elements with a "data-cappuccino-view" attribute that contains
|
||||
// the class name of the view that created them. Comment this or set to false to disable.
|
||||
appkit_tag_dom_elements = true;
|
||||
</script>
|
||||
|
||||
<style type="text/css">
|
||||
html, body, h1, p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* We need a body wrapper because Cappuccino is unhappy if we change the body element */
|
||||
#cappuccino-body {
|
||||
/* Position it absolutely so it will fill the height without content */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
/* Put it at the bottom of the stack so it doesn't interfere with UI */
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#cappuccino-body .container {
|
||||
display: table;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#cappuccino-body .content {
|
||||
display: table-cell;
|
||||
height: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: relative;
|
||||
top: 35%;
|
||||
}
|
||||
|
||||
#loading-text {
|
||||
height: 1.5em;
|
||||
color: #555;
|
||||
font: normal bold 36px/36px Arial, sans-serif;
|
||||
}
|
||||
|
||||
#progress-indicator {
|
||||
padding: 0px;
|
||||
height: 16px;
|
||||
border: 5px solid #555;
|
||||
border-radius: 18px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#progress-bar {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
display: block;
|
||||
height: 18px;
|
||||
|
||||
/* Compensate for moving the bar left 1px to overlap the indicator border */
|
||||
border-right: 1px solid #555;
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
#noscript {
|
||||
position: relative;
|
||||
top: 35%;
|
||||
padding: 1em 1.5em;
|
||||
border: 5px solid #555;
|
||||
border-radius: 16px;
|
||||
background-color: white;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
font: bold 24px Arial, sans-serif;
|
||||
}
|
||||
|
||||
#noscript a {
|
||||
color: #98c0ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="cappuccino-body">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<script type="text/javascript">
|
||||
document.write(loadingHTML);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<noscript style="position:absolute; top:0; left:0; width:100%; height:100%">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<div id="noscript">
|
||||
<p style="font-size:120%; margin-bottom:.75em">JavaScript is required for this application.</p>
|
||||
<p><a href="http://www.enable-javascript.com" target="_blank">Enable JavaScript</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,166 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!--
|
||||
index.html
|
||||
__project.name__
|
||||
|
||||
Created by __user.name__ on __project.date__.
|
||||
Copyright __project.year__, __organization.name__ All rights reserved.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<!--[if lte IE 8]>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
|
||||
<![endif]-->
|
||||
<!--[if gte IE 9]>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
|
||||
<![endif]-->
|
||||
|
||||
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
|
||||
<link rel="apple-touch-icon" href="Resources/icon.png">
|
||||
<link rel="apple-touch-startup-image" href="Resources/default.png">
|
||||
|
||||
<title>__project.name__</title>
|
||||
|
||||
<!-- Custom javascript goes here -->
|
||||
<!-- End custom javascript -->
|
||||
|
||||
<script type="text/javascript">
|
||||
OBJJ_MAIN_FILE = "main.j";
|
||||
// The below will tell the compiler to not generate debug symbols but will generate type signatures and inline objj_msgSend functions.
|
||||
// This will affect only Objective-J code that is compiled when loading the application. It will not affect precompiled
|
||||
// code like the Cappuccino frameworks.
|
||||
// Uncomment or comment on the line below to change the flags
|
||||
OBJJ_COMPILER_FLAGS = [/*"IncludeDebugSymbols"*/, "IncludeTypeSignatures"/*, "SourceMap"*/, "InlineMsgSend"];
|
||||
|
||||
var progressBar = null;
|
||||
|
||||
OBJJ_PROGRESS_CALLBACK = function(percent, appSize, path)
|
||||
{
|
||||
percent = percent * 100;
|
||||
|
||||
if (!progressBar)
|
||||
progressBar = document.getElementById("progress-bar");
|
||||
|
||||
if (progressBar)
|
||||
progressBar.style.width = Math.min(percent, 100) + "%";
|
||||
}
|
||||
|
||||
var loadingHTML =
|
||||
'<div id="loading">' +
|
||||
' <div id="loading-text">Loading...</div>' +
|
||||
' <div id="progress-indicator">' +
|
||||
' <span id="progress-bar" style="width:0%"></span>' +
|
||||
' </div>' +
|
||||
'</div>';
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="Frameworks/Objective-J/Objective-J.js" charset="UTF-8"></script>
|
||||
|
||||
<style type="text/css">
|
||||
html, body, h1, p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* We need a body wrapper because Cappuccino is unhappy if we change the body element */
|
||||
#cappuccino-body {
|
||||
/* Position it absolutely so it will fill the height without content */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
/* Put it at the bottom of the stack so it doesn't interfere with UI */
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#cappuccino-body .container {
|
||||
display: table;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#cappuccino-body .content {
|
||||
display: table-cell;
|
||||
height: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: relative;
|
||||
top: 35%;
|
||||
}
|
||||
|
||||
#loading-text {
|
||||
height: 1.5em;
|
||||
color: #555;
|
||||
font: normal bold 36px/36px Arial, sans-serif;
|
||||
}
|
||||
|
||||
#progress-indicator {
|
||||
padding: 0px;
|
||||
height: 16px;
|
||||
border: 5px solid #555;
|
||||
border-radius: 18px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#progress-bar {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
display: block;
|
||||
height: 18px;
|
||||
|
||||
/* Compensate for moving the bar left 1px to overlap the indicator border */
|
||||
border-right: 1px solid #555;
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
#noscript {
|
||||
position: relative;
|
||||
top: 35%;
|
||||
padding: 1em 1.5em;
|
||||
border: 5px solid #555;
|
||||
border-radius: 16px;
|
||||
background-color: white;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
font: bold 24px Arial, sans-serif;
|
||||
}
|
||||
|
||||
#noscript a {
|
||||
color: #98c0ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="cappuccino-body">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<script type="text/javascript">
|
||||
document.write(loadingHTML);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<noscript style="position:absolute; top:0; left:0; width:100%; height:100%">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<div id="noscript">
|
||||
<p style="font-size:120%; margin-bottom:.75em">JavaScript is required for this application.</p>
|
||||
<p><a href="http://www.enable-javascript.com" target="_blank">Enable JavaScript</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* AppController.j
|
||||
* CPStackViewTest
|
||||
*
|
||||
* Created by Daniel Böhringer on December 26, 2025.
|
||||
* Copyright 2025, All rights reserved.
|
||||
*/
|
||||
|
||||
@import <Foundation/Foundation.j>
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
@import "AppController.j"
|
||||
|
||||
|
||||
function main(args, namedArgs)
|
||||
{
|
||||
CPApplicationMain(args, namedArgs);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* AppController.j
|
||||
* TreeControllerBindingsTest
|
||||
*
|
||||
* Created for testing CPOutlineView and CPTreeController bindings.
|
||||
*/
|
||||
|
||||
@import <AppKit/AppKit.j>
|
||||
@import <AppKit/CPTreeController.j>
|
||||
|
||||
@implementation AppController : CPObject
|
||||
{
|
||||
CPTreeController treeController;
|
||||
CPTextField logField;
|
||||
}
|
||||
|
||||
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
|
||||
{
|
||||
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
|
||||
contentView = [theWindow contentView];
|
||||
|
||||
// 1. Create the Data Model
|
||||
var root1 = [[Node alloc] initWithName:@"Root 1" children:[]],
|
||||
child1 = [[Node alloc] initWithName:@"Child 1.1" children:[]],
|
||||
child2 = [[Node alloc] initWithName:@"Child 1.2" children:[]],
|
||||
root2 = [[Node alloc] initWithName:@"Root 2" children:[]],
|
||||
child3 = [[Node alloc] initWithName:@"Child 2.1" children:[]];
|
||||
|
||||
[root1 setChildren:[child1, child2]];
|
||||
[root2 setChildren:[child3]];
|
||||
var contentArray = [root1, root2];
|
||||
|
||||
// 2. Setup the Tree Controller
|
||||
treeController = [[CPTreeController alloc] init];
|
||||
[treeController setChildrenKeyPath:@"children"];
|
||||
[treeController setContent:contentArray];
|
||||
|
||||
// 3. Setup the Outline View
|
||||
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(20, 20, 250, 300)];
|
||||
[scrollView setAutohidesScrollers:YES];
|
||||
|
||||
var outlineView = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 250, 300)];
|
||||
var column = [[CPTableColumn alloc] initWithIdentifier:@"name"];
|
||||
[[column headerView] setStringValue:@"Node Name"];
|
||||
[column setWidth:240];
|
||||
[column setEditable:YES]; // Editable to test bidirectional bindings in the tree
|
||||
|
||||
[outlineView addTableColumn:column];
|
||||
[outlineView setOutlineTableColumn:column];
|
||||
[outlineView setAllowsMultipleSelection:YES];
|
||||
[scrollView setDocumentView:outlineView];
|
||||
[contentView addSubview:scrollView];
|
||||
|
||||
// 4. Establish Bindings for the Outline View
|
||||
[outlineView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
|
||||
[outlineView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
|
||||
|
||||
var scrollView2 = [[CPScrollView alloc] initWithFrame:CGRectMake(300, 20, 250, 300)];
|
||||
[scrollView2 setAutohidesScrollers:YES];
|
||||
var outlineView2 = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 250, 300)];
|
||||
var column2 = [[CPTableColumn alloc] initWithIdentifier:@"name"];
|
||||
[[column2 headerView] setStringValue:@"Node Name"];
|
||||
[column2 setWidth:240];
|
||||
[column2 setEditable:YES]; // Editable to test bidirectional bindings in the tree
|
||||
|
||||
[outlineView2 addTableColumn:column2];
|
||||
[outlineView2 setOutlineTableColumn:column2];
|
||||
[outlineView2 setAllowsMultipleSelection:YES];
|
||||
[scrollView2 setDocumentView:outlineView2];
|
||||
[contentView addSubview:scrollView2];
|
||||
|
||||
// 4. Establish Bindings for the Outline View
|
||||
[outlineView2 bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
|
||||
[outlineView2 bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
|
||||
|
||||
|
||||
|
||||
[theWindow orderFront:self];
|
||||
}
|
||||
|
||||
- (void)selectSpecificNode:(id)sender
|
||||
{
|
||||
// Programmatically select index path [0, 1] which is "Child 1.2"
|
||||
// This tests the `_CPOutlineViewSelectionIndexPathsBinder` auto-expand logic.
|
||||
var path =[CPIndexPath indexPathWithIndexes:[0, 1]];
|
||||
[treeController setSelectionIndexPath:path];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context
|
||||
{
|
||||
if (keyPath === @"selectionIndexPaths")
|
||||
{
|
||||
var selectedObjects = [treeController selectedObjects];
|
||||
if ([selectedObjects count] > 0)
|
||||
{
|
||||
var names = [CPMutableArray array];
|
||||
for (var i = 0; i <[selectedObjects count]; i++)
|
||||
[names addObject:[selectedObjects[i] name]];[logField setStringValue:[names componentsJoinedByString:@", "]];
|
||||
}
|
||||
else
|
||||
{
|
||||
[logField setStringValue:@"Nothing selected"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// --- Custom Data Model ---
|
||||
|
||||
@implementation Node : CPObject
|
||||
{
|
||||
CPString name;
|
||||
CPArray children;
|
||||
}
|
||||
|
||||
- (id)initWithName:(CPString)aName children:(CPArray)someChildren
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
name = aName;
|
||||
children = someChildren;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// Explicit accessors to ensure Key-Value Observing (KVO) works flawlessly.
|
||||
- (void)setName:(CPString)aName
|
||||
{
|
||||
[self willChangeValueForKey:@"name"];
|
||||
name = aName;[self didChangeValueForKey:@"name"];
|
||||
}
|
||||
|
||||
- (CPString)name
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
- (void)setChildren:(CPArray)someChildren
|
||||
{
|
||||
[self willChangeValueForKey:@"children"];
|
||||
children = someChildren;
|
||||
[self didChangeValueForKey:@"children"];
|
||||
}
|
||||
|
||||
- (CPArray)children
|
||||
{
|
||||
return children;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CPApplicationDelegateClass</key>
|
||||
<string>AppController</string>
|
||||
<key>CPBundleName</key>
|
||||
<string>treecontroller</string>
|
||||
<key>CPPrincipalClass</key>
|
||||
<string>CPApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Jakefile
|
||||
* tooltips
|
||||
*
|
||||
* Created by You on April 26, 2011.
|
||||
* Copyright 2011, Your Company All rights reserved.
|
||||
*/
|
||||
|
||||
var ENV = require("system").env,
|
||||
FILE = require("file"),
|
||||
JAKE = require("jake"),
|
||||
task = JAKE.task,
|
||||
FileList = JAKE.FileList,
|
||||
app = require("cappuccino/jake").app,
|
||||
configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug",
|
||||
OS = require("os");
|
||||
|
||||
app ("tooltips", function(task)
|
||||
{
|
||||
task.setBuildIntermediatesPath(FILE.join("Build", "tooltips.build", configuration));
|
||||
task.setBuildPath(FILE.join("Build", configuration));
|
||||
|
||||
task.setProductName("tooltips");
|
||||
task.setIdentifier("com.yourcompany.tooltips");
|
||||
task.setVersion("1.0");
|
||||
task.setAuthor("Your Company");
|
||||
task.setEmail("feedback @nospam@ yourcompany.com");
|
||||
task.setSummary("tooltips");
|
||||
task.setSources((new FileList("**/*.j")).exclude(FILE.join("Build", "**")));
|
||||
task.setResources(new FileList("Resources/**"));
|
||||
task.setIndexFilePath("index.html");
|
||||
task.setInfoPlistPath("Info.plist");
|
||||
|
||||
if (configuration === "Debug")
|
||||
task.setCompilerFlags("-DDEBUG -g");
|
||||
else
|
||||
task.setCompilerFlags("-O");
|
||||
});
|
||||
|
||||
task ("default", ["tooltips"], function()
|
||||
{
|
||||
printResults(configuration);
|
||||
});
|
||||
|
||||
task ("build", ["default"]);
|
||||
|
||||
task ("debug", function()
|
||||
{
|
||||
ENV["CONFIGURATION"] = "Debug";
|
||||
JAKE.subjake(["."], "build", ENV);
|
||||
});
|
||||
|
||||
task ("release", function()
|
||||
{
|
||||
ENV["CONFIGURATION"] = "Release";
|
||||
JAKE.subjake(["."], "build", ENV);
|
||||
});
|
||||
|
||||
task ("run", ["debug"], function()
|
||||
{
|
||||
OS.system(["open", FILE.join("Build", "Debug", "tooltips", "index.html")]);
|
||||
});
|
||||
|
||||
task ("run-release", ["release"], function()
|
||||
{
|
||||
OS.system(["open", FILE.join("Build", "Release", "tooltips", "index.html")]);
|
||||
});
|
||||
|
||||
task ("deploy", ["release"], function()
|
||||
{
|
||||
FILE.mkdirs(FILE.join("Build", "Deployment", "tooltips"));
|
||||
OS.system(["press", "-f", FILE.join("Build", "Release", "tooltips"), FILE.join("Build", "Deployment", "tooltips")]);
|
||||
printResults("Deployment")
|
||||
});
|
||||
|
||||
task ("desktop", ["release"], function()
|
||||
{
|
||||
FILE.mkdirs(FILE.join("Build", "Desktop", "tooltips"));
|
||||
require("cappuccino/nativehost").buildNativeHost(FILE.join("Build", "Release", "tooltips"), FILE.join("Build", "Desktop", "tooltips", "tooltips.app"));
|
||||
printResults("Desktop")
|
||||
});
|
||||
|
||||
task ("run-desktop", ["desktop"], function()
|
||||
{
|
||||
OS.system([FILE.join("Build", "Desktop", "tooltips", "tooltips.app", "Contents", "MacOS", "NativeHost"), "-i"]);
|
||||
});
|
||||
|
||||
function printResults(configuration)
|
||||
{
|
||||
print("----------------------------");
|
||||
print(configuration+" app built at path: "+FILE.join("Build", configuration, "tooltips"));
|
||||
print("----------------------------");
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!--
|
||||
index-debug.html
|
||||
__project.name__
|
||||
|
||||
Created by __user.name__ on __project.date__.
|
||||
Copyright __project.year__, __organization.name__ All rights reserved.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<!--[if lte IE 8]>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
|
||||
<![endif]-->
|
||||
<!--[if gte IE 9]>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
|
||||
<![endif]-->
|
||||
|
||||
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
|
||||
<link rel="apple-touch-icon" href="Resources/icon.png">
|
||||
<link rel="apple-touch-startup-image" href="Resources/default.png">
|
||||
|
||||
<title>__project.name__</title>
|
||||
|
||||
<!-- Custom javascript goes here -->
|
||||
<!-- End custom javascript -->
|
||||
|
||||
<script type="text/javascript">
|
||||
OBJJ_MAIN_FILE = "main.j";
|
||||
OBJJ_INCLUDE_PATHS = ["Frameworks/Debug", "Frameworks"];
|
||||
// The below will tell the compiler to generate debug symbols, type signatures and not inline objj_msgSend functions.
|
||||
// This will affect only Objective-J code that is compiled when loading the application. It will not affect precompiled
|
||||
// code like the Cappuccino frameworks.
|
||||
//
|
||||
// Debug symbols will give each Objective-J method a Javascript function name. Without a name it will be very hard to find
|
||||
// the methods in the debugger.
|
||||
// Type Signatures will give type information to the Objective-J runtime for instance variables and methods.
|
||||
// Inline objj_msgSend will give a speed increase but decorators will not work. Check below on debug options for
|
||||
// more information on decorators.
|
||||
//
|
||||
// Uncomment or comment on the line below to change the flags
|
||||
OBJJ_COMPILER_FLAGS = ["IncludeDebugSymbols", "IncludeTypeSignatures", "SourceMap", "InlineMsgSend"];
|
||||
|
||||
var progressBar = null;
|
||||
|
||||
OBJJ_PROGRESS_CALLBACK = function(percent, appSize, path)
|
||||
{
|
||||
percent = percent * 100;
|
||||
|
||||
if (!progressBar)
|
||||
progressBar = document.getElementById("progress-bar");
|
||||
|
||||
if (progressBar)
|
||||
progressBar.style.width = Math.min(percent, 100) + "%";
|
||||
}
|
||||
|
||||
var loadingHTML =
|
||||
'<div id="loading">' +
|
||||
' <div id="loading-text">Loading...</div>' +
|
||||
' <div id="progress-indicator">' +
|
||||
' <span id="progress-bar" style="width:0%"></span>' +
|
||||
' </div>' +
|
||||
'</div>';
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="Frameworks/Debug/Objective-J/Objective-J.js" charset="UTF-8"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
objj_msgSend_reset();
|
||||
|
||||
// DEBUG OPTIONS:
|
||||
// Decorators will only work when the code is compiled without the compiler option 'InlineMsgSend'. Check above.
|
||||
|
||||
// Uncomment to enable printing of backtraces on exceptions:
|
||||
//objj_msgSend_decorate(objj_backtrace_decorator);
|
||||
|
||||
// Uncomment to supress exceptions that take place inside a message
|
||||
//objj_msgSend_decorate(objj_supress_exceptions_decorator)
|
||||
|
||||
// Uncomment to enable runtime type checking:
|
||||
//objj_msgSend_decorate(objj_typecheck_decorator);
|
||||
|
||||
// Uncomment (along with both above) to print backtraces on type check errors:
|
||||
//objj_typecheck_prints_backtrace = true;
|
||||
|
||||
// Uncomment to disable the default logger (CPLogConsole if window.console exists, CPLogPopup otherwise):
|
||||
//CPLogUnregister(CPLogDefault);
|
||||
|
||||
// Uncomment to enable a specific logger:
|
||||
//CPLogRegister(CPLogConsole);
|
||||
//CPLogRegister(CPLogPopup);
|
||||
|
||||
// Tag view DOM elements with a "data-cappuccino-view" attribute that contains
|
||||
// the class name of the view that created them. Comment this or set to false to disable.
|
||||
appkit_tag_dom_elements = true;
|
||||
</script>
|
||||
|
||||
<style type="text/css">
|
||||
html, body, h1, p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* We need a body wrapper because Cappuccino is unhappy if we change the body element */
|
||||
#cappuccino-body {
|
||||
/* Position it absolutely so it will fill the height without content */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
/* Put it at the bottom of the stack so it doesn't interfere with UI */
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#cappuccino-body .container {
|
||||
display: table;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#cappuccino-body .content {
|
||||
display: table-cell;
|
||||
height: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: relative;
|
||||
top: 35%;
|
||||
}
|
||||
|
||||
#loading-text {
|
||||
height: 1.5em;
|
||||
color: #555;
|
||||
font: normal bold 36px/36px Arial, sans-serif;
|
||||
}
|
||||
|
||||
#progress-indicator {
|
||||
padding: 0px;
|
||||
height: 16px;
|
||||
border: 5px solid #555;
|
||||
border-radius: 18px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#progress-bar {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
display: block;
|
||||
height: 18px;
|
||||
|
||||
/* Compensate for moving the bar left 1px to overlap the indicator border */
|
||||
border-right: 1px solid #555;
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
#noscript {
|
||||
position: relative;
|
||||
top: 35%;
|
||||
padding: 1em 1.5em;
|
||||
border: 5px solid #555;
|
||||
border-radius: 16px;
|
||||
background-color: white;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
font: bold 24px Arial, sans-serif;
|
||||
}
|
||||
|
||||
#noscript a {
|
||||
color: #98c0ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="cappuccino-body">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<script type="text/javascript">
|
||||
document.write(loadingHTML);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<noscript style="position:absolute; top:0; left:0; width:100%; height:100%">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<div id="noscript">
|
||||
<p style="font-size:120%; margin-bottom:.75em">JavaScript is required for this application.</p>
|
||||
<p><a href="http://www.enable-javascript.com" target="_blank">Enable JavaScript</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,166 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!--
|
||||
index.html
|
||||
__project.name__
|
||||
|
||||
Created by __user.name__ on __project.date__.
|
||||
Copyright __project.year__, __organization.name__ All rights reserved.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<!--[if lte IE 8]>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
|
||||
<![endif]-->
|
||||
<!--[if gte IE 9]>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
|
||||
<![endif]-->
|
||||
|
||||
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
|
||||
<link rel="apple-touch-icon" href="Resources/icon.png">
|
||||
<link rel="apple-touch-startup-image" href="Resources/default.png">
|
||||
|
||||
<title>__project.name__</title>
|
||||
|
||||
<!-- Custom javascript goes here -->
|
||||
<!-- End custom javascript -->
|
||||
|
||||
<script type="text/javascript">
|
||||
OBJJ_MAIN_FILE = "main.j";
|
||||
// The below will tell the compiler to not generate debug symbols but will generate type signatures and inline objj_msgSend functions.
|
||||
// This will affect only Objective-J code that is compiled when loading the application. It will not affect precompiled
|
||||
// code like the Cappuccino frameworks.
|
||||
// Uncomment or comment on the line below to change the flags
|
||||
OBJJ_COMPILER_FLAGS = [/*"IncludeDebugSymbols"*/, "IncludeTypeSignatures"/*, "SourceMap"*/, "InlineMsgSend"];
|
||||
|
||||
var progressBar = null;
|
||||
|
||||
OBJJ_PROGRESS_CALLBACK = function(percent, appSize, path)
|
||||
{
|
||||
percent = percent * 100;
|
||||
|
||||
if (!progressBar)
|
||||
progressBar = document.getElementById("progress-bar");
|
||||
|
||||
if (progressBar)
|
||||
progressBar.style.width = Math.min(percent, 100) + "%";
|
||||
}
|
||||
|
||||
var loadingHTML =
|
||||
'<div id="loading">' +
|
||||
' <div id="loading-text">Loading...</div>' +
|
||||
' <div id="progress-indicator">' +
|
||||
' <span id="progress-bar" style="width:0%"></span>' +
|
||||
' </div>' +
|
||||
'</div>';
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="Frameworks/Objective-J/Objective-J.js" charset="UTF-8"></script>
|
||||
|
||||
<style type="text/css">
|
||||
html, body, h1, p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* We need a body wrapper because Cappuccino is unhappy if we change the body element */
|
||||
#cappuccino-body {
|
||||
/* Position it absolutely so it will fill the height without content */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
/* Put it at the bottom of the stack so it doesn't interfere with UI */
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#cappuccino-body .container {
|
||||
display: table;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#cappuccino-body .content {
|
||||
display: table-cell;
|
||||
height: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: relative;
|
||||
top: 35%;
|
||||
}
|
||||
|
||||
#loading-text {
|
||||
height: 1.5em;
|
||||
color: #555;
|
||||
font: normal bold 36px/36px Arial, sans-serif;
|
||||
}
|
||||
|
||||
#progress-indicator {
|
||||
padding: 0px;
|
||||
height: 16px;
|
||||
border: 5px solid #555;
|
||||
border-radius: 18px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#progress-bar {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
display: block;
|
||||
height: 18px;
|
||||
|
||||
/* Compensate for moving the bar left 1px to overlap the indicator border */
|
||||
border-right: 1px solid #555;
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
#noscript {
|
||||
position: relative;
|
||||
top: 35%;
|
||||
padding: 1em 1.5em;
|
||||
border: 5px solid #555;
|
||||
border-radius: 16px;
|
||||
background-color: white;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
font: bold 24px Arial, sans-serif;
|
||||
}
|
||||
|
||||
#noscript a {
|
||||
color: #98c0ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="cappuccino-body">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<script type="text/javascript">
|
||||
document.write(loadingHTML);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<noscript style="position:absolute; top:0; left:0; width:100%; height:100%">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<div id="noscript">
|
||||
<p style="font-size:120%; margin-bottom:.75em">JavaScript is required for this application.</p>
|
||||
<p><a href="http://www.enable-javascript.com" target="_blank">Enable JavaScript</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* AppController.j
|
||||
* tooltips
|
||||
*
|
||||
* Created by You on April 26, 2011.
|
||||
* Copyright 2011, Your Company All rights reserved.
|
||||
*/
|
||||
|
||||
@import <Foundation/Foundation.j>
|
||||
@import <AppKit/AppKit.j>
|
||||
|
||||
@import "AppController.j"
|
||||
|
||||
|
||||
function main(args, namedArgs)
|
||||
{
|
||||
CPApplicationMain(args, namedArgs);
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user