From 6ce85bae7e260a8ac9207070bebc2b7f715ba650 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 23 Jun 2025 07:21:08 +0200 Subject: [PATCH 001/103] fixed: Keyboard-event handling is based on the deprecated charCode --- AppKit/CPTextView/CPTextView.j | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index f51a0da11..56a4296f5 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2701,11 +2701,12 @@ var _CPCopyPlaceholder = '-'; { _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 + // Filter out non-printable keys like modifiers, cursor keys, etc. + // A key with a name longer than one character is typically a non-printable control key. + // We exclude 'Dead' and 'Process' which are handled as part of dead-key composition. + if (e.key.length > 1 && e.key !== 'Dead' && e.key !== 'Process') { - if (e.which == 13) + if (e.key === 'Enter') _CPNativeInputField.innerHTML = ''; if (_CPNativeInputField.innerHTML.length == 0 || _CPNativeInputField.innerHTML.length > 2) // backspace @@ -2752,7 +2753,7 @@ var _CPCopyPlaceholder = '-'; 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) + if (!CPBrowserIsEngine(CPGeckoBrowserEngine) && e.key.startsWith('Arrow')) _CPNativeInputFieldKeyPressedCalled = YES; if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)]) From 36d4bd32b55cf116b7099f835fc8d11f20b4584e Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 23 Jun 2025 08:58:12 +0200 Subject: [PATCH 002/103] Update CPPlatformWindow+DOM.j --- AppKit/Platform/DOM/CPPlatformWindow+DOM.j | 257 ++++++++------------- 1 file changed, 97 insertions(+), 160 deletions(-) diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index 3cde84b3e..3fa5546bb 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -20,93 +20,6 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ - -/* - * THIS DOCUMENTATION STOLEN DIRECTLY FROM GOOGLE CLOSURE (licensed under Apache 2) - * - * Different web browsers have very different keyboard event handling. Most - * importantly is that only certain browsers repeat keydown events: - * IE, Opera, FF/Win32, and Safari 3 repeat keydown events. - * FF/Mac and Safari 2 do not. - * - * For the purposes of this code, "Safari 3" means WebKit 525+, when WebKit - * decided that they should try to match IE's key handling behavior. - * Safari 3.0.4, which shipped with Leopard (WebKit 523), has the - * Safari 2 behavior. - * - * Firefox, Safari, Opera prevent on keypress - * - * IE prevents on keydown - * - * Firefox does not fire keypress for shift, ctrl, alt - * Firefox does fire keydown for shift, ctrl, alt, meta - * Firefox does not repeat keydown for shift, ctrl, alt, meta - * - * Firefox does not fire keypress for up and down in an input - * - * Opera fires keypress for shift, ctrl, alt, meta - * Opera does not repeat keypress for shift, ctrl, alt, meta - * - * Safari 2 and 3 do not fire keypress for shift, ctrl, alt - * Safari 2 does not fire keydown for shift, ctrl, alt - * Safari 3 *does* fire keydown for shift, ctrl, alt - * - * IE provides the keycode for keyup/down events and the charcode (in the - * keycode field) for keypress. - * - * Mozilla provides the keycode for keyup/down and the charcode for keypress - * unless it's a non text modifying key in which case the keycode is provided. - * - * Safari 3 provides the keycode and charcode for all events. - * - * Opera provides the keycode for keyup/down event and either the charcode or - * the keycode (in the keycode field) for keypress events. - * - * Firefox x11 doesn't fire keydown events if a another key is already held down - * until the first key is released. This can cause a key event to be fired with - * a keyCode for the first key and a charCode for the second key. - * - * Safari 2 in keypress (not supported) - * - * charCode keyCode which - * ENTER: 13 13 13 - * F1: 63236 63236 63236 - * F8: 63243 63243 63243 - * ... - * p: 112 112 112 - * P: 80 80 80 - * - * Firefox, keypress: - * - * charCode keyCode which - * ENTER: 0 13 13 - * F1: 0 112 0 - * F8: 0 119 0 - * ... - * p: 112 0 112 - * P: 80 0 80 - * - * Opera, Mac+Win32, keypress: - * - * charCode keyCode which - * ENTER: undefined 13 13 - * F1: undefined 112 0 - * F8: undefined 119 0 - * ... - * p: undefined 112 112 - * P: undefined 80 80 - * - * IE7, keydown - * - * charCode keyCode which - * ENTER: undefined 13 undefined - * F1: undefined 112 undefined - * F8: undefined 119 undefined - * ... - * p: undefined 80 undefined - * P: undefined 80 undefined - */ - @import @import @import @@ -152,6 +65,29 @@ var KeyCodesToPrevent = {}, }, KeyCodesToUnicodeMap = {}; +// New map from event.key to our internal Unicode function keys. +// This is more reliable than mapping from keyCode. +var KeyToUnicodeMapFromKey = { + "Backspace": CPDeleteCharacter, + "Delete": CPDeleteFunctionKey, + "Tab": CPTabCharacter, + "Enter": CPCarriageReturnCharacter, + "Escape": CPEscapeFunctionKey, + "PageUp": CPPageUpFunctionKey, + "PageDown": CPPageDownFunctionKey, + "ArrowLeft": CPLeftArrowFunctionKey, + "ArrowUp": CPUpArrowFunctionKey, + "ArrowRight": CPRightArrowFunctionKey, + "ArrowDown": CPDownArrowFunctionKey, + "Home": CPHomeFunctionKey, + "End": CPEndFunctionKey +}; + +// Map F-keys dynamically. Assumes CP_F1_KEY (0xF704) to CP_F12_KEY (0xF70F) are defined elsewhere. +for (var i = 1; i <= 12; i++) + KeyToUnicodeMapFromKey['F' + i] = 0xF703 + i; + + KeyCodesToPrevent[CPKeyCodes.A] = YES; KeyCodesToAllow[CPKeyCodes.F1] = YES; @@ -691,6 +627,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio timestamp = [CPEvent currentTimestamp], sourceElement = aDOMEvent.target || aDOMEvent.srcElement, windowNumber = [[CPApp keyWindow] windowNumber], + eventKey = aDOMEvent.key, modifierFlags = (aDOMEvent.shiftKey ? CPShiftKeyMask : 0) | (aDOMEvent.ctrlKey ? CPControlKeyMask : 0) | (aDOMEvent.altKey ? CPAlternateKeyMask : 0) | @@ -698,22 +635,14 @@ _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. We check against blacklists and whitelists. StopDOMEventPropagation = YES; + // Use event.key for checking character keys. This is more reliable across keyboard layouts. + var charToTest = (eventKey && eventKey.length === 1) ? eventKey.toLowerCase() : null; + // Make sure it is not in the blacklists. - if (!(CharacterKeysToPrevent[String.fromCharCode(aDOMEvent.keyCode || aDOMEvent.charCode).toLowerCase()] || KeyCodesToPrevent[aDOMEvent.keyCode])) + if (!((charToTest && CharacterKeysToPrevent[charToTest]) || KeyCodesToPrevent[aDOMEvent.keyCode])) { // It is not in the blacklist, let it through if the ctrl/cmd key is // also down or it's in the whitelist. @@ -727,7 +656,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio switch (aDOMEvent.type) { case "keydown": - // Grab and store the keycode now since it is correct and consistent at this point. + // Grab and store the keycode for compatibility with other parts of the system. if (aDOMEvent.keyCode in MozKeyCodeToKeyCodeMap) _keyCode = MozKeyCodeToKeyCodeMap[aDOMEvent.keyCode]; else @@ -735,62 +664,68 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio 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) + // Determine characters using modern event.key property first. + if (eventKey) { - _capsLockActive = YES; - - // Make sure the caps lock flag is set in modifierFlags - modifierFlags |= CPAlphaShiftKeyMask; + if (eventKey.length === 1) + characters = eventKey; // Printable character, already correctly cased. + else + characters = String.fromCharCode(KeyToUnicodeMapFromKey[eventKey]); // Special key. } - if ([ModifierKeyCodes containsObject:_keyCode]) + // Fallback for older browsers or unhandled keys. + if (!characters) { - // A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break. + if (aDOMEvent.charCode === 0) // Keydown for non-printable keys have charCode === 0 + characters = KeyCodesToUnicodeMap[_keyCode]; + if (!characters) + characters = String.fromCharCode(_keyCode); + } + + // Set characters for the event. event.key is already cased correctly for printable keys. + if (eventKey && eventKey.length === 1) + { + overrideCharacters = eventKey; + charactersIgnoringModifiers = eventKey.toLowerCase(); + } + else + { + charactersIgnoringModifiers = (characters || "").toLowerCase(); + overrideCharacters = (modifierFlags & CPShiftKeyMask || _capsLockActive) ? (characters || "").toUpperCase() : charactersIgnoringModifiers; + } + + // Handle caps lock state. + if (eventKey === "CapsLock") + { + // This is a simplification; getModifierState("CapsLock") would be ideal but is not used here to avoid further changes. + _capsLockActive = !_capsLockActive; + modifierFlags = (modifierFlags & ~CPAlphaShiftKeyMask) | (_capsLockActive ? CPAlphaShiftKeyMask : 0); + } + + var isModifier = (eventKey === "Control" || eventKey === "Shift" || eventKey === "Alt" || eventKey === "Meta" || eventKey === "CapsLock"); + if (isModifier) + { + // A modifier key will never fire keypress. We fire a CPFlagsChanged event and break. event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:_keyCode]; - break; } else if (modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) { - //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 + // Let Cmd/Ctrl combinations be sent on keydown to allow for early cancellation. } - else if (CPKeyCodes.firesKeyPressEvent(_keyCode, aDOMEvent.key, _lastKey, aDOMEvent.shiftKey, aDOMEvent.ctrlKey, aDOMEvent.altKey)) + else if (CPKeyCodes.firesKeyPressEvent(_keyCode, eventKey, _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) + // This branch is for keys that fire a `keypress` event. + // We allow propagation to let the browser handle input in text fields. StopDOMEventPropagation = NO; break; } - else - { - //this branch is taken by "remedial" key events - // In this state we continue to keypress and send the CPEvent - } case "keypress": - // we unconditionally break on keypress events with modifiers, - // because we forced the event to be sent on the keydown + // We unconditionally break on keypress events with modifiers, + // as we forced the event to be sent on the keydown. if (aDOMEvent.type === "keypress" && (modifierFlags & (CPControlKeyMask | CPCommandKeyMask))) break; @@ -801,15 +736,20 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio _lastKey = keyCode; _charCodes[keyCode] = charCode; + // Use the character determined during keydown if available. 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); + { + // Fallback for printable keys on keypress. + if (eventKey && eventKey.length === 1) + characters = eventKey; + else if (charCode !== 0) + characters = String.fromCharCode(charCode); + else + characters = KeyCodesToUnicodeMap[keyCode] || ""; + } - charactersIgnoringModifiers = characters.toLowerCase(); // FIXME: This isn't correct. It SHOULD include Shift. + charactersIgnoringModifiers = characters.toLowerCase(); // Safari won't send proper capitalization during cmd-key events if (!overrideCharacters && (modifierFlags & CPCommandKeyMask) && ((modifierFlags & CPShiftKeyMask) || _capsLockActive)) @@ -818,7 +758,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio event = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:charCode]; - break; case "keyup": @@ -829,26 +768,26 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio _lastKey = -1; _charCodes[keyCode] = nil; - // check for caps lock state - if (keyCode === CPKeyCodes.CAPS_LOCK) + if (eventKey === "CapsLock") { - _capsLockActive = NO; - - // Make sure the caps lock flag is cleared in modifierFlags - modifierFlags &= ~CPAlphaShiftKeyMask; + // The state was handled on keydown. Nothing to do for state change on keyup for a toggle key. } - if ([ModifierKeyCodes containsObject:keyCode]) + var isModifier = (eventKey === "Control" || eventKey === "Shift" || eventKey === "Alt" || eventKey === "Meta" || eventKey === "CapsLock"); + if (isModifier) { - // 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]; break; } - var characters = KeyCodesToUnicodeMap[charCode] || String.fromCharCode(charCode); + var characters; + if (eventKey && eventKey.length === 1) + characters = eventKey; + else + characters = KeyCodesToUnicodeMap[keyCode] || String.fromCharCode(charCode) || ""; + charactersIgnoringModifiers = characters.toLowerCase(); if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive) @@ -857,7 +796,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio event = [CPEvent keyEventWithType:CPKeyUp location:location modifierFlags:modifierFlags timestamp: timestamp windowNumber:windowNumber context:nil characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode]; - break; } @@ -869,7 +807,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio if (event && ![_platformPasteboard windowShouldSuppressKeyEvent]) { [CPApp sendEvent:event]; - [_platformPasteboard windowDidSendKeyEvent:event]; } @@ -1226,7 +1163,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio return; } } - + // cancel other touch cases preventively if (aDOMEvent.preventDefault) From 751e1d27a637c4f5b8bcdae967e2b6e09adef2c5 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 23 Jun 2025 09:02:28 +0200 Subject: [PATCH 003/103] Update CPPlatformWindow+DOMKeys.j --- .../Platform/DOM/CPPlatformWindow+DOMKeys.j | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOMKeys.j b/AppKit/Platform/DOM/CPPlatformWindow+DOMKeys.j index a24d241f3..a76bed207 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOMKeys.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOMKeys.j @@ -132,38 +132,36 @@ CPKeyCodes = { /*! * Returns true if the key fires a keypress event in the current browser. + * The keypress event is deprecated, but this function helps manage legacy + * event handling by predicting its behavior. * - * Accoridng to MSDN [1] IE only fires keypress events for the following keys: - * - Letters: A - Z (uppercase and lowercase) - * - Numerals: 0 - 9 - * - Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~ - * - System: ESC, SPACEBAR, ENTER - * - * That's not entirely correct though, for instance there's no distinction - * between upper and lower case letters. - * - * [1] http://msdn2.microsoft.com/en-us/library/ms536939(VS.85).aspx) - * - * Safari is similar to IE, but does not fire keypress for ESC. - * - * Additionally, IE6 does not fire keydown or keypress events for letters when - * the control or alt keys are held down and the shift key is not. IE7 does - * fire keydown in these cases, though, but not keypress. - * - * @param keyCode A key code. - * @param opt_heldKeyCode Key code of a currently-held key. - * @param opt_shiftKey Whether the shift key is held down. - * @param opt_ctrlKey Whether the control key is held down. - * @param opt_altKey Whether the alt key is held down. - * @return Returns YES if it's a key that fires a keypress event. + * @param {number} keyCode A key code. + * @param {string} key The `key` property from the keyboard event. + * @param {number} opt_heldKeyCode Key code of a currently-held key. + * @param {boolean} opt_shiftKey Whether the shift key is held down. + * @param {boolean} opt_ctrlKey Whether the control key is held down. + * @param {boolean} opt_altKey Whether the alt key is held down. + * @return {boolean} Returns YES if it's a key that fires a keypress event. */ CPKeyCodes.firesKeyPressEvent = function(keyCode, key, opt_heldKeyCode, opt_shiftKey, opt_ctrlKey, opt_altKey) { - // The property key from event is one character wide in case of 'regular' keys (as opposed e.g. to arrow keys) - // Regular keys all fire the keypress event + // Modern approach: Use event.key if available, as it is the most reliable standard. + if (key) + { + // Any key that produces a single, printable character fires a keypress event. + if (key.length === 1) + return true; - if (key && key.length == 1) - return true; + // "Enter" is a special non-printable key that historically fires keypress for compatibility. + if (key === "Enter") + return true; + + // For all other non-printable keys (e.g., "ArrowLeft", "Escape", "F1"), + // modern browsers do not fire a keypress event. + return false; + } + + // --- Legacy Fallback Logic (for browsers that don't support event.key) --- if (!CPFeatureIsCompatible(CPJavaScriptRemedialKeySupport)) return true; @@ -196,9 +194,11 @@ CPKeyCodes.firesKeyPressEvent = function(keyCode, key, opt_heldKeyCode, opt_shif /*! * Test for whether or not a given keyCode represents a character key. + * NOTE: This is a legacy function for browsers that don't support `event.key`. + * It is unreliable because `keyCode` represents a physical key, not the character produced. * - * @param keyCode A key code. - * @return Returns YES if the keyCode is a character key. + * @param {number} keyCode A key code. + * @return {boolean} Returns YES if the keyCode is a character key. */ CPKeyCodes.isCharacterKey = function(keyCode) { From c814b5daed98459d0a0b4c4fb73205b5c8e5f7e2 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 1 Jul 2025 19:36:02 +0200 Subject: [PATCH 004/103] fixed: arrow keys did not work --- AppKit/Platform/DOM/CPPlatformWindow+DOM.j | 264 ++++++++++++--------- dist/cappuccino/bin/flatten | 2 +- dist/cappuccino/bin/fontinfo | Bin 69888 -> 70736 bytes dist/cappuccino/bin/imagesize | Bin 69536 -> 0 bytes dist/cappuccino/bin/objj2objcskeleton | 22 +- 5 files changed, 175 insertions(+), 113 deletions(-) delete mode 100755 dist/cappuccino/bin/imagesize diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index 3fa5546bb..fe17ec2c8 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -20,6 +20,92 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +/* + * THIS DOCUMENTATION STOLEN DIRECTLY FROM GOOGLE CLOSURE (licensed under Apache 2) + * + * Different web browsers have very different keyboard event handling. Most + * importantly is that only certain browsers repeat keydown events: + * IE, Opera, FF/Win32, and Safari 3 repeat keydown events. + * FF/Mac and Safari 2 do not. + * + * For the purposes of this code, "Safari 3" means WebKit 525+, when WebKit + * decided that they should try to match IE's key handling behavior. + * Safari 3.0.4, which shipped with Leopard (WebKit 523), has the + * Safari 2 behavior. + * + * Firefox, Safari, Opera prevent on keypress + * + * IE prevents on keydown + * + * Firefox does not fire keypress for shift, ctrl, alt + * Firefox does fire keydown for shift, ctrl, alt, meta + * Firefox does not repeat keydown for shift, ctrl, alt, meta + * + * Firefox does not fire keypress for up and down in an input + * + * Opera fires keypress for shift, ctrl, alt, meta + * Opera does not repeat keypress for shift, ctrl, alt, meta + * + * Safari 2 and 3 do not fire keypress for shift, ctrl, alt + * Safari 2 does not fire keydown for shift, ctrl, alt + * Safari 3 *does* fire keydown for shift, ctrl, alt + * + * IE provides the keycode for keyup/down events and the charcode (in the + * keycode field) for keypress. + * + * Mozilla provides the keycode for keyup/down and the charcode for keypress + * unless it's a non text modifying key in which case the keycode is provided. + * + * Safari 3 provides the keycode and charcode for all events. + * + * Opera provides the keycode for keyup/down event and either the charcode or + * the keycode (in the keycode field) for keypress events. + * + * Firefox x11 doesn't fire keydown events if a another key is already held down + * until the first key is released. This can cause a key event to be fired with + * a keyCode for the first key and a charCode for the second key. + * + * Safari 2 in keypress (not supported) + * + * charCode keyCode which + * ENTER: 13 13 13 + * F1: 63236 63236 63236 + * F8: 63243 63243 63243 + * ... + * p: 112 112 112 + * P: 80 80 80 + * + * Firefox, keypress: + * + * charCode keyCode which + * ENTER: 0 13 13 + * F1: 0 112 0 + * F8: 0 119 0 + * ... + * p: 112 0 112 + * P: 80 0 80 + * + * Opera, Mac+Win32, keypress: + * + * charCode keyCode which + * ENTER: undefined 13 13 + * F1: undefined 112 0 + * F8: undefined 119 0 + * ... + * p: undefined 112 112 + * P: undefined 80 80 + * + * IE7, keydown + * + * charCode keyCode which + * ENTER: undefined 13 undefined + * F1: undefined 112 undefined + * F8: undefined 119 undefined + * ... + * p: undefined 80 undefined + * P: undefined 80 undefined + */ + @import @import @import @@ -65,29 +151,6 @@ var KeyCodesToPrevent = {}, }, KeyCodesToUnicodeMap = {}; -// New map from event.key to our internal Unicode function keys. -// This is more reliable than mapping from keyCode. -var KeyToUnicodeMapFromKey = { - "Backspace": CPDeleteCharacter, - "Delete": CPDeleteFunctionKey, - "Tab": CPTabCharacter, - "Enter": CPCarriageReturnCharacter, - "Escape": CPEscapeFunctionKey, - "PageUp": CPPageUpFunctionKey, - "PageDown": CPPageDownFunctionKey, - "ArrowLeft": CPLeftArrowFunctionKey, - "ArrowUp": CPUpArrowFunctionKey, - "ArrowRight": CPRightArrowFunctionKey, - "ArrowDown": CPDownArrowFunctionKey, - "Home": CPHomeFunctionKey, - "End": CPEndFunctionKey -}; - -// Map F-keys dynamically. Assumes CP_F1_KEY (0xF704) to CP_F12_KEY (0xF70F) are defined elsewhere. -for (var i = 1; i <= 12; i++) - KeyToUnicodeMapFromKey['F' + i] = 0xF703 + i; - - KeyCodesToPrevent[CPKeyCodes.A] = YES; KeyCodesToAllow[CPKeyCodes.F1] = YES; @@ -627,7 +690,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio timestamp = [CPEvent currentTimestamp], sourceElement = aDOMEvent.target || aDOMEvent.srcElement, windowNumber = [[CPApp keyWindow] windowNumber], - eventKey = aDOMEvent.key, modifierFlags = (aDOMEvent.shiftKey ? CPShiftKeyMask : 0) | (aDOMEvent.ctrlKey ? CPControlKeyMask : 0) | (aDOMEvent.altKey ? CPAlternateKeyMask : 0) | @@ -635,14 +697,22 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio (_capsLockActive ? CPAlphaShiftKeyMask : 0); // With a few exceptions, all key events are blocked from propagating to - // the browser. We check against blacklists and whitelists. + // 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. + StopDOMEventPropagation = YES; - // Use event.key for checking character keys. This is more reliable across keyboard layouts. - var charToTest = (eventKey && eventKey.length === 1) ? eventKey.toLowerCase() : null; - // Make sure it is not in the blacklists. - if (!((charToTest && CharacterKeysToPrevent[charToTest]) || KeyCodesToPrevent[aDOMEvent.keyCode])) + if (!(CharacterKeysToPrevent[String.fromCharCode(aDOMEvent.keyCode || aDOMEvent.charCode).toLowerCase()] || KeyCodesToPrevent[aDOMEvent.keyCode])) { // It is not in the blacklist, let it through if the ctrl/cmd key is // also down or it's in the whitelist. @@ -656,7 +726,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio switch (aDOMEvent.type) { case "keydown": - // Grab and store the keycode for compatibility with other parts of the system. + // 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 @@ -664,68 +734,62 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio var characters; - // Determine characters using modern event.key property first. - if (eventKey) - { - if (eventKey.length === 1) - characters = eventKey; // Printable character, already correctly cased. - else - characters = String.fromCharCode(KeyToUnicodeMapFromKey[eventKey]); // Special key. - } + // 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 - // Fallback for older browsers or unhandled keys. 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 (aDOMEvent.charCode === 0) // Keydown for non-printable keys have charCode === 0 - characters = KeyCodesToUnicodeMap[_keyCode]; - if (!characters) - characters = String.fromCharCode(_keyCode); + _capsLockActive = YES; + + // Make sure the caps lock flag is set in modifierFlags + modifierFlags |= CPAlphaShiftKeyMask; } - // Set characters for the event. event.key is already cased correctly for printable keys. - if (eventKey && eventKey.length === 1) + if ([ModifierKeyCodes containsObject:_keyCode]) { - overrideCharacters = eventKey; - charactersIgnoringModifiers = eventKey.toLowerCase(); - } - else - { - charactersIgnoringModifiers = (characters || "").toLowerCase(); - overrideCharacters = (modifierFlags & CPShiftKeyMask || _capsLockActive) ? (characters || "").toUpperCase() : charactersIgnoringModifiers; - } - - // Handle caps lock state. - if (eventKey === "CapsLock") - { - // This is a simplification; getModifierState("CapsLock") would be ideal but is not used here to avoid further changes. - _capsLockActive = !_capsLockActive; - modifierFlags = (modifierFlags & ~CPAlphaShiftKeyMask) | (_capsLockActive ? CPAlphaShiftKeyMask : 0); - } - - var isModifier = (eventKey === "Control" || eventKey === "Shift" || eventKey === "Alt" || eventKey === "Meta" || eventKey === "CapsLock"); - if (isModifier) - { - // A modifier key will never fire keypress. We fire a CPFlagsChanged event and break. + // 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]; + break; } else if (modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) { - // Let Cmd/Ctrl combinations be sent on keydown to allow for early cancellation. + //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, eventKey, _lastKey, aDOMEvent.shiftKey, aDOMEvent.ctrlKey, aDOMEvent.altKey)) + else if (CPKeyCodes.firesKeyPressEvent(_keyCode, aDOMEvent.key, _lastKey, aDOMEvent.shiftKey, aDOMEvent.ctrlKey, aDOMEvent.altKey)) { - // This branch is for keys that fire a `keypress` event. - // We allow propagation to let the browser handle input in text fields. + // 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; } + else + { + //this branch is taken by "remedial" key events + // In this state we continue to keypress and send the CPEvent + } case "keypress": - // We unconditionally break on keypress events with modifiers, - // as we forced the event to be sent on the keydown. + // 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; @@ -736,20 +800,15 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio _lastKey = keyCode; _charCodes[keyCode] = charCode; - // Use the character determined during keydown if available. var characters = overrideCharacters; - if (!characters) - { - // Fallback for printable keys on keypress. - if (eventKey && eventKey.length === 1) - characters = eventKey; - else if (charCode !== 0) - characters = String.fromCharCode(charCode); - else - characters = KeyCodesToUnicodeMap[keyCode] || ""; - } + // Is this a special key? + if (!characters && (aDOMEvent.which === 0 || aDOMEvent.charCode === 0)) + characters = KeyCodesToUnicodeMap[charCode]; - charactersIgnoringModifiers = characters.toLowerCase(); + 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)) @@ -758,6 +817,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio event = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:charCode]; + break; case "keyup": @@ -768,26 +828,26 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio _lastKey = -1; _charCodes[keyCode] = nil; - if (eventKey === "CapsLock") + // check for caps lock state + if (keyCode === CPKeyCodes.CAPS_LOCK) { - // The state was handled on keydown. Nothing to do for state change on keyup for a toggle key. + _capsLockActive = NO; + + // Make sure the caps lock flag is cleared in modifierFlags + modifierFlags &= ~CPAlphaShiftKeyMask; } - var isModifier = (eventKey === "Control" || eventKey === "Shift" || eventKey === "Alt" || eventKey === "Meta" || eventKey === "CapsLock"); - if (isModifier) + 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]; + break; } - var characters; - if (eventKey && eventKey.length === 1) - characters = eventKey; - else - characters = KeyCodesToUnicodeMap[keyCode] || String.fromCharCode(charCode) || ""; - + var characters = KeyCodesToUnicodeMap[charCode] || String.fromCharCode(charCode); charactersIgnoringModifiers = characters.toLowerCase(); if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive) @@ -796,6 +856,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]; + break; } @@ -807,6 +868,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio if (event && ![_platformPasteboard windowShouldSuppressKeyEvent]) { [CPApp sendEvent:event]; + [_platformPasteboard windowDidSendKeyEvent:event]; } @@ -1163,7 +1225,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio return; } } - + // cancel other touch cases preventively if (aDOMEvent.preventDefault) @@ -1846,13 +1908,3 @@ function CPWindowObjectList() return windowObjects; } - -function CPWindowList() -{ - var windowObjectList = CPWindowObjectList(); - - return [windowObjectList arrayByApplyingBlock:function(windowObject) - { - return [windowObject windowNumber]; - }]; -} diff --git a/dist/cappuccino/bin/flatten b/dist/cappuccino/bin/flatten index 1df033252..80b9ecf8e 100755 --- a/dist/cappuccino/bin/flatten +++ b/dist/cappuccino/bin/flatten @@ -233,7 +233,7 @@ ObjectiveJFlattener.prototype.serializeFunctions = function() { var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK var relative = this.rootPath.relative(path).toString(); - this.functionsBuffer.push("ObjectiveJ.StaticResource._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); + this.functionsBuffer.push("ObjectiveJ.FileExecutable._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); } var bundle = this.context.global.CFBundle.bundleContainingURL(path); diff --git a/dist/cappuccino/bin/fontinfo b/dist/cappuccino/bin/fontinfo index f1ff06158a2525d34541aff240713c0067bd8633..1a91ba1c8040bbcfe787719cb7e44c0e7578d180 100755 GIT binary patch literal 70736 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 delta 3394 zcmZ`*3s6+o89w`DsqFGx+h670NB67 z0BER_-s?-eqvet#M>G=>)tO2(O5#IV%?yIz_H@(>o1IN<&UUZ~LaljSEpq}>J}u!b zVr7xn4nNt^c`Qus7M+>LN3Ns}%e^# zxB<8vC=Ez+f{-lWvAl8B06|2vfltV9N$Vy>N(-^UM2r-KiqFN|))av-q#`Yxy;N9bap; zbl)tk_TTZ}vjob6WVpw18Q|(jmB3tzoS61`(Bi_#J{o&92UPh z1kMEhI4Gn~<~Ls&Pedbs907*Sm!KzryabTV!B&|iP}1kEA|#@JLkYP4ll)U(?U-NX zAFB>59U7~hoVp=??dgX2CkHG6b144R477I-%gz1=@l%E%bP;IYh+YSQ<68b0W_{yhNVO;gS%)DY9|CFu^Cw_= zYy98vk0JR4z<0VfKmeuBn;21b1(;^GjCA5h9;5Oo%! z21?$Q5G{eau*LtS6tcp9AL}t6+`C0LOp)vbxba+Y#jpKaRba;@D5&``bSwIxt2;VA z7m6Rkl)~s0(;9<9JrV}$1}Ip05ODcBLvl+{l?1>wLG3sMukeR!;;Ta7kO9ZS>T&*{ z{zEzOLM8vvr;O+WxQhNk^R7t8bcndWOpKTit&nIF3W1UdG-GYnqaG}e!0VceCM^D; zqR<$gLFfq+Os>atD9(nKXg?6aV+ z31%YzZ;ceFP(~ny?u;8q%gFHMF*P z*19~KSK6ALWp3At&N8}5o0%b<<}Q0}yUpcs6Pw%aY;nLP>2T5{?Mzys&7!~17U$a9 z+Q4r0w7Xgw%Sc`AGo_MD95AkhP+wdpy{^rjho{UUm=x(7xTUe73@bt6;B-ll&Mk6b zok~V|ozWY{djeYYs%w5>{$De{>i_urm>(8vvT=m;M>tsp975~7 z#q3f}VLXR%BjZaMk6zvlOz&cR7vq%i1C0NM@!vCkj`5p}$4H6gkT{lM4&(C}UyNKl zEc;jK_Gyz6^9p^6wAC}orWEl_k~sS8sTu0k?y#(fqfMs_THkqCezBqDpP?MD&)CBW z&n;VCDOA?*R;!RNthBNPoEn_WNf&anyU_~EP1-duOuVAoolQ=g+evy-I7e5LL;9bp zcZpl2N6!J=GqAsS4{VV74v!Nz8S!0>rjZSKJUTtb6L%kGxNjgw(UZ#2PX;pOpvDj7 zZEQD$6lpz#xJe2p*gUNbq=&dQ(fn*pO|5pcB>cc4xLRBuG^a()t!*Bm#km!21^{Y< z?dt(3`fwo4I~_hNsom+2_*U2)IY80>&K)4@bVU41z^&d{@~((mC%WL|DnN|GIIl{@Jw!dTys4T{x9RzodJaZ z4tRN|17!j|7So@7$|alo*un|T`0c_^6Rs%KW{b_^Bnv#vZFED}sE~LjJrZ7`>?}12 z^j0`ibN5PI2(t0gw9_+V&CZvc1KBrsh$+yI+k6k&Di|x%8J~FIq82mvzXg|eCJcY(|mto zMx}1;LO?SZmK-?9EuXVz>5=5^X}>L7k@!{fg5Ehp)(=xh-#lb|+cpY89Bmol413={ z-q`FoHlKWPG2x=~EpMW(JNwJmf~muwCO=3yz5WY&c7)^fYphqZ_NR{CzQ7&xoUOR< zAJ@MQUj86?*7l0)Tdq}e?|z(>fA3J|=hi;mdw(7(Fm6j396lJl*4(&vgpTzG?tKj) zn+%nU^QL|O$r072fIIj3og;U;LIdSB8QV|n`;}GwcKVJL7_=y2daz~N;7i#ZKlc9g ziFc;uy>z+dxtB-kDrb~^yD_)tRtD4(7aR|Ea`(y$F95qo>p%S?-EgKtQCwuCBWGsE Q%I;p7hEba639vimze2ENzW@LL diff --git a/dist/cappuccino/bin/imagesize b/dist/cappuccino/bin/imagesize deleted file mode 100755 index 5d67a1ce0a9a1e20577f82ccdfe0d3b3e94143b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69536 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 Z009sH0T2KI5C8!X009sHfo}nU{{v@rhV}pe diff --git a/dist/cappuccino/bin/objj2objcskeleton b/dist/cappuccino/bin/objj2objcskeleton index b7972dbb3..51ecab38d 100755 --- a/dist/cappuccino/bin/objj2objcskeleton +++ b/dist/cappuccino/bin/objj2objcskeleton @@ -4,14 +4,11 @@ var fs = require("fs"), acorn = require("objj-parser"), - walk = require("objj-parser/util/walk"), + walk = require("acorn-walk"), stream = ObjectiveJ.term; - debugger; - function main(args) { - debugger; args.shift(); if (args.length < 1) @@ -40,6 +37,8 @@ function raise(pos, message) throw syntaxError; } +function ignore(_node, _st, _c) {} + var errors = [], xcc = walk.make( { @@ -115,7 +114,18 @@ var errors = [], else raise(node.loc.start, "Action methods must have exactly one parameter"); } - } + }, + TypeDefStatement: ignore, + ClassStatement: ignore, + MessageSendExpression: ignore, + GlobalStatement: ignore, + ProtocolDeclarationStatement: ignore, + ArrayLiteral: ignore, + Reference: ignore, + DictionaryLiteral: ignore, + Dereference: ignore, + ImportStatement: ignore, + SelectorLiteralExpression: ignore } ); @@ -154,7 +164,7 @@ function parser(args) outputHeaderURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".h"]), outputImplementationURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".m"]), source = fs.readFileSync(sourcePath, { encoding: "utf8" }), - tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath }), + tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath, ecmaVersion: 2022 }), classesInformation = [], ObjectiveCSource = "", ObjectiveCHeader = "", From 0867e718d10df151c47a748ec4462e1ccfc2ff0b Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 1 Jul 2025 19:40:47 +0200 Subject: [PATCH 005/103] Revert "fixed: arrow keys did not work" This reverts commit c814b5daed98459d0a0b4c4fb73205b5c8e5f7e2. --- AppKit/Platform/DOM/CPPlatformWindow+DOM.j | 266 +++++++++------------ dist/cappuccino/bin/flatten | 2 +- dist/cappuccino/bin/fontinfo | Bin 70736 -> 69888 bytes dist/cappuccino/bin/imagesize | Bin 0 -> 69536 bytes dist/cappuccino/bin/objj2objcskeleton | 22 +- 5 files changed, 114 insertions(+), 176 deletions(-) create mode 100755 dist/cappuccino/bin/imagesize diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index fe17ec2c8..3fa5546bb 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -20,92 +20,6 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -/* - * THIS DOCUMENTATION STOLEN DIRECTLY FROM GOOGLE CLOSURE (licensed under Apache 2) - * - * Different web browsers have very different keyboard event handling. Most - * importantly is that only certain browsers repeat keydown events: - * IE, Opera, FF/Win32, and Safari 3 repeat keydown events. - * FF/Mac and Safari 2 do not. - * - * For the purposes of this code, "Safari 3" means WebKit 525+, when WebKit - * decided that they should try to match IE's key handling behavior. - * Safari 3.0.4, which shipped with Leopard (WebKit 523), has the - * Safari 2 behavior. - * - * Firefox, Safari, Opera prevent on keypress - * - * IE prevents on keydown - * - * Firefox does not fire keypress for shift, ctrl, alt - * Firefox does fire keydown for shift, ctrl, alt, meta - * Firefox does not repeat keydown for shift, ctrl, alt, meta - * - * Firefox does not fire keypress for up and down in an input - * - * Opera fires keypress for shift, ctrl, alt, meta - * Opera does not repeat keypress for shift, ctrl, alt, meta - * - * Safari 2 and 3 do not fire keypress for shift, ctrl, alt - * Safari 2 does not fire keydown for shift, ctrl, alt - * Safari 3 *does* fire keydown for shift, ctrl, alt - * - * IE provides the keycode for keyup/down events and the charcode (in the - * keycode field) for keypress. - * - * Mozilla provides the keycode for keyup/down and the charcode for keypress - * unless it's a non text modifying key in which case the keycode is provided. - * - * Safari 3 provides the keycode and charcode for all events. - * - * Opera provides the keycode for keyup/down event and either the charcode or - * the keycode (in the keycode field) for keypress events. - * - * Firefox x11 doesn't fire keydown events if a another key is already held down - * until the first key is released. This can cause a key event to be fired with - * a keyCode for the first key and a charCode for the second key. - * - * Safari 2 in keypress (not supported) - * - * charCode keyCode which - * ENTER: 13 13 13 - * F1: 63236 63236 63236 - * F8: 63243 63243 63243 - * ... - * p: 112 112 112 - * P: 80 80 80 - * - * Firefox, keypress: - * - * charCode keyCode which - * ENTER: 0 13 13 - * F1: 0 112 0 - * F8: 0 119 0 - * ... - * p: 112 0 112 - * P: 80 0 80 - * - * Opera, Mac+Win32, keypress: - * - * charCode keyCode which - * ENTER: undefined 13 13 - * F1: undefined 112 0 - * F8: undefined 119 0 - * ... - * p: undefined 112 112 - * P: undefined 80 80 - * - * IE7, keydown - * - * charCode keyCode which - * ENTER: undefined 13 undefined - * F1: undefined 112 undefined - * F8: undefined 119 undefined - * ... - * p: undefined 80 undefined - * P: undefined 80 undefined - */ - @import @import @import @@ -151,6 +65,29 @@ var KeyCodesToPrevent = {}, }, KeyCodesToUnicodeMap = {}; +// New map from event.key to our internal Unicode function keys. +// This is more reliable than mapping from keyCode. +var KeyToUnicodeMapFromKey = { + "Backspace": CPDeleteCharacter, + "Delete": CPDeleteFunctionKey, + "Tab": CPTabCharacter, + "Enter": CPCarriageReturnCharacter, + "Escape": CPEscapeFunctionKey, + "PageUp": CPPageUpFunctionKey, + "PageDown": CPPageDownFunctionKey, + "ArrowLeft": CPLeftArrowFunctionKey, + "ArrowUp": CPUpArrowFunctionKey, + "ArrowRight": CPRightArrowFunctionKey, + "ArrowDown": CPDownArrowFunctionKey, + "Home": CPHomeFunctionKey, + "End": CPEndFunctionKey +}; + +// Map F-keys dynamically. Assumes CP_F1_KEY (0xF704) to CP_F12_KEY (0xF70F) are defined elsewhere. +for (var i = 1; i <= 12; i++) + KeyToUnicodeMapFromKey['F' + i] = 0xF703 + i; + + KeyCodesToPrevent[CPKeyCodes.A] = YES; KeyCodesToAllow[CPKeyCodes.F1] = YES; @@ -690,6 +627,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio timestamp = [CPEvent currentTimestamp], sourceElement = aDOMEvent.target || aDOMEvent.srcElement, windowNumber = [[CPApp keyWindow] windowNumber], + eventKey = aDOMEvent.key, modifierFlags = (aDOMEvent.shiftKey ? CPShiftKeyMask : 0) | (aDOMEvent.ctrlKey ? CPControlKeyMask : 0) | (aDOMEvent.altKey ? CPAlternateKeyMask : 0) | @@ -697,22 +635,14 @@ _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. We check against blacklists and whitelists. StopDOMEventPropagation = YES; + // Use event.key for checking character keys. This is more reliable across keyboard layouts. + var charToTest = (eventKey && eventKey.length === 1) ? eventKey.toLowerCase() : null; + // Make sure it is not in the blacklists. - if (!(CharacterKeysToPrevent[String.fromCharCode(aDOMEvent.keyCode || aDOMEvent.charCode).toLowerCase()] || KeyCodesToPrevent[aDOMEvent.keyCode])) + if (!((charToTest && CharacterKeysToPrevent[charToTest]) || KeyCodesToPrevent[aDOMEvent.keyCode])) { // It is not in the blacklist, let it through if the ctrl/cmd key is // also down or it's in the whitelist. @@ -726,7 +656,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio switch (aDOMEvent.type) { case "keydown": - // Grab and store the keycode now since it is correct and consistent at this point. + // Grab and store the keycode for compatibility with other parts of the system. if (aDOMEvent.keyCode in MozKeyCodeToKeyCodeMap) _keyCode = MozKeyCodeToKeyCodeMap[aDOMEvent.keyCode]; else @@ -734,62 +664,68 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio 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) + // Determine characters using modern event.key property first. + if (eventKey) { - _capsLockActive = YES; - - // Make sure the caps lock flag is set in modifierFlags - modifierFlags |= CPAlphaShiftKeyMask; + if (eventKey.length === 1) + characters = eventKey; // Printable character, already correctly cased. + else + characters = String.fromCharCode(KeyToUnicodeMapFromKey[eventKey]); // Special key. } - if ([ModifierKeyCodes containsObject:_keyCode]) + // Fallback for older browsers or unhandled keys. + if (!characters) { - // A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break. + if (aDOMEvent.charCode === 0) // Keydown for non-printable keys have charCode === 0 + characters = KeyCodesToUnicodeMap[_keyCode]; + if (!characters) + characters = String.fromCharCode(_keyCode); + } + + // Set characters for the event. event.key is already cased correctly for printable keys. + if (eventKey && eventKey.length === 1) + { + overrideCharacters = eventKey; + charactersIgnoringModifiers = eventKey.toLowerCase(); + } + else + { + charactersIgnoringModifiers = (characters || "").toLowerCase(); + overrideCharacters = (modifierFlags & CPShiftKeyMask || _capsLockActive) ? (characters || "").toUpperCase() : charactersIgnoringModifiers; + } + + // Handle caps lock state. + if (eventKey === "CapsLock") + { + // This is a simplification; getModifierState("CapsLock") would be ideal but is not used here to avoid further changes. + _capsLockActive = !_capsLockActive; + modifierFlags = (modifierFlags & ~CPAlphaShiftKeyMask) | (_capsLockActive ? CPAlphaShiftKeyMask : 0); + } + + var isModifier = (eventKey === "Control" || eventKey === "Shift" || eventKey === "Alt" || eventKey === "Meta" || eventKey === "CapsLock"); + if (isModifier) + { + // A modifier key will never fire keypress. We fire a CPFlagsChanged event and break. event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:_keyCode]; - break; } else if (modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) { - //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 + // Let Cmd/Ctrl combinations be sent on keydown to allow for early cancellation. } - else if (CPKeyCodes.firesKeyPressEvent(_keyCode, aDOMEvent.key, _lastKey, aDOMEvent.shiftKey, aDOMEvent.ctrlKey, aDOMEvent.altKey)) + else if (CPKeyCodes.firesKeyPressEvent(_keyCode, eventKey, _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) + // This branch is for keys that fire a `keypress` event. + // We allow propagation to let the browser handle input in text fields. StopDOMEventPropagation = NO; break; } - else - { - //this branch is taken by "remedial" key events - // In this state we continue to keypress and send the CPEvent - } case "keypress": - // we unconditionally break on keypress events with modifiers, - // because we forced the event to be sent on the keydown + // We unconditionally break on keypress events with modifiers, + // as we forced the event to be sent on the keydown. if (aDOMEvent.type === "keypress" && (modifierFlags & (CPControlKeyMask | CPCommandKeyMask))) break; @@ -800,15 +736,20 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio _lastKey = keyCode; _charCodes[keyCode] = charCode; + // Use the character determined during keydown if available. 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); + { + // Fallback for printable keys on keypress. + if (eventKey && eventKey.length === 1) + characters = eventKey; + else if (charCode !== 0) + characters = String.fromCharCode(charCode); + else + characters = KeyCodesToUnicodeMap[keyCode] || ""; + } - charactersIgnoringModifiers = characters.toLowerCase(); // FIXME: This isn't correct. It SHOULD include Shift. + charactersIgnoringModifiers = characters.toLowerCase(); // Safari won't send proper capitalization during cmd-key events if (!overrideCharacters && (modifierFlags & CPCommandKeyMask) && ((modifierFlags & CPShiftKeyMask) || _capsLockActive)) @@ -817,7 +758,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio event = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:charCode]; - break; case "keyup": @@ -828,26 +768,26 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio _lastKey = -1; _charCodes[keyCode] = nil; - // check for caps lock state - if (keyCode === CPKeyCodes.CAPS_LOCK) + if (eventKey === "CapsLock") { - _capsLockActive = NO; - - // Make sure the caps lock flag is cleared in modifierFlags - modifierFlags &= ~CPAlphaShiftKeyMask; + // The state was handled on keydown. Nothing to do for state change on keyup for a toggle key. } - if ([ModifierKeyCodes containsObject:keyCode]) + var isModifier = (eventKey === "Control" || eventKey === "Shift" || eventKey === "Alt" || eventKey === "Meta" || eventKey === "CapsLock"); + if (isModifier) { - // 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]; break; } - var characters = KeyCodesToUnicodeMap[charCode] || String.fromCharCode(charCode); + var characters; + if (eventKey && eventKey.length === 1) + characters = eventKey; + else + characters = KeyCodesToUnicodeMap[keyCode] || String.fromCharCode(charCode) || ""; + charactersIgnoringModifiers = characters.toLowerCase(); if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive) @@ -856,7 +796,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio event = [CPEvent keyEventWithType:CPKeyUp location:location modifierFlags:modifierFlags timestamp: timestamp windowNumber:windowNumber context:nil characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode]; - break; } @@ -868,7 +807,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio if (event && ![_platformPasteboard windowShouldSuppressKeyEvent]) { [CPApp sendEvent:event]; - [_platformPasteboard windowDidSendKeyEvent:event]; } @@ -1225,7 +1163,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio return; } } - + // cancel other touch cases preventively if (aDOMEvent.preventDefault) @@ -1908,3 +1846,13 @@ function CPWindowObjectList() return windowObjects; } + +function CPWindowList() +{ + var windowObjectList = CPWindowObjectList(); + + return [windowObjectList arrayByApplyingBlock:function(windowObject) + { + return [windowObject windowNumber]; + }]; +} diff --git a/dist/cappuccino/bin/flatten b/dist/cappuccino/bin/flatten index 80b9ecf8e..1df033252 100755 --- a/dist/cappuccino/bin/flatten +++ b/dist/cappuccino/bin/flatten @@ -233,7 +233,7 @@ ObjectiveJFlattener.prototype.serializeFunctions = function() { var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK var relative = this.rootPath.relative(path).toString(); - this.functionsBuffer.push("ObjectiveJ.FileExecutable._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); + this.functionsBuffer.push("ObjectiveJ.StaticResource._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); } var bundle = this.context.global.CFBundle.bundleContainingURL(path); diff --git a/dist/cappuccino/bin/fontinfo b/dist/cappuccino/bin/fontinfo index 1a91ba1c8040bbcfe787719cb7e44c0e7578d180..f1ff06158a2525d34541aff240713c0067bd8633 100755 GIT binary patch delta 3394 zcmZ`*3s6+o89w`DsqFGx+h670NB67 z0BER_-s?-eqvet#M>G=>)tO2(O5#IV%?yIz_H@(>o1IN<&UUZ~LaljSEpq}>J}u!b zVr7xn4nNt^c`Qus7M+>LN3Ns}%e^# zxB<8vC=Ez+f{-lWvAl8B06|2vfltV9N$Vy>N(-^UM2r-KiqFN|))av-q#`Yxy;N9bap; zbl)tk_TTZ}vjob6WVpw18Q|(jmB3tzoS61`(Bi_#J{o&92UPh z1kMEhI4Gn~<~Ls&Pedbs907*Sm!KzryabTV!B&|iP}1kEA|#@JLkYP4ll)U(?U-NX zAFB>59U7~hoVp=??dgX2CkHG6b144R477I-%gz1=@l%E%bP;IYh+YSQ<68b0W_{yhNVO;gS%)DY9|CFu^Cw_= zYy98vk0JR4z<0VfKmeuBn;21b1(;^GjCA5h9;5Oo%! z21?$Q5G{eau*LtS6tcp9AL}t6+`C0LOp)vbxba+Y#jpKaRba;@D5&``bSwIxt2;VA z7m6Rkl)~s0(;9<9JrV}$1}Ip05ODcBLvl+{l?1>wLG3sMukeR!;;Ta7kO9ZS>T&*{ z{zEzOLM8vvr;O+WxQhNk^R7t8bcndWOpKTit&nIF3W1UdG-GYnqaG}e!0VceCM^D; zqR<$gLFfq+Os>atD9(nKXg?6aV+ z31%YzZ;ceFP(~ny?u;8q%gFHMF*P z*19~KSK6ALWp3At&N8}5o0%b<<}Q0}yUpcs6Pw%aY;nLP>2T5{?Mzys&7!~17U$a9 z+Q4r0w7Xgw%Sc`AGo_MD95AkhP+wdpy{^rjho{UUm=x(7xTUe73@bt6;B-ll&Mk6b zok~V|ozWY{djeYYs%w5>{$De{>i_urm>(8vvT=m;M>tsp975~7 z#q3f}VLXR%BjZaMk6zvlOz&cR7vq%i1C0NM@!vCkj`5p}$4H6gkT{lM4&(C}UyNKl zEc;jK_Gyz6^9p^6wAC}orWEl_k~sS8sTu0k?y#(fqfMs_THkqCezBqDpP?MD&)CBW z&n;VCDOA?*R;!RNthBNPoEn_WNf&anyU_~EP1-duOuVAoolQ=g+evy-I7e5LL;9bp zcZpl2N6!J=GqAsS4{VV74v!Nz8S!0>rjZSKJUTtb6L%kGxNjgw(UZ#2PX;pOpvDj7 zZEQD$6lpz#xJe2p*gUNbq=&dQ(fn*pO|5pcB>cc4xLRBuG^a()t!*Bm#km!21^{Y< z?dt(3`fwo4I~_hNsom+2_*U2)IY80>&K)4@bVU41z^&d{@~((mC%WL|DnN|GIIl{@Jw!dTys4T{x9RzodJaZ z4tRN|17!j|7So@7$|alo*un|T`0c_^6Rs%KW{b_^Bnv#vZFED}sE~LjJrZ7`>?}12 z^j0`ibN5PI2(t0gw9_+V&CZvc1KBrsh$+yI+k6k&Di|x%8J~FIq82mvzXg|eCJcY(|mto zMx}1;LO?SZmK-?9EuXVz>5=5^X}>L7k@!{fg5Ehp)(=xh-#lb|+cpY89Bmol413={ z-q`FoHlKWPG2x=~EpMW(JNwJmf~muwCO=3yz5WY&c7)^fYphqZ_NR{CzQ7&xoUOR< zAJ@MQUj86?*7l0)Tdq}e?|z(>fA3J|=hi;mdw(7(Fm6j396lJl*4(&vgpTzG?tKj) zn+%nU^QL|O$r072fIIj3og;U;LIdSB8QV|n`;}GwcKVJL7_=y2daz~N;7i#ZKlc9g ziFc;uy>z+dxtB-kDrb~^yD_)tRtD4(7aR|Ea`(y$F95qo>p%S?-EgKtQCwuCBWGsE Q%I;p7hEba639vimze2ENzW@LL literal 70736 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 diff --git a/dist/cappuccino/bin/imagesize b/dist/cappuccino/bin/imagesize new file mode 100755 index 0000000000000000000000000000000000000000..5d67a1ce0a9a1e20577f82ccdfe0d3b3e94143b4 GIT binary patch literal 69536 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 Z009sH0T2KI5C8!X009sHfo}nU{{v@rhV}pe literal 0 HcmV?d00001 diff --git a/dist/cappuccino/bin/objj2objcskeleton b/dist/cappuccino/bin/objj2objcskeleton index 51ecab38d..b7972dbb3 100755 --- a/dist/cappuccino/bin/objj2objcskeleton +++ b/dist/cappuccino/bin/objj2objcskeleton @@ -4,11 +4,14 @@ var fs = require("fs"), acorn = require("objj-parser"), - walk = require("acorn-walk"), + walk = require("objj-parser/util/walk"), stream = ObjectiveJ.term; + debugger; + function main(args) { + debugger; args.shift(); if (args.length < 1) @@ -37,8 +40,6 @@ function raise(pos, message) throw syntaxError; } -function ignore(_node, _st, _c) {} - var errors = [], xcc = walk.make( { @@ -114,18 +115,7 @@ var errors = [], else raise(node.loc.start, "Action methods must have exactly one parameter"); } - }, - TypeDefStatement: ignore, - ClassStatement: ignore, - MessageSendExpression: ignore, - GlobalStatement: ignore, - ProtocolDeclarationStatement: ignore, - ArrayLiteral: ignore, - Reference: ignore, - DictionaryLiteral: ignore, - Dereference: ignore, - ImportStatement: ignore, - SelectorLiteralExpression: ignore + } } ); @@ -164,7 +154,7 @@ function parser(args) outputHeaderURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".h"]), outputImplementationURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".m"]), source = fs.readFileSync(sourcePath, { encoding: "utf8" }), - tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath, ecmaVersion: 2022 }), + tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath }), classesInformation = [], ObjectiveCSource = "", ObjectiveCHeader = "", From 223bbc86e1d7400817d5930449c04000f1a13ecd Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 1 Jul 2025 19:42:12 +0200 Subject: [PATCH 006/103] fixed: arrow keys did not work --- AppKit/Platform/DOM/CPPlatformWindow+DOM.j | 256 +++++++++++++-------- 1 file changed, 160 insertions(+), 96 deletions(-) diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index 3fa5546bb..7c8ad47f5 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -20,6 +20,93 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +/* + * THIS DOCUMENTATION STOLEN DIRECTLY FROM GOOGLE CLOSURE (licensed under Apache 2) + * + * Different web browsers have very different keyboard event handling. Most + * importantly is that only certain browsers repeat keydown events: + * IE, Opera, FF/Win32, and Safari 3 repeat keydown events. + * FF/Mac and Safari 2 do not. + * + * For the purposes of this code, "Safari 3" means WebKit 525+, when WebKit + * decided that they should try to match IE's key handling behavior. + * Safari 3.0.4, which shipped with Leopard (WebKit 523), has the + * Safari 2 behavior. + * + * Firefox, Safari, Opera prevent on keypress + * + * IE prevents on keydown + * + * Firefox does not fire keypress for shift, ctrl, alt + * Firefox does fire keydown for shift, ctrl, alt, meta + * Firefox does not repeat keydown for shift, ctrl, alt, meta + * + * Firefox does not fire keypress for up and down in an input + * + * Opera fires keypress for shift, ctrl, alt, meta + * Opera does not repeat keypress for shift, ctrl, alt, meta + * + * Safari 2 and 3 do not fire keypress for shift, ctrl, alt + * Safari 2 does not fire keydown for shift, ctrl, alt + * Safari 3 *does* fire keydown for shift, ctrl, alt + * + * IE provides the keycode for keyup/down events and the charcode (in the + * keycode field) for keypress. + * + * Mozilla provides the keycode for keyup/down and the charcode for keypress + * unless it's a non text modifying key in which case the keycode is provided. + * + * Safari 3 provides the keycode and charcode for all events. + * + * Opera provides the keycode for keyup/down event and either the charcode or + * the keycode (in the keycode field) for keypress events. + * + * Firefox x11 doesn't fire keydown events if a another key is already held down + * until the first key is released. This can cause a key event to be fired with + * a keyCode for the first key and a charCode for the second key. + * + * Safari 2 in keypress (not supported) + * + * charCode keyCode which + * ENTER: 13 13 13 + * F1: 63236 63236 63236 + * F8: 63243 63243 63243 + * ... + * p: 112 112 112 + * P: 80 80 80 + * + * Firefox, keypress: + * + * charCode keyCode which + * ENTER: 0 13 13 + * F1: 0 112 0 + * F8: 0 119 0 + * ... + * p: 112 0 112 + * P: 80 0 80 + * + * Opera, Mac+Win32, keypress: + * + * charCode keyCode which + * ENTER: undefined 13 13 + * F1: undefined 112 0 + * F8: undefined 119 0 + * ... + * p: undefined 112 112 + * P: undefined 80 80 + * + * IE7, keydown + * + * charCode keyCode which + * ENTER: undefined 13 undefined + * F1: undefined 112 undefined + * F8: undefined 119 undefined + * ... + * p: undefined 80 undefined + * P: undefined 80 undefined + */ + @import @import @import @@ -65,29 +152,6 @@ var KeyCodesToPrevent = {}, }, KeyCodesToUnicodeMap = {}; -// New map from event.key to our internal Unicode function keys. -// This is more reliable than mapping from keyCode. -var KeyToUnicodeMapFromKey = { - "Backspace": CPDeleteCharacter, - "Delete": CPDeleteFunctionKey, - "Tab": CPTabCharacter, - "Enter": CPCarriageReturnCharacter, - "Escape": CPEscapeFunctionKey, - "PageUp": CPPageUpFunctionKey, - "PageDown": CPPageDownFunctionKey, - "ArrowLeft": CPLeftArrowFunctionKey, - "ArrowUp": CPUpArrowFunctionKey, - "ArrowRight": CPRightArrowFunctionKey, - "ArrowDown": CPDownArrowFunctionKey, - "Home": CPHomeFunctionKey, - "End": CPEndFunctionKey -}; - -// Map F-keys dynamically. Assumes CP_F1_KEY (0xF704) to CP_F12_KEY (0xF70F) are defined elsewhere. -for (var i = 1; i <= 12; i++) - KeyToUnicodeMapFromKey['F' + i] = 0xF703 + i; - - KeyCodesToPrevent[CPKeyCodes.A] = YES; KeyCodesToAllow[CPKeyCodes.F1] = YES; @@ -627,7 +691,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio timestamp = [CPEvent currentTimestamp], sourceElement = aDOMEvent.target || aDOMEvent.srcElement, windowNumber = [[CPApp keyWindow] windowNumber], - eventKey = aDOMEvent.key, modifierFlags = (aDOMEvent.shiftKey ? CPShiftKeyMask : 0) | (aDOMEvent.ctrlKey ? CPControlKeyMask : 0) | (aDOMEvent.altKey ? CPAlternateKeyMask : 0) | @@ -635,14 +698,22 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio (_capsLockActive ? CPAlphaShiftKeyMask : 0); // With a few exceptions, all key events are blocked from propagating to - // the browser. We check against blacklists and whitelists. + // 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. + StopDOMEventPropagation = YES; - // Use event.key for checking character keys. This is more reliable across keyboard layouts. - var charToTest = (eventKey && eventKey.length === 1) ? eventKey.toLowerCase() : null; - // Make sure it is not in the blacklists. - if (!((charToTest && CharacterKeysToPrevent[charToTest]) || KeyCodesToPrevent[aDOMEvent.keyCode])) + if (!(CharacterKeysToPrevent[String.fromCharCode(aDOMEvent.keyCode || aDOMEvent.charCode).toLowerCase()] || KeyCodesToPrevent[aDOMEvent.keyCode])) { // It is not in the blacklist, let it through if the ctrl/cmd key is // also down or it's in the whitelist. @@ -656,7 +727,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio switch (aDOMEvent.type) { case "keydown": - // Grab and store the keycode for compatibility with other parts of the system. + // 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 @@ -664,68 +735,62 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio var characters; - // Determine characters using modern event.key property first. - if (eventKey) - { - if (eventKey.length === 1) - characters = eventKey; // Printable character, already correctly cased. - else - characters = String.fromCharCode(KeyToUnicodeMapFromKey[eventKey]); // Special key. - } + // 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 - // Fallback for older browsers or unhandled keys. 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 (aDOMEvent.charCode === 0) // Keydown for non-printable keys have charCode === 0 - characters = KeyCodesToUnicodeMap[_keyCode]; - if (!characters) - characters = String.fromCharCode(_keyCode); + _capsLockActive = YES; + + // Make sure the caps lock flag is set in modifierFlags + modifierFlags |= CPAlphaShiftKeyMask; } - // Set characters for the event. event.key is already cased correctly for printable keys. - if (eventKey && eventKey.length === 1) + if ([ModifierKeyCodes containsObject:_keyCode]) { - overrideCharacters = eventKey; - charactersIgnoringModifiers = eventKey.toLowerCase(); - } - else - { - charactersIgnoringModifiers = (characters || "").toLowerCase(); - overrideCharacters = (modifierFlags & CPShiftKeyMask || _capsLockActive) ? (characters || "").toUpperCase() : charactersIgnoringModifiers; - } - - // Handle caps lock state. - if (eventKey === "CapsLock") - { - // This is a simplification; getModifierState("CapsLock") would be ideal but is not used here to avoid further changes. - _capsLockActive = !_capsLockActive; - modifierFlags = (modifierFlags & ~CPAlphaShiftKeyMask) | (_capsLockActive ? CPAlphaShiftKeyMask : 0); - } - - var isModifier = (eventKey === "Control" || eventKey === "Shift" || eventKey === "Alt" || eventKey === "Meta" || eventKey === "CapsLock"); - if (isModifier) - { - // A modifier key will never fire keypress. We fire a CPFlagsChanged event and break. + // 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]; + break; } else if (modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) { - // Let Cmd/Ctrl combinations be sent on keydown to allow for early cancellation. + //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, eventKey, _lastKey, aDOMEvent.shiftKey, aDOMEvent.ctrlKey, aDOMEvent.altKey)) + else if (CPKeyCodes.firesKeyPressEvent(_keyCode, aDOMEvent.key, _lastKey, aDOMEvent.shiftKey, aDOMEvent.ctrlKey, aDOMEvent.altKey)) { - // This branch is for keys that fire a `keypress` event. - // We allow propagation to let the browser handle input in text fields. + // 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; } + else + { + //this branch is taken by "remedial" key events + // In this state we continue to keypress and send the CPEvent + } case "keypress": - // We unconditionally break on keypress events with modifiers, - // as we forced the event to be sent on the keydown. + // 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; @@ -736,20 +801,15 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio _lastKey = keyCode; _charCodes[keyCode] = charCode; - // Use the character determined during keydown if available. var characters = overrideCharacters; - if (!characters) - { - // Fallback for printable keys on keypress. - if (eventKey && eventKey.length === 1) - characters = eventKey; - else if (charCode !== 0) - characters = String.fromCharCode(charCode); - else - characters = KeyCodesToUnicodeMap[keyCode] || ""; - } + // Is this a special key? + if (!characters && (aDOMEvent.which === 0 || aDOMEvent.charCode === 0)) + characters = KeyCodesToUnicodeMap[charCode]; - charactersIgnoringModifiers = characters.toLowerCase(); + 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)) @@ -758,6 +818,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio event = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:charCode]; + break; case "keyup": @@ -768,26 +829,26 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio _lastKey = -1; _charCodes[keyCode] = nil; - if (eventKey === "CapsLock") + // check for caps lock state + if (keyCode === CPKeyCodes.CAPS_LOCK) { - // The state was handled on keydown. Nothing to do for state change on keyup for a toggle key. + _capsLockActive = NO; + + // Make sure the caps lock flag is cleared in modifierFlags + modifierFlags &= ~CPAlphaShiftKeyMask; } - var isModifier = (eventKey === "Control" || eventKey === "Shift" || eventKey === "Alt" || eventKey === "Meta" || eventKey === "CapsLock"); - if (isModifier) + 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]; + break; } - var characters; - if (eventKey && eventKey.length === 1) - characters = eventKey; - else - characters = KeyCodesToUnicodeMap[keyCode] || String.fromCharCode(charCode) || ""; - + var characters = KeyCodesToUnicodeMap[charCode] || String.fromCharCode(charCode); charactersIgnoringModifiers = characters.toLowerCase(); if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive) @@ -796,6 +857,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]; + break; } @@ -807,6 +869,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio if (event && ![_platformPasteboard windowShouldSuppressKeyEvent]) { [CPApp sendEvent:event]; + [_platformPasteboard windowDidSendKeyEvent:event]; } @@ -1163,7 +1226,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio return; } } - + // cancel other touch cases preventively if (aDOMEvent.preventDefault) @@ -1856,3 +1919,4 @@ function CPWindowList() return [windowObject windowNumber]; }]; } + From 5ecdaafadfa5f0413415ea7eda0ffc6550bc35d1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 1 Jul 2025 20:23:29 +0200 Subject: [PATCH 007/103] fixed: arrow keys did not work --- AppKit/Platform/DOM/CPPlatformWindow+DOM.j | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index 7c8ad47f5..2ecd0c48d 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -736,8 +736,9 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio 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)) + // This condition is for identifying non-printing keys based on the keydown event. + // We've replaced the deprecated '.which' with a direct check on charCode. + if (!aDOMEvent.charCode) characters = KeyCodesToUnicodeMap[_keyCode]; // The problem with keyCode is that this property refers to keys on the keyboard and not to characters @@ -802,11 +803,19 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio _charCodes[keyCode] = charCode; var characters = overrideCharacters; - // Is this a special key? - if (!characters && (aDOMEvent.which === 0 || aDOMEvent.charCode === 0)) - characters = KeyCodesToUnicodeMap[charCode]; - if (!characters) + // This condition is met for non-printing keys (like Enter, Tab, arrows) that fall through + // from the 'keydown' case, as their charCode is 0 in the keydown DOM event. + if (!characters && (!aDOMEvent.charCode || aDOMEvent.charCode === 0)) + characters = KeyCodesToUnicodeMap[charCode]; // Note: for fall-through, charCode is actually the keyCode from the keydown event. + + // For modern browsers, event.key is the most reliable way to get the actual character, + // especially for international keyboards. We only use it for single-character keys. + if (!characters && aDOMEvent.key && aDOMEvent.key.length === 1) + characters = aDOMEvent.key; + + // Fallback for older browsers that support charCode but not key. + if (!characters && charCode > 0) characters = String.fromCharCode(charCode); charactersIgnoringModifiers = characters.toLowerCase(); // FIXME: This isn't correct. It SHOULD include Shift. From b1d96dfdee03abd4378aeefb9124f6dddaf66785 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 1 Jul 2025 21:53:53 +0200 Subject: [PATCH 008/103] improved reliance of key --- AppKit/Platform/DOM/CPPlatformWindow+DOM.j | 38 +++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index 2ecd0c48d..dc134912e 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -505,6 +505,8 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio _DOMBodyElement.ondrag = function () { return NO; }; _DOMBodyElement.onselectstart = function () { return _DOMWindow.event.srcElement === _platformPasteboard._DOMPasteboardElement; }; + + _DOMWindow.attachEvent("onunload", function() { _DOMWindow.detachEvent("unload", arguments.callee); @@ -713,6 +715,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio StopDOMEventPropagation = YES; // Make sure it is not in the blacklists. + // Note: keyCode is deprecated but is kept here for legacy compatibility with the KeyCodesToPrevent/Allow maps. if (!(CharacterKeysToPrevent[String.fromCharCode(aDOMEvent.keyCode || aDOMEvent.charCode).toLowerCase()] || KeyCodesToPrevent[aDOMEvent.keyCode])) { // It is not in the blacklist, let it through if the ctrl/cmd key is @@ -727,7 +730,8 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio switch (aDOMEvent.type) { case "keydown": - // Grab and store the keycode now since it is correct and consistent at this point. + // Grab and store the keyCode now since it is correct and consistent at this point. + // Note: keyCode is deprecated but is required for the existing compatibility logic. if (aDOMEvent.keyCode in MozKeyCodeToKeyCodeMap) _keyCode = MozKeyCodeToKeyCodeMap[aDOMEvent.keyCode]; else @@ -737,28 +741,22 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio // Handle key codes for which String.fromCharCode won't work. // This condition is for identifying non-printing keys based on the keydown event. - // We've replaced the deprecated '.which' with a direct check on charCode. if (!aDOMEvent.charCode) 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 - + // The problem with the deprecated `keyCode` is that it refers to keys on the keyboard, not characters. + // This is why String.fromCharCode does not always work, e.g. on international layouts. + // The modern `key` property provides the actual character, so we prefer it when available. if (!characters) - characters = (aDOMEvent.key && aDOMEvent.key.length == 1) ? aDOMEvent.key.toLowerCase() : String.fromCharCode(_keyCode).toLowerCase(); + 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) { - _capsLockActive = YES; - - // Make sure the caps lock flag is set in modifierFlags - modifierFlags |= CPAlphaShiftKeyMask; + // The original logic was incorrect, treating Caps Lock as a momentary key. + // It is now correctly handled as a toggle on keyup. } if ([ModifierKeyCodes containsObject:_keyCode]) @@ -814,7 +812,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio if (!characters && aDOMEvent.key && aDOMEvent.key.length === 1) characters = aDOMEvent.key; - // Fallback for older browsers that support charCode but not key. + // Fallback for older browsers that use the deprecated charCode property. if (!characters && charCode > 0) characters = String.fromCharCode(charCode); @@ -838,13 +836,16 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio _lastKey = -1; _charCodes[keyCode] = nil; - // check for caps lock state + // check for caps lock state toggle if (keyCode === CPKeyCodes.CAPS_LOCK) { - _capsLockActive = NO; + _capsLockActive = !_capsLockActive; - // Make sure the caps lock flag is cleared in modifierFlags - modifierFlags &= ~CPAlphaShiftKeyMask; + // Update modifierFlags to reflect the new state of Caps Lock for this event + if (_capsLockActive) + modifierFlags |= CPAlphaShiftKeyMask; + else + modifierFlags &= ~CPAlphaShiftKeyMask; } if ([ModifierKeyCodes containsObject:keyCode]) @@ -1928,4 +1929,3 @@ function CPWindowList() return [windowObject windowNumber]; }]; } - From b3cb22caef0a7b8b7492470254246e8093253771 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 1 Jul 2025 22:16:33 +0200 Subject: [PATCH 009/103] more modernisations --- AppKit/Platform/DOM/CPPlatformWindow+DOM.j | 221 ++++++++++----------- 1 file changed, 104 insertions(+), 117 deletions(-) diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index dc134912e..6439aa5da 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -192,6 +192,32 @@ 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; +KeyNameToUnicodeMap[";"] = ";"; +KeyNameToUnicodeMap["-"] = "-"; +KeyNameToUnicodeMap["="] = "="; +KeyNameToUnicodeMap[","] = ","; +KeyNameToUnicodeMap["."] = "."; +KeyNameToUnicodeMap["/"] = "/"; +KeyNameToUnicodeMap["`"] = "`"; +KeyNameToUnicodeMap["'"] = "'"; +KeyNameToUnicodeMap["["] = "["; +KeyNameToUnicodeMap["\\"] = "\\"; +KeyNameToUnicodeMap["]"] = "]"; + var ModifierKeyCodes = [ CPKeyCodes.META, CPKeyCodes.WEBKIT_RIGHT_META, @@ -427,7 +453,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}); @@ -459,7 +485,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); @@ -492,7 +517,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio theDocument.attachEvent("onkeyup", keyEventCallback); theDocument.attachEvent("onkeydown", keyEventCallback); - theDocument.attachEvent("onkeypress", keyEventCallback); + // "onkeypress" listener removed. _DOMWindow.attachEvent("onresize", resizeEventCallback); @@ -505,8 +530,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio _DOMBodyElement.ondrag = function () { return NO; }; _DOMBodyElement.onselectstart = function () { return _DOMWindow.event.srcElement === _platformPasteboard._DOMPasteboardElement; }; - - _DOMWindow.attachEvent("onunload", function() { _DOMWindow.detachEvent("unload", arguments.callee); @@ -524,7 +547,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio theDocument.detachEvent("onkeyup", keyEventCallback); theDocument.detachEvent("onkeydown", keyEventCallback); - theDocument.detachEvent("onkeypress", keyEventCallback); _DOMWindow.detachEvent("onresize", resizeEventCallback); @@ -700,165 +722,130 @@ _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. - // Note: keyCode is deprecated but is kept here for legacy compatibility with the KeyCodesToPrevent/Allow maps. - 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 = @""; + // Grab and store the keyCode for mapping and compatibility. + // This property is deprecated but necessary for the key maps. + var keyCode = aDOMEvent.keyCode; + if (keyCode in MozKeyCodeToKeyCodeMap) + keyCode = MozKeyCodeToKeyCodeMap[keyCode]; + switch (aDOMEvent.type) { case "keydown": - // Grab and store the keyCode now since it is correct and consistent at this point. - // Note: keyCode is deprecated but is required for the existing compatibility logic. - 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. - // This condition is for identifying non-printing keys based on the keydown event. - if (!aDOMEvent.charCode) - characters = KeyCodesToUnicodeMap[_keyCode]; - - // The problem with the deprecated `keyCode` is that it refers to keys on the keyboard, not characters. - // This is why String.fromCharCode does not always work, e.g. on international layouts. - // The modern `key` property provides the actual character, so we prefer it when available. - 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) + // For modifier keys, we create a CPFlagsChanged event and stop processing. + if ([ModifierKeyCodes containsObject:keyCode]) { - // The original logic was incorrect, treating Caps Lock as a momentary key. - // It is now correctly handled as a toggle on keyup. - } - - 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]; + break; + } - break; - } - else if (modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) + // Determine if the event is a repeat. Use the modern `event.repeat` property + // with a fallback to our manual state tracking for older browsers. + var isARepeat = !!aDOMEvent.repeat || (_charCodes[keyCode] != nil); + _charCodes[keyCode] = YES; // Mark the key as down for fallback repeat detection. + + // Determine the character for the event. + // Priority 1: Use the modern `event.key` property. It's the most reliable. + 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) + { + // This is a printing character (e.g., "a", "P", "#"). + characters = aDOMEvent.key; + } + else + { + // This is a named, non-printing key (e.g., "Enter", "ArrowLeft"). + // Map the standard key name to our framework's internal constant. + characters = KeyNameToUnicodeMap[aDOMEvent.key] || aDOMEvent.key; + } } + // Priority 2: Fallback for older browsers without `event.key`. else { - //this branch is taken by "remedial" key events - // In this state we continue to keypress and send the CPEvent + // First, check if it's a known non-printing key in our legacy map. + characters = KeyCodesToUnicodeMap[keyCode]; + + // If not, it's likely a printing character. Use the deprecated fromCharCode. + // This is less reliable for international layouts but is the best fallback. + if (!characters) + { + characters = String.fromCharCode(keyCode); + // Manually handle capitalization for this fallback path. + 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; - - // This condition is met for non-printing keys (like Enter, Tab, arrows) that fall through - // from the 'keydown' case, as their charCode is 0 in the keydown DOM event. - if (!characters && (!aDOMEvent.charCode || aDOMEvent.charCode === 0)) - characters = KeyCodesToUnicodeMap[charCode]; // Note: for fall-through, charCode is actually the keyCode from the keydown event. - - // For modern browsers, event.key is the most reliable way to get the actual character, - // especially for international keyboards. We only use it for single-character keys. - if (!characters && aDOMEvent.key && aDOMEvent.key.length === 1) - characters = aDOMEvent.key; - - // Fallback for older browsers that use the deprecated charCode property. - if (!characters && charCode > 0) - 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(); + // This is a simplification; a fully correct implementation would require extensive mapping. + 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]; break; case "keyup": - var keyCode = aDOMEvent.keyCode, - charCode = _charCodes[keyCode]; - - _keyCode = -1; - _lastKey = -1; + // Clear the key's state for our fallback repeat detection. _charCodes[keyCode] = nil; - // check for caps lock state toggle + // Handle the toggling of Caps Lock state. if (keyCode === CPKeyCodes.CAPS_LOCK) { _capsLockActive = !_capsLockActive; - - // Update modifierFlags to reflect the new state of Caps Lock for this event + // Update modifierFlags to reflect the new state for this event. if (_capsLockActive) modifierFlags |= CPAlphaShiftKeyMask; else modifierFlags &= ~CPAlphaShiftKeyMask; } + // For modifier keys, create a CPFlagsChanged event and stop. 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]; break; } - var characters = KeyCodesToUnicodeMap[charCode] || String.fromCharCode(charCode); + // Determine the character for the keyup event using the same logic as keydown + // for consistency, as this event no longer has access to `keypress` state. + if (aDOMEvent.key) + { + if (aDOMEvent.key.length === 1) + characters = aDOMEvent.key; + else + characters = KeyNameToUnicodeMap[aDOMEvent.key] || aDOMEvent.key; + } + else + { + characters = KeyCodesToUnicodeMap[keyCode] || String.fromCharCode(keyCode); + } + charactersIgnoringModifiers = characters.toLowerCase(); if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive) @@ -884,7 +871,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio } var didStop = NO; - // Platform pasteboard can overrule the decision to stop propagation either way, or it might have no opinion. + // Platform pasteboard can overrule the decision to stop propagation. if ([_platformPasteboard windowShouldStopPropagation] || (StopDOMEventPropagation && ![_platformPasteboard windowShouldNotStopPropagation])) { didStop = YES; From f4ac0630b8e2b9c826962bad7ed71504358d9cc5 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 1 Jul 2025 22:25:20 +0200 Subject: [PATCH 010/103] formatting --- AppKit/Platform/DOM/CPPlatformWindow+DOM.j | 1 + 1 file changed, 1 insertion(+) diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index 6439aa5da..a4fc8cae8 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -206,6 +206,7 @@ KeyNameToUnicodeMap["ArrowRight"] = CPRightArrowFunctionKey; KeyNameToUnicodeMap["ArrowDown"] = CPDownArrowFunctionKey; KeyNameToUnicodeMap["Home"] = CPHomeFunctionKey; KeyNameToUnicodeMap["End"] = CPEndFunctionKey; +// Add safeguards for punctuation KeyNameToUnicodeMap[";"] = ";"; KeyNameToUnicodeMap["-"] = "-"; KeyNameToUnicodeMap["="] = "="; From 1226f5120cd7aa5fbc5bb1018272f00f13732709 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 1 Jul 2025 22:55:53 +0200 Subject: [PATCH 011/103] improved: deadkey management --- AppKit/CPTextView/CPTextView.j | 232 +++++++++++++++------------------ 1 file changed, 103 insertions(+), 129 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 56a4296f5..4a8d0bc3a 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2626,9 +2626,6 @@ var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey", var _CPNativeInputField, - _CPNativeInputFieldKeyDownCalled, - _CPNativeInputFieldKeyUpCalled, - _CPNativeInputFieldKeyPressedCalled, _CPNativeInputFieldActive; var _CPCopyPlaceholder = '-'; @@ -2639,22 +2636,26 @@ var _CPCopyPlaceholder = '-'; { return _CPNativeInputFieldActive; } + + (void)isDeadKey:(CPEvent)event { #if PLATFORM(DOM) - return event._DOMEvent && (event._DOMEvent.key == 'Dead' || event._DOMEvent.key == 'Process'); + // This identifies dead key events during the keydown phase on some platforms/layouts. + return event._DOMEvent && (event._DOMEvent.key === 'Dead' || event._DOMEvent.key === 'Process'); #endif - return NO; } + + (void)cancelCurrentNativeInputSession { + if (!_CPNativeInputFieldActive) + return; #if PLATFORM(DOM) _CPNativeInputField.innerHTML = ''; #endif - [self _endInputSessionWithString:_CPNativeInputField.innerHTML]; + [self _endInputSessionWithString:@""]; } + (void)cancelCurrentInputSessionIfNeeded @@ -2668,17 +2669,25 @@ var _CPCopyPlaceholder = '-'; + (void)_endInputSessionWithString:(CPString)aStr { _CPNativeInputFieldActive = NO; + var currentFirstResponder = [[CPApp keyWindow] firstResponder]; - var currentFirstResponder = [[CPApp keyWindow] firstResponder], - placeholderRange = CPMakeRange([currentFirstResponder selectedRange].location - 1, 1); + if (currentFirstResponder && [currentFirstResponder respondsToSelector:@selector(insertText:)]) + { + var placeholderRange = CPMakeRange([currentFirstResponder selectedRange].location - 1, 1); + [currentFirstResponder setSelectedRange:placeholderRange]; - [currentFirstResponder setSelectedRange:placeholderRange]; - [currentFirstResponder insertText:aStr]; + if(aStr) + [currentFirstResponder insertText:aStr]; + } + +#if PLATFORM(DOM) _CPNativeInputField.innerHTML = ''; - +#endif [self hideInputElement]; - [currentFirstResponder updateInsertionPointStateAndRestartTimer:YES]; + + if (currentFirstResponder) + [currentFirstResponder updateInsertionPointStateAndRestartTimer:YES]; } + (void)initialize @@ -2697,95 +2706,59 @@ var _CPCopyPlaceholder = '-'; document.body.appendChild(_CPNativeInputField); - _CPNativeInputField.addEventListener("keyup", function(e) - { - _CPNativeInputFieldKeyUpCalled = YES; - - // Filter out non-printable keys like modifiers, cursor keys, etc. - // A key with a name longer than one character is typically a non-printable control key. - // We exclude 'Dead' and 'Process' which are handled as part of dead-key composition. - if (e.key.length > 1 && e.key !== 'Dead' && e.key !== 'Process') - { - if (e.key === 'Enter') - _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); - + // The 'keydown' event is used to detect non-printable/control keys + // that should either be handled by the text view directly or should + // cancel the native input session. _CPNativeInputField.addEventListener("keydown", function(e) { - // this protects from heavy typing and the shift key - if (_CPNativeInputFieldKeyDownCalled) - return true; + // Filter out non-printable keys like modifiers, cursor keys, etc. + // A key with a name longer than one character is typically a non-printable control key. + if (e.key.length > 1 && e.key !== 'Dead' && e.key !== 'Process') + { + if (e.key === 'Enter' || e.key === 'Escape') + [self cancelCurrentInputSessionIfNeeded]; + + // For backspace, if the field is empty, cancel the session. + // Otherwise, let the 'input' event handle the change. + if (e.key === 'Backspace' && _CPNativeInputField.innerHTML.length === 0) + [self cancelCurrentInputSessionIfNeeded]; + + // Let the browser handle other control keys within the contentEditable, + // but don't propagate to our text view. + return; + } - _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.key.startsWith('Arrow')) - _CPNativeInputFieldKeyPressedCalled = YES; - if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)]) return; - // FF-trigger: here the best way to detect a dead key is the missing keyup event - if (CPBrowserIsEngine(CPGeckoBrowserEngine)) - setTimeout(function(){ - _CPNativeInputFieldKeyDownCalled = NO; + // If not already active, start a new input session. + if (!_CPNativeInputFieldActive) + { + _CPNativeInputFieldActive = YES; + [currentFirstResponder _activateNativeInputElement:_CPNativeInputField]; + } - 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); + }, true); - return false; - }, true); // capture mode - - _CPNativeInputField.addEventListener("keypress", function(e) + // The 'input' event is the modern, unified way to handle all character input. + // It fires after a regular keypress, a dead key composition, or an IME insertion. + // This replaces the need for complex keypress/keyup timing logic. + _CPNativeInputField.addEventListener("input", function(e) { - _CPNativeInputFieldKeyUpCalled = YES; - _CPNativeInputFieldKeyPressedCalled = YES; - return false; + var inputText = _CPNativeInputField.innerHTML; - }, true); // capture mode + // An empty input could be from a backspace, so we just end the session. + // We pass the final content of the div to the handler. + [self _endInputSessionWithString:inputText]; + + }, true); _CPNativeInputField.onpaste = function(e) { + e.preventDefault(); + var nativeClipboard = (e.originalEvent || e).clipboardData, richtext, pasteboard = [CPPasteboard generalPasteboard], @@ -2795,21 +2768,18 @@ var _CPCopyPlaceholder = '-'; 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) + // Rich text path for browsers supporting text/rtf + // handle shift key to trigger plain text paste + if (!isPlain && (richtext = nativeClipboard.getData('text/rtf')) && !(!!(e.originalEvent || e).shiftKey)) { - e.preventDefault(); - // setTimeout to prevent flickering in FF setTimeout(function(){ [currentFirstResponder insertText:[[_CPRTFParser new] parseRTF:richtext]] }, 20); - - return false; + return; } - // plain is the same in all browsers... - + // Plain text for all other cases var data = e.clipboardData.getData('text/plain'), cappString = [pasteboard stringForType:CPStringPboardType]; @@ -2819,50 +2789,54 @@ var _CPCopyPlaceholder = '-'; [pasteboard setString:data forType:CPStringPboardType]; } - setTimeout(function(){ // prevent dom-flickering (only needed for FF) + setTimeout(function(){ // prevent dom-flickering [currentFirstResponder paste:self]; }, 20); - - return false; }; - if (CPBrowserIsEngine(CPGeckoBrowserEngine)) + // Unify oncopy/oncut for all browsers + _CPNativeInputField.oncopy = function(e) { - _CPNativeInputField.oncopy = function(e) - { - var pasteboard = [CPPasteboard generalPasteboard], - string, - currentFirstResponder = [[CPApp keyWindow] firstResponder]; + e.preventDefault(); + var pasteboard = [CPPasteboard generalPasteboard], + currentFirstResponder = [[CPApp keyWindow] firstResponder]; - [currentFirstResponder copy:self]; + [currentFirstResponder copy:self]; - var stringForPasting = [pasteboard stringForType:CPStringPboardType]; - e.clipboardData.setData('text/plain', stringForPasting); + 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; + var rtfForPasting = [pasteboard stringForType:CPRTFPboardType]; + if (rtfForPasting) { + e.clipboardData.setData('text/rtf', rtfForPasting); } - } + }; + + _CPNativeInputField.oncut = function(e) + { + e.preventDefault(); + var pasteboard = [CPPasteboard generalPasteboard], + currentFirstResponder = [[CPApp keyWindow] firstResponder]; + + // This is necessary because cut will only execute in the future. + // We copy first to populate the clipboard data for the event. + [currentFirstResponder copy:self]; + + var stringForPasting = [pasteboard stringForType:CPStringPboardType]; + e.clipboardData.setData('text/plain', stringForPasting); + var rtfForPasting = [pasteboard stringForType:CPRTFPboardType]; + + if (rtfForPasting) + { + e.clipboardData.setData('text/rtf', rtfForPasting); + } + + // Then, perform the actual cut operation from the text view. + // setTimeout prevents DOM flickering. + setTimeout(function(){ + [currentFirstResponder cut:self]; + }, 20); + }; #endif } From 266965f5061970fa4043c3815b54424ed692f0be Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 2 Jul 2025 07:50:46 +0200 Subject: [PATCH 012/103] improved: native input manager --- AppKit/CPTextView/CPTextView.j | 289 +++++++++++---------------------- 1 file changed, 92 insertions(+), 197 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 4a8d0bc3a..c2357c51d 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -1012,35 +1012,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]; @@ -1054,7 +1025,12 @@ Sets the selection to a range of characters in response to user action. [[_window platformWindow] _propagateCurrentDOMEvent:YES]; // for the _CPNativeInputManager (necessary at least on FF and chrome) - if (![_CPNativeInputManager isNativeInputFieldActive] && ![_CPNativeInputManager isDeadKey:event]) + // Only call interpretKeyEvents for non-printable keys (navigation, commands). + // Printable characters are handled exclusively by _CPNativeInputManager. + + var key = event.key; + + if (key && (key.length > 1 || (event.modifierFlags & (CPCommandKeyMask | CPAlternateKeyMask | CPControlKeyMask)))) [self interpretKeyEvents:[event]]; [_caret setPermanentlyVisible:YES]; @@ -2626,68 +2602,35 @@ var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey", var _CPNativeInputField, - _CPNativeInputFieldActive; + _isComposing = NO; // Flag to track if an IME/dead key session is active. var _CPCopyPlaceholder = '-'; @implementation _CPNativeInputManager : CPObject +// This method is no longer used by the new design, but we keep it +// for any other part of the system that might call it. + (BOOL)isNativeInputFieldActive { - return _CPNativeInputFieldActive; + return NO; } + (void)isDeadKey:(CPEvent)event { #if PLATFORM(DOM) - // This identifies dead key events during the keydown phase on some platforms/layouts. return event._DOMEvent && (event._DOMEvent.key === 'Dead' || event._DOMEvent.key === 'Process'); #endif return NO; } -+ (void)cancelCurrentNativeInputSession -{ - if (!_CPNativeInputFieldActive) - return; - -#if PLATFORM(DOM) - _CPNativeInputField.innerHTML = ''; -#endif - - [self _endInputSessionWithString:@""]; -} - + (void)cancelCurrentInputSessionIfNeeded { - if (!_CPNativeInputFieldActive) - return; - - [self cancelCurrentNativeInputSession]; -} - -+ (void)_endInputSessionWithString:(CPString)aStr -{ - _CPNativeInputFieldActive = NO; - var currentFirstResponder = [[CPApp keyWindow] firstResponder]; - - if (currentFirstResponder && [currentFirstResponder respondsToSelector:@selector(insertText:)]) - { - var placeholderRange = CPMakeRange([currentFirstResponder selectedRange].location - 1, 1); - [currentFirstResponder setSelectedRange:placeholderRange]; - - if(aStr) - [currentFirstResponder insertText:aStr]; - } - #if PLATFORM(DOM) - _CPNativeInputField.innerHTML = ''; + if (_CPNativeInputField) { + _CPNativeInputField.innerHTML = ''; + } + _isComposing = NO; #endif - - [self hideInputElement]; - - if (currentFirstResponder) - [currentFirstResponder updateInsertionPointStateAndRestartTimer:YES]; } + (void)initialize @@ -2695,115 +2638,89 @@ 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); - // The 'keydown' event is used to detect non-printable/control keys - // that should either be handled by the text view directly or should - // cancel the native input session. - _CPNativeInputField.addEventListener("keydown", function(e) + // Central function to handle inserting text into the CPTextView + var handleInput = function(textToInsert) { - // Filter out non-printable keys like modifiers, cursor keys, etc. - // A key with a name longer than one character is typically a non-printable control key. - if (e.key.length > 1 && e.key !== 'Dead' && e.key !== 'Process') - { - if (e.key === 'Enter' || e.key === 'Escape') - [self cancelCurrentInputSessionIfNeeded]; - - // For backspace, if the field is empty, cancel the session. - // Otherwise, let the 'input' event handle the change. - if (e.key === 'Backspace' && _CPNativeInputField.innerHTML.length === 0) - [self cancelCurrentInputSessionIfNeeded]; - - // Let the browser handle other control keys within the contentEditable, - // but don't propagate to our text view. + if (!textToInsert) return; - } var currentFirstResponder = [[CPApp keyWindow] firstResponder]; - if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)]) + if (currentFirstResponder && [currentFirstResponder respondsToSelector:@selector(insertText:)]) + [currentFirstResponder insertText:textToInsert]; + + // CRUCIAL: Clear the field immediately after grabbing its content. + _CPNativeInputField.innerHTML = ''; + }; + + // Fires for simple key presses (a, b, 1, 2) + _CPNativeInputField.addEventListener('input', function(e) { + // If we are in a composition (e.g., IME), we do nothing. + // We wait for 'compositionend' to get the final, complete text. + if (_isComposing) { return; - - // If not already active, start a new input session. - if (!_CPNativeInputFieldActive) - { - _CPNativeInputFieldActive = YES; - [currentFirstResponder _activateNativeInputElement:_CPNativeInputField]; } + // If not composing, this is a simple character. Handle it immediately. + handleInput(e.target.innerHTML); + }); - }, true); + // Fires when a composition session starts (e.g., user presses a dead key or starts an IME). + _CPNativeInputField.addEventListener('compositionstart', function(e) { + _isComposing = YES; + }); - // The 'input' event is the modern, unified way to handle all character input. - // It fires after a regular keypress, a dead key composition, or an IME insertion. - // This replaces the need for complex keypress/keyup timing logic. - _CPNativeInputField.addEventListener("input", function(e) - { - var inputText = _CPNativeInputField.innerHTML; - - // An empty input could be from a backspace, so we just end the session. - // We pass the final content of the div to the handler. - [self _endInputSessionWithString:inputText]; - - }, true); + // 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 richtext; + var currentFirstResponder = [[CPApp keyWindow] firstResponder]; + var isPlain = ![currentFirstResponder isRichText]; - 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; - - // Rich text path for browsers supporting text/rtf - // handle shift key to trigger plain text paste - if (!isPlain && (richtext = nativeClipboard.getData('text/rtf')) && !(!!(e.originalEvent || e).shiftKey)) - { - // setTimeout to prevent flickering in FF - setTimeout(function(){ + // Correctly check for shift key to force plain text paste. + if (!isPlain && !e.shiftKey && (richtext = nativeClipboard.getData('text/rtf'))) { + setTimeout(function() { [currentFirstResponder insertText:[[_CPRTFParser new] parseRTF:richtext]] - }, 20); + }, 0); return; } - // Plain text for all other cases - 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 - [currentFirstResponder paste:self]; - }, 20); + var data = nativeClipboard.getData('text/plain'); + [currentFirstResponder insertText:data]; }; - // Unify oncopy/oncut for all browsers + // COPY handler _CPNativeInputField.oncopy = function(e) { e.preventDefault(); - var pasteboard = [CPPasteboard generalPasteboard], - currentFirstResponder = [[CPApp keyWindow] firstResponder]; + var pasteboard = [CPPasteboard generalPasteboard]; + var currentFirstResponder = [[CPApp keyWindow] firstResponder]; - [currentFirstResponder copy:self]; + [currentFirstResponder copy:self]; // This populates the CP pasteboard - var stringForPasting = [pasteboard stringForType:CPStringPboardType]; + var stringForPasting = [pasteboard stringForType:CPStringPboardType] || ''; e.clipboardData.setData('text/plain', stringForPasting); var rtfForPasting = [pasteboard stringForType:CPRTFPboardType]; @@ -2812,85 +2729,63 @@ var _CPCopyPlaceholder = '-'; } }; + // CUT handler _CPNativeInputField.oncut = function(e) { e.preventDefault(); - var pasteboard = [CPPasteboard generalPasteboard], - currentFirstResponder = [[CPApp keyWindow] firstResponder]; + var pasteboard = [CPPasteboard generalPasteboard]; + var currentFirstResponder = [[CPApp keyWindow] firstResponder]; - // This is necessary because cut will only execute in the future. - // We copy first to populate the clipboard data for the event. + // First, copy the data to populate the clipboard [currentFirstResponder copy:self]; - var stringForPasting = [pasteboard stringForType:CPStringPboardType]; + var stringForPasting = [pasteboard stringForType:CPStringPboardType] || ''; e.clipboardData.setData('text/plain', stringForPasting); var rtfForPasting = [pasteboard stringForType:CPRTFPboardType]; - - if (rtfForPasting) - { + if (rtfForPasting) { e.clipboardData.setData('text/rtf', rtfForPasting); } - // Then, perform the actual cut operation from the text view. - // setTimeout prevents DOM flickering. - setTimeout(function(){ - [currentFirstResponder cut:self]; - }, 20); + // Then, perform the delete part of the cut operation in the text view + [currentFirstResponder delete:self]; }; #endif } + (void)focusForTextView:(CPTextView)currentFirstResponder { - if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)]) - return; - - [self hideInputElement]; - #if PLATFORM(DOM) - _CPNativeInputField.focus(); + // The field no longer needs to move. It just needs focus. + 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] 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 From 6b2494ede49b6284df507b918aed7b320c38ae4e Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 2 Jul 2025 19:54:52 +0200 Subject: [PATCH 013/103] fixed: arrow navigation --- AppKit/CPEvent.j | 24 ++++++- AppKit/CPTextView/CPTextView.j | 23 ++++--- AppKit/Platform/DOM/CPPlatformWindow+DOM.j | 75 +++++++++++++--------- 3 files changed, 78 insertions(+), 44 deletions(-) diff --git a/AppKit/CPEvent.j b/AppKit/CPEvent.j index 7bc06d501..41d1cefef 100644 --- a/AppKit/CPEvent.j +++ b/AppKit/CPEvent.j @@ -70,6 +70,7 @@ var _CPEventPeriodicEventPeriod = 0, BOOL _isARepeat; unsigned _keyCode; DOMEvent _DOMEvent; + BOOL _isActionKey; int _data1; int _data2; short _subtype; @@ -110,17 +111,18 @@ 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]; } /*! @@ -252,7 +254,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 +266,7 @@ var _CPEventPeriodicEventPeriod = 0, _charactersIgnoringModifiers = unmodCharacters; _isARepeat = isARepeat; _keyCode = code; + _isActionKey = isAnActionKey; _windowNumber = aWindowNumber; } @@ -571,6 +574,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 | CPAlternateKeyMask)) || + + // Is it a key that doesn't produce a character? + ([_characters length] === 0) || + + // Was it identified as an action key by the platform-specific layer? + _isActionKey + ); +} /*! Return YES if this event is a part of processing a browser controlled cut or paste event where the browser will go ahead and do the work of cutting or pasting within the input diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index c2357c51d..938cc7c32 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -1022,16 +1022,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) - - // Only call interpretKeyEvents for non-printable keys (navigation, commands). - // Printable characters are handled exclusively by _CPNativeInputManager. - - var key = event.key; - - if (key && (key.length > 1 || (event.modifierFlags & (CPCommandKeyMask | CPAlternateKeyMask | CPControlKeyMask)))) + 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]; } @@ -2661,7 +2663,10 @@ var _CPCopyPlaceholder = '-'; var currentFirstResponder = [[CPApp keyWindow] firstResponder]; if (currentFirstResponder && [currentFirstResponder respondsToSelector:@selector(insertText:)]) - [currentFirstResponder insertText:textToInsert]; + // setTimeout to prevent flickering + setTimeout(function(){ + [currentFirstResponder insertText:textToInsert] + }, 20); // CRUCIAL: Clear the field immediately after grabbing its content. _CPNativeInputField.innerHTML = ''; diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index a4fc8cae8..ece9c92c3 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -725,16 +725,13 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio // With a few exceptions, all key events are blocked from propagating to // the browser. The logic here allows browser shortcuts (Cmd/Ctrl keys) // and function keys (F1-F12) to pass through, unless explicitly blacklisted. - StopDOMEventPropagation = YES; + StopDOMEventPropagation = YES; var keyCodeForPropagationCheck = aDOMEvent.keyCode || 0; var charForPropagationCheck = String.fromCharCode(keyCodeForPropagationCheck).toLowerCase(); - // Make sure it is not in the blacklists. 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[keyCodeForPropagationCheck]) StopDOMEventPropagation = NO; } @@ -742,57 +739,75 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio var characters = @"", charactersIgnoringModifiers = @""; - // Grab and store the keyCode for mapping and compatibility. - // This property is deprecated but necessary for the key maps. var keyCode = aDOMEvent.keyCode; if (keyCode in MozKeyCodeToKeyCodeMap) keyCode = MozKeyCodeToKeyCodeMap[keyCode]; + var isActionKey; + var key = aDOMEvent.key; + + // Modern Browser Path (use event.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'; + } + // Legacy Browser Fallback (use event.keyCode) + else + { + isActionKey = + (keyCode === 13) || // Enter + (keyCode === 8) || // Backspace + (keyCode === 9) || // Tab + (keyCode === 27) || // Escape + (keyCode === 46) || // Delete + (keyCode >= 37 && keyCode <= 40); // Arrow keys + } + + switch (aDOMEvent.type) { case "keydown": - // For modifier keys, we create a CPFlagsChanged event and stop processing. + // For modifier keys, create a CPFlagsChanged event and stop processing. + // These are always considered "action keys". if ([ModifierKeyCodes containsObject:keyCode]) { 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; } - // Determine if the event is a repeat. Use the modern `event.repeat` property - // with a fallback to our manual state tracking for older browsers. var isARepeat = !!aDOMEvent.repeat || (_charCodes[keyCode] != nil); - _charCodes[keyCode] = YES; // Mark the key as down for fallback repeat detection. + _charCodes[keyCode] = YES; - // Determine the character for the event. - // Priority 1: Use the modern `event.key` property. It's the most reliable. if (aDOMEvent.key) { if (aDOMEvent.key.length === 1) { - // This is a printing character (e.g., "a", "P", "#"). characters = aDOMEvent.key; } else { - // This is a named, non-printing key (e.g., "Enter", "ArrowLeft"). - // Map the standard key name to our framework's internal constant. characters = KeyNameToUnicodeMap[aDOMEvent.key] || aDOMEvent.key; } } - // Priority 2: Fallback for older browsers without `event.key`. else { - // First, check if it's a known non-printing key in our legacy map. characters = KeyCodesToUnicodeMap[keyCode]; - // If not, it's likely a printing character. Use the deprecated fromCharCode. - // This is less reliable for international layouts but is the best fallback. if (!characters) { characters = String.fromCharCode(keyCode); - // Manually handle capitalization for this fallback path. + if (modifierFlags & CPShiftKeyMask || _capsLockActive) characters = characters.toUpperCase(); else @@ -800,24 +815,21 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio } } - // This is a simplification; a fully correct implementation would require extensive mapping. charactersIgnoringModifiers = characters.toLowerCase(); + // Pass the determined `isActionKey` flag. event = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil - characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:keyCode]; + characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:keyCode isActionKey:isActionKey]; break; case "keyup": - // Clear the key's state for our fallback repeat detection. _charCodes[keyCode] = nil; - // Handle the toggling of Caps Lock state. if (keyCode === CPKeyCodes.CAPS_LOCK) { _capsLockActive = !_capsLockActive; - // Update modifierFlags to reflect the new state for this event. if (_capsLockActive) modifierFlags |= CPAlphaShiftKeyMask; else @@ -825,16 +837,15 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio } // For modifier keys, create a CPFlagsChanged event and stop. + // These are always considered "action keys". if ([ModifierKeyCodes containsObject:keyCode]) { 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; } - // Determine the character for the keyup event using the same logic as keydown - // for consistency, as this event no longer has access to `keypress` state. if (aDOMEvent.key) { if (aDOMEvent.key.length === 1) @@ -852,9 +863,10 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive) characters = charactersIgnoringModifiers; + // Pass the determined `isActionKey` flag for keyup as well. 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; } @@ -867,12 +879,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. + if ([_platformPasteboard windowShouldStopPropagation] || (StopDOMEventPropagation && ![_platformPasteboard windowShouldNotStopPropagation])) { didStop = YES; From 9e15700f9335ce44b0958346737def58771b974d Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 3 Jul 2025 22:10:57 +0200 Subject: [PATCH 014/103] cleanup stuff --- AppKit/CPTextView/CPTextView.j | 22 ++++--------- AppKit/Platform/DOM/CPPlatformPasteboard.j | 2 +- AppKit/Platform/DOM/CPPlatformWindow+DOM.j | 38 ++++++++++------------ 3 files changed, 25 insertions(+), 37 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 938cc7c32..eaaad5466 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2610,13 +2610,6 @@ var _CPCopyPlaceholder = '-'; @implementation _CPNativeInputManager : CPObject -// This method is no longer used by the new design, but we keep it -// for any other part of the system that might call it. -+ (BOOL)isNativeInputFieldActive -{ - return NO; -} - + (void)isDeadKey:(CPEvent)event { #if PLATFORM(DOM) @@ -2707,13 +2700,13 @@ var _CPCopyPlaceholder = '-'; // Correctly check for shift key to force plain text paste. if (!isPlain && !e.shiftKey && (richtext = nativeClipboard.getData('text/rtf'))) { setTimeout(function() { - [currentFirstResponder insertText:[[_CPRTFParser new] parseRTF:richtext]] + [currentFirstResponder paste:self]; }, 0); return; } var data = nativeClipboard.getData('text/plain'); - [currentFirstResponder insertText:data]; + [currentFirstResponder paste:self]; }; // COPY handler @@ -2747,9 +2740,9 @@ var _CPCopyPlaceholder = '-'; var stringForPasting = [pasteboard stringForType:CPStringPboardType] || ''; e.clipboardData.setData('text/plain', stringForPasting); var rtfForPasting = [pasteboard stringForType:CPRTFPboardType]; - if (rtfForPasting) { + + if (rtfForPasting) e.clipboardData.setData('text/rtf', rtfForPasting); - } // Then, perform the delete part of the cut operation in the text view [currentFirstResponder delete:self]; @@ -2760,10 +2753,9 @@ var _CPCopyPlaceholder = '-'; + (void)focusForTextView:(CPTextView)currentFirstResponder { #if PLATFORM(DOM) - // The field no longer needs to move. It just needs focus. - if (_CPNativeInputField && document.activeElement !== _CPNativeInputField) { + + if (_CPNativeInputField && document.activeElement !== _CPNativeInputField) _CPNativeInputField.focus(); - } #endif } @@ -2773,7 +2765,7 @@ var _CPCopyPlaceholder = '-'; 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] substringWithRange:selectedRange]; + var textToCopy = [[[textview textStorage] string] substringWithRange:selectedRange]; _CPNativeInputField.innerHTML = textToCopy; } else { // For paste, we just need the field to be focusable. diff --git a/AppKit/Platform/DOM/CPPlatformPasteboard.j b/AppKit/Platform/DOM/CPPlatformPasteboard.j index 50310503f..102feb670 100644 --- a/AppKit/Platform/DOM/CPPlatformPasteboard.j +++ b/AppKit/Platform/DOM/CPPlatformPasteboard.j @@ -305,7 +305,7 @@ var hasEditableTarget = function(aDOMEvent) location = [[CPApp currentEvent] locationInWindow], anEvent = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil - characters:characters charactersIgnoringModifiers:characters isARepeat:NO keyCode:keyCode]; + characters:characters charactersIgnoringModifiers:characters isARepeat:NO keyCode:keyCode isActionKey:YES]; anEvent._data1 = @{ "simulated": YES }; anEvent._DOMEvent = aDOMEvent; diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index ece9c92c3..1ece9b38c 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -746,7 +746,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio var isActionKey; var key = aDOMEvent.key; - // Modern Browser Path (use event.key) if (key) { isActionKey = key === 'Enter' || @@ -760,24 +759,16 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio key === 'PageUp' || key === 'PageDown'; } - // Legacy Browser Fallback (use event.keyCode) else { isActionKey = - (keyCode === 13) || // Enter - (keyCode === 8) || // Backspace - (keyCode === 9) || // Tab - (keyCode === 27) || // Escape - (keyCode === 46) || // Delete - (keyCode >= 37 && keyCode <= 40); // Arrow keys + (keyCode === 13) || (keyCode === 8) || (keyCode === 9) || + (keyCode === 27) || (keyCode === 46) || (keyCode >= 37 && keyCode <= 40); } - switch (aDOMEvent.type) { case "keydown": - // For modifier keys, create a CPFlagsChanged event and stop processing. - // These are always considered "action keys". if ([ModifierKeyCodes containsObject:keyCode]) { event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags @@ -795,9 +786,14 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio { 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] || aDOMEvent.key; + characters = KeyNameToUnicodeMap[aDOMEvent.key] || @""; } } else @@ -807,7 +803,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio if (!characters) { characters = String.fromCharCode(keyCode); - if (modifierFlags & CPShiftKeyMask || _capsLockActive) characters = characters.toUpperCase(); else @@ -817,7 +812,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio charactersIgnoringModifiers = characters.toLowerCase(); - // Pass the determined `isActionKey` flag. event = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:keyCode isActionKey:isActionKey]; @@ -836,8 +830,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio modifierFlags &= ~CPAlphaShiftKeyMask; } - // For modifier keys, create a CPFlagsChanged event and stop. - // These are always considered "action keys". if ([ModifierKeyCodes containsObject:keyCode]) { event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags @@ -848,22 +840,26 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio if (aDOMEvent.key) { - if (aDOMEvent.key.length === 1) + 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] || aDOMEvent.key; + { + characters = KeyNameToUnicodeMap[aDOMEvent.key] || @""; + } } else - { characters = KeyCodesToUnicodeMap[keyCode] || String.fromCharCode(keyCode); - } charactersIgnoringModifiers = characters.toLowerCase(); if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive) characters = charactersIgnoringModifiers; - // Pass the determined `isActionKey` flag for keyup as well. event = [CPEvent keyEventWithType:CPKeyUp location:location modifierFlags:modifierFlags timestamp: timestamp windowNumber:windowNumber context:nil characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode isActionKey:isActionKey]; From b0186bf4f2458dbcc6d9e243a017776da99923f8 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 5 Jul 2025 08:49:08 +0200 Subject: [PATCH 015/103] fixed: smart copy/paste --- AppKit/CPTextView/CPTextView.j | 26 ++++++++++++++++---------- dist/cappuccino/bin/flatten | 2 +- dist/cappuccino/bin/fontinfo | Bin 69888 -> 70736 bytes dist/cappuccino/bin/imagesize | Bin 69536 -> 0 bytes dist/cappuccino/bin/objj2objcskeleton | 22 ++++++++++++++++------ 5 files changed, 33 insertions(+), 17 deletions(-) delete mode 100755 dist/cappuccino/bin/imagesize diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index eaaad5466..3666c321c 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -449,19 +449,26 @@ 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,9 +517,6 @@ var kDelegateRespondsTo_textShouldBeginEditing - (void)paste:(id)sender { - if (![sender isKindOfClass:_CPNativeInputManager] && [[CPApp currentEvent] type] != CPAppKitDefined) - return - [self _pasteString:[self _stringForPasting]]; } @@ -2697,9 +2701,11 @@ var _CPCopyPlaceholder = '-'; var currentFirstResponder = [[CPApp keyWindow] firstResponder]; var isPlain = ![currentFirstResponder isRichText]; - // Correctly check for shift key to force plain text paste. - if (!isPlain && !e.shiftKey && (richtext = nativeClipboard.getData('text/rtf'))) { - setTimeout(function() { + // Check for shift key to force plain text paste. + if (!isPlain && !e.shiftKey && (richtext = nativeClipboard.getData('text/rtf'))) + { + setTimeout(function() + { [currentFirstResponder paste:self]; }, 0); return; @@ -2722,9 +2728,9 @@ var _CPCopyPlaceholder = '-'; e.clipboardData.setData('text/plain', stringForPasting); var rtfForPasting = [pasteboard stringForType:CPRTFPboardType]; - if (rtfForPasting) { + + if (rtfForPasting) e.clipboardData.setData('text/rtf', rtfForPasting); - } }; // CUT handler diff --git a/dist/cappuccino/bin/flatten b/dist/cappuccino/bin/flatten index 1df033252..80b9ecf8e 100755 --- a/dist/cappuccino/bin/flatten +++ b/dist/cappuccino/bin/flatten @@ -233,7 +233,7 @@ ObjectiveJFlattener.prototype.serializeFunctions = function() { var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK var relative = this.rootPath.relative(path).toString(); - this.functionsBuffer.push("ObjectiveJ.StaticResource._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); + this.functionsBuffer.push("ObjectiveJ.FileExecutable._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); } var bundle = this.context.global.CFBundle.bundleContainingURL(path); diff --git a/dist/cappuccino/bin/fontinfo b/dist/cappuccino/bin/fontinfo index f1ff06158a2525d34541aff240713c0067bd8633..1a91ba1c8040bbcfe787719cb7e44c0e7578d180 100755 GIT binary patch literal 70736 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 delta 3394 zcmZ`*3s6+o89w`DsqFGx+h670NB67 z0BER_-s?-eqvet#M>G=>)tO2(O5#IV%?yIz_H@(>o1IN<&UUZ~LaljSEpq}>J}u!b zVr7xn4nNt^c`Qus7M+>LN3Ns}%e^# zxB<8vC=Ez+f{-lWvAl8B06|2vfltV9N$Vy>N(-^UM2r-KiqFN|))av-q#`Yxy;N9bap; zbl)tk_TTZ}vjob6WVpw18Q|(jmB3tzoS61`(Bi_#J{o&92UPh z1kMEhI4Gn~<~Ls&Pedbs907*Sm!KzryabTV!B&|iP}1kEA|#@JLkYP4ll)U(?U-NX zAFB>59U7~hoVp=??dgX2CkHG6b144R477I-%gz1=@l%E%bP;IYh+YSQ<68b0W_{yhNVO;gS%)DY9|CFu^Cw_= zYy98vk0JR4z<0VfKmeuBn;21b1(;^GjCA5h9;5Oo%! z21?$Q5G{eau*LtS6tcp9AL}t6+`C0LOp)vbxba+Y#jpKaRba;@D5&``bSwIxt2;VA z7m6Rkl)~s0(;9<9JrV}$1}Ip05ODcBLvl+{l?1>wLG3sMukeR!;;Ta7kO9ZS>T&*{ z{zEzOLM8vvr;O+WxQhNk^R7t8bcndWOpKTit&nIF3W1UdG-GYnqaG}e!0VceCM^D; zqR<$gLFfq+Os>atD9(nKXg?6aV+ z31%YzZ;ceFP(~ny?u;8q%gFHMF*P z*19~KSK6ALWp3At&N8}5o0%b<<}Q0}yUpcs6Pw%aY;nLP>2T5{?Mzys&7!~17U$a9 z+Q4r0w7Xgw%Sc`AGo_MD95AkhP+wdpy{^rjho{UUm=x(7xTUe73@bt6;B-ll&Mk6b zok~V|ozWY{djeYYs%w5>{$De{>i_urm>(8vvT=m;M>tsp975~7 z#q3f}VLXR%BjZaMk6zvlOz&cR7vq%i1C0NM@!vCkj`5p}$4H6gkT{lM4&(C}UyNKl zEc;jK_Gyz6^9p^6wAC}orWEl_k~sS8sTu0k?y#(fqfMs_THkqCezBqDpP?MD&)CBW z&n;VCDOA?*R;!RNthBNPoEn_WNf&anyU_~EP1-duOuVAoolQ=g+evy-I7e5LL;9bp zcZpl2N6!J=GqAsS4{VV74v!Nz8S!0>rjZSKJUTtb6L%kGxNjgw(UZ#2PX;pOpvDj7 zZEQD$6lpz#xJe2p*gUNbq=&dQ(fn*pO|5pcB>cc4xLRBuG^a()t!*Bm#km!21^{Y< z?dt(3`fwo4I~_hNsom+2_*U2)IY80>&K)4@bVU41z^&d{@~((mC%WL|DnN|GIIl{@Jw!dTys4T{x9RzodJaZ z4tRN|17!j|7So@7$|alo*un|T`0c_^6Rs%KW{b_^Bnv#vZFED}sE~LjJrZ7`>?}12 z^j0`ibN5PI2(t0gw9_+V&CZvc1KBrsh$+yI+k6k&Di|x%8J~FIq82mvzXg|eCJcY(|mto zMx}1;LO?SZmK-?9EuXVz>5=5^X}>L7k@!{fg5Ehp)(=xh-#lb|+cpY89Bmol413={ z-q`FoHlKWPG2x=~EpMW(JNwJmf~muwCO=3yz5WY&c7)^fYphqZ_NR{CzQ7&xoUOR< zAJ@MQUj86?*7l0)Tdq}e?|z(>fA3J|=hi;mdw(7(Fm6j396lJl*4(&vgpTzG?tKj) zn+%nU^QL|O$r072fIIj3og;U;LIdSB8QV|n`;}GwcKVJL7_=y2daz~N;7i#ZKlc9g ziFc;uy>z+dxtB-kDrb~^yD_)tRtD4(7aR|Ea`(y$F95qo>p%S?-EgKtQCwuCBWGsE Q%I;p7hEba639vimze2ENzW@LL diff --git a/dist/cappuccino/bin/imagesize b/dist/cappuccino/bin/imagesize deleted file mode 100755 index 5d67a1ce0a9a1e20577f82ccdfe0d3b3e94143b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69536 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 Z009sH0T2KI5C8!X009sHfo}nU{{v@rhV}pe diff --git a/dist/cappuccino/bin/objj2objcskeleton b/dist/cappuccino/bin/objj2objcskeleton index b7972dbb3..51ecab38d 100755 --- a/dist/cappuccino/bin/objj2objcskeleton +++ b/dist/cappuccino/bin/objj2objcskeleton @@ -4,14 +4,11 @@ var fs = require("fs"), acorn = require("objj-parser"), - walk = require("objj-parser/util/walk"), + walk = require("acorn-walk"), stream = ObjectiveJ.term; - debugger; - function main(args) { - debugger; args.shift(); if (args.length < 1) @@ -40,6 +37,8 @@ function raise(pos, message) throw syntaxError; } +function ignore(_node, _st, _c) {} + var errors = [], xcc = walk.make( { @@ -115,7 +114,18 @@ var errors = [], else raise(node.loc.start, "Action methods must have exactly one parameter"); } - } + }, + TypeDefStatement: ignore, + ClassStatement: ignore, + MessageSendExpression: ignore, + GlobalStatement: ignore, + ProtocolDeclarationStatement: ignore, + ArrayLiteral: ignore, + Reference: ignore, + DictionaryLiteral: ignore, + Dereference: ignore, + ImportStatement: ignore, + SelectorLiteralExpression: ignore } ); @@ -154,7 +164,7 @@ function parser(args) outputHeaderURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".h"]), outputImplementationURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".m"]), source = fs.readFileSync(sourcePath, { encoding: "utf8" }), - tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath }), + tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath, ecmaVersion: 2022 }), classesInformation = [], ObjectiveCSource = "", ObjectiveCHeader = "", From d12770c65d4962fa5b0db9413e924681dda7cbde Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 5 Jul 2025 08:57:29 +0200 Subject: [PATCH 016/103] fixed: testcase for keyevent --- Tests/AppKit/CPResponderTest.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AppKit/CPResponderTest.j b/Tests/AppKit/CPResponderTest.j index 4c3159ab3..5c4729b71 100644 --- a/Tests/AppKit/CPResponderTest.j +++ b/Tests/AppKit/CPResponderTest.j @@ -45,7 +45,7 @@ var keyEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:character charactersIgnoringModifiers:character isARepeat:NO keyCode:keyCode]; + characters:character charactersIgnoringModifiers:character isARepeat:NO keyCode:keyCode isActionKey:YES]; [responder interpretKeyEvents:[keyEvent]]; [self assert:[selector] equals:responder.doCommandCalls]; } From a0136e21442d6f8fcccd6baf0518a15afd3ae3e2 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 5 Jul 2025 08:58:57 +0200 Subject: [PATCH 017/103] removed accidentally committed stuff --- dist/cappuccino/bin/flatten | 368 --------------- dist/cappuccino/bin/fontinfo | Bin 70736 -> 0 bytes dist/cappuccino/bin/objj2objcskeleton | 623 -------------------------- 3 files changed, 991 deletions(-) delete mode 100755 dist/cappuccino/bin/flatten delete mode 100755 dist/cappuccino/bin/fontinfo delete mode 100755 dist/cappuccino/bin/objj2objcskeleton diff --git a/dist/cappuccino/bin/flatten b/dist/cappuccino/bin/flatten deleted file mode 100755 index 80b9ecf8e..000000000 --- a/dist/cappuccino/bin/flatten +++ /dev/null @@ -1,368 +0,0 @@ -#!/usr/bin/env objj - -require("narwhal").ensureEngine("rhino"); - -@import - -@import "../lib/cappuccino/objj-analysis-tools.j" - -var FILE = require("file"); -var OS = require("os"); -var UTIL = require("narwhal/util"); - -var CACHEMANIFEST = require("objective-j/cache-manifest"); - -var stream = require("narwhal/term").stream; -var parser = new (require("narwhal/args").Parser)(); - -parser.usage("INPUT_PROJECT OUTPUT_PROJECT"); -parser.help("Combine a Cappuccino application into a single JavaScript file."); - -parser.option("-m", "--main", "main") - .def("main.j") - .set() - .help("The relative path (from INPUT_PROJECT) to the main file (default: 'main.j')"); - -parser.option("-F", "--framework", "frameworks") - .push() - .help("Add a frameworks directory, relative to INPUT_PROJECT (default: ['Frameworks'])"); - -parser.option("-P", "--path", "paths") - .push() - .help("Add a path (relative to the application root) to inline."); - -parser.option("-f", "--force", "force") - .def(false) - .set(true) - .help("Force overwriting OUTPUT_PROJECT if it exists"); - -parser.option("--index", "index") - .def("index.html") - .set() - .help("The root HTML file to modify (default: index.html)"); - -parser.option("-s", "--split", "number", "split") - .natural() - .def(0) - .help("Split into multiple files"); - -parser.option("-c", "--compressor", "compressor") - .def("shrinksafe") - .set() - .help("Select a compressor to use (closure-compiler, yuicompressor, shrinksafe), or \"none\" (default: shrinksafe)"); - -parser.option("--manifest", "manifest") - .set(true) - .help("Generate HTML5 cache manifest."); - -parser.option("-v", "--verbose", "verbose") - .def(false) - .set(true) - .help("Verbose logging"); - -parser.helpful(); - -function main(args) -{ - var options = parser.parse(args); - - if (options.args.length < 2) { - parser.printUsage(options); - return; - } - - var rootPath = FILE.path(options.args[0]).join("").absolute(); - var outputPath = FILE.path(options.args[1]).join("").absolute(); - - if (outputPath.exists()) { - if (options.force) { - // FIXME: why doesn't this work?! - //outputPath.rmtree(); - OS.system(["rm", "-rf", outputPath]); - } else { - stream.print("\0red(OUTPUT_PROJECT " + outputPath + " exists. Use -f to overwrite.\0)"); - OS.exit(1); - } - } - - options.frameworks.push("Frameworks"); - - var mainPath = String(rootPath.join(options.main)); - var frameworks = options.frameworks.map(function(framework) { return rootPath.join(framework); }); - var environment = "Browser"; - - stream.print("\0yellow("+Array(81).join("=")+"\0)"); - stream.print("Application root: \0green(" + rootPath + "\0)"); - stream.print("Output directory: \0green(" + outputPath + "\0)"); - - stream.print("\0yellow("+Array(81).join("=")+"\0)"); - stream.print("Main file: \0green(" + mainPath + "\0)"); - stream.print("Frameworks: \0green(" + frameworks + "\0)"); - stream.print("Environment: \0green(" + environment + "\0)"); - - var flattener = new ObjectiveJFlattener(rootPath); - - flattener.options = options; - - flattener.setIncludePaths(frameworks); - flattener.setEnvironments([environment, "ObjJ"]); - - print("Loading application."); - flattener.load(mainPath); - - print("Loading default theme."); - flattener.require("objective-j").objj_eval("("+(function() { - - var defaultThemeName = [CPApplication defaultThemeName], - bundle = nil; - - if (defaultThemeName === @"Aristo" || defaultThemeName === @"Aristo2") - bundle = [CPBundle bundleForClass:[CPApplication class]]; - else - bundle = [CPBundle mainBundle]; - - var blend = [[CPThemeBlend alloc] initWithContentsOfURL:[bundle pathForResource:defaultThemeName + @".blend"]]; - [blend loadWithDelegate:nil]; - - })+")")(); - - var applicationJSs = flattener.buildApplicationJS(); - - FILE.copyTree(rootPath, outputPath); - - applicationJSs.forEach(function(applicationJS, n) { - var name = "Application"+(n||"")+".js"; - if (options.compressor === "none") { - print("skipping compression: " + name); - } else { - print("compressing: " + name); - applicationJS = require("minify/"+options.compressor).compress(applicationJS, { charset : "UTF-8", useServer : true }); - } - outputPath.join(name).write(applicationJS, { charset : "UTF-8" }); - }); - - rewriteMainHTML(outputPath.join(options.index)); - - if (options.manifest) { - CACHEMANIFEST.generateManifest(outputPath, { - index : outputPath.join(options.index), - exclude : Object.keys(flattener.filesToCache).map(function(path) { return outputPath.join(path).toString(); }) - }); - } -} - -// ObjectiveJFlattener inherits from ObjectiveJRuntimeAnalyzer -function ObjectiveJFlattener(rootPath) { - ObjectiveJRuntimeAnalyzer.apply(this, arguments); - - this.filesToCache = {}; - this.fileCacheBuffer = []; - this.functionsBuffer = []; -} - -ObjectiveJFlattener.prototype = Object.create(ObjectiveJRuntimeAnalyzer.prototype); - -ObjectiveJFlattener.prototype.buildApplicationJS = function() { - - this.setupFileCache(); - this.serializeFunctions(); - this.serializeFileCache(); - - var additions = FILE.read(FILE.join(FILE.dirname(module.path), "..", "..", "cappuccino", "lib", "cappuccino", "objj-flatten-additions.js"), { charset:"UTF-8" }); - - var applicationJSs = []; - - if (this.options.split === 0) { - var buffer = []; - buffer.push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);"); - buffer.push(additions); - buffer.push(this.fileCacheBuffer.join("\n")); - buffer.push(this.functionsBuffer.join("\n")); - buffer.push("ObjectiveJ.bootstrap();"); - applicationJSs.push(buffer.join("\n")); - } else { - var appFilesCount = this.options.split; - - var buffers = []; - for (var i = 0; i <= appFilesCount; i++) - buffers.push([]); - - var chunks = this.fileCacheBuffer.concat(this.functionsBuffer).sort(function(chunkA, chunkB) { - return chunkA.length - chunkB.length; - }); - - // try to equally distribute the chunks. could be better but good enough for now. - var n = 0; - while (chunks.length) { - buffers[(n++ % appFilesCount) + 1].push(chunks.pop()); - } - - buffers[0].push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);"); - buffers[0].push(additions); - - buffers[0].push("var appFilesCount = " + appFilesCount +";"); - buffers[0].push("for (var i = 1; i <= appFilesCount; i++) {"); - buffers[0].push(" var script = document.createElement(\"script\");"); - buffers[0].push(" script.src = \"Application\"+i+\".js\";"); - buffers[0].push(" script.charset = \"UTF-8\";"); - buffers[0].push(" script.onload = function() { if (--appFilesCount === 0) ObjectiveJ.bootstrap(); };"); - buffers[0].push(" document.getElementsByTagName(\"head\")[0].appendChild(script);"); - buffers[0].push("}"); - - buffers.forEach(function(buffer) { - applicationJSs.push(buffer.join("\n")); - }); - } - - return applicationJSs; -} - -ObjectiveJFlattener.prototype.serializeFunctions = function() { - var inlineFunctions = true;//this.options.inlineFunctions; - - var outputFiles = {}; - - var _cachedExecutableFunctions = {}; - - this.require("objective-j").FileExecutable.allFileExecutables().forEach(function(executable) { - var path = executable.path(); - - if (inlineFunctions) - { - // stringify the function, replacing arguments - var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK - - var relative = this.rootPath.relative(path).toString(); - this.functionsBuffer.push("ObjectiveJ.FileExecutable._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); - } - - var bundle = this.context.global.CFBundle.bundleContainingURL(path); - if (bundle && bundle.infoDictionary()) - { - var executablePath = bundle.executablePath(), - relativeToBundle = FILE.relative(FILE.join(bundle.path(), ""), path); - - if (executablePath) - { - if (inlineFunctions) - { - // remove the code since we're inlining the functions - executable._code = "alert("+JSON.stringify(relativeToBundle)+");"; - } - - if (!outputFiles[executablePath]) - { - outputFiles[executablePath] = []; - outputFiles[executablePath].push("@STATIC;1.0;"); - } - - var fileContents = executable.toMarkedString(); - - outputFiles[executablePath].push("p;" + relativeToBundle.length + ";" + relativeToBundle); - outputFiles[executablePath].push("t;" + fileContents.length + ";" + fileContents); - - // stream.print("Adding \0green(" + this.rootPath.relative(path) + "\0) to \0cyan(" + this.rootPath.relative(executablePath) + "\0)"); - } - } - else - CPLog.warn("No bundle (or info dictionary for) " + rootPath.relative(path)); - }, this); - - for (var executablePath in outputFiles) - { - var relative = this.rootPath.relative(executablePath).toString(); - var contents = outputFiles[executablePath].join(""); - this.filesToCache[relative] = contents; - } -} - -ObjectiveJFlattener.prototype.serializeFileCache = function() { - for (var relative in this.filesToCache) { - var contents = this.filesToCache[relative]; - print("caching: " + relative + " => " + (contents == null ? 404 : 200)); - if (contents == null) - this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 404);"); - else - this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 200, {}, "+JSON.stringify(contents)+");"); - } -} - -ObjectiveJFlattener.prototype.setupFileCache = function() { - var paths = {}; - - UTIL.update(paths, this.requestedURLs); - - this.options.paths.forEach(function(relativePath) { - paths[this.rootPath.join(relativePath)] = true; - }, this); - - Object.keys(paths).forEach(function(absolute) { - var relative = this.rootPath.relative(absolute).toString(); - if (relative.indexOf("..") === 0) - { - print("skipping (parent of app root): " + absolute); - return; - } - - if (FILE.isFile(absolute)) - { - // if (this.options.maxCachedSize && FILE.size(absolute) > this.options.maxCachedSize) - // { - // print("skipping (larger than "+this.options.maxCachedSize+" bytes): " + absolute); - // return; - // } - - var contents = FILE.read(absolute, { charset : "UTF-8" }); - this.filesToCache[relative] = contents; - } else { - this.filesToCache[relative] = null; - } - }, this); -} - -// "$1" is the matching indentation -var scriptTagsBefore = - '$1'; - -var scriptTagsAfter = - '$1'; - -// enable CPLog: -// scriptTagsAfter = '$1\n' + scriptTagsAfter; - -function rewriteMainHTML(indexHTMLPath) { - if (indexHTMLPath.isFile()) { - var indexHTML = indexHTMLPath.read({ charset : "UTF-8" }); - - // inline the Application.js if it's smallish - var applicationJSPath = indexHTMLPath.dirname().join("Application.js"); - if (applicationJSPath.size() < 10*1024) { - // escape any dollar signs by replacing them with two - // then indent by splitting/joining on newlines - scriptTagsAfter = - '$1'; - } - - // attempt to find Objective-J script tag and add ours - var newIndexHTML = indexHTML.replace(/([ \t]+)]+Objective-J\.js[^>]+>(?:\s*<\/script>)?/, - scriptTagsBefore+'\n$&\n'+scriptTagsAfter); - - if (newIndexHTML !== indexHTML) { - stream.print("\0green(Modified: "+indexHTMLPath+".\0)"); - indexHTMLPath.write(newIndexHTML, { charset : "UTF-8" }); - return; - } - } else { - stream.print("\0yellow(Warning: "+indexHTMLPath+" does not exist. Specify an alternate index HTML file with the --index option.\0)"); - } - - stream.print("\0yellow(Warning: Unable to automatically modify "+indexHTMLPath + ".\0)"); - stream.print("\nAdd the following before the Objective-J script tag:"); - stream.print(scriptTagsBefore.replace(/\$1/g, " ")); - stream.print("\nAdd the following after the Objective-J script tag:"); - stream.print(scriptTagsAfter.replace(/\$1/g, " ")); -} diff --git a/dist/cappuccino/bin/fontinfo b/dist/cappuccino/bin/fontinfo deleted file mode 100755 index 1a91ba1c8040bbcfe787719cb7e44c0e7578d180..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70736 zcmeI5eQ;FO6~OQACJ;h^1yK+YmgU3nk-PvA!-B9OAAH3mLl%j&?3ZpyJRPbmx_ioUv5-(a^H?;YI6xLpX%D8sm06b*U1!3|K^`Od)H zF<&b#1`67|YBdzFq*bwgMNv26em&ikoo`jKl@A$UK|3>F5zhw<%_54be@kE>X?DH? zJYO$|hBmG*U2^6Z&<#zAw($*M)O^CW0z zmfU>tXonIFib^!3LdWd+ofyTh2Ty`_c8<*axKFBpjVRVu`>Ms7rp6X)w|My`1#Olr z?SRIVIH4|zZED6g88Tmr8-sippDMWDQhN^DL@^M`yb#&*+wQgIm&DFc(9X!3uWHW|@n=j(%b+#*Fh2JL*@_CsGQ4-Z0>%;E;NS-h4?G2`12;;sEFhc!6_x5ZHT z(%h1R*9m(W@u$nc`O&btacy1g>eRj8!{?RcUBSlE%>6i@)SR%r5>{d+?g0ZpEKJ!i zFy^wF*hJ8!V9x>V`ZHrEU~wa%-VA~T=YoQHCxHx_DA<|h@ShmFJhOqej%6$2f@a26$sAb@c8auexlAlz4OWG$#=rDN*6$z3k1hzg=0*?y17RR_ZHxs zGv`wZ>%%;Feq|OMw-p3^)_%a6jR`FY-H;s8L<0wp&MgVQXy-obGzVT%opvzA`IA~Q0ZQ{mDvaMG526{qT0Y^`E^`C2R= z%mwqdgAC^Mz*pt1Q8jsh#h2yB3z69e^3MXn{osQAjAy`XZsY#Oay1D0oi8!wU5hOj zS1$a~e&mC>1(B1}3nS;+hIRFc!@EA+P}Fs{X++og9mQSeciKD8I&9saI9NCA!F{aI z+rO8w`#%QzP%+!~asl)09_H=8(&k;f{5iJm4~+SW3cZV`{>HZLT?g}RWXW}~FUNa8 zUkCBMqrLsD!`aw7Hr~?r3FP?{@-U}7Wvb)yt*{hda^v>CZO~ri+C1g7jrm$fva#m4 z^S%A=Ij-nF489&aJBV=@^N^kSFy|c*_b&K8gZ8m7UL}mz>$t4@5oqgmjPE|bV-2in zHLPtFtZ`-6xwiVQlhf-WAIzt80y7lzlq%|q5d4q-R!s6+mE`a%-ip< zS!=^}&ADdQS>kE~9=$Mtw>~ z-JMaNW7V^FaW&+f#=QMz&Pi4u7jwOl64PIh$6u7kUy{e~%;O)Q$3HQTe{vqbE02Fh z9)D>b|GYf@iah?xJpQVo{X5(tDWZfEjqtc~d)za#)L8Be(w=qMD%Pb^?b(s~jnL1nVtFLOYUaX~9 z46<5=ho#0~K{+JF!$w0oX}zk|t5L%P9zHG$Dgk`{!2@@LVzf8;x5xp*6H>Lcass+( zk`je6Q_Eu^h^~cLO^F$*W||m=OZVu?EwYCxQN_R}J{ie?8T#Xfta})CVAdNb_JGMm zB$&%U%fO1DH4y=ZO27wGFw8%0Y;a>u<~LWSwnH1fV|YQM;NLe`yyow~55A|Az;o9L zf@O?@!4X7-2oM1xKm>>Y5g-CYfCvx)B0vO)01+SpM1Tko0U|&IhyW2F0z`la5CI}U z1c(3;AOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)01+SpMBu+ez?rnZ>To9Udl2}lFlm05 z3flU}!$6m6vWWK|!L{}Ih%<@b-C`d6UJ^BYOloTUeim)`0v3uZ zdDu$Ctd1_$X12rkC0YIxW+GSmqmyj^Y3{e?nH&GHH4*;xiZzIrk41_I5CI}U1c(3; zAOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)01+SpM1Tko0U|&IhyW2F0z`la5CI}U1c(3; zAOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)01+SpM1Tko0U|&Ih`={Z!20*yoy=i=pzma3 zxt_#zCD+Ti{ub8~*I}-AaJ`4?F0S!kk%sU6Sr6AoP5n);n#LyrM1Tko0U|&IhyW2F z0z`la5CI}U1c(3;AOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)01+SpM1Tko0U|&IhyW2F z0z`la5CI}U1c(3;AOb{y2oM1xKm>>Y5g-CYfCvx)BJjUQzy(uk2YCX-31S0DmBLg7 z^hA&fZeyK!hk@`0P#BkPPY!Ok+ZnUhg4_wx!VgWOVMh8UGQxW&h3qGYHc zCdOc*2I$6?dNpdKSI{U$FbzCTls~S|wm8YE;q^OcceM`WiK= z8&cG;)>0FebbXy1k~KLRkkePkCW?`EQQj^GfTh?jOF_td6}kaBV`s+TFJmFNxKlvR z47aXx7;Oh*2Z}9w7q@%3{WEU&a{JfZW+Scsz1((jyN}ymZcm5NG5=<6U&rkvx0|_r zfZGvn_i%d;w|lw$9Jg7CHU3|@?c(++ZhN^s8pbxq=Qd1_1@VWMx!@0`4A3`a*+H;F z8Fvzfmcvhq-#g%+kzu2M21qN+0}I-XS^muNvD+9M-mO;OD=eM9UFTXh&&F6omL0Zi zxPh&5w`Hf}AI!3UYuV|1f6KBzvg~xeu`qY|uK=3OGQqN8)mE9FWmjg|E3)k7EPIn> zr}IyTw8(EJ$Tc9PAhSVmCr<`(flLEQ^_d0rR6mTxzLP+vfJ_Cs8U%|AZl&D&op*W94StjjoQ0QkE~ZYzh@?aZpQkg{`|7I(ox=Q}ogu|y zaNb%jPganm2f&ACqdCD;NKg(*@vzaLMCDbo($;R&s~RHA4iCyh#HUZz4T{m;QkdrnM@vC7a;DaR_Iuq9GvZf0`$*;;4^95ZC z2m!cO@jyU{s)7%m!McEJQlcSMV5wSp}Kmi>19z|Vpx#0 z$fC-DJ}MKSRkHD!Ud+<>Jc9K-K(8N%GGRUAri>aK0R@ zS||k;EUc;s1{VbuELu>-zUW+X=SbVD1#dm_{L~ZD_=Am!Blnd&`TE*tMpr$xe7Enz zRO+Ghvkp11>g2=AtBQWV<9GFr!|c1Ko?W@^?O?~Tr|-G!X5-VL;x4v*f5Nq|@#pgB z`1T(bt~v0`y2dTZLoZHxxUBE>>mHj>(|=dKJWF=|DtIRT&ZE6=9$vch+pTvu-22M+ zl)vwp)B594V@D;&H9U3ulh3xQe>oZIDR}e9k*)To*1`+79=!FIU2i@AgH_+3_xPpy zXWPp6?LE5h*pf}7KHoI?#s@!hANc&iSLU+}i`#YJ0=c`nrLdF6gjaCW0{ z&utyWk3QP=qw|M6a3{cX+1lynj&IurcfyjT%eP0uuAAX#fOpgD-Sf*U+%9+nRfF)- ze!Uw`m$EAN@@2(Kr`0yq_-<;hbH&1nZn&D)uV`3Z<8qe?LUk+_mIa~KSLKa2tr3kM>!ldVYxh@MzEtUG;3;1){I00BrAh}@}LoP!!TC%LDPZO zARO?^ibpKnDkqjf3Z4v8Dgg;!s>_t13`bW;k+r1)_JYpV8M$n!z$-cNI=obCijgR9 zlZ`S%(zll3UlJYZ1cnxu1sGkhhF>a}lUr5{=rq9iomh+AW_zZwVn&H8-=s=d#jKKP z1HFbA+Ge|O?)XT&Mn(DVHS!YyB0vO)01+SpM1Tko0U|&IhyW2F0z`la5CI}U1c(3; zAOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)01+SpM1Tko0U|&IhyW2F0z`la5CI}U1c(3; hAOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)!2c70e*+qW#^L|~ diff --git a/dist/cappuccino/bin/objj2objcskeleton b/dist/cappuccino/bin/objj2objcskeleton deleted file mode 100755 index 51ecab38d..000000000 --- a/dist/cappuccino/bin/objj2objcskeleton +++ /dev/null @@ -1,623 +0,0 @@ -#!/usr/bin/env objj - -@import - -var fs = require("fs"), - acorn = require("objj-parser"), - walk = require("acorn-walk"), - stream = ObjectiveJ.term; - -function main(args) -{ - args.shift(); - - if (args.length < 1) - return printUsage(); - - parser(args); -} - -function printUsage() -{ - console.log("objj2objskeleton [FILE] [DESTINATION]"); - console.log("Convert a objective-j file to an objective-c files skeleton (.h and .m)") -} - -// Debug function to print some JS objects -function dump(obj) -{ - console.log(JSON.stringify(obj)); -} - -function raise(pos, message) -{ - var syntaxError = new SyntaxError(message); - syntaxError.line = pos.line; - - throw syntaxError; -} - -function ignore(_node, _st, _c) {} - -var errors = [], - xcc = walk.make( - { - ClassDeclarationStatement: function(node, st, c) - { - var className = node.classname.name, - superclassname = node.superclassname ? node.superclassname.name : "", - declaredOutletsName = [], - classInfo = { - "name": className, - "category": node.categoryname ? node.categoryname.name : "", - "superClass": superclassname, - "outlets": [], - "actions": [], - "actionNames": [] - }; - - if (node.ivardeclarations) - { - for (var i = 0; i < node.ivardeclarations.length; ++i) - { - var ivarDecl = node.ivardeclarations[i], - ivarType = ivarDecl.ivartype ? ivarDecl.ivartype.name : null, - ivarName = ivarDecl.id.name, - ivarHasOutlet = ivarDecl.outlet ? "@outlet" : null; - - if (ivarHasOutlet) - { - if (declaredOutletsName.indexOf(ivarName) !== -1) - raise(ivarDecl.loc.start, "Outlet '" + ivarName + "' declared more than once"); - - declaredOutletsName.push(ivarName); - classInfo.outlets.push({"type": ivarType, "name": ivarName}); - } - } - } - - st.push(classInfo) - - for (var i = 0; i < node.body.length; ++i) - c(node.body[i], classInfo, "Statement"); - }, - - MethodDeclarationStatement: function(node, st, c) - { - var selectors = node.selectors, - arguments = node.arguments, - //methodReturnType = [node.returntype ? node.returntype.name : "id"], - methodHasAction = node.action ? "IBAction" : null, - selector = selectors[0].name, - actionInfo = {"name": selector, "arguments":[]}; - - if (methodHasAction) - { - if (arguments.length == 1) - { - if (st.actionNames.indexOf(selector) !== -1) - raise(node.loc.start, "Action '" + selector + "' declared more than once"); - - st.actionNames.push(selector); - - for (var i = 0; i < arguments.length; i++) - { - var argument = arguments[i], - argumentName = argument.identifier.name, - argumentType = argument.type ? argument.type.name : null; - - actionInfo.arguments.push({"type": argumentType, "name": argumentName}); - } - - st.actions.push(actionInfo) - } - else - raise(node.loc.start, "Action methods must have exactly one parameter"); - } - }, - TypeDefStatement: ignore, - ClassStatement: ignore, - MessageSendExpression: ignore, - GlobalStatement: ignore, - ProtocolDeclarationStatement: ignore, - ArrayLiteral: ignore, - Reference: ignore, - DictionaryLiteral: ignore, - Dereference: ignore, - ImportStatement: ignore, - SelectorLiteralExpression: ignore - } -); - -function compile(node, state, visitor) -{ - function c(node, st, override) - { - visitor[override || node.type](node, st, c); - } - - c(node, state); -}; - -function removeLastSlashIfNecessary(path) -{ - if (path[path.length - 1] == "/") - return path.substring(0, path.length - 1); - - return path; -} - -/* - $1 Full project source path - $2 Destination - $-n name of the cocoa files -*/ -function parser(args) -{ - try - { - var sourcePath = args.shift(), - projectBasePath = removeLastSlashIfNecessary(args.shift()), - outputDirectory = projectBasePath, - baseFilename = [sourcePath lastPathComponent], - baseFilenameWithNoExtension = args.shift() == "-n" ? args.shift() : baseFilename.substring(0, baseFilename.length - 2), - outputHeaderURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".h"]), - outputImplementationURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".m"]), - source = fs.readFileSync(sourcePath, { encoding: "utf8" }), - tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath, ecmaVersion: 2022 }), - classesInformation = [], - ObjectiveCSource = "", - ObjectiveCHeader = "", - hasErrors = NO; - - compile(tokens, classesInformation, xcc); - - // dump(classesInformation) - - ObjectiveCHeader += - "#import \n" + - '#import "xcc_general_include.h"\n'; - - ObjectiveCSource += "#import \"" + outputHeaderURL.lastPathComponent() + "\"\n"; - - // Traverse each found classes - classesInformation.forEach(function(aClass) - { - // add new class definition - if (aClass.superClass) - ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ : %@", aClass.name, NSCompatibleClassName(aClass.superClass)]; - else - ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ (%@)", NSCompatibleClassName(aClass.name, NO), aClass.category]; - - // add each outlet in header - if (aClass.outlets.length > 0) - ObjectiveCHeader += "\n"; - - aClass.outlets.forEach(function(anOutlet) - { - ObjectiveCHeader += [CPString stringWithFormat:@"\n@property (assign) IBOutlet %@ %@;", NSCompatibleClassName(anOutlet.type, YES), anOutlet.name]; - }); - - if (aClass.actions.length > 0) - ObjectiveCHeader += "\n"; - - // add each action in header - aClass.actions.forEach(function(anAction) - { - ObjectiveCHeader += [CPString stringWithFormat:@"\n- (IBAction)%@:(%@)%@;", anAction.name, anAction.arguments[0].type, anAction.arguments[0].name]; - }); - - if (aClass.outlets.length > 0 || aClass.actions.length > 0) - ObjectiveCHeader += "\n"; - - ObjectiveCHeader += "\n@end\n"; - - // fill up the implementation file - ObjectiveCSource += "\n@implementation " + NSCompatibleClassName(aClass.name, NO) + "\n@end\n"; - }); - - if (ObjectiveCSource.length) - fs.writeFileSync(outputImplementationURL.absoluteString(), ObjectiveCSource, 'utf8'); - - if (ObjectiveCHeader.length) - fs.writeFileSync(outputHeaderURL.absoluteString(), ObjectiveCHeader, 'utf8'); - } - catch (e) - { - [errors addObject:@{ - @"message": e.message, - @"sourcePath": sourcePath, - @"line": e.line - }]; - - hasErrors = YES; - } - - if ([errors count]) - { - var plist = [CPPropertyListSerialization dataFromPropertyList:errors format:CPPropertyListXMLFormat_v1_0]; - - stream.printError([plist rawString]); - - // If there were category warnings, hasErrors is NO, so return a warning status - process.exit(hasErrors ? 1 : 2); - } -} - -function NSCompatibleClassName(aClassName, asPointer) -{ - if (aClassName === "var" || aClassName === "id") - return "id"; - - var prefix = aClassName.substr(0, 2), - asterisk = asPointer ? "*" : ""; - - if (prefix !== "CP") - return aClassName + asterisk; - - var NSClassName = "NS" + aClassName.substr(2); - - if (NSClasses[NSClassName]) - return NSClassName + asterisk; - - if (ReplacementClasses[aClassName]) - return ReplacementClasses[aClassName] + asterisk; - - return aClassName + asterisk; -} - -var ReplacementClasses = { - "CPWebView": "WebView", - "CPRadio": "NSButtonCell", - "CPRadioGroup": "NSMatrix" - }; - -var NSClasses = { - "NSAffineTransform" : YES, - "NSAppleEventDescriptor" : YES, - "NSAppleEventManager" : YES, - "NSAppleScript" : YES, - "NSArchiver" : YES, - "NSArray" : YES, - "NSAssertionHandler" : YES, - "NSAttributedString" : YES, - "NSAutoreleasePool" : YES, - "NSBlockOperation" : YES, - "NSBundle" : YES, - "NSCache" : YES, - "NSCachedURLResponse" : YES, - "NSCalendar" : YES, - "NSCharacterSet" : YES, - "NSClassDescription" : YES, - "NSCloneCommand" : YES, - "NSCloseCommand" : YES, - "NSCoder" : YES, - "NSComparisonPredicate" : YES, - "NSCompoundPredicate" : YES, - "NSCondition" : YES, - "NSConditionLock" : YES, - "NSConnection" : YES, - "NSCountCommand" : YES, - "NSCountedSet" : YES, - "NSCreateCommand" : YES, - "NSData" : YES, - "NSDate" : YES, - "NSDateComponents" : YES, - "NSDateFormatter" : YES, - "NSDecimalNumber" : YES, - "NSDecimalNumberHandler" : YES, - "NSDeleteCommand" : YES, - "NSDeserializer" : YES, - "NSDictionary" : YES, - "NSDirectoryEnumerator" : YES, - "NSDistantObject" : YES, - "NSDistantObjectRequest" : YES, - "NSDistributedLock" : YES, - "NSDistributedNotificationCenter" : YES, - "NSEnumerator" : YES, - "NSError" : YES, - "NSException" : YES, - "NSExistsCommand" : YES, - "NSExpression" : YES, - "NSFileHandle" : YES, - "NSFileManager" : YES, - "NSFileWrapper" : YES, - "NSFormatter" : YES, - "NSGarbageCollector" : YES, - "NSGetCommand" : YES, - "NSHashTable" : YES, - "NSHost" : YES, - "NSHTTPCookie" : YES, - "NSHTTPCookieStorage" : YES, - "NSHTTPURLResponse" : YES, - "NSIndexPath" : YES, - "NSIndexSet" : YES, - "NSIndexSpecifier" : YES, - "NSInputStream" : YES, - "NSInvocation" : YES, - "NSInvocationOperation" : YES, - "NSKeyedArchiver" : YES, - "NSKeyedUnarchiver" : YES, - "NSLocale" : YES, - "NSLock" : YES, - "NSLogicalTest" : YES, - "NSMachBootstrapServer" : YES, - "NSMachPort" : YES, - "NSMapTable" : YES, - "NSMessagePort" : YES, - "NSMessagePortNameServer" : YES, - "NSMetadataItem" : YES, - "NSMetadataQuery" : YES, - "NSMetadataQueryAttributeValueTuple" : YES, - "NSMetadataQueryResultGroup" : YES, - "NSMethodSignature" : YES, - "NSMiddleSpecifier" : YES, - "NSMoveCommand" : YES, - "NSMutableArray" : YES, - "NSMutableAttributedString" : YES, - "NSMutableCharacterSet" : YES, - "NSMutableData" : YES, - "NSMutableDictionary" : YES, - "NSMutableIndexSet" : YES, - "NSMutableSet" : YES, - "NSMutableString" : YES, - "NSMutableURLRequest" : YES, - "NSNameSpecifier" : YES, - "NSNetService" : YES, - "NSNetServiceBrowser" : YES, - "NSNotification" : YES, - "NSNotificationCenter" : YES, - "NSNotificationQueue" : YES, - "NSNull" : YES, - "NSNumber" : YES, - "NSNumberFormatter" : YES, - "NSObject" : YES, - "NSOperation" : YES, - "NSOperationQueue" : YES, - "NSOrthography" : YES, - "NSOutputStream" : YES, - "NSPipe" : YES, - "NSPointerArray" : YES, - "NSPointerFunctions" : YES, - "NSPort" : YES, - "NSPortCoder" : YES, - "NSPortMessage" : YES, - "NSPortNameServer" : YES, - "NSPositionalSpecifier" : YES, - "NSPredicate" : YES, - "NSProcessInfo" : YES, - "NSPropertyListSerialization" : YES, - "NSPropertySpecifier" : YES, - "NSProtocolChecker" : YES, - "NSProxy" : YES, - "NSPurgeableData" : YES, - "NSQuitCommand" : YES, - "NSRandomSpecifier" : YES, - "NSRangeSpecifier" : YES, - "NSRecursiveLock" : YES, - "NSRelativeSpecifier" : YES, - "NSRunLoop" : YES, - "NSScanner" : YES, - "NSScriptClassDescription" : YES, - "NSScriptCoercionHandler" : YES, - "NSScriptCommand" : YES, - "NSScriptCommandDescription" : YES, - "NSScriptExecutionContext" : YES, - "NSScriptObjectSpecifier" : YES, - "NSScriptSuiteRegistry" : YES, - "NSScriptWhoseTest" : YES, - "NSSerializer" : YES, - "NSSet" : YES, - "NSSetCommand" : YES, - "NSSocketPort" : YES, - "NSSocketPortNameServer" : YES, - "NSSortDescriptor" : YES, - "NSSpecifierTest" : YES, - "NSSpellServer" : YES, - "NSStream" : YES, - "NSString" : YES, - "NSTask" : YES, - "NSTextCheckingResult" : YES, - "NSThread" : YES, - "NSTimer" : YES, - "NSTimeZone" : YES, - "NSUnarchiver" : YES, - "NSUndoManager" : YES, - "NSUniqueIDSpecifier" : YES, - "NSURL" : YES, - "NSURLAuthenticationChallenge" : YES, - "NSURLCache" : YES, - "NSURLConnection" : YES, - "NSURLCredential" : YES, - "NSURLCredentialStorage" : YES, - "NSURLDownload" : YES, - "NSURLHandle" : YES, - "NSURLProtectionSpace" : YES, - "NSURLProtocol" : YES, - "NSURLRequest" : YES, - "NSURLResponse" : YES, - "NSUserDefaults" : YES, - "NSValue" : YES, - "NSValueTransformer" : YES, - "NSWhoseSpecifier" : YES, - "NSXMLDocument" : YES, - "NSXMLDTD" : YES, - "NSXMLDTDNode" : YES, - "NSXMLElement" : YES, - "NSXMLNode" : YES, - "NSXMLParser" : YES, - "NSActionCell" : YES, - "NSAffineTransform Additions" : YES, - "NSAlert" : YES, - "NSAnimation" : YES, - "NSAnimationContext" : YES, - "NSAppleScript Additions" : YES, - "NSApplication" : YES, - "NSArrayController" : YES, - "NSATSTypesetter" : YES, - "NSAttributedString Application Kit Additions" : YES, - "NSBezierPath" : YES, - "NSBitmapImageRep" : YES, - "NSBox" : YES, - "NSBrowser" : YES, - "NSBrowserCell" : YES, - "NSBundle Additions" : YES, - "NSButton" : YES, - "NSButtonCell" : YES, - "NSCachedImageRep" : YES, - "NSCell" : YES, - "NSCIImageRep" : YES, - "NSClipView" : YES, - "NSCoder Application Kit Additions" : YES, - "NSCollectionView" : YES, - "NSCollectionViewItem" : YES, - "NSColor" : YES, - "NSColorList" : YES, - "NSColorPanel" : YES, - "NSColorPicker" : YES, - "NSColorSpace" : YES, - "NSColorWell" : YES, - "NSComboBox" : YES, - "NSComboBoxCell" : YES, - "NSControl" : YES, - "NSController" : YES, - "NSCursor" : YES, - "NSCustomImageRep" : YES, - "NSDatePicker" : YES, - "NSDatePickerCell" : YES, - "NSDictionaryController" : YES, - "NSDockTile" : YES, - "NSDocument" : YES, - "NSDocumentController" : YES, - "NSDrawer" : YES, - "NSEPSImageRep" : YES, - "NSEvent" : YES, - "NSFileWrapper" : YES, - "NSFont" : YES, - "NSFontDescriptor" : YES, - "NSFontManager" : YES, - "NSFontPanel" : YES, - "NSForm" : YES, - "NSFormCell" : YES, - "NSGlyphGenerator" : YES, - "NSGlyphInfo" : YES, - "NSGradient" : YES, - "NSGraphicsContext" : YES, - "NSHelpManager" : YES, - "NSImage" : YES, - "NSImageCell" : YES, - "NSImageRep" : YES, - "NSImageView" : YES, - "NSLayoutManager" : YES, - "NSLevelIndicator" : YES, - "NSLevelIndicatorCell" : YES, - "NSMatrix" : YES, - "NSMenu" : YES, - "NSMenuItem" : YES, - "NSMenuItemCell" : YES, - "NSMenuView" : YES, - "NSMutableAttributedString Additions" : YES, - "NSMutableParagraphStyle" : YES, - "NSNib" : YES, - "NSNibConnector" : YES, - "NSNibControlConnector" : YES, - "NSNibOutletConnector" : YES, - "NSObjectController" : YES, - "NSOpenGLContext" : YES, - "NSOpenGLLayer" : YES, - "NSOpenGLPixelBuffer" : YES, - "NSOpenGLPixelFormat" : YES, - "NSOpenGLView" : YES, - "NSOpenPanel" : YES, - "NSOutlineView" : YES, - "NSPageLayout" : YES, - "NSPanel" : YES, - "NSParagraphStyle" : YES, - "NSPasteboard" : YES, - "NSPasteboardItem" : YES, - "NSPathCell" : YES, - "NSPathComponentCell" : YES, - "NSPathControl" : YES, - "NSPDFImageRep" : YES, - "NSPersistentDocument" : YES, - "NSPICTImageRep" : YES, - "NSPopUpButton" : YES, - "NSPopUpButtonCell" : YES, - "NSPredicateEditor" : YES, - "NSPredicateEditorRowTemplate" : YES, - "NSPrinter" : YES, - "NSPrintInfo" : YES, - "NSPrintOperation" : YES, - "NSPrintPanel" : YES, - "NSProgressIndicator" : YES, - "NSResponder" : YES, - "NSRuleEditor" : YES, - "NSRulerMarker" : YES, - "NSRulerView" : YES, - "NSRunningApplication" : YES, - "NSSavePanel" : YES, - "NSScreen" : YES, - "NSScroller" : YES, - "NSScrollView" : YES, - "NSSearchField" : YES, - "NSSearchFieldCell" : YES, - "NSSecureTextField" : YES, - "NSSecureTextFieldCell" : YES, - "NSSegmentedCell" : YES, - "NSSegmentedControl" : YES, - "NSShadow" : YES, - "NSSlider" : YES, - "NSSliderCell" : YES, - "NSSound" : YES, - "NSSpeechRecognizer" : YES, - "NSSpeechSynthesizer" : YES, - "NSSpellChecker" : YES, - "NSSplitView" : YES, - "NSStatusBar" : YES, - "NSStatusItem" : YES, - "NSStepper" : YES, - "NSStepperCell" : YES, - "NSString Application Kit Additions" : YES, - "NSTableCellView" : YES, - "NSTableColumn" : YES, - "NSTableHeaderCell" : YES, - "NSTableHeaderView" : YES, - "NSTableView" : YES, - "NSTabView" : YES, - "NSTabViewItem" : YES, - "NSText" : YES, - "NSTextAttachment" : YES, - "NSTextAttachmentCell" : YES, - "NSTextBlock" : YES, - "NSTextContainer" : YES, - "NSTextField" : YES, - "NSTextFieldCell" : YES, - "NSTextInputContext" : YES, - "NSTextList" : YES, - "NSTextStorage" : YES, - "NSTextTab" : YES, - "NSTextTable" : YES, - "NSTextTableBlock" : YES, - "NSTextView" : YES, - "NSTokenField" : YES, - "NSTokenFieldCell" : YES, - "NSToolbar" : YES, - "NSToolbarItem" : YES, - "NSToolbarItemGroup" : YES, - "NSTouch" : YES, - "NSTrackingArea" : YES, - "NSTreeController" : YES, - "NSTreeNode" : YES, - "NSTypesetter" : YES, - "NSURL Additions" : YES, - "NSUserDefaultsController" : YES, - "NSView" : YES, - "NSViewAnimation" : YES, - "NSViewController" : YES, - "NSWindow" : YES, - "NSWindowController" : YES, - "NSWorkspace" : YES, - "NSPopover": YES, - "NSAppearance" : YES, - "NSVisualEffectView" : YES, - }; From 0eb87f85b4523ccd1057bfd6d6fcd95104316151 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 5 Jul 2025 09:01:12 +0200 Subject: [PATCH 018/103] fixed: accidentally removed files --- dist/cappuccino/bin/flatten | 368 ++++++++++++++++ dist/cappuccino/bin/fontinfo | Bin 0 -> 69888 bytes dist/cappuccino/bin/imagesize | Bin 0 -> 69536 bytes dist/cappuccino/bin/objj2objcskeleton | 613 ++++++++++++++++++++++++++ 4 files changed, 981 insertions(+) create mode 100755 dist/cappuccino/bin/flatten create mode 100755 dist/cappuccino/bin/fontinfo create mode 100755 dist/cappuccino/bin/imagesize create mode 100755 dist/cappuccino/bin/objj2objcskeleton diff --git a/dist/cappuccino/bin/flatten b/dist/cappuccino/bin/flatten new file mode 100755 index 000000000..1df033252 --- /dev/null +++ b/dist/cappuccino/bin/flatten @@ -0,0 +1,368 @@ +#!/usr/bin/env objj + +require("narwhal").ensureEngine("rhino"); + +@import + +@import "../lib/cappuccino/objj-analysis-tools.j" + +var FILE = require("file"); +var OS = require("os"); +var UTIL = require("narwhal/util"); + +var CACHEMANIFEST = require("objective-j/cache-manifest"); + +var stream = require("narwhal/term").stream; +var parser = new (require("narwhal/args").Parser)(); + +parser.usage("INPUT_PROJECT OUTPUT_PROJECT"); +parser.help("Combine a Cappuccino application into a single JavaScript file."); + +parser.option("-m", "--main", "main") + .def("main.j") + .set() + .help("The relative path (from INPUT_PROJECT) to the main file (default: 'main.j')"); + +parser.option("-F", "--framework", "frameworks") + .push() + .help("Add a frameworks directory, relative to INPUT_PROJECT (default: ['Frameworks'])"); + +parser.option("-P", "--path", "paths") + .push() + .help("Add a path (relative to the application root) to inline."); + +parser.option("-f", "--force", "force") + .def(false) + .set(true) + .help("Force overwriting OUTPUT_PROJECT if it exists"); + +parser.option("--index", "index") + .def("index.html") + .set() + .help("The root HTML file to modify (default: index.html)"); + +parser.option("-s", "--split", "number", "split") + .natural() + .def(0) + .help("Split into multiple files"); + +parser.option("-c", "--compressor", "compressor") + .def("shrinksafe") + .set() + .help("Select a compressor to use (closure-compiler, yuicompressor, shrinksafe), or \"none\" (default: shrinksafe)"); + +parser.option("--manifest", "manifest") + .set(true) + .help("Generate HTML5 cache manifest."); + +parser.option("-v", "--verbose", "verbose") + .def(false) + .set(true) + .help("Verbose logging"); + +parser.helpful(); + +function main(args) +{ + var options = parser.parse(args); + + if (options.args.length < 2) { + parser.printUsage(options); + return; + } + + var rootPath = FILE.path(options.args[0]).join("").absolute(); + var outputPath = FILE.path(options.args[1]).join("").absolute(); + + if (outputPath.exists()) { + if (options.force) { + // FIXME: why doesn't this work?! + //outputPath.rmtree(); + OS.system(["rm", "-rf", outputPath]); + } else { + stream.print("\0red(OUTPUT_PROJECT " + outputPath + " exists. Use -f to overwrite.\0)"); + OS.exit(1); + } + } + + options.frameworks.push("Frameworks"); + + var mainPath = String(rootPath.join(options.main)); + var frameworks = options.frameworks.map(function(framework) { return rootPath.join(framework); }); + var environment = "Browser"; + + stream.print("\0yellow("+Array(81).join("=")+"\0)"); + stream.print("Application root: \0green(" + rootPath + "\0)"); + stream.print("Output directory: \0green(" + outputPath + "\0)"); + + stream.print("\0yellow("+Array(81).join("=")+"\0)"); + stream.print("Main file: \0green(" + mainPath + "\0)"); + stream.print("Frameworks: \0green(" + frameworks + "\0)"); + stream.print("Environment: \0green(" + environment + "\0)"); + + var flattener = new ObjectiveJFlattener(rootPath); + + flattener.options = options; + + flattener.setIncludePaths(frameworks); + flattener.setEnvironments([environment, "ObjJ"]); + + print("Loading application."); + flattener.load(mainPath); + + print("Loading default theme."); + flattener.require("objective-j").objj_eval("("+(function() { + + var defaultThemeName = [CPApplication defaultThemeName], + bundle = nil; + + if (defaultThemeName === @"Aristo" || defaultThemeName === @"Aristo2") + bundle = [CPBundle bundleForClass:[CPApplication class]]; + else + bundle = [CPBundle mainBundle]; + + var blend = [[CPThemeBlend alloc] initWithContentsOfURL:[bundle pathForResource:defaultThemeName + @".blend"]]; + [blend loadWithDelegate:nil]; + + })+")")(); + + var applicationJSs = flattener.buildApplicationJS(); + + FILE.copyTree(rootPath, outputPath); + + applicationJSs.forEach(function(applicationJS, n) { + var name = "Application"+(n||"")+".js"; + if (options.compressor === "none") { + print("skipping compression: " + name); + } else { + print("compressing: " + name); + applicationJS = require("minify/"+options.compressor).compress(applicationJS, { charset : "UTF-8", useServer : true }); + } + outputPath.join(name).write(applicationJS, { charset : "UTF-8" }); + }); + + rewriteMainHTML(outputPath.join(options.index)); + + if (options.manifest) { + CACHEMANIFEST.generateManifest(outputPath, { + index : outputPath.join(options.index), + exclude : Object.keys(flattener.filesToCache).map(function(path) { return outputPath.join(path).toString(); }) + }); + } +} + +// ObjectiveJFlattener inherits from ObjectiveJRuntimeAnalyzer +function ObjectiveJFlattener(rootPath) { + ObjectiveJRuntimeAnalyzer.apply(this, arguments); + + this.filesToCache = {}; + this.fileCacheBuffer = []; + this.functionsBuffer = []; +} + +ObjectiveJFlattener.prototype = Object.create(ObjectiveJRuntimeAnalyzer.prototype); + +ObjectiveJFlattener.prototype.buildApplicationJS = function() { + + this.setupFileCache(); + this.serializeFunctions(); + this.serializeFileCache(); + + var additions = FILE.read(FILE.join(FILE.dirname(module.path), "..", "..", "cappuccino", "lib", "cappuccino", "objj-flatten-additions.js"), { charset:"UTF-8" }); + + var applicationJSs = []; + + if (this.options.split === 0) { + var buffer = []; + buffer.push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);"); + buffer.push(additions); + buffer.push(this.fileCacheBuffer.join("\n")); + buffer.push(this.functionsBuffer.join("\n")); + buffer.push("ObjectiveJ.bootstrap();"); + applicationJSs.push(buffer.join("\n")); + } else { + var appFilesCount = this.options.split; + + var buffers = []; + for (var i = 0; i <= appFilesCount; i++) + buffers.push([]); + + var chunks = this.fileCacheBuffer.concat(this.functionsBuffer).sort(function(chunkA, chunkB) { + return chunkA.length - chunkB.length; + }); + + // try to equally distribute the chunks. could be better but good enough for now. + var n = 0; + while (chunks.length) { + buffers[(n++ % appFilesCount) + 1].push(chunks.pop()); + } + + buffers[0].push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);"); + buffers[0].push(additions); + + buffers[0].push("var appFilesCount = " + appFilesCount +";"); + buffers[0].push("for (var i = 1; i <= appFilesCount; i++) {"); + buffers[0].push(" var script = document.createElement(\"script\");"); + buffers[0].push(" script.src = \"Application\"+i+\".js\";"); + buffers[0].push(" script.charset = \"UTF-8\";"); + buffers[0].push(" script.onload = function() { if (--appFilesCount === 0) ObjectiveJ.bootstrap(); };"); + buffers[0].push(" document.getElementsByTagName(\"head\")[0].appendChild(script);"); + buffers[0].push("}"); + + buffers.forEach(function(buffer) { + applicationJSs.push(buffer.join("\n")); + }); + } + + return applicationJSs; +} + +ObjectiveJFlattener.prototype.serializeFunctions = function() { + var inlineFunctions = true;//this.options.inlineFunctions; + + var outputFiles = {}; + + var _cachedExecutableFunctions = {}; + + this.require("objective-j").FileExecutable.allFileExecutables().forEach(function(executable) { + var path = executable.path(); + + if (inlineFunctions) + { + // stringify the function, replacing arguments + var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK + + var relative = this.rootPath.relative(path).toString(); + this.functionsBuffer.push("ObjectiveJ.StaticResource._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); + } + + var bundle = this.context.global.CFBundle.bundleContainingURL(path); + if (bundle && bundle.infoDictionary()) + { + var executablePath = bundle.executablePath(), + relativeToBundle = FILE.relative(FILE.join(bundle.path(), ""), path); + + if (executablePath) + { + if (inlineFunctions) + { + // remove the code since we're inlining the functions + executable._code = "alert("+JSON.stringify(relativeToBundle)+");"; + } + + if (!outputFiles[executablePath]) + { + outputFiles[executablePath] = []; + outputFiles[executablePath].push("@STATIC;1.0;"); + } + + var fileContents = executable.toMarkedString(); + + outputFiles[executablePath].push("p;" + relativeToBundle.length + ";" + relativeToBundle); + outputFiles[executablePath].push("t;" + fileContents.length + ";" + fileContents); + + // stream.print("Adding \0green(" + this.rootPath.relative(path) + "\0) to \0cyan(" + this.rootPath.relative(executablePath) + "\0)"); + } + } + else + CPLog.warn("No bundle (or info dictionary for) " + rootPath.relative(path)); + }, this); + + for (var executablePath in outputFiles) + { + var relative = this.rootPath.relative(executablePath).toString(); + var contents = outputFiles[executablePath].join(""); + this.filesToCache[relative] = contents; + } +} + +ObjectiveJFlattener.prototype.serializeFileCache = function() { + for (var relative in this.filesToCache) { + var contents = this.filesToCache[relative]; + print("caching: " + relative + " => " + (contents == null ? 404 : 200)); + if (contents == null) + this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 404);"); + else + this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 200, {}, "+JSON.stringify(contents)+");"); + } +} + +ObjectiveJFlattener.prototype.setupFileCache = function() { + var paths = {}; + + UTIL.update(paths, this.requestedURLs); + + this.options.paths.forEach(function(relativePath) { + paths[this.rootPath.join(relativePath)] = true; + }, this); + + Object.keys(paths).forEach(function(absolute) { + var relative = this.rootPath.relative(absolute).toString(); + if (relative.indexOf("..") === 0) + { + print("skipping (parent of app root): " + absolute); + return; + } + + if (FILE.isFile(absolute)) + { + // if (this.options.maxCachedSize && FILE.size(absolute) > this.options.maxCachedSize) + // { + // print("skipping (larger than "+this.options.maxCachedSize+" bytes): " + absolute); + // return; + // } + + var contents = FILE.read(absolute, { charset : "UTF-8" }); + this.filesToCache[relative] = contents; + } else { + this.filesToCache[relative] = null; + } + }, this); +} + +// "$1" is the matching indentation +var scriptTagsBefore = + '$1'; + +var scriptTagsAfter = + '$1'; + +// enable CPLog: +// scriptTagsAfter = '$1\n' + scriptTagsAfter; + +function rewriteMainHTML(indexHTMLPath) { + if (indexHTMLPath.isFile()) { + var indexHTML = indexHTMLPath.read({ charset : "UTF-8" }); + + // inline the Application.js if it's smallish + var applicationJSPath = indexHTMLPath.dirname().join("Application.js"); + if (applicationJSPath.size() < 10*1024) { + // escape any dollar signs by replacing them with two + // then indent by splitting/joining on newlines + scriptTagsAfter = + '$1'; + } + + // attempt to find Objective-J script tag and add ours + var newIndexHTML = indexHTML.replace(/([ \t]+)]+Objective-J\.js[^>]+>(?:\s*<\/script>)?/, + scriptTagsBefore+'\n$&\n'+scriptTagsAfter); + + if (newIndexHTML !== indexHTML) { + stream.print("\0green(Modified: "+indexHTMLPath+".\0)"); + indexHTMLPath.write(newIndexHTML, { charset : "UTF-8" }); + return; + } + } else { + stream.print("\0yellow(Warning: "+indexHTMLPath+" does not exist. Specify an alternate index HTML file with the --index option.\0)"); + } + + stream.print("\0yellow(Warning: Unable to automatically modify "+indexHTMLPath + ".\0)"); + stream.print("\nAdd the following before the Objective-J script tag:"); + stream.print(scriptTagsBefore.replace(/\$1/g, " ")); + stream.print("\nAdd the following after the Objective-J script tag:"); + stream.print(scriptTagsAfter.replace(/\$1/g, " ")); +} diff --git a/dist/cappuccino/bin/fontinfo b/dist/cappuccino/bin/fontinfo new file mode 100755 index 0000000000000000000000000000000000000000..f1ff06158a2525d34541aff240713c0067bd8633 GIT binary patch literal 69888 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 literal 0 HcmV?d00001 diff --git a/dist/cappuccino/bin/imagesize b/dist/cappuccino/bin/imagesize new file mode 100755 index 0000000000000000000000000000000000000000..5d67a1ce0a9a1e20577f82ccdfe0d3b3e94143b4 GIT binary patch literal 69536 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 Z009sH0T2KI5C8!X009sHfo}nU{{v@rhV}pe literal 0 HcmV?d00001 diff --git a/dist/cappuccino/bin/objj2objcskeleton b/dist/cappuccino/bin/objj2objcskeleton new file mode 100755 index 000000000..b7972dbb3 --- /dev/null +++ b/dist/cappuccino/bin/objj2objcskeleton @@ -0,0 +1,613 @@ +#!/usr/bin/env objj + +@import + +var fs = require("fs"), + acorn = require("objj-parser"), + walk = require("objj-parser/util/walk"), + stream = ObjectiveJ.term; + + debugger; + +function main(args) +{ + debugger; + args.shift(); + + if (args.length < 1) + return printUsage(); + + parser(args); +} + +function printUsage() +{ + console.log("objj2objskeleton [FILE] [DESTINATION]"); + console.log("Convert a objective-j file to an objective-c files skeleton (.h and .m)") +} + +// Debug function to print some JS objects +function dump(obj) +{ + console.log(JSON.stringify(obj)); +} + +function raise(pos, message) +{ + var syntaxError = new SyntaxError(message); + syntaxError.line = pos.line; + + throw syntaxError; +} + +var errors = [], + xcc = walk.make( + { + ClassDeclarationStatement: function(node, st, c) + { + var className = node.classname.name, + superclassname = node.superclassname ? node.superclassname.name : "", + declaredOutletsName = [], + classInfo = { + "name": className, + "category": node.categoryname ? node.categoryname.name : "", + "superClass": superclassname, + "outlets": [], + "actions": [], + "actionNames": [] + }; + + if (node.ivardeclarations) + { + for (var i = 0; i < node.ivardeclarations.length; ++i) + { + var ivarDecl = node.ivardeclarations[i], + ivarType = ivarDecl.ivartype ? ivarDecl.ivartype.name : null, + ivarName = ivarDecl.id.name, + ivarHasOutlet = ivarDecl.outlet ? "@outlet" : null; + + if (ivarHasOutlet) + { + if (declaredOutletsName.indexOf(ivarName) !== -1) + raise(ivarDecl.loc.start, "Outlet '" + ivarName + "' declared more than once"); + + declaredOutletsName.push(ivarName); + classInfo.outlets.push({"type": ivarType, "name": ivarName}); + } + } + } + + st.push(classInfo) + + for (var i = 0; i < node.body.length; ++i) + c(node.body[i], classInfo, "Statement"); + }, + + MethodDeclarationStatement: function(node, st, c) + { + var selectors = node.selectors, + arguments = node.arguments, + //methodReturnType = [node.returntype ? node.returntype.name : "id"], + methodHasAction = node.action ? "IBAction" : null, + selector = selectors[0].name, + actionInfo = {"name": selector, "arguments":[]}; + + if (methodHasAction) + { + if (arguments.length == 1) + { + if (st.actionNames.indexOf(selector) !== -1) + raise(node.loc.start, "Action '" + selector + "' declared more than once"); + + st.actionNames.push(selector); + + for (var i = 0; i < arguments.length; i++) + { + var argument = arguments[i], + argumentName = argument.identifier.name, + argumentType = argument.type ? argument.type.name : null; + + actionInfo.arguments.push({"type": argumentType, "name": argumentName}); + } + + st.actions.push(actionInfo) + } + else + raise(node.loc.start, "Action methods must have exactly one parameter"); + } + } + } +); + +function compile(node, state, visitor) +{ + function c(node, st, override) + { + visitor[override || node.type](node, st, c); + } + + c(node, state); +}; + +function removeLastSlashIfNecessary(path) +{ + if (path[path.length - 1] == "/") + return path.substring(0, path.length - 1); + + return path; +} + +/* + $1 Full project source path + $2 Destination + $-n name of the cocoa files +*/ +function parser(args) +{ + try + { + var sourcePath = args.shift(), + projectBasePath = removeLastSlashIfNecessary(args.shift()), + outputDirectory = projectBasePath, + baseFilename = [sourcePath lastPathComponent], + baseFilenameWithNoExtension = args.shift() == "-n" ? args.shift() : baseFilename.substring(0, baseFilename.length - 2), + outputHeaderURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".h"]), + outputImplementationURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".m"]), + source = fs.readFileSync(sourcePath, { encoding: "utf8" }), + tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath }), + classesInformation = [], + ObjectiveCSource = "", + ObjectiveCHeader = "", + hasErrors = NO; + + compile(tokens, classesInformation, xcc); + + // dump(classesInformation) + + ObjectiveCHeader += + "#import \n" + + '#import "xcc_general_include.h"\n'; + + ObjectiveCSource += "#import \"" + outputHeaderURL.lastPathComponent() + "\"\n"; + + // Traverse each found classes + classesInformation.forEach(function(aClass) + { + // add new class definition + if (aClass.superClass) + ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ : %@", aClass.name, NSCompatibleClassName(aClass.superClass)]; + else + ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ (%@)", NSCompatibleClassName(aClass.name, NO), aClass.category]; + + // add each outlet in header + if (aClass.outlets.length > 0) + ObjectiveCHeader += "\n"; + + aClass.outlets.forEach(function(anOutlet) + { + ObjectiveCHeader += [CPString stringWithFormat:@"\n@property (assign) IBOutlet %@ %@;", NSCompatibleClassName(anOutlet.type, YES), anOutlet.name]; + }); + + if (aClass.actions.length > 0) + ObjectiveCHeader += "\n"; + + // add each action in header + aClass.actions.forEach(function(anAction) + { + ObjectiveCHeader += [CPString stringWithFormat:@"\n- (IBAction)%@:(%@)%@;", anAction.name, anAction.arguments[0].type, anAction.arguments[0].name]; + }); + + if (aClass.outlets.length > 0 || aClass.actions.length > 0) + ObjectiveCHeader += "\n"; + + ObjectiveCHeader += "\n@end\n"; + + // fill up the implementation file + ObjectiveCSource += "\n@implementation " + NSCompatibleClassName(aClass.name, NO) + "\n@end\n"; + }); + + if (ObjectiveCSource.length) + fs.writeFileSync(outputImplementationURL.absoluteString(), ObjectiveCSource, 'utf8'); + + if (ObjectiveCHeader.length) + fs.writeFileSync(outputHeaderURL.absoluteString(), ObjectiveCHeader, 'utf8'); + } + catch (e) + { + [errors addObject:@{ + @"message": e.message, + @"sourcePath": sourcePath, + @"line": e.line + }]; + + hasErrors = YES; + } + + if ([errors count]) + { + var plist = [CPPropertyListSerialization dataFromPropertyList:errors format:CPPropertyListXMLFormat_v1_0]; + + stream.printError([plist rawString]); + + // If there were category warnings, hasErrors is NO, so return a warning status + process.exit(hasErrors ? 1 : 2); + } +} + +function NSCompatibleClassName(aClassName, asPointer) +{ + if (aClassName === "var" || aClassName === "id") + return "id"; + + var prefix = aClassName.substr(0, 2), + asterisk = asPointer ? "*" : ""; + + if (prefix !== "CP") + return aClassName + asterisk; + + var NSClassName = "NS" + aClassName.substr(2); + + if (NSClasses[NSClassName]) + return NSClassName + asterisk; + + if (ReplacementClasses[aClassName]) + return ReplacementClasses[aClassName] + asterisk; + + return aClassName + asterisk; +} + +var ReplacementClasses = { + "CPWebView": "WebView", + "CPRadio": "NSButtonCell", + "CPRadioGroup": "NSMatrix" + }; + +var NSClasses = { + "NSAffineTransform" : YES, + "NSAppleEventDescriptor" : YES, + "NSAppleEventManager" : YES, + "NSAppleScript" : YES, + "NSArchiver" : YES, + "NSArray" : YES, + "NSAssertionHandler" : YES, + "NSAttributedString" : YES, + "NSAutoreleasePool" : YES, + "NSBlockOperation" : YES, + "NSBundle" : YES, + "NSCache" : YES, + "NSCachedURLResponse" : YES, + "NSCalendar" : YES, + "NSCharacterSet" : YES, + "NSClassDescription" : YES, + "NSCloneCommand" : YES, + "NSCloseCommand" : YES, + "NSCoder" : YES, + "NSComparisonPredicate" : YES, + "NSCompoundPredicate" : YES, + "NSCondition" : YES, + "NSConditionLock" : YES, + "NSConnection" : YES, + "NSCountCommand" : YES, + "NSCountedSet" : YES, + "NSCreateCommand" : YES, + "NSData" : YES, + "NSDate" : YES, + "NSDateComponents" : YES, + "NSDateFormatter" : YES, + "NSDecimalNumber" : YES, + "NSDecimalNumberHandler" : YES, + "NSDeleteCommand" : YES, + "NSDeserializer" : YES, + "NSDictionary" : YES, + "NSDirectoryEnumerator" : YES, + "NSDistantObject" : YES, + "NSDistantObjectRequest" : YES, + "NSDistributedLock" : YES, + "NSDistributedNotificationCenter" : YES, + "NSEnumerator" : YES, + "NSError" : YES, + "NSException" : YES, + "NSExistsCommand" : YES, + "NSExpression" : YES, + "NSFileHandle" : YES, + "NSFileManager" : YES, + "NSFileWrapper" : YES, + "NSFormatter" : YES, + "NSGarbageCollector" : YES, + "NSGetCommand" : YES, + "NSHashTable" : YES, + "NSHost" : YES, + "NSHTTPCookie" : YES, + "NSHTTPCookieStorage" : YES, + "NSHTTPURLResponse" : YES, + "NSIndexPath" : YES, + "NSIndexSet" : YES, + "NSIndexSpecifier" : YES, + "NSInputStream" : YES, + "NSInvocation" : YES, + "NSInvocationOperation" : YES, + "NSKeyedArchiver" : YES, + "NSKeyedUnarchiver" : YES, + "NSLocale" : YES, + "NSLock" : YES, + "NSLogicalTest" : YES, + "NSMachBootstrapServer" : YES, + "NSMachPort" : YES, + "NSMapTable" : YES, + "NSMessagePort" : YES, + "NSMessagePortNameServer" : YES, + "NSMetadataItem" : YES, + "NSMetadataQuery" : YES, + "NSMetadataQueryAttributeValueTuple" : YES, + "NSMetadataQueryResultGroup" : YES, + "NSMethodSignature" : YES, + "NSMiddleSpecifier" : YES, + "NSMoveCommand" : YES, + "NSMutableArray" : YES, + "NSMutableAttributedString" : YES, + "NSMutableCharacterSet" : YES, + "NSMutableData" : YES, + "NSMutableDictionary" : YES, + "NSMutableIndexSet" : YES, + "NSMutableSet" : YES, + "NSMutableString" : YES, + "NSMutableURLRequest" : YES, + "NSNameSpecifier" : YES, + "NSNetService" : YES, + "NSNetServiceBrowser" : YES, + "NSNotification" : YES, + "NSNotificationCenter" : YES, + "NSNotificationQueue" : YES, + "NSNull" : YES, + "NSNumber" : YES, + "NSNumberFormatter" : YES, + "NSObject" : YES, + "NSOperation" : YES, + "NSOperationQueue" : YES, + "NSOrthography" : YES, + "NSOutputStream" : YES, + "NSPipe" : YES, + "NSPointerArray" : YES, + "NSPointerFunctions" : YES, + "NSPort" : YES, + "NSPortCoder" : YES, + "NSPortMessage" : YES, + "NSPortNameServer" : YES, + "NSPositionalSpecifier" : YES, + "NSPredicate" : YES, + "NSProcessInfo" : YES, + "NSPropertyListSerialization" : YES, + "NSPropertySpecifier" : YES, + "NSProtocolChecker" : YES, + "NSProxy" : YES, + "NSPurgeableData" : YES, + "NSQuitCommand" : YES, + "NSRandomSpecifier" : YES, + "NSRangeSpecifier" : YES, + "NSRecursiveLock" : YES, + "NSRelativeSpecifier" : YES, + "NSRunLoop" : YES, + "NSScanner" : YES, + "NSScriptClassDescription" : YES, + "NSScriptCoercionHandler" : YES, + "NSScriptCommand" : YES, + "NSScriptCommandDescription" : YES, + "NSScriptExecutionContext" : YES, + "NSScriptObjectSpecifier" : YES, + "NSScriptSuiteRegistry" : YES, + "NSScriptWhoseTest" : YES, + "NSSerializer" : YES, + "NSSet" : YES, + "NSSetCommand" : YES, + "NSSocketPort" : YES, + "NSSocketPortNameServer" : YES, + "NSSortDescriptor" : YES, + "NSSpecifierTest" : YES, + "NSSpellServer" : YES, + "NSStream" : YES, + "NSString" : YES, + "NSTask" : YES, + "NSTextCheckingResult" : YES, + "NSThread" : YES, + "NSTimer" : YES, + "NSTimeZone" : YES, + "NSUnarchiver" : YES, + "NSUndoManager" : YES, + "NSUniqueIDSpecifier" : YES, + "NSURL" : YES, + "NSURLAuthenticationChallenge" : YES, + "NSURLCache" : YES, + "NSURLConnection" : YES, + "NSURLCredential" : YES, + "NSURLCredentialStorage" : YES, + "NSURLDownload" : YES, + "NSURLHandle" : YES, + "NSURLProtectionSpace" : YES, + "NSURLProtocol" : YES, + "NSURLRequest" : YES, + "NSURLResponse" : YES, + "NSUserDefaults" : YES, + "NSValue" : YES, + "NSValueTransformer" : YES, + "NSWhoseSpecifier" : YES, + "NSXMLDocument" : YES, + "NSXMLDTD" : YES, + "NSXMLDTDNode" : YES, + "NSXMLElement" : YES, + "NSXMLNode" : YES, + "NSXMLParser" : YES, + "NSActionCell" : YES, + "NSAffineTransform Additions" : YES, + "NSAlert" : YES, + "NSAnimation" : YES, + "NSAnimationContext" : YES, + "NSAppleScript Additions" : YES, + "NSApplication" : YES, + "NSArrayController" : YES, + "NSATSTypesetter" : YES, + "NSAttributedString Application Kit Additions" : YES, + "NSBezierPath" : YES, + "NSBitmapImageRep" : YES, + "NSBox" : YES, + "NSBrowser" : YES, + "NSBrowserCell" : YES, + "NSBundle Additions" : YES, + "NSButton" : YES, + "NSButtonCell" : YES, + "NSCachedImageRep" : YES, + "NSCell" : YES, + "NSCIImageRep" : YES, + "NSClipView" : YES, + "NSCoder Application Kit Additions" : YES, + "NSCollectionView" : YES, + "NSCollectionViewItem" : YES, + "NSColor" : YES, + "NSColorList" : YES, + "NSColorPanel" : YES, + "NSColorPicker" : YES, + "NSColorSpace" : YES, + "NSColorWell" : YES, + "NSComboBox" : YES, + "NSComboBoxCell" : YES, + "NSControl" : YES, + "NSController" : YES, + "NSCursor" : YES, + "NSCustomImageRep" : YES, + "NSDatePicker" : YES, + "NSDatePickerCell" : YES, + "NSDictionaryController" : YES, + "NSDockTile" : YES, + "NSDocument" : YES, + "NSDocumentController" : YES, + "NSDrawer" : YES, + "NSEPSImageRep" : YES, + "NSEvent" : YES, + "NSFileWrapper" : YES, + "NSFont" : YES, + "NSFontDescriptor" : YES, + "NSFontManager" : YES, + "NSFontPanel" : YES, + "NSForm" : YES, + "NSFormCell" : YES, + "NSGlyphGenerator" : YES, + "NSGlyphInfo" : YES, + "NSGradient" : YES, + "NSGraphicsContext" : YES, + "NSHelpManager" : YES, + "NSImage" : YES, + "NSImageCell" : YES, + "NSImageRep" : YES, + "NSImageView" : YES, + "NSLayoutManager" : YES, + "NSLevelIndicator" : YES, + "NSLevelIndicatorCell" : YES, + "NSMatrix" : YES, + "NSMenu" : YES, + "NSMenuItem" : YES, + "NSMenuItemCell" : YES, + "NSMenuView" : YES, + "NSMutableAttributedString Additions" : YES, + "NSMutableParagraphStyle" : YES, + "NSNib" : YES, + "NSNibConnector" : YES, + "NSNibControlConnector" : YES, + "NSNibOutletConnector" : YES, + "NSObjectController" : YES, + "NSOpenGLContext" : YES, + "NSOpenGLLayer" : YES, + "NSOpenGLPixelBuffer" : YES, + "NSOpenGLPixelFormat" : YES, + "NSOpenGLView" : YES, + "NSOpenPanel" : YES, + "NSOutlineView" : YES, + "NSPageLayout" : YES, + "NSPanel" : YES, + "NSParagraphStyle" : YES, + "NSPasteboard" : YES, + "NSPasteboardItem" : YES, + "NSPathCell" : YES, + "NSPathComponentCell" : YES, + "NSPathControl" : YES, + "NSPDFImageRep" : YES, + "NSPersistentDocument" : YES, + "NSPICTImageRep" : YES, + "NSPopUpButton" : YES, + "NSPopUpButtonCell" : YES, + "NSPredicateEditor" : YES, + "NSPredicateEditorRowTemplate" : YES, + "NSPrinter" : YES, + "NSPrintInfo" : YES, + "NSPrintOperation" : YES, + "NSPrintPanel" : YES, + "NSProgressIndicator" : YES, + "NSResponder" : YES, + "NSRuleEditor" : YES, + "NSRulerMarker" : YES, + "NSRulerView" : YES, + "NSRunningApplication" : YES, + "NSSavePanel" : YES, + "NSScreen" : YES, + "NSScroller" : YES, + "NSScrollView" : YES, + "NSSearchField" : YES, + "NSSearchFieldCell" : YES, + "NSSecureTextField" : YES, + "NSSecureTextFieldCell" : YES, + "NSSegmentedCell" : YES, + "NSSegmentedControl" : YES, + "NSShadow" : YES, + "NSSlider" : YES, + "NSSliderCell" : YES, + "NSSound" : YES, + "NSSpeechRecognizer" : YES, + "NSSpeechSynthesizer" : YES, + "NSSpellChecker" : YES, + "NSSplitView" : YES, + "NSStatusBar" : YES, + "NSStatusItem" : YES, + "NSStepper" : YES, + "NSStepperCell" : YES, + "NSString Application Kit Additions" : YES, + "NSTableCellView" : YES, + "NSTableColumn" : YES, + "NSTableHeaderCell" : YES, + "NSTableHeaderView" : YES, + "NSTableView" : YES, + "NSTabView" : YES, + "NSTabViewItem" : YES, + "NSText" : YES, + "NSTextAttachment" : YES, + "NSTextAttachmentCell" : YES, + "NSTextBlock" : YES, + "NSTextContainer" : YES, + "NSTextField" : YES, + "NSTextFieldCell" : YES, + "NSTextInputContext" : YES, + "NSTextList" : YES, + "NSTextStorage" : YES, + "NSTextTab" : YES, + "NSTextTable" : YES, + "NSTextTableBlock" : YES, + "NSTextView" : YES, + "NSTokenField" : YES, + "NSTokenFieldCell" : YES, + "NSToolbar" : YES, + "NSToolbarItem" : YES, + "NSToolbarItemGroup" : YES, + "NSTouch" : YES, + "NSTrackingArea" : YES, + "NSTreeController" : YES, + "NSTreeNode" : YES, + "NSTypesetter" : YES, + "NSURL Additions" : YES, + "NSUserDefaultsController" : YES, + "NSView" : YES, + "NSViewAnimation" : YES, + "NSViewController" : YES, + "NSWindow" : YES, + "NSWindowController" : YES, + "NSWorkspace" : YES, + "NSPopover": YES, + "NSAppearance" : YES, + "NSVisualEffectView" : YES, + }; From b8b988c10b41886015e96a51e91f131fca3d6f7c Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 5 Jul 2025 09:04:26 +0200 Subject: [PATCH 019/103] Update CPResponderTest.j --- Tests/AppKit/CPResponderTest.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AppKit/CPResponderTest.j b/Tests/AppKit/CPResponderTest.j index 5c4729b71..2aba5adc3 100644 --- a/Tests/AppKit/CPResponderTest.j +++ b/Tests/AppKit/CPResponderTest.j @@ -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]; } From 1c4d9b1d8fc8c83406c8c1445e4363093ec8b2ef Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 5 Jul 2025 12:29:11 +0200 Subject: [PATCH 020/103] Update CPButtonTest.j --- Tests/AppKit/CPButtonTest.j | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/AppKit/CPButtonTest.j b/Tests/AppKit/CPButtonTest.j index 19c0172fb..9abf42e27 100644 --- a/Tests/AppKit/CPButtonTest.j +++ b/Tests/AppKit/CPButtonTest.j @@ -44,11 +44,11 @@ [button setKeyEquivalent:"a"]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0]]; + characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:wasClicked]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]]; + characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertTrue:wasClicked]; } @@ -60,11 +60,11 @@ [button setKeyEquivalentModifierMask:CPAlternateKeyMask]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]]; + characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:wasClicked]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:CPAlternateKeyMask timestamp:0 windowNumber:0 context:nil - characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]]; + characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertTrue:wasClicked]; } @@ -76,12 +76,12 @@ [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]]; + characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:wasClicked]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:CPShiftKeyMask timestamp:0 windowNumber:0 context:nil - characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]]; + characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertTrue:wasClicked]; } @@ -92,15 +92,15 @@ [button setKeyEquivalent:CPEscapeFunctionKey]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:CPDeleteCharacter charactersIgnoringModifiers:CPDeleteCharacter isARepeat:NO keyCode:0]]; + characters:CPDeleteCharacter charactersIgnoringModifiers:CPDeleteCharacter isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:wasClicked]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]]; + characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:wasClicked]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]]; + characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertTrue:wasClicked]; } From 98ef6fd902d00e2532dec2508988419b69b1c68a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 5 Jul 2025 12:30:58 +0200 Subject: [PATCH 021/103] Update CPEventTest.j --- Tests/AppKit/CPEventTest.j | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/AppKit/CPEventTest.j b/Tests/AppKit/CPEventTest.j index 7570583dc..b64f92312 100644 --- a/Tests/AppKit/CPEventTest.j +++ b/Tests/AppKit/CPEventTest.j @@ -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"]; From ce7fec170a56579d1feb99c9bb0bf4532fd385d1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 5 Jul 2025 12:33:21 +0200 Subject: [PATCH 022/103] Update CPEventTest.j --- Tests/AppKit/CPEventTest.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AppKit/CPEventTest.j b/Tests/AppKit/CPEventTest.j index b64f92312..feef12694 100644 --- a/Tests/AppKit/CPEventTest.j +++ b/Tests/AppKit/CPEventTest.j @@ -31,7 +31,7 @@ timestamp:400.5 windowNumber:300 context:nil eventNumber:0 clickCount:2 pressure:0.5]; [self assert:@"CPEvent: type=2 loc={50, 50} time=400.5 flags=0x20000 win=undefined winNum=0 ctxt=null evNum=0 click=2 buttonNumber=0 pressure=0.5" equals:[anEvent description]]; - anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask | CPCommandKeyMask timestamp:12345.6 windowNumber:10 context:nil characters:"X" charactersIgnoringModifiers:"x" isARepeat:NO keyCode:10]; + anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask | CPCommandKeyMask timestamp:12345.6 windowNumber:10 context:nil characters:"X" charactersIgnoringModifiers:"x" isARepeat:NO keyCode:10 isActionKey:NO]; [self assert:@"CPEvent: type=10 loc={0, 0} time=12345.6 flags=0x120000 win=null winNum=10 ctxt=null chars=\"X\" unmodchars=\"x\" repeat=0 keyCode=10" equals:[anEvent description]]; From 27280f6da1e1b1b6644b63c2c8014af0f5f29140 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 5 Jul 2025 12:35:43 +0200 Subject: [PATCH 023/103] Update CPMenuTest.j --- Tests/AppKit/CPMenuTest.j | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/AppKit/CPMenuTest.j b/Tests/AppKit/CPMenuTest.j index 6449b65aa..b50afdea3 100644 --- a/Tests/AppKit/CPMenuTest.j +++ b/Tests/AppKit/CPMenuTest.j @@ -149,24 +149,24 @@ // Don't match anything. [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask timestamp:0 windowNumber:0 context:nil - characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0]]; + characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled]; [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0]]; + characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled]; [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask timestamp:0 windowNumber:0 context:nil - characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0]]; + characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || undoWasCalled]; [self assertTrue:openDocumentWasCalled message:"expect openDocumentWasCalled"]; openDocumentWasCalled = NO; [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask timestamp:0 windowNumber:0 context:nil - characters:CPUndoKeyEquivalent charactersIgnoringModifiers:CPUndoKeyEquivalent isARepeat:NO keyCode:0]]; + characters:CPUndoKeyEquivalent charactersIgnoringModifiers:CPUndoKeyEquivalent isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled]; [self assertTrue:undoWasCalled]; } @@ -177,7 +177,7 @@ [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]]; + characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || openDocumentWasCalled || undoWasCalled]; [self assertTrue:escapeNoModifierWasCalled]; @@ -185,7 +185,7 @@ [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask timestamp:0 windowNumber:0 context:nil - characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]]; + characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled]; [self assertTrue:escapeWasCalled]; } @@ -196,7 +196,7 @@ [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask timestamp:0 windowNumber:0 context:nil - characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0]]; + characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || saveDocumentAsWasCalled || undoWasCalled]; [self assertTrue:saveDocumentWasCalled message:"saveDocumentWasCalled"]; @@ -204,7 +204,7 @@ [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask | CPShiftKeyMask timestamp:0 windowNumber:0 context:nil - characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0]]; + characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || saveDocumentWasCalled || undoWasCalled]; [self assertTrue:saveDocumentAsWasCalled message:"saveDocumentAsWasCalled"]; } From 2fe29d4a262ec9f54a1c60ef7134d44e71d951d0 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 5 Jul 2025 13:36:14 +0200 Subject: [PATCH 024/103] Update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fdcc77f81..36beffcd1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ node_modules /dist/objective-j/lib /dist/cappuccino/package.json /dist/cappuccino/lib - +/dist/cappuccino/bin From ce00fdaae82709c2eaea946295742589ca504df8 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 5 Jul 2025 15:37:32 +0200 Subject: [PATCH 025/103] fixed: copy-paste-issues --- AppKit/CPTextView/CPTextView.j | 39 +++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 3666c321c..a6585087e 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -449,7 +449,6 @@ var kDelegateRespondsTo_textShouldBeginEditing { [super copy:sender]; - var selectedRange = [self selectedRange], pasteboard = [CPPasteboard generalPasteboard], stringForPasting = [[self textStorage] attributedSubstringFromRange:CPMakeRangeCopy(selectedRange)], @@ -517,6 +516,9 @@ var kDelegateRespondsTo_textShouldBeginEditing - (void)paste:(id)sender { + if ([[CPApp currentEvent] type] != CPAppKitDefined) + return; + [self _pasteString:[self _stringForPasting]]; } @@ -1705,6 +1707,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) @@ -2670,7 +2675,8 @@ var _CPCopyPlaceholder = '-'; }; // Fires for simple key presses (a, b, 1, 2) - _CPNativeInputField.addEventListener('input', function(e) { + _CPNativeInputField.addEventListener('input', function(e) + { // If we are in a composition (e.g., IME), we do nothing. // We wait for 'compositionend' to get the final, complete text. if (_isComposing) { @@ -2699,20 +2705,16 @@ var _CPCopyPlaceholder = '-'; var nativeClipboard = (e.originalEvent || e).clipboardData; var richtext; var currentFirstResponder = [[CPApp keyWindow] firstResponder]; - var isPlain = ![currentFirstResponder isRichText]; - // Check for shift key to force plain text paste. - if (!isPlain && !e.shiftKey && (richtext = nativeClipboard.getData('text/rtf'))) + // Check for richtext (shift key to force plain text paste). + if ([currentFirstResponder isRichText] && !e.shiftKey && (richtext = nativeClipboard.getData('text/rtf'))) { - setTimeout(function() - { - [currentFirstResponder paste:self]; - }, 0); + [currentFirstResponder _pasteString:richtext]; return; } var data = nativeClipboard.getData('text/plain'); - [currentFirstResponder paste:self]; + [currentFirstResponder _pasteString:data]; }; // COPY handler @@ -2720,10 +2722,11 @@ var _CPCopyPlaceholder = '-'; { e.preventDefault(); var pasteboard = [CPPasteboard generalPasteboard]; - var currentFirstResponder = [[CPApp keyWindow] firstResponder]; - [currentFirstResponder copy:self]; // This populates the CP pasteboard + // First, copy the data to populate the CP clipboard + [[[CPApp keyWindow] firstResponder] copy:self]; + // Now, copy the data to the native clipboard var stringForPasting = [pasteboard stringForType:CPStringPboardType] || ''; e.clipboardData.setData('text/plain', stringForPasting); @@ -2738,20 +2741,22 @@ var _CPCopyPlaceholder = '-'; { e.preventDefault(); var pasteboard = [CPPasteboard generalPasteboard]; + var nativeClipboard = (e.originalEvent || e).clipboardData; var currentFirstResponder = [[CPApp keyWindow] firstResponder]; - - // First, copy the data to populate the clipboard +debugger + // 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] || ''; - e.clipboardData.setData('text/plain', stringForPasting); + nativeClipboard.setData('text/plain', stringForPasting); var rtfForPasting = [pasteboard stringForType:CPRTFPboardType]; if (rtfForPasting) - e.clipboardData.setData('text/rtf', rtfForPasting); + nativeClipboard.setData('text/rtf', rtfForPasting); // Then, perform the delete part of the cut operation in the text view - [currentFirstResponder delete:self]; + [currentFirstResponder deleteBackward:self]; }; #endif } From 570c31f24825d4e054dc46623890884753c73952 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 6 Jul 2025 14:04:35 +0200 Subject: [PATCH 026/103] fixed: richtext copypaste --- AppKit/CPTextView/CPTextView.j | 42 +++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index a6585087e..0b0f00253 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2703,18 +2703,37 @@ var _CPCopyPlaceholder = '-'; { e.preventDefault(); var nativeClipboard = (e.originalEvent || e).clipboardData; - var richtext; var currentFirstResponder = [[CPApp keyWindow] firstResponder]; - // Check for richtext (shift key to force plain text paste). - if ([currentFirstResponder isRichText] && !e.shiftKey && (richtext = nativeClipboard.getData('text/rtf'))) + // Can we accept richtext? Then this is our preference (fixme: shift key to force plain text paste) + if ([currentFirstResponder isRichText]) { - [currentFirstResponder _pasteString:richtext]; - return; + 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 data = nativeClipboard.getData('text/plain'); - [currentFirstResponder _pasteString:data]; + var nativeString = nativeClipboard.getData('text/plain'); + [currentFirstResponder _pasteString:nativeString || [pasteboard stringForType:CPStringPboardType] || '']; }; // COPY handler @@ -2722,18 +2741,19 @@ var _CPCopyPlaceholder = '-'; { 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 to the native clipboard + // Now, copy the data over to the native clipboard var stringForPasting = [pasteboard stringForType:CPStringPboardType] || ''; - e.clipboardData.setData('text/plain', stringForPasting); + nativeClipboard.setData('text/plain', stringForPasting); var rtfForPasting = [pasteboard stringForType:CPRTFPboardType]; if (rtfForPasting) - e.clipboardData.setData('text/rtf', rtfForPasting); + nativeClipboard.setData('text/rtf', rtfForPasting); }; // CUT handler @@ -2743,7 +2763,7 @@ var _CPCopyPlaceholder = '-'; var pasteboard = [CPPasteboard generalPasteboard]; var nativeClipboard = (e.originalEvent || e).clipboardData; var currentFirstResponder = [[CPApp keyWindow] firstResponder]; -debugger + // First, copy the data to populate the CP clipboard [currentFirstResponder copy:self]; From 38f523b3c278aa98e986c59ad2e9312c7ffb5a5a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 15 Jul 2025 18:54:09 +0200 Subject: [PATCH 027/103] new: UIBuilder demo application --- Tests/Manual/UIBuilderDemo/AppController.j | 422 ++++++ Tests/Manual/UIBuilderDemo/ConnectionView.j | 97 ++ Tests/Manual/UIBuilderDemo/Info.plist | 14 + .../UIBuilderDemo/InspectorController.j | 215 +++ .../UIBuilderDemo/Resources/spinner.gif | Bin 0 -> 1849 bytes .../Manual/UIBuilderDemo/UIBuilderConstants.j | 10 + .../UIBuilderDemo/UIBuilderController.j | 645 ++++++++ Tests/Manual/UIBuilderDemo/UICanvasView.j | 996 ++++++++++++ Tests/Manual/UIBuilderDemo/UIElementView.j | 1349 +++++++++++++++++ Tests/Manual/UIBuilderDemo/index.html | 164 ++ Tests/Manual/UIBuilderDemo/main.j | 10 + 11 files changed, 3922 insertions(+) create mode 100644 Tests/Manual/UIBuilderDemo/AppController.j create mode 100644 Tests/Manual/UIBuilderDemo/ConnectionView.j create mode 100755 Tests/Manual/UIBuilderDemo/Info.plist create mode 100644 Tests/Manual/UIBuilderDemo/InspectorController.j create mode 100755 Tests/Manual/UIBuilderDemo/Resources/spinner.gif create mode 100644 Tests/Manual/UIBuilderDemo/UIBuilderConstants.j create mode 100644 Tests/Manual/UIBuilderDemo/UIBuilderController.j create mode 100644 Tests/Manual/UIBuilderDemo/UICanvasView.j create mode 100644 Tests/Manual/UIBuilderDemo/UIElementView.j create mode 100644 Tests/Manual/UIBuilderDemo/index.html create mode 100755 Tests/Manual/UIBuilderDemo/main.j diff --git a/Tests/Manual/UIBuilderDemo/AppController.j b/Tests/Manual/UIBuilderDemo/AppController.j new file mode 100644 index 000000000..6d9fa30a7 --- /dev/null +++ b/Tests/Manual/UIBuilderDemo/AppController.j @@ -0,0 +1,422 @@ +// +// AppController.j +// Main application controller. Sets up the window, canvas, palette, +// and controllers on launch. +// + +@import +@import "UIBuilderController.j" +@import "UICanvasView.j" +@import "UIElementView.j"; +@import "InspectorController.j"; + +@implementation CPColor (StandardColors) + +// A standard light gray for control backgrounds, like buttons. ++ (CPColor)controlColor +{ + return [CPColor colorWithCalibratedWhite:0.9 alpha:1.0]; +} + +// A medium gray for shadows or borders. ++ (CPColor)controlShadowColor +{ + return [CPColor grayColor]; +} + +// A dark gray for text on light controls. ++ (CPColor)controlDarkShadowColor +{ + return [CPColor darkGrayColor]; +} + +// The primary color for selected items. ++ (CPColor)selectedControlColor +{ + // Corresponds to the default blue selection color in macOS. + return [CPColor colorWithCalibratedRed:0.0 green:0.478 blue:1.0 alpha:1.0]; +} + +// A secondary selection color, often used for inactive windows or rubber-band selections. ++ (CPColor)alternateSelectedControlColor +{ + return [CPColor colorWithCalibratedRed:0.2 green:0.5 blue:0.9 alpha:1.0]; +} + +// The color for an inactive or secondary selection, like a window title bar. ++ (CPColor)secondarySelectedControlColor +{ + return [CPColor lightGrayColor]; +} + +// The highlight color for an element that has keyboard focus. ++ (CPColor)keyboardFocusIndicatorColor +{ + return [CPColor colorWithCalibratedRed:0.3 green:0.6 blue:1.0 alpha:1.0]; +} + +// The standard background color for a window's content area. ++ (CPColor)windowBackgroundColor +{ + return [CPColor colorWithCalibratedWhite:0.93 alpha:1.0]; +} + +// The background color for text-editing views. ++ (CPColor)textBackgroundColor +{ + return [CPColor whiteColor]; +} + +@end + +// Required additions from original EFView.j for graphics and text handling +@implementation CPString(SizingAddition) +- (CPSize)sizeWithAttributes:(CPDictionary)stringAttributes +{ + var font = [stringAttributes objectForKey:CPFontAttributeName] || [CPFont systemFontOfSize:12]; + // This is a simplified implementation. For more complex text, you might need a more robust solution. + var ctx = [[CPGraphicsContext currentContext] graphicsPort]; + var oldFont = ctx.font; + ctx.font = [font cssString]; + var metrics = ctx.measureText(self); + ctx.font = oldFont; + return CGSizeMake(metrics.width, [[font fontDescriptor] pointSize]); +} +- (void)drawAtPoint:(CGPoint)aPoint withAttributes:(CPDictionary)attributes +{ + var ctx = [[CPGraphicsContext currentContext] graphicsPort]; + var font = [attributes objectForKey:CPFontAttributeName] || [CPFont systemFontOfSize:12]; + var color = [attributes objectForKey:CPForegroundColorAttributeName] || [CPColor blackColor]; + + ctx.font = [font cssString]; + [color setFill]; + ctx.fillText(self, aPoint.x, aPoint.y + [[font fontDescriptor] pointSize]); +} +@end + +@implementation CPBezierPath(RoundedRectangle) ++ (CPBezierPath)bezierPathWithRoundedRect:(CPRect)aRect radius:(float)radius +{ + return [self bezierPathWithRoundedRect:aRect xRadius:radius yRadius:radius]; +} +@end + + +// A simple draggable symbol for the palette +@implementation DraggableSymbolView : CPView +{ + CPString _dragType; +} + +- (void)setDragType:(CPString)aType +{ + _dragType = aType; +} +-(BOOL)acceptsFirstMouse:(CPEvent)aEvent +{ + return YES; +} + +- (void)mouseDown:(CPEvent)theEvent +{ + // 1. Create a placeholder view that is a visual copy of this one. + var dragPlaceholder = [[DraggableSymbolView alloc] initWithFrame:[self bounds]]; + [dragPlaceholder setDragType:_dragType]; // Ensure it can draw its title correctly + [dragPlaceholder setAlphaValue:0.75]; // Make it semi-transparent for good UX + + var pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard]; + [pasteboard declareTypes:[_dragType] owner:nil]; + [pasteboard setString:@"1" forType:_dragType]; + + [self dragView:dragPlaceholder + at:[self bounds].origin + offset:nil + event:theEvent + pasteboard:pasteboard + source:self + slideBack:YES]; +} + +// The drawRect: method defines what the view looks like, and therefore +// what the dragged placeholder view will look like. +- (void)drawRect:(CGRect)rect +{ + var bounds = [self bounds]; + + // Background + [[CPColor controlColor] set]; + [CPBezierPath fillRect:bounds]; + [[CPColor controlShadowColor] set]; + [CPBezierPath strokeRect:bounds]; + + if ([_dragType isEqualToString:UIWindowDragType]) + { + // Draw a window + var windowRect = CGRectInset(bounds, 5, 5); + var titleBarHeight = 10; + + // Draw the title bar + var titleBarRect = CGRectMake(windowRect.origin.x, windowRect.origin.y, windowRect.size.width, titleBarHeight); + [[CPColor grayColor] set]; + [CPBezierPath fillRect:titleBarRect]; + + // Draw the content area + var contentRect = CGRectMake(windowRect.origin.x, windowRect.origin.y + titleBarHeight, windowRect.size.width, windowRect.size.height - titleBarHeight); + [[CPColor whiteColor] set]; + [CPBezierPath fillRect:contentRect]; + + // Draw the border for the whole window + [[CPColor blackColor] set]; + [CPBezierPath strokeRect:windowRect]; + } + else if ([_dragType isEqualToString:UIButtonDragType]) + { + // Draw a button + var buttonRect = CGRectInset(bounds, 8, 10); + var path = [CPBezierPath bezierPathWithRoundedRect:buttonRect radius:5]; + [[CPColor whiteColor] set]; + [path fill]; + [[CPColor blackColor] set]; + [path stroke]; + } + else if ([_dragType isEqualToString:UISliderDragType]) + { + // Draw a slider + var sliderY = bounds.size.height / 2; + var path = [CPBezierPath bezierPath]; + [path moveToPoint:CGPointMake(bounds.origin.x + 5, sliderY)]; + [path lineToPoint:CGPointMake(bounds.origin.x + bounds.size.width - 5, sliderY)]; + [[CPColor blackColor] set]; + [path stroke]; + + var knobRect = CGRectMake(bounds.size.width / 2 - 5, sliderY - 5, 10, 10); + var knobPath = [CPBezierPath bezierPathWithOvalInRect:knobRect]; + [[CPColor whiteColor] set]; + [knobPath fill]; + [[CPColor blackColor] set]; + [knobPath stroke]; + } + else if ([_dragType isEqualToString:UITextFieldDragType]) + { + // Draw a text field + var fieldRect = CGRectInset(bounds, 5, 12); + [[CPColor whiteColor] set]; + [CPBezierPath fillRect:fieldRect]; + [[CPColor blackColor] set]; + [CPBezierPath strokeRect:fieldRect]; + + // Draw an I-beam cursor + var ibeamX = CGRectGetMidX(fieldRect); + var ibeamY1 = CGRectGetMinY(fieldRect) + 3; + var ibeamY2 = CGRectGetMaxY(fieldRect) - 3; + + var ibeamPath = [CPBezierPath bezierPath]; + [ibeamPath moveToPoint:CGPointMake(ibeamX, ibeamY1)]; + [ibeamPath lineToPoint:CGPointMake(ibeamX, ibeamY2)]; + [ibeamPath moveToPoint:CGPointMake(ibeamX - 2, ibeamY1)]; + [ibeamPath lineToPoint:CGPointMake(ibeamX + 2, ibeamY1)]; + [ibeamPath moveToPoint:CGPointMake(ibeamX - 2, ibeamY2)]; + [ibeamPath lineToPoint:CGPointMake(ibeamX + 2, ibeamY2)]; + + [ibeamPath setLineWidth:0.5]; + [[CPColor blackColor] set]; + [ibeamPath stroke]; + } + else + { + // Fallback to original text drawing + var title = [[_dragType componentsSeparatedByString:@"DragType"] objectAtIndex:0]; + var textAttributes = @{ + CPFontAttributeName: [CPFont systemFontOfSize:10], + CPForegroundColorAttributeName: [CPColor blackColor] + }; + var titleSize = [title sizeWithAttributes:textAttributes]; + var titlePoint = CGPointMake( + (bounds.size.width - titleSize.width) / 2.0, + (bounds.size.height - titleSize.height) / 2.0 + ); + [title drawAtPoint:titlePoint withAttributes:textAttributes]; + } +} + +@end + +@implementation AppController : CPObject +{ + CPWindow _window; + CPPanel _palette; + UIBuilderController _builderController; + UICanvasView _canvasView; + InspectorController _inspectorController; +} + +- (void)applicationDidFinishLaunching:(CPNotification)aNotification +{ + // 1. Create the main window and canvas + _window = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask]; + [_window setTitle:@"Cappuccino UI Builder"]; + [_window setAcceptsMouseMovedEvents:YES]; + + _canvasView = [[UICanvasView alloc] initWithFrame:[[_window contentView] bounds]]; + [_canvasView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [[_window contentView] addSubview:_canvasView]; + + // 2. Create the controllers + _builderController = [[UIBuilderController alloc] init]; + + // 3. Wire everything together + [_canvasView setDelegate:_builderController]; + + // Bind the canvas to the controller's data model. This is the core of the architecture. + [_canvasView bind:"dataObjects" toObject:_builderController withKeyPath:@"elementsController.arrangedObjects" options:nil]; + [_canvasView bind:"selectionIndexes" toObject:_builderController withKeyPath:@"elementsController.selectionIndexes" options:nil]; + [_canvasView bind:"connections" toObject:_builderController withKeyPath:@"connectionsController.arrangedObjects" options:nil]; + [_canvasView bind:"selectedConnections" toObject:_builderController withKeyPath:@"connectionsController.selectedObjects" options:nil]; + + [self createPalette]; + [self createInspector]; + + // 5. Create the main menu + var mainMenuBar = [[CPMenu alloc] initWithTitle:@"MainMenu"]; + var editMenuItem = [[CPMenuItem alloc] initWithTitle:@"Edit" action:nil keyEquivalent:@""]; + + + var editMenu = [[CPMenu alloc] initWithTitle:@"Edit"]; + [editMenu addItemWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"]; + [editMenu addItemWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"Z"]; + [editMenu addItem:[CPMenuItem separatorItem]]; + [editMenu addItemWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"]; + [editMenu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"]; + [editMenu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"]; + [editMenu addItemWithTitle:@"Delete" action:@selector(delete:) keyEquivalent:@""]; + + [editMenuItem setSubmenu:editMenu]; + + var fileMenuItem = [[CPMenuItem alloc] initWithTitle:@"File" action:nil keyEquivalent:@""]; + var fileMenu = [[CPMenu alloc] initWithTitle:@"File"]; + [fileMenu addItemWithTitle:@"Run" action:@selector(run:) keyEquivalent:@"r"]; + [fileMenuItem setSubmenu:fileMenu]; + [mainMenuBar addItem:fileMenuItem]; + + [mainMenuBar addItem:editMenuItem]; + + [CPApp setMainMenu:mainMenuBar]; + [CPMenu setMenuBarVisible:YES]; + + [_window makeKeyAndOrderFront:self]; +} + +- (void)createPalette +{ + var screenWidth = window.innerWidth; + var paletteWidth = 220; + var paletteHeight = 60; + var paletteX = (screenWidth - paletteWidth) / 2; + var paletteY = 22; // Position near the top of the screen + + _palette = [[CPPanel alloc] initWithContentRect:CGRectMake(paletteX, paletteY, paletteWidth, paletteHeight) + styleMask:CPHUDBackgroundWindowMask | CPTitledWindowMask | CPClosableWindowMask]; + [_palette setTitle:@"Elements"]; + [_palette setFloatingPanel:YES]; + + var xPos = 10; + var types = [UIWindowDragType, UIButtonDragType, UISliderDragType, UITextFieldDragType]; + + // Create draggable symbols for each type + [_canvasView registerForDraggedTypes:types]; + + for (var i=0; i < [types count]; i++) { + var symbol = [[DraggableSymbolView alloc] initWithFrame:CGRectMake(xPos, 10, 40, 40)]; + symbol._dragType = types[i]; + + [[_palette contentView] addSubview:symbol]; + xPos += 50; + } + + [_palette orderFront:self]; +} + +- (void)createInspector +{ + var inspectorPanel = [[CPPanel alloc] initWithContentRect:CGRectMake(20, 200, 300, 150) + styleMask:CPTitledWindowMask | CPClosableWindowMask]; + [inspectorPanel setTitle:@"Inspector"]; + [inspectorPanel setFloatingPanel:YES]; + + var contentView = [inspectorPanel contentView]; + + _inspectorController = [[InspectorController alloc] init]; + [_inspectorController setBuilderController:_builderController]; + [_inspectorController setPanel:inspectorPanel]; + [_inspectorController setView:contentView]; + + [_inspectorController awakeFromMarkup]; // Manually call this + + [inspectorPanel orderFront:self]; +} + +- (void)run:(id)sender +{ + console.log("Run: Starting native UI generation..."); + var canvasSubviews = [_canvasView subviews]; + var nativeElementMap = [CPMutableDictionary dictionary]; + + // First pass: create all native elements and map them by their ID + console.log("Run: Creating native elements and building map..."); + for (var i = 0; i < [canvasSubviews count]; i++) + { + var view = [canvasSubviews objectAtIndex:i]; + if ([view isKindOfClass:[UIElementView class]]) + { + // This will now recursively build the map + [view nativeUIElementWithMap:nativeElementMap]; + } + } + + // Second pass: connect the native elements + console.log("Run: Processing connections..."); + var connections = [[_builderController connectionsController] content]; + for (var i = 0; i < [connections count]; i++) + { + var connection = [connections objectAtIndex:i]; + var sourceID = [connection valueForKey:@"sourceID"]; + var targetID = [connection valueForKey:@"targetID"]; + var action = [connection valueForKey:@"action"]; + + console.log(" - Connecting: " + sourceID + " -> " + targetID + " (Action: " + action + ")"); + + var nativeSource = [nativeElementMap objectForKey:sourceID]; + var nativeTarget = [nativeElementMap objectForKey:targetID]; + + if (nativeSource && nativeTarget && action) + { + console.log(" - Found native source and target. Applying connection."); + [nativeSource setTarget:nativeTarget]; + [nativeSource setAction:CPSelectorFromString(action)]; + } + else + { + console.log(" - WARNING: Could not find native source or target for connection."); + } + } + + // Third pass: show the windows + console.log("Run: Showing windows..."); + for (var i = 0; i < [canvasSubviews count]; i++) + { + var view = [canvasSubviews objectAtIndex:i]; + if ([view isKindOfClass:[UIWindowView class]]) + { + var elementID = [[view dataObject] valueForKey:@"id"]; + var nativeWindow = [nativeElementMap objectForKey:elementID]; + if (nativeWindow) + { + console.log(" - Showing window for ID: " + elementID); + [nativeWindow makeKeyAndOrderFront:self]; + } + } + } + console.log("Run: Finished."); +} + +@end diff --git a/Tests/Manual/UIBuilderDemo/ConnectionView.j b/Tests/Manual/UIBuilderDemo/ConnectionView.j new file mode 100644 index 000000000..d77154cfb --- /dev/null +++ b/Tests/Manual/UIBuilderDemo/ConnectionView.j @@ -0,0 +1,97 @@ + +@import + +function treshold(value, limit) +{ + return value > 0 ? Math.min(value, limit) : Math.max(value, -limit); +} + +@implementation ConnectionView : CPView +{ + CGPoint _startPoint; + CGPoint _endPoint; + CPColor _color; +} + +- (id)initWithFrame:(CGRect)aRect +{ + self = [super initWithFrame:aRect]; + if (self) + { + [self setBackgroundColor:[CPColor clearColor]]; + _color = [CPColor redColor]; + [self setHidden:YES]; // Hidden by default + } + return self; +} + +- (void)setStartPoint:(CGPoint)startPoint { _startPoint = startPoint; } +- (void)setEndPoint:(CGPoint)endPoint { _endPoint = endPoint; } +- (void)setColor:(CPColor)color { _color = color; } + +- (void)drawRect:(CGRect)rect +{ + console.log("ConnectionView: drawRect - Drawing connection from ", _startPoint, " to ", _endPoint); + if (_startPoint && _endPoint) + { + [self drawLinkFrom:_startPoint to:_endPoint color:_color]; + } +} + +- (void)drawLinkFrom:(CGPoint)startPoint to:(CGPoint)endPoint color:(CPColor)insideColor +{ + var dist = Math.sqrt(Math.pow(startPoint.x - endPoint.x, 2) + Math.pow(startPoint.y - endPoint.y, 2)); + var p0 = CGPointMake(startPoint.x, startPoint.y); + var p3 = CGPointMake(endPoint.x, endPoint.y); + var p1 = CGPointMake(startPoint.x + treshold((endPoint.x - startPoint.x) / 2, 50), startPoint.y); + var p2 = CGPointMake(endPoint.x - treshold((endPoint.x - startPoint.x) / 2, 50), endPoint.y); + var path = [CPBezierPath bezierPath]; + [path setLineWidth:0]; + [[CPColor grayColor] set]; + [path appendBezierPathWithOvalInRect:CGRectMake(startPoint.x-2.5,startPoint.y-2.5,5,5)]; + [path fill]; + path = [CPBezierPath bezierPath]; + [path setLineWidth:0]; + [insideColor set]; + [path appendBezierPathWithOvalInRect:CGRectMake(startPoint.x-1.5,startPoint.y-1.5,3,3)]; + [path fill]; + path = [CPBezierPath bezierPath]; + [path setLineWidth:0]; + [[CPColor grayColor] set]; + [path appendBezierPathWithOvalInRect:CGRectMake(endPoint.x-2.5,endPoint.y-2.5,5,5)]; + [path fill]; + path = [CPBezierPath bezierPath]; + [path setLineWidth:0]; + [insideColor set]; + [path appendBezierPathWithOvalInRect:CGRectMake(endPoint.x-1.5,endPoint.y-1.5,3,3)]; + [path fill]; + if (dist < 40) + { + path = [CPBezierPath bezierPath]; + [path setLineWidth:5]; + [path moveToPoint:startPoint]; + [path lineToPoint:endPoint]; + [[CPColor grayColor] set]; + [path stroke]; + path = [CPBezierPath bezierPath]; + [path setLineWidth:3]; + [path moveToPoint:startPoint]; + [path lineToPoint:endPoint]; + [insideColor set]; + [path stroke]; + return; + } + path = [CPBezierPath bezierPath]; + [path setLineWidth:5]; + [path moveToPoint:p0]; + [path curveToPoint:p3 controlPoint1:p1 controlPoint2:p2]; + [[CPColor grayColor] set]; + [path stroke]; + path = [CPBezierPath bezierPath]; + [path setLineWidth:3]; + [path moveToPoint:p0]; + [path curveToPoint:p3 controlPoint1:p1 controlPoint2:p2]; + [insideColor set]; + [path stroke]; +} +@end diff --git a/Tests/Manual/UIBuilderDemo/Info.plist b/Tests/Manual/UIBuilderDemo/Info.plist new file mode 100755 index 000000000..8bbc29491 --- /dev/null +++ b/Tests/Manual/UIBuilderDemo/Info.plist @@ -0,0 +1,14 @@ + + + + + CPApplicationDelegateClass + AppController + CPBundleName + UIBuilderDemo + CPPrincipalClass + CPApplication + CPDefaultTheme + Aristo2 + + diff --git a/Tests/Manual/UIBuilderDemo/InspectorController.j b/Tests/Manual/UIBuilderDemo/InspectorController.j new file mode 100644 index 000000000..dd3d0af2f --- /dev/null +++ b/Tests/Manual/UIBuilderDemo/InspectorController.j @@ -0,0 +1,215 @@ +@import + +@class UIBuilderController; + +@implementation InspectorController : CPViewController +{ + UIBuilderController _builderController @accessors(property=builderController); + CPPanel _panel @accessors(property=panel); + CPTableView _connectionsTableView; +} + +- (void)awakeFromMarkup +{ + [_builderController addObserver:self forKeyPath:@"elementsController.selectionIndexes" options:CPKeyValueObservingOptionNew context:nil]; + + // Create Tab View + var tabView = [[CPTabView alloc] initWithFrame:[[_panel contentView] bounds]]; + [tabView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [tabView setDelegate:self]; + + // Properties Tab + var propertiesView = [[CPView alloc] initWithFrame:CGRectMakeZero()]; + var propertiesTabItem = [[CPTabViewItem alloc] initWithIdentifier:@"properties"]; + [propertiesTabItem setLabel:@"Properties"]; + [propertiesTabItem setView:propertiesView]; + [tabView addTabViewItem:propertiesTabItem]; + + // Connections Tab + var connectionsView = [[CPView alloc] initWithFrame:CGRectMakeZero()]; + var connectionsTabItem = [[CPTabViewItem alloc] initWithIdentifier:@"connections"]; + [connectionsTabItem setLabel:@"Connections"]; + [connectionsTabItem setView:connectionsView]; + [tabView addTabViewItem:connectionsTabItem]; + + // Connections TableView + _connectionsTableView = [[CPTableView alloc] initWithFrame:[connectionsView bounds]]; + [_connectionsTableView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + + var columns = [ + {identifier: "outlet", title: "Outlet", width: 80}, + {identifier: "action", title: "Action", width: 120} + ]; + + // Keep a reference to the array controller + var connectionsController = [_builderController connectionsController]; + + for (var i = 0; i < [columns count]; i++) { + var colInfo = columns[i]; + var column = [[CPTableColumn alloc] initWithIdentifier:colInfo.identifier]; + [[column headerView] setStringValue:colInfo.title]; + [column setWidth:colInfo.width]; + [_connectionsTableView addTableColumn:column]; + // Bind the value of each column to the corresponding key path on the arranged objects + [column bind:CPValueBinding toObject:connectionsController withKeyPath:("arrangedObjects." + colInfo.identifier) options:nil]; + } + + // Bind the table's selection to the array controller's selection + [_connectionsTableView bind:@"selectionIndexes" toObject:connectionsController withKeyPath:@"selectionIndexes" options:nil]; + + var connectionsViewBounds = [connectionsView bounds]; + var buttonBarHeight = 28; + var tableHeight = connectionsViewBounds.size.height - buttonBarHeight; + + var scrollViewFrame = CGRectMake(3, 3, connectionsViewBounds.size.width - 6, tableHeight - 6); + var buttonBarFrame = CGRectMake(0, tableHeight, connectionsViewBounds.size.width, buttonBarHeight); + + var scrollView = [[CPScrollView alloc] initWithFrame:scrollViewFrame]; + [scrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [scrollView setDocumentView:_connectionsTableView]; + [connectionsView addSubview:scrollView]; + + var buttonBar = [[CPView alloc] initWithFrame:buttonBarFrame]; + [buttonBar setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin]; // Stick to bottom + [connectionsView addSubview:buttonBar]; + + var deleteButton = [CPButtonBar minusButton]; + [deleteButton setAction:@selector(deleteSelectedConnection:)]; + [deleteButton setTarget:self]; + [buttonBar addSubview:deleteButton]; + + // Replace panel's content view with the tab view + [_panel setContentView:tabView]; + + [self updateInspector]; +} + +- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aTableColumn row:(int)aRow +{ + var connection = [[[_builderController connectionsController] arrangedObjects] objectAtIndex:aRow]; + var identifier = [aTableColumn identifier]; + + return [connection valueForKey:identifier]; +} + +- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context +{ + // We only observe selection changes now. + if (keyPath === @"elementsController.selectionIndexes") + { + [self updateInspector]; + [self _updateConnectionVisibility]; + } +} + +- (void)_updateConnectionVisibility +{ + var tabView = [[self panel] contentView]; + if (![tabView isKindOfClass:[CPTabView class]]) + return; + + var selectedTabViewItem = [tabView selectedTabViewItem]; + var connectionsController = [_builderController connectionsController]; + var selectedObjects = [[_builderController elementsController] selectedObjects]; + + // 1. Filter the connections based on the selected UI element. + if ([selectedObjects count] === 1) + { + var selectedID = [[selectedObjects objectAtIndex:0] valueForKey:@"id"]; + var predicate = [CPPredicate predicateWithFormat:@"sourceID == %@ OR targetID == %@", selectedID, selectedID]; + [connectionsController setFilterPredicate:predicate]; + } + else + { + [connectionsController setFilterPredicate:[CPPredicate predicateWithFormat:@"FALSEPREDICATE"]]; + } +} + +- (void)tabView:(CPTabView)aTabView didSelectTabViewItem:(CPTabViewItem)aTabViewItem +{ + [self _updateConnectionVisibility]; +} + +- (void)deleteSelectedConnection:(id)sender +{ + var selectedObjects = [[_builderController connectionsController] selectedObjects]; + if ([selectedObjects count] > 0) + [[_builderController connectionsController] removeObjects:selectedObjects]; +} + +- (void)updateInspector +{ + var selectedObjects = [[_builderController elementsController] selectedObjects]; + var propertiesView = [[[_panel contentView] tabViewItemAtIndex:0] view]; + + // Clear existing views from properties tab + var subviews = [propertiesView subviews]; + for (var i = [subviews count] - 1; i >= 0; i--) { + [subviews[i] removeFromSuperview]; + } + + if ([selectedObjects count] === 1) + { + var selectedObject = selectedObjects[0]; + var elementType = [selectedObject valueForKey:@"type"]; + var viewClass = [UIBuilderController classForElementType:elementType]; + var properties = [viewClass persistentProperties]; + + var yPos = 10; + + // Set panel title + [_panel setTitle:elementType]; + + for (var i = 0; i < [properties count]; i++) + { + var propertyName = properties[i]; + var value = [selectedObject valueForKey:propertyName]; + var propertyType = [[viewClass propertyTypes] valueForKey:propertyName]; + + // Create Label + var label = [[CPTextField alloc] initWithFrame:CGRectMake(10, yPos + 3, 100, 20)]; + [label setStringValue:propertyName]; + [label setBezeled:NO]; + [label setDrawsBackground:NO]; + [label setEditable:NO]; + [propertiesView addSubview:label]; + [label setTextColor:[CPColor grayColor]]; + + // Create Control based on property type + if (propertyType === UIBBoolean) { + var checkbox = [[CPCheckBox alloc] initWithFrame:CGRectMake(120, yPos, 100, 20)]; + [checkbox setTitle:@""]; + [checkbox bind:@"value" toObject:selectedObject withKeyPath:propertyName options:nil]; + [propertiesView addSubview:checkbox]; + } else if (propertyType === UIBString || propertyType === UIBNumber) { + var textField = [[CPTextField alloc] initWithFrame:CGRectMake(120, yPos, 150, 27)]; + [textField bind:@"value" toObject:selectedObject withKeyPath:propertyName options:nil]; + [textField setBezeled:YES]; + [textField setEditable:YES]; + [propertiesView addSubview:textField]; + } else { // Fallback for unknown types + var textField = [[CPTextField alloc] initWithFrame:CGRectMake(120, yPos, 150, 25)]; + [textField bind:@"value" toObject:selectedObject withKeyPath:propertyName options:nil]; + [textField setBezeled:YES]; + [textField setEditable:YES]; + [propertiesView addSubview:textField]; + } + + yPos += 30; + } + + [[self panel] orderFront:self]; + } + else + { + [[self panel] orderOut:self]; + } +} + +- (void)dealloc +{ + [_builderController removeObserver:self forKeyPath:@"elementsController.selectionIndexes"]; + [super dealloc]; +} + +@end diff --git a/Tests/Manual/UIBuilderDemo/Resources/spinner.gif b/Tests/Manual/UIBuilderDemo/Resources/spinner.gif new file mode 100755 index 0000000000000000000000000000000000000000..06dbc2bc21dddcf0e09b566d5b211aee89570f52 GIT binary patch literal 1849 zcma*odr(tX9tZHtz31lM+(&Y`A`Ou`NeG&R#DrIfV%?hng21vs6v>ooRQQFbk7E_K!V( z{?5!fpZWgIZ%*d6t%i*j24bL}AZUJm9)h6R*;%L4IWse3G#VWaN1#$zSSXcBhlYlH zJ|D}n{r&w2f+Qp)SS*&n?*G4}{}!h?td_mjU6Kad-rW*QwWYCUk7d^e+sjpZAn6kT z5rM;`{~_}-wm+L@%+E;JphLm}C3WzQAQD0wBoY|rOTQX{(@pp-WAA8Iw0HKl|15j) zhGh(SFWu#5zUCw^T!}C~eC}~?{xG-r7ugQC>taC+|L^xr&a%{#cgUOZXIM&v?)k^2 z1i5%0!U00wq)r4vlAwXBA_m5J5WNeuBm-XJQ5pG4GD%vc|K){+TJ{AkHqN@SJKczX zU}QRd9n*_bf^h94)6Ctv{CUfz_;qs#RDuQ$DR%y1&5G-a6rhf*4Y zl+UOD1*GE{GQ7vv;c-tTIMkz0ovO>9j%b5hpa@a*CBJ#W!0@ndSwF|%hMkdFtXiU) z`BEp+yhkt8ZY>tg&uyH@%^%K4jDrbA;et&vnnzZ3z22@e6<27z2Vk13kb7OtISyxv z(XoO-LNQCZe3lfzc%*pNqU<9T+p~teThOEB+~e&U0mC|zS{Q)dJZ)!^1f7L(zg*L> z*vSW%?dR~@@0(}x6-V^A`~ImD;)NaBUnP8;RH=bS2@pk`wp>>8r&jGjmGPy%1BW}v zoPo}ca$~bze@efc3kapuEVW1!%teOZT3j2Tw5=hjiAf02d}7dL0oFC%@=RXp5Ow#% z@a>+AM|YWt$n&e`>sB-32gBcuTi>R>*|8@lp`y6thc7ydyqDsuUn~YzZf|{-R@-3y zL#wx{Ii}xxL_csiW*LBn0-A$>zp4WOmkf6=ilrjlW?-l9+bCd7|i?*b5NTmy<<6NZ0T8$SN@mwdC2f z6jqK=N@bS@!=YSj%>h0}*4p+%0HbTIrE$w7UMT6+AZZ&DAo*qZAAi(OtNbIfl#Dw^ zJWeiCp~zi#&t6x}m9)O^eR4HiLV3QA<<0>HZ8%$^lrSE94Wb}=+MM^!b>n#5&-JQR zkr-CEu9C;_F*7DqDisulV6Pmg$nFL0TPn%~*m^-`Z3^BgU(sNpnx%nW(!eV9A&FvI zHL3X3lw8Wji^6=8KbQDE-e%b?svdFxxdh$8r97@7$ojeI`6AL8Ob6QC$qh&>ZWVDX1F{0vJnT%%mE;GveKWR%C} zM&4d;Jf3s9|HAA)yVUPo`Aq;0do#)uHSXi5*QF*)x@MVVHr+cN)uMZ__F|&Ta#p8d z53TOKtce!PJUuie8UWol-S(`c2nH?UGqJP{!4RR4u$LCfn)q-hj0^f=h(VYy)T6eN zhRO!ja-aDBTcgeyP(8Ua4IdiOog^*CQa?R(cP#9AgL9`j>EX-6Yf1lzX(!~``M1XC zNmM<4<6d~wWZ$Xrk0K}UteTrq@LBBk#MsjkK;pbuVhe)NI7(7Pf(l?lxC7=1Z7Pzl zMbS;nV4NI5_N{1$P)&XC)huOGQ+h^zpYUZfb*1n6>!`#*b3y52LGmi+<4sY5jyD#- zw&$d}DTguLAfnRtjrM*JfqtHqUu6rQoU=g%1E9xk%;)TDm^2<8n-0IhTsQFaek)MY}>PK4;nBk=ow4pF>vzkO& gna%Org-kgQCf=+BeMi^RbuY;YE~rTjend;_cdi8t>i_@% literal 0 HcmV?d00001 diff --git a/Tests/Manual/UIBuilderDemo/UIBuilderConstants.j b/Tests/Manual/UIBuilderDemo/UIBuilderConstants.j new file mode 100644 index 000000000..1e5c5b5ec --- /dev/null +++ b/Tests/Manual/UIBuilderDemo/UIBuilderConstants.j @@ -0,0 +1,10 @@ +// +// UIBuilderConstants.j +// Defines global constants for the UI Builder application. +// + +UIBuilderElementPboardType = "UIBuilderElementPboardType"; +UIWindowDragType = "UIWindowDragType"; +UIButtonDragType = "UIButtonDragType"; +UISliderDragType = "UISliderDragType"; +UITextFieldDragType = "UITextFieldDragType"; diff --git a/Tests/Manual/UIBuilderDemo/UIBuilderController.j b/Tests/Manual/UIBuilderDemo/UIBuilderController.j new file mode 100644 index 000000000..cb4ba5c95 --- /dev/null +++ b/Tests/Manual/UIBuilderDemo/UIBuilderController.j @@ -0,0 +1,645 @@ +// +// UIBuilderController.j +// This is the main controller for the UI Builder application. +// It manages the data model for all elements on the canvas and acts +// as a delegate for the UICanvasView to respond to user interactions. +// +// By Daniel Boehringer in 2025. +// + +@import +@import "UIElementView.j" +@import "UICanvasView.j" +@import "UIBuilderConstants.j"; + +// This is a simple data model. In a real app, it might have more properties. +// We use a custom dictionary to ensure KVO compatibility and proper value setting. +@implementation CPConservativeDictionary : CPDictionary +{ } + +- (id)init +{ + self = [super init]; + if (self) { + // Rely on superclass to initialize _buckets + } + return self; +} + ++ (id)dictionary +{ + return [[self alloc] init]; +} + +- (void)setValue:(id)aVal forKey:(CPString)aKey +{ + // Only set the value if it's different from the current value + var currentValue = [super valueForKey:aKey]; + + + // Always set the value if the current value is null or undefined + if (currentValue == null || currentValue == undefined || currentValue != aVal) { + [super setValue:aVal forKey:aKey]; + } +} + +- (BOOL)isEqual:(id)otherObject +{ + return [self valueForKey:'id'] == [otherObject valueForKey:'id']; +} + +- (id)initWithCoder:(CPCoder)aCoder +{ + self = [super initWithCoder:aCoder]; + if (self) + { + var allKeys = [aCoder decodeObjectForKey:@"CPConservativeDictionaryKeys"]; + if (allKeys) + { + for (var i = 0; i < [allKeys count]; i++) + { + var key = allKeys[i]; + var value = [aCoder decodeObjectForKey:key]; + [self setObject:value forKey:key]; + } + } + } + return self; +} + +- (void)encodeWithCoder:(CPCoder)aCoder +{ + [super encodeWithCoder:aCoder]; + var allKeys = [self allKeys]; + [aCoder encodeObject:allKeys forKey:@"CPConservativeDictionaryKeys"]; + for (var i = 0; i < [allKeys count]; i++) + { + var key = allKeys[i]; + [aCoder encodeObject:[self objectForKey:key] forKey:key]; + } +} + +@end + + +@implementation UIBuilderController : CPViewController +{ + CPArrayController _elementsController @accessors(property=elementsController); + CPArrayController _connectionsController @accessors(property=connectionsController); + CPMutableArray _connections; + int _elementCounter; // To generate unique IDs +} + ++ (Class)classForElementType:(CPString)elementType +{ + if (elementType === "window") return UIWindowView; + if (elementType === "button") return UIButtonView; + if (elementType === "slider") return UISliderView; + if (elementType === "textfield") return UITextFieldView; + return UIElementView; +} + +- (id)init +{ + self = [super init]; + if (self) { + _elementsController = [[CPArrayController alloc] init]; + _connectionsController = [[CPArrayController alloc] init]; + _elementCounter = 0; + } + return self; +} + +#pragma mark - +#pragma mark Data Management + +- (CPDictionary)_containerDataAtPoint:(CGPoint)aPoint +{ + var allElements = [_elementsController arrangedObjects]; + for (var i = [allElements count] - 1; i >= 0; i--) + { + var elementData = allElements[i]; + var type = [elementData valueForKey:@"type"]; + if (type === "window") + { + var frame = CGRectMake([elementData valueForKey:@"originX"], [elementData valueForKey:@"originY"], [elementData valueForKey:@"width"], [elementData valueForKey:@"height"]); + if (CGRectContainsPoint(frame, aPoint)) + return elementData; + } + } + return nil; +} + +- (void)addNewElementOfType:(CPString)elementType atPoint:(CGPoint)aPoint +{ + var newElementData = [CPConservativeDictionary dictionary]; + var containerData = [self _containerDataAtPoint:aPoint]; + var viewClass = [UIBuilderController classForElementType:elementType]; + + // Set default properties based on type + [newElementData setValue:elementType forKey:@"type"]; + [newElementData setValue:@"id_" + _elementCounter++ forKey:@"id"]; + + // Set default values from the view class + var defaultValues = [viewClass defaultValues]; + for (var key in defaultValues) { + [newElementData setValue:defaultValues[key] forKey:key]; + } + + // Set default sizes + if (elementType === "window") { + [newElementData setValue:250 forKey:@"width"]; + [newElementData setValue:200 forKey:@"height"]; + [newElementData setValue:[] forKey:@"children"]; + } else if (elementType === "button") { + [newElementData setValue:100 forKey:@"width"]; + [newElementData setValue:24 forKey:@"height"]; + } else if (elementType === "slider") { + [newElementData setValue:150 forKey:@"width"]; + [newElementData setValue:20 forKey:@"height"]; + } else { // textfield + [newElementData setValue:150 forKey:@"width"]; + [newElementData setValue:22 forKey:@"height"]; + } + + // Calculate centered position + var elementWidth = [newElementData valueForKey:@"width"]; + var elementHeight = [newElementData valueForKey:@"height"]; + var centeredX = aPoint.x - (elementWidth / 2); + var centeredY = aPoint.y - (elementHeight / 2); + [newElementData setValue:centeredX forKey:@"originX"]; + [newElementData setValue:centeredY forKey:@"originY"]; + + if (containerData && elementType !== "window") + { + // Convert point to be relative to the container and center the element + var elementWidth = [newElementData valueForKey:@"width"]; + var elementHeight = [newElementData valueForKey:@"height"]; + var relativeX = (aPoint.x - [containerData valueForKey:@"originX"]) - (elementWidth / 2); + var relativeY = (aPoint.y - [containerData valueForKey:@"originY"]) - (elementHeight / 2); + [newElementData setValue:relativeX forKey:@"originX"]; + [newElementData setValue:relativeY forKey:@"originY"]; + + // Add as a child to the container + [newElementData setValue:[containerData valueForKey:@"id"] forKey:@"parentID"]; + [[containerData mutableArrayValueForKey:@"children"] addObject:newElementData]; + } + + // Add to the main controller regardless, so selection works. + [[[[CPApp keyWindow] undoManager] prepareWithInvocationTarget:_elementsController] removeObject:newElementData]; + [[[CPApp keyWindow] undoManager] setActionName:@"Add Element"]; + [_elementsController addObject:newElementData]; + + [_elementsController setSelectedObjects:[CPArray arrayWithObject:newElementData]]; +} + +- (void)removeSelectedElementsWithActionName:(CPString)actionName +{ + var selectedObjects = [[_elementsController selectedObjects] copy]; + if ([selectedObjects count] === 0) return; + + [[[[CPApp keyWindow] undoManager] prepareWithInvocationTarget:_elementsController] addObjects:selectedObjects]; + [[[CPApp keyWindow] undoManager] setActionName:actionName]; + [_elementsController removeObjects:selectedObjects]; +} + +- (void)removeSelectedElements +{ + [self removeSelectedElementsWithActionName:@"Delete"]; +} + +- (void)cut:(id)sender +{ + [self copy:sender]; + [self removeSelectedElementsWithActionName:@"Cut"]; +} + +#pragma mark - +#pragma mark Keyboard Movement + +- (void)moveSelectedElementsByDeltaX:(int)deltaX deltaY:(int)deltaY +{ + var selectedDataObjects = [_elementsController selectedObjects]; + var changes = [CPMutableArray array]; + for (var i = 0; i < [selectedDataObjects count]; i++) + { + var data = selectedDataObjects[i]; + var newFrame = { + origin: { + x: [data valueForKey:@"originX"] + deltaX, + y: [data valueForKey:@"originY"] + deltaY + } + }; + [changes addObject:{ data: data, frame: newFrame }]; + } + [self applyFrameChanges:changes withActionName:@"Move"]; +} + +- (void)moveLeft:(id)sender +{ + [self moveSelectedElementsByDeltaX:-1 deltaY:0]; +} + +- (void)moveRight:(id)sender +{ + [self moveSelectedElementsByDeltaX:1 deltaY:0]; +} + +- (void)moveUp:(id)sender +{ + [self moveSelectedElementsByDeltaX:0 deltaY:-1]; +} + +- (void)moveDown:(id)sender +{ + [self moveSelectedElementsByDeltaX:0 deltaY:1]; +} + +#pragma mark - +#pragma mark Copy & Paste + +- (void)copy:(id)sender +{ + var selectedData = [_elementsController selectedObjects]; + + if ([selectedData count] > 0) + { + var pboard = [CPPasteboard generalPasteboard]; + var data = [CPKeyedArchiver archivedDataWithRootObject:selectedData]; + + // 1. Declare that you are providing BOTH a custom type and a standard string type. + [pboard declareTypes:[UIBuilderElementPboardType, CPStringPboardType] owner:nil]; + + // 2. Set the data for your custom type, for your app's internal 'paste' to use. + [pboard setData:data forType:UIBuilderElementPboardType]; + + // 3. Set a string representation for the browser and other applications. + // This can be a simple description or a more complex JSON representation. + var description = [selectedData count] + " UI element(s) copied."; + [pboard setString:description forType:CPStringPboardType]; + } +} + +- (void)_assignNewIDsToElement:(CPMutableDictionary)elementData +{ + [elementData setValue:@"id_" + _elementCounter++ forKey:@"id"]; + + var children = [elementData valueForKey:@"children"]; + if (children) + { + var newChildren = [CPMutableArray array]; + for (var i = 0; i < [children count]; i++) + { + var child = children[i]; + // Deep copy child before modifying + var newChild = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:child]]; + [newChild setValue:[elementData valueForKey:@"id"] forKey:@"parentID"]; + [self _assignNewIDsToElement:newChild]; + [newChildren addObject:newChild]; + } + [elementData setValue:newChildren forKey:@"children"]; + } +} + +- (void)paste:(id)sender +{ + var pboard = [CPPasteboard generalPasteboard]; + var types = [pboard types]; + + if ([types containsObject:UIBuilderElementPboardType]) + { + var data = [pboard dataForType:UIBuilderElementPboardType]; + var pastedElements = [CPKeyedUnarchiver unarchiveObjectWithData:data]; + var newSelection = [CPMutableArray array]; + + // Determine the target container + var targetContainer = nil; + var selectedObjects = [_elementsController selectedObjects]; + if ([selectedObjects count] > 0) + { + var firstSelected = selectedObjects[0]; + var parentID = [firstSelected valueForKey:@"parentID"]; + if (parentID) + { + // Find the parent container in the elements controller + var allElements = [_elementsController arrangedObjects]; + for (var i = 0; i < [allElements count]; i++) + { + if ([[allElements[i] valueForKey:@"id"] isEqualToString:parentID]) + { + targetContainer = allElements[i]; + break; + } + } + } + else + { + // If the selected object has no parent, it must be a window + targetContainer = firstSelected; + } + } + else + { + // If no selection, find the first window + var allElements = [_elementsController arrangedObjects]; + for (var i = 0; i < [allElements count]; i++) + { + if ([[allElements[i] valueForKey:@"type"] isEqualToString:@"window"]) + { + targetContainer = allElements[i]; + break; + } + } + } + + for (var i = 0; i < [pastedElements count]; i++) + { + var newElement = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:pastedElements[i]]]; + + [newElement setValue:[newElement valueForKey:@"originX"] + 10 forKey:@"originX"]; + [newElement setValue:[newElement valueForKey:@"originY"] + 10 forKey:@"originY"]; + + [self _assignNewIDsToElement:newElement]; + + if (targetContainer && [newElement valueForKey:@"type"] !== @"window") + { + [newElement setValue:[targetContainer valueForKey:@"id"] forKey:@"parentID"]; + [[targetContainer mutableArrayValueForKey:@"children"] addObject:newElement]; + } + else + { + [newElement removeObjectForKey:@"parentID"]; + } + + [_elementsController addObject:newElement]; + + if ([newElement valueForKey:@"children"]) + [_elementsController addObjects:[newElement valueForKey:@"children"]]; + + [newSelection addObject:newElement]; + } + + [[[[CPApp keyWindow] undoManager] prepareWithInvocationTarget:_elementsController] removeObjects:newSelection]; + [[[CPApp keyWindow] undoManager] setActionName:@"Paste"]; + [_elementsController setSelectedObjects:newSelection]; + } +} + +- (void)addNewElementOfType:(CPString)elementType inNewWindowAtPoint:(CGPoint)aPoint +{ + // 1. Create the new element to be placed in the window + var newElementData = [CPConservativeDictionary dictionary]; + var viewClass = [UIBuilderController classForElementType:elementType]; + [newElementData setValue:elementType forKey:@"type"]; + [newElementData setValue:@"id_" + _elementCounter++ forKey:@"id"]; + + var defaultValues = [viewClass defaultValues]; + for (var key in defaultValues) + [newElementData setValue:defaultValues[key] forKey:key]; + + var elementWidth, elementHeight; + + if (elementType === "button") { + elementWidth = 100; + elementHeight = 24; + } else if (elementType === "slider") { + elementWidth = 150; + elementHeight = 20; + } else { // textfield + elementWidth = 150; + elementHeight = 22; + } + [newElementData setValue:elementWidth forKey:@"width"]; + [newElementData setValue:elementHeight forKey:@"height"]; + + // 2. Create the window that will contain the new element + var windowData = [CPConservativeDictionary dictionary]; + var windowClass = [UIBuilderController classForElementType:"window"]; + var windowWidth = 250, windowHeight = 200; + [windowData setValue:@"window" forKey:@"type"]; + [windowData setValue:@"id_" + _elementCounter++ forKey:@"id"]; + [windowData setValue:windowWidth forKey:@"width"]; + [windowData setValue:windowHeight forKey:@"height"]; + [windowData setValue:[] forKey:@"children"]; + + defaultValues = [windowClass defaultValues]; + for (var key in defaultValues) { + [windowData setValue:defaultValues[key] forKey:key]; + } + + // 3. Position the new element in the center of the window + var elementX = (windowWidth - elementWidth) / 2; + var elementY = (windowHeight - elementHeight) / 2; + [newElementData setValue:elementX forKey:@"originX"]; + [newElementData setValue:elementY forKey:@"originY"]; + + // 4. Position the window so the element is at the drop point + var windowX = aPoint.x - elementX; + var windowY = aPoint.y - elementY; + [windowData setValue:windowX forKey:@"originX"]; + [windowData setValue:windowY forKey:@"originY"]; + + // 5. Add the element to the window's children + [newElementData setValue:[windowData valueForKey:@"id"] forKey:@"parentID"]; + [[windowData mutableArrayValueForKey:@"children"] addObject:newElementData]; + + console.log("UIBuilderController: addNewElementOfType:inNewWindowAtPoint: - Adding new element to window's children:", newElementData); + + // 6. Add both to the elements controller + var undoManager = [[CPApp keyWindow] undoManager]; + [undoManager beginUndoGrouping]; + [[undoManager prepareWithInvocationTarget:_elementsController] removeObject:newElementData]; + [[undoManager prepareWithInvocationTarget:_elementsController] removeObject:windowData]; + [undoManager setActionName:@"Add Element in New Window"]; + [_elementsController addObject:windowData]; + [_elementsController addObject:newElementData]; + [undoManager endUndoGrouping]; + + // 7. Select the new element + [_elementsController setSelectedObjects:[CPArray arrayWithObject:newElementData]]; +} + +- (void)addNewElementOfType:(CPString)elementType inWindow:(CPDictionary)windowData atPoint:(CGPoint)aPoint +{ + console.log("UIBuilderController: addNewElementOfType:inWindow:atPoint: - Adding element ", elementType, " to window ", windowData, " at point ", aPoint); + var newElementData = [CPConservativeDictionary dictionary]; + var viewClass = [UIBuilderController classForElementType:elementType]; + + [newElementData setValue:elementType forKey:@"type"]; + [newElementData setValue:@"id_" + _elementCounter++ forKey:@"id"]; + + var defaultValues = [viewClass defaultValues]; + for (var key in defaultValues) + [newElementData setValue:defaultValues[key] forKey:key]; + + var elementWidth, elementHeight; + + if (elementType === "button") { + elementWidth = 100; + elementHeight = 24; + } else if (elementType === "slider") { + elementWidth = 150; + elementHeight = 20; + } else { // textfield + elementWidth = 150; + elementHeight = 22; + } + [newElementData setValue:elementWidth forKey:@"width"]; + [newElementData setValue:elementHeight forKey:@"height"]; + + // Position the new element relative to the window's origin + [newElementData setValue:aPoint.x forKey:@"originX"]; + [newElementData setValue:aPoint.y forKey:@"originY"]; + + // Add as a child to the container window + [newElementData setValue:[windowData valueForKey:@"id"] forKey:@"parentID"]; + [[windowData mutableArrayValueForKey:@"children"] addObject:newElementData]; + + // Add to the main controller + var undoManager = [[CPApp keyWindow] undoManager]; + [undoManager beginUndoGrouping]; + [[undoManager prepareWithInvocationTarget:_elementsController] removeObject:newElementData]; + [undoManager setActionName:@"Add Element to Window"]; + [_elementsController addObject:newElementData]; + [undoManager endUndoGrouping]; + + [_elementsController setSelectedObjects:[CPArray arrayWithObject:newElementData]]; +} + +- (void)addConnectionFrom:(CPDictionary)sourceData to:(CPDictionary)targetData atPoint:(CGPoint)atPoint outlet:(CPString)outlet action:(CPString)action +{ + var newConnection = [CPConservativeDictionary dictionary]; + [newConnection setValue:[sourceData valueForKey:@"id"] forKey:@"sourceID"]; + [newConnection setValue:[targetData valueForKey:@"id"] forKey:@"targetID"]; + [newConnection setValue:outlet forKey:@"outlet"]; + [newConnection setValue:action forKey:@"action"]; + [newConnection setValue:@"connection_" + _elementCounter++ forKey:@"id"]; + + if (atPoint) + [newConnection setValue:{x: atPoint.x, y: atPoint.y} forKey:@"atPoint"]; + + [[[[CPApp keyWindow] undoManager] prepareWithInvocationTarget:_connectionsController] removeObject:newConnection]; + [[[CPApp keyWindow] undoManager] setActionName:@"Add Connection"]; + + [_connectionsController addObject:newConnection]; + + console.log("UIBuilderController: addConnectionFrom:to: - Added connection: ", newConnection); + console.log("Connections controller count after add: " + [[_connectionsController arrangedObjects] count]); +} + +- (void)removeConnection:(CPDictionary)connection +{ + [[[[CPApp keyWindow] undoManager] prepareWithInvocationTarget:_connectionsController] addObject:connection]; + [[[CPApp keyWindow] undoManager] setActionName:@"Remove Connection"]; + + [_connectionsController removeObject:connection]; +} + +#pragma mark - +#pragma mark UICanvasView Delegate Methods + +- (void)applyFrameChanges:(CPArray)changes withActionName:(CPString)actionName +{ + var undoManager = [[CPApp keyWindow] undoManager]; + var undoChanges = [CPMutableArray array]; + + [undoManager beginUndoGrouping]; + [undoManager setActionName:actionName]; + + for (var i = 0; i < [changes count]; i++) + { + var change = changes[i]; + var data = change.data; + var newFrame = change.frame; + var oldValues = { data: data, frame: {} }; + + if (newFrame.origin) + { + oldValues.frame.origin = { + x: [data valueForKey:@"originX"], + y: [data valueForKey:@"originY"] + }; + [data setValue:newFrame.origin.x forKey:@"originX"]; + [data setValue:newFrame.origin.y forKey:@"originY"]; + } + + if (newFrame.size) + { + oldValues.frame.size = { + width: [data valueForKey:@"width"], + height: [data valueForKey:@"height"] + }; + [data setValue:newFrame.size.width forKey:@"width"]; + [data setValue:newFrame.size.height forKey:@"height"]; + } + [undoChanges addObject:oldValues]; + } + + [[undoManager prepareWithInvocationTarget:self] applyFrameChanges:undoChanges withActionName:actionName]; + [undoManager endUndoGrouping]; +} + +- (void)canvasView:(UICanvasView)aCanvas didMoveElement:(UIElementView)anElement +{ + var selectedViews = [aCanvas selectedSubViews]; + var changes = [CPMutableArray array]; + for (var i = 0; i < [selectedViews count]; i++) + { + var view = selectedViews[i]; + [changes addObject:{ data: [view dataObject], frame: { origin: [view frame].origin } }]; + } + [self applyFrameChanges:changes withActionName:@"Move"]; +} + +- (void)canvasView:(UICanvasView)aCanvas didResizeElement:(UIElementView)anElement +{ + var changes = [CPMutableArray array]; + var frame = [anElement frame]; + [changes addObject:{ data: [anElement dataObject], frame: { origin: frame.origin, size: frame.size } }]; + [self applyFrameChanges:changes withActionName:@"Resize"]; +} + +- (void)canvasView:(UICanvasView)aCanvas didConnectElement:(UIElementView)sourceElement toElement:(UIElementView)targetElement asTargetAction:(CPString)actionName +{ + var sourceData = [sourceElement dataObject]; + var targetData = [targetElement dataObject]; + + // For a target-action, the outlet is typically 'target' + var outletName = @"target"; + + [self addConnectionFrom:sourceData to:targetData atPoint:nil outlet:outletName action:actionName]; +} + +- (void)canvasView:(UICanvasView)aCanvas didConnectElement:(UIElementView)sourceElement toElement:(UIElementView)targetElement asOutlet:(CPString)outletName +{ + var sourceData = [sourceElement dataObject]; + var targetData = [targetElement dataObject]; + + // For a simple outlet connection, there is no action. + var actionName = nil; + + [self addConnectionFrom:sourceData to:targetData atPoint:nil outlet:outletName action:actionName]; +} + +- (void)changeValue:(id)newValue forObject:(id)dataObject +{ + var oldValue = [dataObject valueForKey:@"value"]; + if (oldValue != newValue) + { + var undoManager = [[CPApp keyWindow] undoManager]; + [[undoManager prepareWithInvocationTarget:self] changeValue:oldValue forObject:dataObject]; + [undoManager setActionName:@"Change Value"]; + [dataObject setValue:newValue forKey:@"value"]; + } +} + +- (void)changeValueForSelectedObject:(id)newValue +{ + var selectedObjects = [[self elementsController] selectedObjects]; + if ([selectedObjects count] === 1) + { + [self changeValue:newValue forObject:selectedObjects[0]]; + } +} + +@end diff --git a/Tests/Manual/UIBuilderDemo/UICanvasView.j b/Tests/Manual/UIBuilderDemo/UICanvasView.j new file mode 100644 index 000000000..c374e9f42 --- /dev/null +++ b/Tests/Manual/UIBuilderDemo/UICanvasView.j @@ -0,0 +1,996 @@ +// +// UICanvasView.j +// A full-window canvas for the UI Builder. +// +// By Daniel Boehringer in 2025. +// - It acts as a drag-and-drop destination for new UI elements from the palette. +// - It correctly instantiates different UIElementView subclasses based on the data model. +// + +@import "UIBuilderConstants.j"; +@import "UIElementView.j"; +@import "ConnectionView.j"; +@import "UIBuilderConstants.j"; + +function treshold(value, limit) +{ + return value > 0 ? Math.min(value, limit) : Math.max(value, -limit); +} + +@implementation UICanvasView : CPView +{ + // Data binding ivars + id _dataObjectsContainer; + CPString _dataObjectsKeyPath; + id _selectionIndexesContainer; + CPString _selectionIndexesKeyPath; + CPArray _oldDataObjects; + + // Connections ivars + id _connectionsContainer; + CPString _connectionsKeyPath; + CPArray _oldConnections; + id _selectedConnectionsContainer; + CPString _selectedConnectionsKeyPath; + + // Rubber-band selection ivars + CGPoint _rubberStart; + CGPoint _rubberEnd; + BOOL _isRubbing; + + ConnectionView _connectionView; + + id _delegate; + + // Connection Menu ivars + UIElementView _connectionSource; + UIElementView _connectionTarget; + BOOL _connectionMade; +} + +-(BOOL)acceptsFirstMouse:(CPEvent)aEvent +{ + return YES; +} + +// KVO contexts +var _propertyObservationContext = 1091; +var _dataObjectsObservationContext = 1092; +var _selectionIndexesObservationContext = 1093; +var _connectionsObservationContext = 1094; +var _selectedConnectionsObservationContext = 1095; + +- (id)initWithFrame:(CGRect)aRect +{ + self = [super initWithFrame:aRect]; + + if (self) + { + // Register to accept drops from the palette + [self registerForDraggedTypes:[ + UIWindowDragType, + UIButtonDragType, + UISliderDragType, + UITextFieldDragType + ]]; + + _connectionView = [[ConnectionView alloc] initWithFrame:[self bounds]]; + [_connectionView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [self addSubview:_connectionView]; + } + return self; +} + +#pragma mark - Bindings & KVO (Largely from EFLaceView) + ++ (void)initialize +{ + [self exposeBinding:"dataObjects"]; + [self exposeBinding:@"selectionIndexes"]; + [self exposeBinding:@"connections"]; + [self exposeBinding:@"selectedConnections"]; +} + +- (void)bind:(CPString)bindingName toObject:(id)observableObject withKeyPath:(CPString)observableKeyPath options:(CPDictionary)options +{ + if ([bindingName isEqualToString:@"dataObjects"]) + { + _dataObjectsContainer = observableObject; + _dataObjectsKeyPath = observableKeyPath; + [_dataObjectsContainer addObserver:self forKeyPath:_dataObjectsKeyPath options:(CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld) context:_dataObjectsObservationContext]; + [self startObservingDataObjects:[self dataObjects]]; + _oldDataObjects = [[self dataObjects] copy] || @[]; + } + else if ([bindingName isEqualToString:@"selectionIndexes"]) + { + _selectionIndexesContainer = observableObject; + _selectionIndexesKeyPath = observableKeyPath; + [_selectionIndexesContainer addObserver:self forKeyPath:_selectionIndexesKeyPath options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:_selectionIndexesObservationContext]; + } + else if ([bindingName isEqualToString:@"connections"]) + { + _connectionsContainer = observableObject; + _connectionsKeyPath = observableKeyPath; + [_connectionsContainer addObserver:self forKeyPath:_connectionsKeyPath options:(CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld) context:_connectionsObservationContext]; + _oldConnections = [[self connections] copy] || @[]; + } + else if ([bindingName isEqualToString:@"selectedConnections"]) + { + _selectedConnectionsContainer = observableObject; + _selectedConnectionsKeyPath = observableKeyPath; + [_selectedConnectionsContainer addObserver:self forKeyPath:_selectedConnectionsKeyPath options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:_selectedConnectionsObservationContext]; + } + else { [super bind:bindingName toObject:observableObject withKeyPath:observableKeyPath options:options]; } + + [self setNeedsDisplay:YES]; +} + +- (void)unbind:(CPString)bindingName +{ + if ([bindingName isEqualToString:@"dataObjects"]) { + [self stopObservingDataObjects:[self dataObjects]]; + [_dataObjectsContainer removeObserver:self forKeyPath:_dataObjectsKeyPath]; + _dataObjectsContainer = nil; _dataObjectsKeyPath = nil; + } else if ([bindingName isEqualToString:@"selectionIndexes"]) { + [_selectionIndexesContainer removeObserver:self forKeyPath:_selectionIndexesKeyPath]; + _selectionIndexesContainer = nil; _selectionIndexesKeyPath = nil; + } else if ([bindingName isEqualToString:@"connections"]) { + [_connectionsContainer removeObserver:self forKeyPath:_connectionsKeyPath]; + _connectionsContainer = nil; _connectionsKeyPath = nil; + } else if ([bindingName isEqualToString:@"selectedConnections"]) { + [_selectedConnectionsContainer removeObserver:self forKeyPath:_selectedConnectionsKeyPath]; + _selectedConnectionsContainer = nil; _selectedConnectionsKeyPath = nil; + } else { [super unbind:bindingName]; } + [self setNeedsDisplay:YES]; +} + +- (CPArray)dataObjects +{ + var result = [_dataObjectsContainer valueForKeyPath:_dataObjectsKeyPath]; + return (result == [CPNull null]) ? @[] : result; +} + +- (CPIndexSet)selectionIndexes +{ + return [_selectionIndexesContainer valueForKeyPath:_selectionIndexesKeyPath]; +} + +- (CPArray)connections +{ + var result = [_connectionsContainer valueForKeyPath:_connectionsKeyPath]; + return (result == [CPNull null]) ? @[] : result; +} + +- (CPArray)selectedConnections +{ + var result = [_selectedConnectionsContainer valueForKeyPath:_selectedConnectionsKeyPath]; + return (result == [CPNull null]) ? @[] : result; +} + +- (void)setSelectionIndexes:(CPIndexSet)indexes +{ + [_selectionIndexesContainer setValue:indexes forKeyPath:_selectionIndexesKeyPath]; +} + +- (void)startObservingDataObjects:(CPArray)dataObjects +{ + if (!dataObjects || dataObjects == [CPNull null]) + return; + + for (var i = 0; i < [dataObjects count]; i++) + { + var newDataObject = dataObjects[i]; + // Only create views for top-level objects. Children are handled by their parents. + if (![newDataObject valueForKey:@"parentID"]) + [self _createViewForDataObject:newDataObject superview:self]; + } +} + +- (void)_createViewForDataObject:(CPDictionary)dataObject superview:(CPView)superview +{ + var type = [dataObject valueForKey:@"type"]; + var newView; + + // Instantiate the correct view based on the data model's 'type' + if (type === "window") + newView = [[UIWindowView alloc] init]; + else if (type === "button") + newView = [[UIButtonView alloc] init]; + else if (type === "slider") + newView = [[UISliderView alloc] init]; + else if (type === "textfield") + newView = [[UITextFieldView alloc] init]; + else + newView = [[UIElementView alloc] init]; // Fallback + + [newView setDataObject:dataObject]; + + // Bind view properties to the data model + [newView bind:@"originX" toObject:dataObject withKeyPath:@"originX" options:nil]; + [newView bind:@"originY" toObject:dataObject withKeyPath:@"originY" options:nil]; + [newView bind:@"width" toObject:dataObject withKeyPath:@"width" options:nil]; + [newView bind:@"height" toObject:dataObject withKeyPath:@"height" options:nil]; + + if (type === "window") + { + var children = [dataObject valueForKey:@"children"]; + for (var j = 0; j < [children count]; j++) + { + [self _createViewForDataObject:children[j] superview:newView]; + } + } + + [superview addSubview:newView]; + // i have no idea why this is needed, but it is to make the initial click work + [CPApp._delegate._window makeKeyAndOrderFront:self]; +} + +- (void)stopObservingDataObjects:(CPArray)dataObjects +{ + if (!dataObjects || dataObjects == [CPNull null]) return; + + var viewsToRemove = [CPMutableArray array]; + [self _findViewsForDataObjects:dataObjects inView:self foundViews:viewsToRemove]; + + for (var i = 0; i < [viewsToRemove count]; i++) { + var viewToRemove = viewsToRemove[i]; + [self _removeViewAndChildren:viewToRemove]; + } +} + +- (void)_removeViewAndChildren:(UIElementView)viewToRemove +{ + // Unbind everything before removing + [viewToRemove unbind:@"value"]; + [viewToRemove unbind:@"originX"]; + [viewToRemove unbind:@"originY"]; + [viewToRemove unbind:@"width"]; + [viewToRemove unbind:@"height"]; + + var subviews = [[viewToRemove subviews] copy]; + for (var i = 0; i < [subviews count]; i++) + { + [self _removeViewAndChildren:subviews[i]]; + } + + [viewToRemove removeFromSuperview]; +} + +- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context +{ + if (context == _dataObjectsObservationContext) + { + var newDataObjects = [object valueForKeyPath:_dataObjectsKeyPath]; + var oldDataObjects = _oldDataObjects; + + var added = [newDataObjects mutableCopy]; + [added removeObjectsInArray:oldDataObjects]; + [self startObservingDataObjects:added]; + + var removed = [oldDataObjects mutableCopy]; + [removed removeObjectsInArray:newDataObjects]; + [self stopObservingDataObjects:removed]; + + _oldDataObjects = [newDataObjects copy]; + [self setNeedsDisplay:YES]; + } + else if (context == _selectionIndexesObservationContext) + { + var allDataObjects = [self dataObjects]; + var newIndexes = [change objectForKey:CPKeyValueChangeNewKey] || [CPIndexSet indexSet]; + var oldIndexes = [change objectForKey:CPKeyValueChangeOldKey] || [CPIndexSet indexSet]; + + // Find views for newly selected objects and redraw them + var newSelectedDataObjects = [allDataObjects objectsAtIndexes:newIndexes]; + var newlySelectedViews = [CPMutableArray array]; + [self _findViewsForDataObjects:newSelectedDataObjects inView:self foundViews:newlySelectedViews]; + [newlySelectedViews makeObjectsPerformSelector:@selector(setNeedsDisplay:) withObject:YES]; + + // Find views for deselected objects and redraw them, but only if those objects still exist. + var previouslySelectedViews = [CPMutableArray array]; + var oldSelectedDataObjects = [CPMutableArray array]; + var lastIndex = [oldIndexes lastIndex]; + + if (lastIndex != CPNotFound && lastIndex < [allDataObjects count]) + { + oldSelectedDataObjects = [allDataObjects objectsAtIndexes:oldIndexes]; + } + else + { + // If the indexes are out of bounds, it likely means the objects were deleted. + // We need to find the views that were associated with the old indexes another way. + // This is a tricky state to recover from. For now, we will just redraw all views. + // A more sophisticated solution might involve caching view-data relationships. + [[self subviews] makeObjectsPerformSelector:@selector(setNeedsDisplay:) withObject:YES]; + return; + } + + [self _findViewsForDataObjects:oldSelectedDataObjects inView:self foundViews:previouslySelectedViews]; + [previouslySelectedViews makeObjectsPerformSelector:@selector(setNeedsDisplay:) withObject:YES]; + } + else if (context == _connectionsObservationContext) + { + var newConnections = [object valueForKeyPath:_connectionsKeyPath]; + var oldConnections = _oldConnections; + + // For now, simply redraw all connections. A more optimized approach would be to only redraw changed connections. + [self setNeedsDisplay:YES]; + _oldConnections = [newConnections copy]; + } + else if (context == _selectedConnectionsObservationContext) + { + [self setNeedsDisplay:YES]; + } +} + +#pragma mark - Drawing & Mouse + +- (void)drawRect:(CPRect)rect +{ + // === START: Infographic Drawing === + + var bounds = [self bounds]; + + // 1. Define text attributes for the infographic + var titleFont = [CPFont fontWithName:@"Helvetica-Bold" size:36]; + var subtitleFont = [CPFont fontWithName:@"Helvetica" size:18]; + var featureFont = [CPFont fontWithName:@"Helvetica" size:14]; + var watermarkColor = [CPColor colorWithCalibratedWhite:0.85 alpha:1.0]; // A light gray for the watermark effect + + var titleAttributes = @{ + CPFontAttributeName: titleFont, + CPForegroundColorAttributeName: watermarkColor + }; + var subtitleAttributes = @{ + CPFontAttributeName: subtitleFont, + CPForegroundColorAttributeName: watermarkColor + }; + var featureAttributes = @{ + CPFontAttributeName: featureFont, + CPForegroundColorAttributeName: watermarkColor + }; + + // 2. Prepare the text content + var title = @"Cappuccino JS"; + var subtitle = @"Desktop-Quality Applications in the Browser"; + var features = [ + @"• Drag-and-Drop UI Creation", + @"• Direct Manipulation: Move & Resize (Keyboard / Mouse)", + @"• Undo/Redo & Keyboard Navigation", + @"• Control-Draggin -> Target-Action & Outlet Connections", + @"• Context sensitive inspector panel", + @"• Run the 'real thing' in a separate native window", + @"• Source: https://github.com/daboe01/UIBuilder" + + ]; + + // 3. Calculate positions and draw the text, centering it on the canvas + var titleSize = [title sizeWithAttributes:titleAttributes]; + var subtitleSize = [subtitle sizeWithAttributes:subtitleAttributes]; + var totalHeight = titleSize.height + subtitleSize.height + ([features count] * 20) + 40; // Approximate total height + var currentY = (bounds.size.height - totalHeight) / 2.0; + + // Draw Title + var titlePoint = CGPointMake((bounds.size.width - titleSize.width) / 2.0, currentY); + [title drawAtPoint:titlePoint withAttributes:titleAttributes]; + currentY += titleSize.height + 10; + + // Draw Subtitle + var subtitlePoint = CGPointMake((bounds.size.width - subtitleSize.width) / 2.0, currentY); + [subtitle drawAtPoint:subtitlePoint withAttributes:subtitleAttributes]; + currentY += subtitleSize.height + 30; + + // Draw Feature List + for (var i = 0; i < [features count]; i++) { + var feature = features[i]; + var featureSize = [feature sizeWithAttributes:featureAttributes]; + var featurePoint = CGPointMake((bounds.size.width - featureSize.width) / 2.0, currentY); + [feature drawAtPoint:featurePoint withAttributes:featureAttributes]; + currentY += featureSize.height + 5; + } + + // === END: Infographic Drawing === + + // The background is drawn by the window. We only draw the rubber-band. + if (_isRubbing) + { + var rubber = CGRectUnion(CGRectMake(_rubberStart.x, _rubberStart.y, 0.1, 0.1), CGRectMake(_rubberEnd.x, _rubberEnd.y, 0.1, 0.1)); + [[[[CPColor alternateSelectedControlColor] colorWithAlphaComponent:0.2] setFill]]; + [CPBezierPath fillRect:rubber]; + [[CPColor alternateSelectedControlColor] setStroke]; + [CPBezierPath setDefaultLineWidth:1.0]; + [CPBezierPath strokeRect:rubber]; + } + + // Draw existing connections that are selected in the connections controller. + var selectedConnections = [self selectedConnections]; + + if (selectedConnections && [selectedConnections count] > 0) + { + for (var i = 0; i < [selectedConnections count]; i++) + { + var connection = [selectedConnections objectAtIndex:i]; + var sourceID = [connection valueForKey:@"sourceID"]; + var targetID = [connection valueForKey:@"targetID"]; + var sourceView = [self viewForElementWithID:sourceID]; + var targetView = [self viewForElementWithID:targetID]; + + if (sourceView && targetView) + { + var startPoint = [sourceView convertPoint:CGPointMake(CGRectGetMidX([sourceView bounds]), CGRectGetMidY([sourceView bounds])) toView:self]; + var endPoint; + var connectionPoint = [connection valueForKey:@"atPoint"]; + + if (connectionPoint) { + endPoint = CGPointMake(connectionPoint.x, connectionPoint.y); + } else { + endPoint = [targetView convertPoint:CGPointMake(CGRectGetMidX([targetView bounds]), CGRectGetMidY([targetView bounds])) toView:self]; + } + + // Draw the link with a distinct color, like blue. + [self drawLinkFrom:startPoint to:endPoint color:[CPColor blueColor]]; + } + } + } +} + +- (void)drawLinkFrom:(CGPoint)startPoint to:(CGPoint)endPoint color:(CPColor)insideColor +{ + + var dist = Math.sqrt(Math.pow(startPoint.x - endPoint.x, 2) + Math.pow(startPoint.y - endPoint.y, 2)); + + // a lace is made of an outside gray line of width 5, and a inside insideColor(ed) line of width 3 + var p0 = CGPointMake(startPoint.x, startPoint.y); + var p3 = CGPointMake(endPoint.x, endPoint.y); + + var p1 = CGPointMake(startPoint.x + treshold((endPoint.x - startPoint.x) / 2, 50), startPoint.y); + var p2 = CGPointMake(endPoint.x - treshold((endPoint.x - startPoint.x) / 2, 50), endPoint.y); + + // p0 and p1 are on the same horizontal line + // distance between p0 and p1 is set with the treshold fuction + // the same holds for p2 and p3 + + var path = [CPBezierPath bezierPath]; + [path setLineWidth:0]; + [[CPColor grayColor] set]; + [path appendBezierPathWithOvalInRect:CGRectMake(startPoint.x-2.5,startPoint.y-2.5,5,5)]; + [path fill]; + + path = [CPBezierPath bezierPath]; + [path setLineWidth:0]; + [insideColor set]; + [path appendBezierPathWithOvalInRect:CGRectMake(startPoint.x-1.5,startPoint.y-1.5,3,3)]; + [path fill]; + + path = [CPBezierPath bezierPath]; + [path setLineWidth:0]; + [[CPColor grayColor] set]; + [path appendBezierPathWithOvalInRect:CGRectMake(endPoint.x-2.5,endPoint.y-2.5,5,5)]; + [path fill]; + + path = [CPBezierPath bezierPath]; + [path setLineWidth:0]; + [insideColor set]; + [path appendBezierPathWithOvalInRect:CGRectMake(endPoint.x-1.5,endPoint.y-1.5,3,3)]; + [path fill]; + + // if the line is rather short, draw a straight line. the curve would look rather odd in this case. + if (dist < 40) + { + path = [CPBezierPath bezierPath]; + [path setLineWidth:5]; + [path moveToPoint:startPoint]; + [path lineToPoint:endPoint]; + [[CPColor grayColor] set]; + [path stroke]; + + path = [CPBezierPath bezierPath]; + [path setLineWidth:3]; + [path moveToPoint:startPoint]; + [path lineToPoint:endPoint]; + [insideColor set]; + [path stroke]; + + return; + } + + path = [CPBezierPath bezierPath]; + [path setLineWidth:5]; + [path moveToPoint:p0]; + [path curveToPoint:p3 controlPoint1:p1 controlPoint2:p2]; + [[CPColor grayColor] set]; + [path stroke]; + + path = [CPBezierPath bezierPath]; + [path setLineWidth:3]; + [path moveToPoint:p0]; + [path curveToPoint:p3 controlPoint1:p1 controlPoint2:p2]; + [insideColor set]; + [path stroke]; +} + +#pragma mark - View Lookup + +// Private recursive helper method to search the entire view hierarchy. +- (UIElementView)_findViewForElementWithID:(CPString)elementID inView:(CPView)aView +{ + // Iterate through all subviews of the current view + var subviews = [aView subviews]; + for (var i = 0; i < [subviews count]; i++) + { + var subview = subviews[i]; + + // We are only interested in UIElementView subclasses + if (![subview isKindOfClass:[UIElementView class]]) + continue; + + // 1. Check if the current subview is the one we are looking for. + if ([[subview dataObject] valueForKey:@"id"] === elementID) + { + return subview; // Found it! + } + + // 2. If not, and this subview has children, recurse into it. + // This is the key step to search inside containers like UIWindowView. + if ([[subview subviews] count] > 0) + { + var foundView = [self _findViewForElementWithID:elementID inView:subview]; + if (foundView) + { + return foundView; // Found it in a nested hierarchy. + } + } + } + + // If we've searched this entire branch and found nothing, return nil. + return nil; +} + +// Public method to start the search from the canvas itself. +- (UIElementView)viewForElementWithID:(CPString)elementID +{ + if (!elementID) + return nil; + + // Start the recursive search from the top-level canvas view. + return [self _findViewForElementWithID:elementID inView:self]; +} + +- (void)drawConnectionFrom:(CGPoint)startPoint to:(CGPoint)endPoint +{ + [_connectionView setStartPoint:startPoint]; + [_connectionView setEndPoint:endPoint]; + [_connectionView setHidden:NO]; // Ensure it's visible when drawing + [self addSubview:_connectionView]; // Bring to front + [_connectionView setNeedsDisplay:YES]; +} + +- (void)clearConnection +{ + [_connectionView setHidden:YES]; + [_connectionView setNeedsDisplay:YES]; // Request redraw to clear old line +} + +- (UIElementView)viewAtPoint:(CGPoint)aPoint +{ + return [self _findDeepestUIElementViewAtPoint:aPoint inView:self]; +} + +- (UIElementView)_findDeepestUIElementViewAtPoint:(CGPoint)aPoint inView:(CPView)currentView +{ + // Iterate through subviews in reverse order to get the topmost view + for (var i = [[currentView subviews] count] - 1; i >= 0; i--) + { + var subview = [[currentView subviews] objectAtIndex:i]; + + // Convert the point to the subview's coordinate system + var localPoint = [subview convertPoint:aPoint fromView:currentView]; + + if ([subview isKindOfClass:[UIElementView class]]) + { + if (CGRectContainsPoint([subview bounds], localPoint)) + { + // If this is a container view, recursively search its subviews + if (subview._isContainer) + { + var deepestView = [self _findDeepestUIElementViewAtPoint:localPoint inView:subview]; + + if (deepestView) + return deepestView; + } + // If not a container, or no deeper view found, return this view + return subview; + } + } + } + + return nil; +} + +- (void)mouseDown:(CPEvent)theEvent +{ + if (_connectionSource) + { + [self menuDidEndTracking:nil]; + return; + } + // A click on the canvas background starts a rubber-band selection. + [self deselectViews]; + _isRubbing = YES; + _rubberStart = [self convertPoint:[theEvent locationInWindow] fromView:nil]; + _rubberEnd = _rubberStart; + + [CPApp setTarget:self selector:@selector(_dragOpenSpaceWithEvent:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES]; +} + +- (void)_dragOpenSpaceWithEvent:(CPEvent)theEvent +{ + var mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; + _rubberEnd = mouseLoc; + var rubberRect = CGRectUnion(CGRectMake(_rubberStart.x, _rubberStart.y, 1, 1), CGRectMake(_rubberEnd.x, _rubberEnd.y, 1, 1)); + + switch ([theEvent type]) + { + case CPLeftMouseDragged: + var indexesToSelect = [CPMutableIndexSet indexSet]; + var allDataObjects = [self dataObjects]; + for (var i = 0; i < [[self subviews] count]; i++) { + var aView = [self subviews][i]; + if ([aView isKindOfClass:[UIElementView class]] && CGRectIntersectsRect([aView frame], rubberRect)) { + var dataIndex = [allDataObjects indexOfObject:[aView dataObject]]; + if (dataIndex != CPNotFound) { + [indexesToSelect addIndex:dataIndex]; + } + } + } + [_selectionIndexesContainer setValue:indexesToSelect forKeyPath:_selectionIndexesKeyPath]; + [self setNeedsDisplay:YES]; + [CPApp setTarget:self selector:@selector(_dragOpenSpaceWithEvent:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES]; + break; + + case CPLeftMouseUp: + _isRubbing = NO; + [self setNeedsDisplay:YES]; + break; + } +} + +- (void)delete:(id)sender +{ + // Forward the delete action to the delegate/controller + if (_delegate && [_delegate respondsToSelector:@selector(removeSelectedElements)]) { + [_delegate removeSelectedElements]; + } +} + +- (void)cut:(id)sender +{ + if (_delegate && [_delegate respondsToSelector:@selector(cut:)]) { + [_delegate cut:sender]; + } +} + +- (void)copy:(id)sender +{ + if (_delegate && [_delegate respondsToSelector:@selector(copy:)]) { + [_delegate copy:sender]; + } +} + +- (void)paste:(id)sender +{ + if (_delegate && [_delegate respondsToSelector:@selector(paste:)]) { + [_delegate paste:sender]; + } +} + +- (void)viewDidMoveToWindow +{ + [super viewDidMoveToWindow]; + + if ([self window]) + { + [[self window] makeFirstResponder:self]; + } +} + +- (BOOL)acceptsFirstResponder +{ + return YES; +} + +/* +- (BOOL)validateMenuItem:(CPMenuItem)aMenuItem +{ + var action = [aMenuItem action]; + + if (action == @selector(copy:) || action == @selector(cut:) || action == @selector(delete:)) + { + return [[self selectionIndexes] count] > 0; + } + + if (action == @selector(paste:)) + { + return [[[CPPasteboard generalPasteboard] types] containsObject:UIBuilderElementPboardType]; + } + + if (action == @selector(createTargetActionConnection:) || action == @selector(createOutletConnection:)) + { + return YES; + } + + var undoManager = [[self window] undoManager]; + + if (action == @selector(undo:)) + { + return [undoManager canUndo]; + } + + if (action == @selector(redo:)) + { + return [undoManager canRedo]; + } + + return [super validateMenuItem:aMenuItem]; +} +*/ + +- (void)keyDown:(CPEvent)theEvent +{ + var characters = [theEvent characters]; + var flags = [theEvent modifierFlags]; + var selectors = [CPKeyBinding selectorsForKey:characters modifierFlags:flags]; + var delegate = [self delegate]; + var handled = NO; + + if (selectors && delegate) + { + for (var i = 0; i < [selectors count]; i++) + { + var selectorName = selectors[i]; + if ([delegate respondsToSelector:selectorName]) + { + [delegate performSelector:selectorName withObject:self]; + handled = YES; + break; + } + } + } + + if (!handled) + [super keyDown:theEvent]; +} + +#pragma mark - Drag and Drop Destination + +- (CPDragOperation)draggingEntered:(CPDraggingInfo)sender +{ + // We accept any of the registered types + return CPDragOperationCopy; +} + +- (BOOL)performDragOperation:(CPDraggingInfo)sender +{ + var dropPoint = [self convertPoint:[sender draggingLocation] fromView:nil]; + var pasteboard = [sender draggingPasteboard]; + var types = [pasteboard types]; + var draggedType = types[0]; // Assuming only one type is being dragged + var elementType; + + if (draggedType === UIWindowDragType) elementType = "window"; + else if (draggedType === UIButtonDragType) elementType = "button"; + else if (draggedType === UISliderDragType) elementType = "slider"; + else if (draggedType === UITextFieldDragType) elementType = "textfield"; + + if (elementType && _delegate) + { + if (elementType === "window") { + if ([_delegate respondsToSelector:@selector(addNewElementOfType:atPoint:)]) + { + [_delegate addNewElementOfType:elementType atPoint:dropPoint]; + [self setNeedsDisplay:YES]; + return YES; + } + } else { + if ([_delegate respondsToSelector:@selector(addNewElementOfType:inNewWindowAtPoint:)]) + { + [_delegate addNewElementOfType:elementType inNewWindowAtPoint:dropPoint]; + [self setNeedsDisplay:YES]; + return YES; + } + } + } + + return NO; +} + +#pragma mark - Delegate & Selection Management + +- (id)delegate { return _delegate; } +- (void)setDelegate:(id)newDelegate { _delegate = newDelegate; } + +- (void)deselectViews +{ + [_selectionIndexesContainer setValue:nil forKeyPath:_selectionIndexesKeyPath]; +} + +- (void)selectView:(UIElementView)aView state:(BOOL)select +{ + var selection = [[self selectionIndexes] mutableCopy] || [CPMutableIndexSet indexSet]; + var dataObjectIndex = [[self dataObjects] indexOfObject:[aView dataObject]]; + + + + if (dataObjectIndex != CPNotFound) + { + if (select) + [selection addIndex:dataObjectIndex]; + + else [selection removeIndex:dataObjectIndex]; + } + + [_selectionIndexesContainer setValue:selection forKeyPath:_selectionIndexesKeyPath]; +} + +- (CPArray)selectedSubViews +{ + var selectedDataObjects = [[self dataObjects] objectsAtIndexes:[self selectionIndexes]]; + var selectedViews = [CPMutableArray array]; + + [self _findViewsForDataObjects:selectedDataObjects inView:self foundViews:selectedViews]; + + return selectedViews; +} + +- (BOOL)isViewSelected:(CPView)aView +{ + var selected = [self selectedSubViews]; + + return [selected containsObject:aView]; +} + +- (void)_findViewsForDataObjects:(CPArray)dataObjects inView:(CPView)aView foundViews:(CPMutableArray)foundViews +{ + var subviews = [aView subviews]; + + for (var i = 0; i < [subviews count]; i++) + { + var subview = subviews[i]; + + // Skip the connection view and any other non-UIElementView instances + if (![subview isKindOfClass:[UIElementView class]]) + continue; + + var contains = [dataObjects containsObject:[subview dataObject]]; + + if (contains) + { + [foundViews addObject:subview]; + } + + // Recurse into subviews + [self _findViewsForDataObjects:dataObjects inView:subview foundViews:foundViews]; + } +} + +// These methods are called by the UIElementView children to notify the controller +- (void)elementDidMove:(UIElementView)anElement +{ + if (_delegate && [_delegate respondsToSelector:@selector(canvasView:didMoveElement:)]) { + [_delegate canvasView:self didMoveElement:anElement]; + } +} + +- (void)elementDidResize:(UIElementView)anElement +{ + if (_delegate && [_delegate respondsToSelector:@selector(canvasView:didResizeElement:)]) { + [_delegate canvasView:self didResizeElement:anElement]; + } +} + +- (void)elementDidConnect:(UIElementView)sourceElement to:(UIElementView)targetElement atPoint:(CGPoint)aPoint +{ + if (_delegate && [_delegate respondsToSelector:@selector(canvasView:didConnectElement:toElement:atPoint:)]) { + [_delegate canvasView:self didConnectElement:sourceElement toElement:targetElement atPoint:aPoint]; + } +} + +#pragma mark - Connection Menu + +- (void)showConnectionMenuForSource:(UIElementView)sourceView target:(UIElementView)targetView at:(CGPoint)aPoint +{ + _connectionSource = sourceView; + _connectionTarget = targetView; + _connectionMade = NO; + + var menu = [[CPMenu alloc] initWithTitle:@"Connection Menu"]; + [menu setDelegate:self]; + + // 1. Add Target's Actions + var targetActions = [[_connectionTarget dataObject] valueForKey:@"actions"]; + if (targetActions && [targetActions length] > 0) + { + var actionsArray = [targetActions componentsSeparatedByString:@", "]; + for (var i = 0; i < [actionsArray count]; i++) + { + var actionName = actionsArray[i]; + var menuItem = [[CPMenuItem alloc] initWithTitle:actionName action:@selector(createTargetActionConnection:) keyEquivalent:@""]; + [menu addItem:menuItem]; + } + } + + // 2. Add Separator + if ([menu numberOfItems] > 0) + [menu addItem:[CPMenuItem separatorItem]]; + + // 3. Add Source's Outlets + var sourceOutlets = [[_connectionSource dataObject] valueForKey:@"outlets"]; + if (sourceOutlets && [sourceOutlets length] > 0) + { + var outletsArray = [sourceOutlets componentsSeparatedByString:@", "]; + for (var i = 0; i < [outletsArray count]; i++) + { + var outletName = outletsArray[i]; + if (outletName === @"target") continue; // Skip 'target' outlet as requested + var menuItem = [[CPMenuItem alloc] initWithTitle:outletName action:@selector(createOutletConnection:) keyEquivalent:@""]; + [menu addItem:menuItem]; + } + } + + if ([menu numberOfItems] > 0) + { + [CPMenu popUpContextMenu:menu withEvent:[CPApp currentEvent] forView:self]; + } + else + { + [self menuDidEndTracking:menu]; // No items, so clean up immediately + } +} + +- (void)createTargetActionConnection:(CPMenuItem)sender +{ + var actionName = [sender title]; + if (_delegate && [_delegate respondsToSelector:@selector(canvasView:didConnectElement:toElement:asTargetAction:)]) + { + _connectionMade = YES; + [self clearConnection]; + if (_connectionTarget) + [_connectionTarget setAsDropTarget:NO]; + + [_delegate canvasView:self didConnectElement:_connectionSource toElement:_connectionTarget asTargetAction:actionName]; + } +} + +- (void)createOutletConnection:(CPMenuItem)sender +{ + var outletName = [sender title]; + if (_delegate && [_delegate respondsToSelector:@selector(canvasView:didConnectElement:toElement:asOutlet:)]) + { + _connectionMade = YES; + [self clearConnection]; + if (_connectionTarget) + [_connectionTarget setAsDropTarget:NO]; + + [_delegate canvasView:self didConnectElement:_connectionSource toElement:_connectionTarget asOutlet:outletName]; + } +} + +- (void)menuDidEndTracking:(CPMenu)aMenu +{ + // This delegate method is called after a menu item is selected OR the menu is cancelled. + if (!_connectionMade) + { + [self clearConnection]; + if (_connectionTarget) + [_connectionTarget setAsDropTarget:NO]; + } + + // Reset state + _connectionSource = nil; + _connectionTarget = nil; + _connectionMade = NO; + + [self setNeedsDisplay:YES]; +} + +@end diff --git a/Tests/Manual/UIBuilderDemo/UIElementView.j b/Tests/Manual/UIBuilderDemo/UIElementView.j new file mode 100644 index 000000000..d220fc7fd --- /dev/null +++ b/Tests/Manual/UIBuilderDemo/UIElementView.j @@ -0,0 +1,1349 @@ +// +// UIElementView.j by Daniel Böhringer in 2025 + +// This file is a drawing engine for a UI builder, with features such as: +// - Skeleton drawing for common UI elements (Window, Button, Slider, TextField). +// - Selection highlights. +// - Resize handles ("dimples") on selected views. +// - Mouse logic for moving and resizing elements. +// - Visual hints for drop targets (e.g., a Window accepting a Button). +// +// + +@import "UIBuilderConstants.j"; + +// --- Property Types --- +UIBString = "UIBString"; +UIBNumber = "UIBNumber"; +UIBBoolean = "UIBBoolean"; + +// --- Constants for Resizing --- +var kUIElementHandleSize = 8.0; +var kUIElementNoHandle = 0; +var kUIElementTopLeftHandle = 1; +var kUIElementTopMiddleHandle = 2; +var kUIElementTopRightHandle = 3; +var kUIElementMiddleLeftHandle = 4; +var kUIElementMiddleRightHandle = 5; +var kUIElementBottomLeftHandle = 6; +var kUIElementBottomMiddleHandle = 7; +var kUIElementBottomRightHandle = 8; + + +@class UIWindowView +@class UIButtonView +@class UISliderView +@class UITextFieldView; + +@implementation UIElementView : CPView +{ + CPMutableDictionary _stringAttributes; + id _dataObject @accessors(property=dataObject); + + // State for dragging and resizing + CGPoint _lastMouseLoc; + int _activeHandle; + BOOL _isDragTarget; // Used by subclasses (e.g. UIWindowView) + CPTrackingArea _trackingArea; + BOOL _isContainer; + BOOL _isConnecting; +} + +#pragma mark - +#pragma mark *** Class Methods *** + ++ (CPArray)persistentProperties +{ + return ["value"]; +} + ++ (CPDictionary)defaultValues +{ + return {value: "Element"}; +} + ++ (CPDictionary)propertyTypes +{ + return [CPDictionary dictionaryWithObjects:[UIBString] forKeys:["value"]]; +} + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) + { + _stringAttributes = [[CPMutableDictionary alloc] init]; + [_stringAttributes setObject:[CPFont boldSystemFontOfSize:12] forKey:CPFontAttributeName]; + [_stringAttributes setObject:[CPColor blackColor] forKey:CPForegroundColorAttributeName]; + + + _activeHandle = kUIElementNoHandle; + + if ([self frame].size.width < 50 || [self frame].size.height < 20) + [self setFrameSize:CGSizeMake(MAX(50, [self frame].size.width), MAX(20, [self frame].size.height))]; + + [self setNeedsDisplay:YES]; + + _trackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() + options:CPTrackingMouseMoved | CPTrackingActiveInKeyWindow | CPTrackingInVisibleRect | CPTrackingMouseEnteredAndExited + owner:self + userInfo:nil]; + [self addTrackingArea:_trackingArea]; + _isContainer = NO; + _isConnecting = NO; + } + return self; +} + +- (void)dealloc +{ + [self setDataObject:nil]; + [super dealloc]; +} + +- (void)setDataObject:(id)newDataObject +{ + var oldDataObject = [self dataObject]; + if (newDataObject != oldDataObject) + { + var properties = [[self class] persistentProperties]; + if (oldDataObject) + for (var i = 0; i < [properties count]; i++) + [oldDataObject removeObserver:self forKeyPath:properties[i]]; + + _dataObject = newDataObject; + + if (newDataObject) + { + for (var i = 0; i < [properties count]; i++) + { + var propertyName = properties[i]; + [newDataObject addObserver:self forKeyPath:propertyName options:CPKeyValueObservingOptionNew context:self]; + } + } + } +} + +- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context +{ + if (context == self) + { + // When a property on the dataObject changes, simply tell the view to redraw itself. + [self setNeedsDisplay:YES]; + } + else + { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (BOOL)acceptsFirstMouse +{ + // This view should accept first mouse events for interaction. + return YES; +} + +- (void)removeFromSuperview +{ + // This is the correct place to clean up view-related resources. + // When the view is removed from its superview, we no longer need to track + // mouse events within its bounds. + [self removeTrackingArea:_trackingArea]; + + // It's crucial to call the superclass's implementation at the end. + [super removeFromSuperview]; +} + +#pragma mark - +#pragma mark *** Geometry Accessors (for KVC Binding) *** + +- (float)originX +{ + return [self frame].origin.x; +} + +- (void)setOriginX:(float)aFloat +{ + // Only update if the value has actually changed. + if (aFloat !== [self originX]) + { + var frame = [self frame]; + frame.origin.x = aFloat; + [self setFrame:frame]; + + // Notify the superview (the canvas) that it might need to redraw + // if anything depends on this view's position. + [[self superview] setNeedsDisplay:YES]; + } +} + +- (float)originY +{ + return [self frame].origin.y; +} + +- (void)setOriginY:(float)aFloat +{ + if (aFloat !== [self originY]) + { + var frame = [self frame]; + frame.origin.y = aFloat; + [self setFrame:frame]; + [[self superview] setNeedsDisplay:YES]; + } +} + +- (float)width +{ + return [self frame].size.width; +} + +- (void)setWidth:(float)aFloat +{ + if (aFloat !== [self width]) + { + var frame = [self frame]; + // Enforce a minimum width to prevent rendering issues. + frame.size.width = MAX(aFloat, 20.0); + [self setFrame:frame]; + [[self superview] setNeedsDisplay:YES]; + } +} + +- (float)height +{ + return [self frame].size.height; +} + +- (void)setHeight:(float)aFloat +{ + if (aFloat !== [self height]) + { + var frame = [self frame]; + // Enforce a minimum height. + frame.size.height = MAX(aFloat, 20.0); + [self setFrame:frame]; + [[self superview] setNeedsDisplay:YES]; + } +} + +#pragma mark - +#pragma mark *** Accessors *** + +- (id)value +{ + return ([self dataObject] == nil) ? @"" : [[self dataObject] valueForKey:@"value"]; +} + +// You will need a way to get a reference to the canvas. +// This is often done by walking up the superview chain. +- (UICanvasView)canvas +{ + var aView = self; + while (aView = [aView superview]) { + if ([aView isKindOfClass:[UICanvasView class]]) + return aView; + } + return nil; +} + +#pragma mark - +#pragma mark *** Drawing *** + +- (void)drawRect:(CGRect)rect +{ + // 1. Draw the specific skeleton for the element subclass + [self drawSkeleton:rect]; + + // 2. If this view is a drop target, draw a highlight + if (_isDragTarget) + { + [[[CPColor redColor] colorWithAlphaComponent:0.8] setStroke]; + var highlightPath = [CPBezierPath bezierPathWithRect:CGRectInset([self bounds], 1, 1)]; + [highlightPath setLineWidth:2.0]; + [highlightPath stroke]; + } + + // 3. If selected, draw selection outline and resize handles + if ([self isSelected]) + { + // Draw selection highlight + [[CPColor keyboardFocusIndicatorColor] setStroke]; + var selectionPath = [CPBezierPath bezierPathWithRect:CGRectInset([self bounds], -2, -2)]; + [selectionPath setLineWidth:1.0]; + [selectionPath stroke]; + + // Draw resize handles ("dimples") + [self drawHandles]; + } +} + +- (void)drawSkeleton:(CGRect)rect +{ + // Base implementation: a simple placeholder box. + // Subclasses should override this to draw their specific look. + var bounds = [self bounds]; + [[CPColor lightGrayColor] setFill]; + [CPBezierPath fillRect:bounds]; + [[CPColor darkGrayColor] setStroke]; + [CPBezierPath strokeRect:bounds]; + + var valueSize = [[self value] sizeWithAttributes:_stringAttributes]; + [[self value] drawAtPoint:CGPointMake((bounds.size.width - valueSize.width) / 2.0, (bounds.size.height - valueSize.height) / 2.0) withAttributes:_stringAttributes]; +} + +- (CGRect)rectForHandle:(int)handle +{ + var bounds = [self bounds]; + var x, y; + + // Top Row + if (handle >= kUIElementTopLeftHandle && handle <= kUIElementTopRightHandle) + y = bounds.origin.y - kUIElementHandleSize / 2.0; + // Middle Row + if (handle === kUIElementMiddleLeftHandle || handle === kUIElementMiddleRightHandle) + y = bounds.origin.y + bounds.size.height / 2.0 - kUIElementHandleSize / 2.0; + // Bottom Row + if (handle >= kUIElementBottomLeftHandle && handle <= kUIElementBottomRightHandle) + y = bounds.origin.y + bounds.size.height - kUIElementHandleSize / 2.0; + + // Left Column + if (handle === kUIElementTopLeftHandle || handle === kUIElementMiddleLeftHandle || handle === kUIElementBottomLeftHandle) + x = bounds.origin.x - kUIElementHandleSize / 2.0; + // Center Column + if (handle === kUIElementTopMiddleHandle || handle === kUIElementBottomMiddleHandle) + x = bounds.origin.x + bounds.size.width / 2.0 - kUIElementHandleSize / 2.0; + // Right Column + if (handle === kUIElementTopRightHandle || handle === kUIElementMiddleRightHandle || handle === kUIElementBottomRightHandle) + x = bounds.origin.x + bounds.size.width - kUIElementHandleSize / 2.0; + + return CGRectMake(x, y, kUIElementHandleSize, kUIElementHandleSize); +} + +- (void)drawHandles +{ + [[CPColor controlDarkShadowColor] setFill]; + for (var i = 1; i <= 8; i++) + { + [CPBezierPath fillRect:[self rectForHandle:i]]; + } +} + +- (BOOL)isSelected +{ + return [[self canvas] isViewSelected:self]; +} + +#pragma mark - +#pragma mark *** Mouse Handling & Resizing *** + +- (int)handleAtPoint:(CGPoint)aPoint +{ + if (![self isSelected]) return kUIElementNoHandle; + + for (var i = 1; i <= 8; i++) + { + if (CGRectContainsPoint([self rectForHandle:i], aPoint)) + return i; + } + return kUIElementNoHandle; +} + +- (void)rightMouseDown:(CPEvent)theEvent +{ + [self mouseDown:theEvent]; +} +- (void)rightMouseUp:(CPEvent)theEvent +{ + [self mouseUp:theEvent]; +} + +- (void)mouseDown:(CPEvent)theEvent +{ + var canvas = [self canvas]; + var localPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil]; + + _lastMouseLoc = [[self canvas] convertPoint:[theEvent locationInWindow] fromView:nil]; + + // First, check if we clicked a resize handle + _activeHandle = [self handleAtPoint:localPoint]; + + // No handle was clicked, proceed with selection and movement logic + if ([theEvent modifierFlags] & CPShiftKeyMask) + { + [canvas selectView:self state:YES]; + } + else if ([theEvent modifierFlags] & CPCommandKeyMask) + { + [canvas selectView:self state:![self isSelected]]; + } + else if (![self isSelected]) + { + [canvas deselectViews]; + [canvas selectView:self state:YES]; + } +} + + + +- (void)mouseDragged:(CPEvent)theEvent +{ + var canvas = [self canvas]; + var mouseLoc = [canvas convertPoint:[theEvent locationInWindow] fromView:nil]; + + // If _lastMouseLoc is null, it means the drag started outside this view, + // so we initialize it with the current mouse location to prevent errors. + if (!_lastMouseLoc) { + _lastMouseLoc = mouseLoc; + } + + if ([theEvent modifierFlags] & CPControlKeyMask) + { + _isConnecting = YES; + // If control key is pressed, handle connection drawing + var startPointInView = CGPointMake(CGRectGetMidX([self bounds]), CGRectGetMidY([self bounds])); + var startPointInCanvas = [self convertPoint:startPointInView toView:canvas]; + + var canvasSubviews = [canvas subviews]; + for (var k = 0; k < [canvasSubviews count]; k++) { + var subview = [canvasSubviews objectAtIndex:k]; + if ([subview isKindOfClass:[UIElementView class]]) { + [subview setAsDropTarget:NO]; + } + } + var targetView = [canvas viewAtPoint:mouseLoc]; + + if (targetView && targetView != self) + { + var localPoint = [targetView convertPoint:mouseLoc fromView:canvas]; + if ([targetView canAcceptConnectionAtPoint:localPoint]) + { + var endPointInView = CGPointMake(CGRectGetMidX([targetView bounds]), CGRectGetMidY([targetView bounds])); + var endPointInCanvas = [targetView convertPoint:endPointInView toView:canvas]; + [canvas drawConnectionFrom:startPointInCanvas to:endPointInCanvas]; + [targetView setAsDropTarget:YES]; + } + else + { + [canvas drawConnectionFrom:startPointInCanvas to:mouseLoc]; + } + } + else + { + [canvas drawConnectionFrom:startPointInCanvas to:mouseLoc]; + } + } + else if (_activeHandle != kUIElementNoHandle) + { + // Resize logic + var sView = [self superview]; + var deltaX = mouseLoc.x - _lastMouseLoc.x; + var deltaY = mouseLoc.y - _lastMouseLoc.y; + + var frame = [self frame]; + var minSize = CGSizeMake(2 * kUIElementHandleSize, 2 * kUIElementHandleSize); + + // Left handles + if (_activeHandle === kUIElementTopLeftHandle || _activeHandle === kUIElementMiddleLeftHandle || _activeHandle === kUIElementBottomLeftHandle) { + if (frame.size.width - deltaX > minSize.width) { + frame.origin.x += deltaX; + frame.size.width -= deltaX; + } + } + // Right handles + if (_activeHandle === kUIElementTopRightHandle || _activeHandle === kUIElementMiddleRightHandle || _activeHandle === kUIElementBottomRightHandle) { + if (frame.size.width + deltaX > minSize.width) { + frame.size.width += deltaX; + } + } + // Top handles + if (_activeHandle === kUIElementTopLeftHandle || _activeHandle === kUIElementTopMiddleHandle || _activeHandle === kUIElementTopRightHandle) { + if (frame.size.height - deltaY > minSize.height) { + frame.origin.y += deltaY; + frame.size.height -= deltaY; + } + } + // Bottom handles + if (_activeHandle === kUIElementBottomLeftHandle || _activeHandle === kUIElementBottomMiddleHandle || _activeHandle === kUIElementBottomRightHandle) { + if (frame.size.height + deltaY > minSize.height) { + frame.size.height += deltaY; + } + } + + [self setFrame:frame]; + + _lastMouseLoc = mouseLoc; + [canvas setNeedsDisplay:YES]; + } + else + { + // This is the move logic, largely from the original EFView. + [[CPCursor closedHandCursor] set]; + var deltaX = mouseLoc.x - _lastMouseLoc.x; + var deltaY = mouseLoc.y - _lastMouseLoc.y; + + for (var i = 0; i < [[canvas selectedSubViews] count]; i++) + { + var view = [canvas selectedSubViews][i]; + var newOrigin = CGPointMake([view frame].origin.x + deltaX, [view frame].origin.y + deltaY); + + var parentView = [view superview]; + if ([parentView isKindOfClass:[UIWindowView class]]) + { + var parentBounds = [parentView bounds]; + var viewFrame = [view frame]; + newOrigin.x = MAX(0, MIN(newOrigin.x, parentBounds.size.width - viewFrame.size.width)); + newOrigin.y = MAX(0, MIN(newOrigin.y, parentBounds.size.height - viewFrame.size.height)); + } + + [view setFrameOrigin:newOrigin]; + } + + _lastMouseLoc = mouseLoc; + [canvas setNeedsDisplay:YES]; + } +} + +- (void)mouseUp:(CPEvent)theEvent +{ + var canvas = [self canvas]; + var mouseLoc = [canvas convertPoint:[theEvent locationInWindow] fromView:nil]; + + if (_isConnecting) + { + // Handle mouse up for connection + var targetView = [canvas viewAtPoint:mouseLoc]; + + if (targetView && targetView != self) + { + var localPoint = [targetView convertPoint:mouseLoc fromView:canvas]; + if ([targetView canAcceptConnectionAtPoint:localPoint]) + { + [canvas showConnectionMenuForSource:self target:targetView at:mouseLoc]; + } + else + { + [canvas clearConnection]; + } + } + else + { + [canvas clearConnection]; + } + + var canvasSubviews = [canvas subviews]; + + for (var k = 0; k < [canvasSubviews count]; k++) { + var subview = [canvasSubviews objectAtIndex:k]; + if ([subview isKindOfClass:[UIElementView class]] && subview != targetView) { + [subview setAsDropTarget:NO]; + } + } + [canvas setNeedsDisplay:YES]; + _isConnecting = NO; + } + else if (_activeHandle != kUIElementNoHandle) + { + // Handle mouse up for resize + [[CPCursor arrowCursor] set]; + _activeHandle = kUIElementNoHandle; + _lastMouseLoc = null; + [canvas setNeedsDisplay:YES]; + [canvas elementDidResize:self]; + } + else + { + // Handle mouse up for move + [[CPCursor openHandCursor] set]; + _lastMouseLoc = null; + [canvas setNeedsDisplay:YES]; + [canvas elementDidMove:self]; + } +} + +- (void)_resizeWithEvent:(CPEvent)theEvent +{ + var sView = [self superview]; + var canvas = [self canvas]; + var mouseLoc; + + switch ([theEvent type]) + { + case CPLeftMouseDragged: + [[CPCursor crosshairCursor] set]; // A generic resize cursor + mouseLoc = [sView convertPoint:[theEvent locationInWindow] fromView:nil]; + var deltaX = mouseLoc.x - _lastMouseLoc.x; + var deltaY = mouseLoc.y - _lastMouseLoc.y; + + var frame = [self frame]; + var minSize = CGSizeMake(2 * kUIElementHandleSize, 2 * kUIElementHandleSize); + + // Left handles + if (_activeHandle === kUIElementTopLeftHandle || _activeHandle === kUIElementMiddleLeftHandle || _activeHandle === kUIElementBottomLeftHandle) { + if (frame.size.width - deltaX > minSize.width) { + frame.origin.x += deltaX; + frame.size.width -= deltaX; + } + } + // Right handles + if (_activeHandle === kUIElementTopRightHandle || _activeHandle === kUIElementMiddleRightHandle || _activeHandle === kUIElementBottomRightHandle) { + if (frame.size.width + deltaX > minSize.width) { + frame.size.width += deltaX; + } + } + // Top handles + if (_activeHandle === kUIElementTopLeftHandle || _activeHandle === kUIElementTopMiddleHandle || _activeHandle === kUIElementTopRightHandle) { + if (frame.size.height - deltaY > minSize.height) { + frame.origin.y += deltaY; + frame.size.height -= deltaY; + } + } + // Bottom handles + if (_activeHandle === kUIElementBottomLeftHandle || _activeHandle === kUIElementBottomMiddleHandle || _activeHandle === kUIElementBottomRightHandle) { + if (frame.size.height + deltaY > minSize.height) { + frame.size.height += deltaY; + } + } + + [self setFrame:frame]; + + _lastMouseLoc = mouseLoc; + [canvas setNeedsDisplay:YES]; + [CPApp setTarget:self selector:@selector(_resizeWithEvent:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES]; + break; + case CPLeftMouseUp: + [[CPCursor arrowCursor] set]; + _activeHandle = kUIElementNoHandle; + _lastMouseLoc = null; + [canvas setNeedsDisplay:YES]; + [canvas elementDidResize:self]; + break; + } +} + +- (void)setAsDropTarget:(BOOL)isTarget +{ + if (_isDragTarget !== isTarget) + { + _isDragTarget = isTarget; + [self setNeedsDisplay:YES]; + } +} + +- (void)_connectWithEvent:(CPEvent)theEvent +{ + var canvas = [self canvas]; + var mouseLoc = [canvas convertPoint:[theEvent locationInWindow] fromView:nil]; + + // Convert the start point (center of the view) to the canvas's coordinate system + var startPointInView = CGPointMake(CGRectGetMidX([self bounds]), CGRectGetMidY([self bounds])); + var startPointInCanvas = [self convertPoint:startPointInView toView:canvas]; + + var canvasSubviews = [canvas subviews]; + for (var k = 0; k < [canvasSubviews count]; k++) { + var subview = [canvasSubviews objectAtIndex:k]; + if ([subview isKindOfClass:[UIElementView class]]) { + [subview setAsDropTarget:NO]; + } + } + var targetView = [canvas viewAtPoint:mouseLoc]; + var validTargetFound = NO; + var endPointForDrawing = mouseLoc; // Default to follow mouse + + if (targetView && targetView != self) + { + if ([targetView isKindOfClass:[UIWindowView class]]) + { + validTargetFound = YES; + // Snap to center of the window for drawing feedback + endPointForDrawing = CGPointMake(CGRectGetMidX([targetView bounds]), CGRectGetMidY([targetView bounds])); + endPointForDrawing = [targetView convertPoint:endPointForDrawing toView:canvas]; + } + else + { + // For non-window elements, allow connection anywhere on their bounds + validTargetFound = YES; + endPointForDrawing = mouseLoc; // Follow mouse for other elements during drag + } + } + + if ([theEvent type] == CPLeftMouseDragged) + { + if (validTargetFound) + { + [canvas drawConnectionFrom:startPointInCanvas to:endPointForDrawing]; + [targetView setAsDropTarget:YES]; + } + else + { + [canvas drawConnectionFrom:startPointInCanvas to:mouseLoc]; + } + } + else if ([theEvent type] == CPLeftMouseUp) + { + // For final connection, snap to center of non-window elements, or title bar for windows + var finalEndPoint = mouseLoc; + var currentValidTarget = validTargetFound; // Store the initial state + + if (targetView && targetView != self) { + finalEndPoint = CGPointMake(CGRectGetMidX([targetView bounds]), CGRectGetMidY([targetView bounds])); + finalEndPoint = [targetView convertPoint:finalEndPoint toView:canvas]; + } else { + currentValidTarget = NO; // No valid target or target is self + } + + if (currentValidTarget) { + [[self canvas] elementDidConnect:self to:targetView atPoint:finalEndPoint]; // Pass finalEndPoint + } + [[self canvas] clearConnection]; + var canvasSubviews = [canvas subviews]; + for (var k = 0; k < [canvasSubviews count]; k++) { + var subview = [canvasSubviews objectAtIndex:k]; + if ([subview isKindOfClass:[UIElementView class]]) { + [subview setAsDropTarget:NO]; + } + } + } +} + +- (void)mouseEntered:(CPEvent)theEvent +{ + [[CPCursor openHandCursor] set]; +} + +- (void)mouseExited:(CPEvent)theEvent +{ + [[CPCursor arrowCursor] set]; +} + +- (void)mouseMoved:(CPEvent)theEvent +{ + var localPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil]; + var handle = [self handleAtPoint:localPoint]; + + if (handle != kUIElementNoHandle) { + // In a full implementation, you could return a specific two-headed arrow cursor + // based on the handle. For now, we use a generic one. + [[CPCursor crosshairCursor] set]; + } else { + [[CPCursor openHandCursor] set]; + } +} + +- (id)nativeUIElement +{ + return [self nativeUIElementWithMap:nil]; +} + +- (id)nativeUIElementWithMap:(CPMutableDictionary)aMap +{ + // Base implementation returns a generic view with a red background to indicate it's not a real UI element. + var view = [[CPView alloc] initWithFrame:[self frame]]; + [view setBackgroundColor:[CPColor redColor]]; + + if (aMap) + { + var elementID = [[self dataObject] valueForKey:@"id"]; + [aMap setObject:view forKey:elementID]; + } + + return view; +} + +- (BOOL)canAcceptConnectionAtPoint:(CGPoint)aPoint +{ + // By default, any part of the view can be a connection target. + return YES; +} + +@end + + +#pragma mark - +#pragma mark *** UI Element Subclasses *** + +// ================================================================================================= +// UIWindowView +// A skeleton that looks like a window, and can act as a drop target. +// ================================================================================================= + +var _windowChildrenObservationContext = 1094; + +@implementation UIWindowView : UIElementView +{ + CGPoint _rubberStart; + CGPoint _rubberEnd; + BOOL _isRubbing; +} + ++ (CPDictionary)propertyTypes +{ + var types = [super propertyTypes]; + [types setObject:UIBBoolean forKey:@"CPHUDBackgroundWindowMask"]; + [types setObject:UIBBoolean forKey:@"CPTitledWindowMask"]; + [types setObject:UIBBoolean forKey:@"CPClosableWindowMask"]; + return types; +} + ++ (CPArray)persistentProperties +{ + return [super persistentProperties].concat(["CPHUDBackgroundWindowMask", "CPTitledWindowMask", "CPClosableWindowMask"]); +} + ++ (CPDictionary)defaultValues +{ + return { + value: "Untitled Window", + CPHUDBackgroundWindowMask: true, + CPTitledWindowMask: true, + CPClosableWindowMask: true, + outlets: "delegate", + actions: "makeKeyAndOrderFront:, orderOut:" + }; +} + +- (void)drawRect:(CGRect)rect +{ + [super drawRect:rect]; + + if (_isRubbing) + { + var rubber = CGRectUnion(CGRectMake(_rubberStart.x, _rubberStart.y, 0.1, 0.1), CGRectMake(_rubberEnd.x, _rubberEnd.y, 0.1, 0.1)); + [[[[CPColor alternateSelectedControlColor] colorWithAlphaComponent:0.2] setFill]]; + [CPBezierPath fillRect:rubber]; + [[CPColor alternateSelectedControlColor] setStroke]; + [CPBezierPath setDefaultLineWidth:1.0]; + [CPBezierPath strokeRect:rubber]; + } +} + +- (void)mouseDown:(CPEvent)theEvent +{ + var localPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil]; + var titleBarHeight = 30.0; + + // 1. Check for resize handle click first. + if ([self handleAtPoint:localPoint] != kUIElementNoHandle) { + [super mouseDown:theEvent]; + return; + } + + // 2. Check if the click is within the title bar area. + if (localPoint.y <= titleBarHeight) { + // Click is in the title bar. Allow the superclass to handle moving the window. + [super mouseDown:theEvent]; + return; + } + + // On a click into the window's content area, deselect all elements. + [[self canvas] deselectViews]; + + _rubberStart = localPoint; + _rubberEnd = _rubberStart; + _isRubbing = YES; + [CPApp setTarget:self selector:@selector(_dragOpenSpaceWithEvent:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES]; +} + +- (void)_dragOpenSpaceWithEvent:(CPEvent)theEvent +{ + var canvas = [self canvas]; + var mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; + _rubberEnd = mouseLoc; + var rubberRect = CGRectUnion(CGRectMake(_rubberStart.x, _rubberStart.y, 1, 1), CGRectMake(_rubberEnd.x, _rubberEnd.y, 1, 1)); + + switch ([theEvent type]) + { + case CPLeftMouseDragged: + var indexesToSelect = [CPMutableIndexSet indexSet]; + var allDataObjects = [canvas dataObjects]; + + for (var i = 0; i < [[self subviews] count]; i++) { + var aView = [self subviews][i]; + if (CGRectIntersectsRect([aView frame], rubberRect)) { + var dataIndex = [allDataObjects indexOfObject:[aView dataObject]]; + if (dataIndex != CPNotFound) { + [indexesToSelect addIndex:dataIndex]; + } + } + } + [canvas setSelectionIndexes:indexesToSelect]; + [self setNeedsDisplay:YES]; + [CPApp setTarget:self selector:@selector(_dragOpenSpaceWithEvent:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES]; + break; + + case CPLeftMouseUp: + _isRubbing = NO; + [self setNeedsDisplay:YES]; + break; + } +} + +- (void)dealloc +{ + [self setDataObject:nil]; + [super dealloc]; +} + +- (void)setDataObject:(id)newDataObject +{ + var oldDataObject = [self dataObject]; + + if (newDataObject != oldDataObject) + { + if (oldDataObject) + [oldDataObject removeObserver:self forKeyPath:@"children" context:_windowChildrenObservationContext]; + + [super setDataObject:newDataObject]; + + if (newDataObject) + { + [newDataObject addObserver:self forKeyPath:@"children" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:_windowChildrenObservationContext]; + [self _addChildrenViews:[newDataObject valueForKey:@"children"]]; + } + } +} + +- (void)_addChildrenViews:(CPArray)childDataObjects +{ + if (!childDataObjects) return; + + var canvas = [self superview]; + + for (var i = 0; i < [childDataObjects count]; i++) + { + var childData = childDataObjects[i]; + // This is a bit of a hack. We are reaching into the canvas's private method. + // A better solution would be a dedicated ViewFactory or similar. + if ([canvas respondsToSelector:@selector(_createViewForDataObject:superview:)]) + [canvas _createViewForDataObject:childData superview:self]; + } +} + +- (void)_removeChildrenViews:(CPArray)childDataObjects +{ + if (!childDataObjects) return; + + var canvas = [self superview]; + var viewsToRemove = []; + var subviews = [self subviews]; + + for (var i = 0; i < [subviews count]; i++) + { + var subview = subviews[i]; + if ([childDataObjects containsObject:[subview dataObject]]) + [viewsToRemove addObject:subview]; + } + + for (i = 0; i < [viewsToRemove count]; i++) + { + if ([canvas respondsToSelector:@selector(_removeViewAndChildren:)]) + [canvas _removeViewAndChildren:viewsToRemove[i]]; + } +} + + +- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context +{ + if (context == _windowChildrenObservationContext) + { + var oldChildren = [change objectForKey:CPKeyValueChangeOldKey]; + var newChildren = [change objectForKey:CPKeyValueChangeNewKey]; + + var added = [newChildren mutableCopy]; + [added removeObjectsInArray:oldChildren]; + [self _addChildrenViews:added]; + + var removed = [oldChildren mutableCopy]; + [removed removeObjectsInArray:newChildren]; + [self _removeChildrenViews:removed]; + } + else + { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + + +- (id)initWithFrame:(CGRect)aRect +{ + self = [super initWithFrame:aRect]; + if (self) { + + if (CGRectIsEmpty(aRect)) { + [self setFrameSize:CGSizeMake(250, 200)]; + } + _isContainer = YES; + + // This view can accept drops of other elements. + [self registerForDraggedTypes:[ + UIButtonDragType, + UISliderDragType, + UITextFieldDragType + ]]; + } + return self; +} + +- (void)drawSkeleton:(CGRect)rect +{ + var bounds = [self bounds]; + var titleBarHeight = 22.0; + + // Main window background + [[[CPColor windowBackgroundColor] colorWithAlphaComponent:0.9] setFill]; + var bgPath = [CPBezierPath bezierPathWithRoundedRect:bounds radius:6.0]; + [bgPath fill]; + + // Title bar + var titleBarRect = CGRectMake(bounds.origin.x, bounds.origin.y, bounds.size.width, titleBarHeight); + var titleBarPath = [CPBezierPath bezierPathWithRoundedRect:titleBarRect xRadius:6.0 yRadius:6.0]; + [[[CPColor secondarySelectedControlColor] colorWithAlphaComponent:0.6] setFill]; + [titleBarPath fill]; + + // Window border + [[CPColor darkGrayColor] setStroke]; + [bgPath setLineWidth:1.0]; + [bgPath stroke]; + + // Value text + [_stringAttributes setObject:[CPColor whiteColor] forKey:CPForegroundColorAttributeName]; + var valueSize = [[self value] sizeWithAttributes:_stringAttributes]; + [[self value] drawAtPoint:CGPointMake((bounds.size.width - valueSize.width) / 2.0, (titleBarHeight - valueSize.height) / 2.0 - 4) withAttributes:_stringAttributes]; + [_stringAttributes setObject:[CPColor blackColor] forKey:CPForegroundColorAttributeName]; // reset color + + // Traffic light buttons + var circleRadius = 5.0; + var startX = 10.0; + var startY = titleBarHeight / 2.0; + [[CPColor redColor] setFill]; + [CPBezierPath fillRect:CGRectMake(startX, startY - circleRadius, circleRadius*2, circleRadius*2)]; + [[CPColor orangeColor] setFill]; + [CPBezierPath fillRect:CGRectMake(startX + 18, startY - circleRadius, circleRadius*2, circleRadius*2)]; + [[CPColor greenColor] setFill]; + [CPBezierPath fillRect:CGRectMake(startX + 36, startY - circleRadius, circleRadius*2, circleRadius*2)]; +} + +// --- Drag Destination Methods --- + +- (CPDragOperation)draggingEntered:(CPDraggingInfo)sender +{ + var pasteboard = [sender draggingPasteboard]; + var acceptedTypes = [self registeredDraggedTypes]; + var localPoint = [self convertPoint:[sender draggingLocation] fromView:nil]; + var titleBarHeight = 30.0; + + // Check if the dragged type is a new UI element (from the palette) + if ([acceptedTypes containsObject:UIWindowDragType] || [acceptedTypes containsObject:UIButtonDragType] || [acceptedTypes containsObject:UISliderDragType] || [acceptedTypes containsObject:UITextFieldDragType]) + { + _isDragTarget = YES; + [self setNeedsDisplay:YES]; + return CPDragOperationGeneric; + } + // Check if it's a connection drag (control key is pressed) + else if ([sender draggingSourceOperationMask] & CPControlKeyMask && localPoint.y <= titleBarHeight) + + { + debugger + _isDragTarget = YES; + [self setNeedsDisplay:YES]; + return CPDragOperationGeneric; + } + + return CPDragOperationNone; +} + +- (CPDragOperation)draggingUpdated:(CPDraggingInfo)sender +{ + var localPoint = [self convertPoint:[sender draggingLocation] fromView:nil]; + var titleBarHeight = 30.0; + var acceptedTypes = [self registeredDraggedTypes]; + + // Check if the dragged type is a new UI element (from the palette) + if ([acceptedTypes containsObject:UIButtonDragType] || [acceptedTypes containsObject:UISliderDragType] || [acceptedTypes containsObject:UITextFieldDragType]) + { + _isDragTarget = YES; + [self setNeedsDisplay:YES]; + return CPDragOperationGeneric; + } + // Check if it's a connection drag (control key is pressed) + else if ([sender draggingSourceOperationMask] & CPControlKeyMask && localPoint.y <= titleBarHeight) + { + _isDragTarget = YES; + [self setNeedsDisplay:YES]; + return CPDragOperationGeneric; + } + else + { + _isDragTarget = NO; + [self setNeedsDisplay:YES]; + return CPDragOperationNone; + } +} + +- (void)draggingExited:(CPDraggingInfo)sender +{ + _isDragTarget = NO; + [self setNeedsDisplay:YES]; +} + +- (BOOL)performDragOperation:(CPDraggingInfo)sender +{ + var dropPoint = [self convertPoint:[sender draggingLocation] fromView:nil]; + var pasteboard = [sender draggingPasteboard]; + var types = [pasteboard types]; + var draggedType = types[0]; + var elementType; + + // Determine if it's a new UI element drop + if (draggedType === UIButtonDragType) elementType = "button"; + else if (draggedType === UISliderDragType) elementType = "slider"; + else if (draggedType === UITextFieldDragType) elementType = "textfield"; + + if (elementType) + { + // We need to find the canvas and then the delegate + var canvas = [self superview]; + var delegate = [canvas delegate]; + if (delegate && [delegate respondsToSelector:@selector(addNewElementOfType:atPoint:)]) + { + var canvasPoint = [self convertPoint:dropPoint toView:canvas]; + [delegate addNewElementOfType:elementType atPoint:canvasPoint]; + } + } + // If it's a connection drag, the logic is handled in _connectWithEvent: in UIElementView + + _isDragTarget = NO; + [self setNeedsDisplay:YES]; + + return YES; +} + +- (id)nativeUIElementWithMap:(CPMutableDictionary)aMap +{ + var newPlatformWindow = [[CPPlatformWindow alloc] initWithContentRect:[self frame]]; + + var styleMask = 0; + if ([[self dataObject] valueForKey:@"CPHUDBackgroundWindowMask"]) styleMask |= CPHUDBackgroundWindowMask; + if ([[self dataObject] valueForKey:@"CPTitledWindowMask"]) styleMask |= CPTitledWindowMask; + if ([[self dataObject] valueForKey:@"CPClosableWindowMask"]) styleMask |= CPClosableWindowMask; + + var theNewWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0, 0, [self frame].size.width, [self frame].size.height) styleMask:styleMask]; + [theNewWindow setPlatformWindow:newPlatformWindow]; + + if (aMap) + { + var elementID = [[self dataObject] valueForKey:@"id"]; + [aMap setObject:theNewWindow forKey:elementID]; + } + + var contentView = [theNewWindow contentView]; + var subviews = [self subviews]; + for (var i = 0; i < [subviews count]; i++) + { + var subview = subviews[i]; + var nativeSubview = [subview nativeUIElementWithMap:aMap]; + [contentView addSubview:nativeSubview]; + } + + return theNewWindow; +} + +- (BOOL)canAcceptConnectionAtPoint:(CGPoint)aPoint +{ + var titleBarHeight = 22.0; + return aPoint.y <= titleBarHeight; +} + +@end + + +// ================================================================================================= +// UIButtonView +// A skeleton that looks like a push button. +// ================================================================================================= +@implementation UIButtonView : UIElementView + ++ (CPDictionary)defaultValues +{ + return {value: "Button", outlets: "target, delegate", actions: "takeValueFrom:"}; +} + ++ (CPDictionary)propertyTypes +{ + return [super propertyTypes].copy({value: UIBString}); +} +- (id)initWithFrame:(CGRect)aRect +{ + self = [super initWithFrame:aRect]; + if (self) { + if (CGRectIsEmpty(aRect)) { + [self setFrameSize:CGSizeMake(100, 24)]; + } + } + return self; +} + +- (void)drawSkeleton:(CGRect)rect +{ + var bounds = CGRectInset([self bounds], 1, 1); + + // Draw button shape with gradient + var buttonPath = [CPBezierPath bezierPathWithRoundedRect:bounds radius:5.0]; + var gradient = [[CPGradient alloc] initWithStartingColor:[CPColor whiteColor] + endingColor:[CPColor controlColor]]; + [gradient drawInBezierPath:buttonPath angle:90]; + + // Draw button border + [[CPColor grayColor] setStroke]; + [buttonPath setLineWidth:1.0]; + [buttonPath stroke]; + + // Draw value + var valueSize = [[self value] sizeWithAttributes:_stringAttributes]; + [[self value] drawAtPoint:CGPointMake((bounds.size.width - valueSize.width) / 2.0 + 1, (bounds.size.height - valueSize.height) / 2.0 - 2) withAttributes:_stringAttributes]; +} + +- (id)nativeUIElementWithMap:(CPMutableDictionary)aMap +{ + var button = [[CPButton alloc] initWithFrame:[self frame]]; + [button setTitle:[self value]]; + + if (aMap) + { + var elementID = [[self dataObject] valueForKey:@"id"]; + [aMap setObject:button forKey:elementID]; + } + + return button; +} + +@end + +// ================================================================================================= +// UISliderView +// A skeleton that looks like a slider. +// ================================================================================================= +@implementation UISliderView : UIElementView + ++ (CPDictionary)defaultValues +{ + return {value: 0.5, outlets: "target, delegate", actions: "takeFloatValueFrom:, takeIntegerValueFrom:"}; +} + ++ (CPDictionary)propertyTypes +{ + return [super propertyTypes].copy({value: UIBNumber}); +} +- (id)initWithFrame:(CGRect)aRect +{ + self = [super initWithFrame:aRect]; + if (self) { + if (CGRectIsEmpty(aRect)) { + [self setFrameSize:CGSizeMake(150, 20)]; + } + } + return self; +} + +- (void)drawSkeleton:(CGRect)rect +{ + var bounds = CGRectInset([self bounds], 8, 0); + var midY = bounds.size.height / 2.0; + + // Draw track + [[CPColor grayColor] setStroke]; + var trackPath = [CPBezierPath bezierPath]; + [trackPath setLineWidth:3.0]; + [trackPath moveToPoint:CGPointMake(bounds.origin.x, midY)]; + [trackPath lineToPoint:CGPointMake(bounds.origin.x + bounds.size.width, midY)]; + [trackPath stroke]; + + // Draw knob + var knobX = bounds.origin.x + bounds.size.width * [self value]; + var knobRect = CGRectMake(knobX - 8, midY - 8, 16, 16); + var knobPath = [CPBezierPath bezierPathWithOvalInRect:knobRect]; + [[CPColor whiteColor] setFill]; + [knobPath fill]; + [[CPColor darkGrayColor] setStroke]; + [knobPath setLineWidth:1.0]; + [knobPath stroke]; +} + +- (id)nativeUIElementWithMap:(CPMutableDictionary)aMap +{ + var slider = [[CPSlider alloc] initWithFrame:[self frame]]; + [slider setFloatValue:[self value]]; + + if (aMap) + { + var elementID = [[self dataObject] valueForKey:@"id"]; + [aMap setObject:slider forKey:elementID]; + } + + return slider; +} + +@end + +// ================================================================================================= +// UITextFieldView +// A skeleton that looks like a text field. +// ================================================================================================= +@implementation UITextFieldView : UIElementView + ++ (CPDictionary)defaultValues +{ + return {value: "Text Field", outlets: "target, delegate", actions: "takeStringValueFrom:, takeIntegerValueFrom:"}; +} + ++ (CPDictionary)propertyTypes +{ + return [super propertyTypes].copy({value: UIBString}); +} +- (id)initWithFrame:(CGRect)aRect +{ + self = [super initWithFrame:aRect]; + if (self) + { + if (CGRectIsEmpty(aRect)) + { + [self setFrameSize:CGSizeMake(150, 22)]; + } + [_stringAttributes setObject:[CPFont systemFontOfSize:12] forKey:CPFontAttributeName]; + [_stringAttributes setObject:[CPColor grayColor] forKey:CPForegroundColorAttributeName]; + } + return self; +} + +- (void)drawSkeleton:(CGRect)rect +{ + var bounds = CGRectInset([self bounds], 1, 1); + + // Background + [[CPColor textBackgroundColor] setFill]; + [CPBezierPath fillRect:bounds]; + + // Inset border + [[CPColor grayColor] setStroke]; + [CPBezierPath strokeRect:bounds]; + + // Draw placeholder value + var valueSize = [[self value] sizeWithAttributes:_stringAttributes]; + [[self value] drawAtPoint:CGPointMake(5, (bounds.size.height - valueSize.height) / 2.0 - 2) withAttributes:_stringAttributes]; +} + +- (id)nativeUIElementWithMap:(CPMutableDictionary)aMap +{ + var textField = [[CPTextField alloc] initWithFrame:[self frame]]; + [textField setStringValue:[self value]]; + + if (aMap) + { + var elementID = [[self dataObject] valueForKey:@"id"]; + [aMap setObject:textField forKey:elementID]; + } + + return textField; +} + +@end diff --git a/Tests/Manual/UIBuilderDemo/index.html b/Tests/Manual/UIBuilderDemo/index.html new file mode 100644 index 000000000..cea729b96 --- /dev/null +++ b/Tests/Manual/UIBuilderDemo/index.html @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + UIBuilderDemo + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/UIBuilderDemo/main.j b/Tests/Manual/UIBuilderDemo/main.j new file mode 100755 index 000000000..993c34a35 --- /dev/null +++ b/Tests/Manual/UIBuilderDemo/main.j @@ -0,0 +1,10 @@ +@import +@import + +@import "UIElementView.j" +@import "AppController.j" + +function main(args, namedArgs) +{ + CPApplicationMain(); +} From d9f31e8543562379828678c282c619210b306259 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 15 Jul 2025 18:55:23 +0200 Subject: [PATCH 028/103] Revert "new: UIBuilder demo application" This reverts commit 38f523b3c278aa98e986c59ad2e9312c7ffb5a5a. --- Tests/Manual/UIBuilderDemo/AppController.j | 422 ------ Tests/Manual/UIBuilderDemo/ConnectionView.j | 97 -- Tests/Manual/UIBuilderDemo/Info.plist | 14 - .../UIBuilderDemo/InspectorController.j | 215 --- .../UIBuilderDemo/Resources/spinner.gif | Bin 1849 -> 0 bytes .../Manual/UIBuilderDemo/UIBuilderConstants.j | 10 - .../UIBuilderDemo/UIBuilderController.j | 645 -------- Tests/Manual/UIBuilderDemo/UICanvasView.j | 996 ------------ Tests/Manual/UIBuilderDemo/UIElementView.j | 1349 ----------------- Tests/Manual/UIBuilderDemo/index.html | 164 -- Tests/Manual/UIBuilderDemo/main.j | 10 - 11 files changed, 3922 deletions(-) delete mode 100644 Tests/Manual/UIBuilderDemo/AppController.j delete mode 100644 Tests/Manual/UIBuilderDemo/ConnectionView.j delete mode 100755 Tests/Manual/UIBuilderDemo/Info.plist delete mode 100644 Tests/Manual/UIBuilderDemo/InspectorController.j delete mode 100755 Tests/Manual/UIBuilderDemo/Resources/spinner.gif delete mode 100644 Tests/Manual/UIBuilderDemo/UIBuilderConstants.j delete mode 100644 Tests/Manual/UIBuilderDemo/UIBuilderController.j delete mode 100644 Tests/Manual/UIBuilderDemo/UICanvasView.j delete mode 100644 Tests/Manual/UIBuilderDemo/UIElementView.j delete mode 100644 Tests/Manual/UIBuilderDemo/index.html delete mode 100755 Tests/Manual/UIBuilderDemo/main.j diff --git a/Tests/Manual/UIBuilderDemo/AppController.j b/Tests/Manual/UIBuilderDemo/AppController.j deleted file mode 100644 index 6d9fa30a7..000000000 --- a/Tests/Manual/UIBuilderDemo/AppController.j +++ /dev/null @@ -1,422 +0,0 @@ -// -// AppController.j -// Main application controller. Sets up the window, canvas, palette, -// and controllers on launch. -// - -@import -@import "UIBuilderController.j" -@import "UICanvasView.j" -@import "UIElementView.j"; -@import "InspectorController.j"; - -@implementation CPColor (StandardColors) - -// A standard light gray for control backgrounds, like buttons. -+ (CPColor)controlColor -{ - return [CPColor colorWithCalibratedWhite:0.9 alpha:1.0]; -} - -// A medium gray for shadows or borders. -+ (CPColor)controlShadowColor -{ - return [CPColor grayColor]; -} - -// A dark gray for text on light controls. -+ (CPColor)controlDarkShadowColor -{ - return [CPColor darkGrayColor]; -} - -// The primary color for selected items. -+ (CPColor)selectedControlColor -{ - // Corresponds to the default blue selection color in macOS. - return [CPColor colorWithCalibratedRed:0.0 green:0.478 blue:1.0 alpha:1.0]; -} - -// A secondary selection color, often used for inactive windows or rubber-band selections. -+ (CPColor)alternateSelectedControlColor -{ - return [CPColor colorWithCalibratedRed:0.2 green:0.5 blue:0.9 alpha:1.0]; -} - -// The color for an inactive or secondary selection, like a window title bar. -+ (CPColor)secondarySelectedControlColor -{ - return [CPColor lightGrayColor]; -} - -// The highlight color for an element that has keyboard focus. -+ (CPColor)keyboardFocusIndicatorColor -{ - return [CPColor colorWithCalibratedRed:0.3 green:0.6 blue:1.0 alpha:1.0]; -} - -// The standard background color for a window's content area. -+ (CPColor)windowBackgroundColor -{ - return [CPColor colorWithCalibratedWhite:0.93 alpha:1.0]; -} - -// The background color for text-editing views. -+ (CPColor)textBackgroundColor -{ - return [CPColor whiteColor]; -} - -@end - -// Required additions from original EFView.j for graphics and text handling -@implementation CPString(SizingAddition) -- (CPSize)sizeWithAttributes:(CPDictionary)stringAttributes -{ - var font = [stringAttributes objectForKey:CPFontAttributeName] || [CPFont systemFontOfSize:12]; - // This is a simplified implementation. For more complex text, you might need a more robust solution. - var ctx = [[CPGraphicsContext currentContext] graphicsPort]; - var oldFont = ctx.font; - ctx.font = [font cssString]; - var metrics = ctx.measureText(self); - ctx.font = oldFont; - return CGSizeMake(metrics.width, [[font fontDescriptor] pointSize]); -} -- (void)drawAtPoint:(CGPoint)aPoint withAttributes:(CPDictionary)attributes -{ - var ctx = [[CPGraphicsContext currentContext] graphicsPort]; - var font = [attributes objectForKey:CPFontAttributeName] || [CPFont systemFontOfSize:12]; - var color = [attributes objectForKey:CPForegroundColorAttributeName] || [CPColor blackColor]; - - ctx.font = [font cssString]; - [color setFill]; - ctx.fillText(self, aPoint.x, aPoint.y + [[font fontDescriptor] pointSize]); -} -@end - -@implementation CPBezierPath(RoundedRectangle) -+ (CPBezierPath)bezierPathWithRoundedRect:(CPRect)aRect radius:(float)radius -{ - return [self bezierPathWithRoundedRect:aRect xRadius:radius yRadius:radius]; -} -@end - - -// A simple draggable symbol for the palette -@implementation DraggableSymbolView : CPView -{ - CPString _dragType; -} - -- (void)setDragType:(CPString)aType -{ - _dragType = aType; -} --(BOOL)acceptsFirstMouse:(CPEvent)aEvent -{ - return YES; -} - -- (void)mouseDown:(CPEvent)theEvent -{ - // 1. Create a placeholder view that is a visual copy of this one. - var dragPlaceholder = [[DraggableSymbolView alloc] initWithFrame:[self bounds]]; - [dragPlaceholder setDragType:_dragType]; // Ensure it can draw its title correctly - [dragPlaceholder setAlphaValue:0.75]; // Make it semi-transparent for good UX - - var pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard]; - [pasteboard declareTypes:[_dragType] owner:nil]; - [pasteboard setString:@"1" forType:_dragType]; - - [self dragView:dragPlaceholder - at:[self bounds].origin - offset:nil - event:theEvent - pasteboard:pasteboard - source:self - slideBack:YES]; -} - -// The drawRect: method defines what the view looks like, and therefore -// what the dragged placeholder view will look like. -- (void)drawRect:(CGRect)rect -{ - var bounds = [self bounds]; - - // Background - [[CPColor controlColor] set]; - [CPBezierPath fillRect:bounds]; - [[CPColor controlShadowColor] set]; - [CPBezierPath strokeRect:bounds]; - - if ([_dragType isEqualToString:UIWindowDragType]) - { - // Draw a window - var windowRect = CGRectInset(bounds, 5, 5); - var titleBarHeight = 10; - - // Draw the title bar - var titleBarRect = CGRectMake(windowRect.origin.x, windowRect.origin.y, windowRect.size.width, titleBarHeight); - [[CPColor grayColor] set]; - [CPBezierPath fillRect:titleBarRect]; - - // Draw the content area - var contentRect = CGRectMake(windowRect.origin.x, windowRect.origin.y + titleBarHeight, windowRect.size.width, windowRect.size.height - titleBarHeight); - [[CPColor whiteColor] set]; - [CPBezierPath fillRect:contentRect]; - - // Draw the border for the whole window - [[CPColor blackColor] set]; - [CPBezierPath strokeRect:windowRect]; - } - else if ([_dragType isEqualToString:UIButtonDragType]) - { - // Draw a button - var buttonRect = CGRectInset(bounds, 8, 10); - var path = [CPBezierPath bezierPathWithRoundedRect:buttonRect radius:5]; - [[CPColor whiteColor] set]; - [path fill]; - [[CPColor blackColor] set]; - [path stroke]; - } - else if ([_dragType isEqualToString:UISliderDragType]) - { - // Draw a slider - var sliderY = bounds.size.height / 2; - var path = [CPBezierPath bezierPath]; - [path moveToPoint:CGPointMake(bounds.origin.x + 5, sliderY)]; - [path lineToPoint:CGPointMake(bounds.origin.x + bounds.size.width - 5, sliderY)]; - [[CPColor blackColor] set]; - [path stroke]; - - var knobRect = CGRectMake(bounds.size.width / 2 - 5, sliderY - 5, 10, 10); - var knobPath = [CPBezierPath bezierPathWithOvalInRect:knobRect]; - [[CPColor whiteColor] set]; - [knobPath fill]; - [[CPColor blackColor] set]; - [knobPath stroke]; - } - else if ([_dragType isEqualToString:UITextFieldDragType]) - { - // Draw a text field - var fieldRect = CGRectInset(bounds, 5, 12); - [[CPColor whiteColor] set]; - [CPBezierPath fillRect:fieldRect]; - [[CPColor blackColor] set]; - [CPBezierPath strokeRect:fieldRect]; - - // Draw an I-beam cursor - var ibeamX = CGRectGetMidX(fieldRect); - var ibeamY1 = CGRectGetMinY(fieldRect) + 3; - var ibeamY2 = CGRectGetMaxY(fieldRect) - 3; - - var ibeamPath = [CPBezierPath bezierPath]; - [ibeamPath moveToPoint:CGPointMake(ibeamX, ibeamY1)]; - [ibeamPath lineToPoint:CGPointMake(ibeamX, ibeamY2)]; - [ibeamPath moveToPoint:CGPointMake(ibeamX - 2, ibeamY1)]; - [ibeamPath lineToPoint:CGPointMake(ibeamX + 2, ibeamY1)]; - [ibeamPath moveToPoint:CGPointMake(ibeamX - 2, ibeamY2)]; - [ibeamPath lineToPoint:CGPointMake(ibeamX + 2, ibeamY2)]; - - [ibeamPath setLineWidth:0.5]; - [[CPColor blackColor] set]; - [ibeamPath stroke]; - } - else - { - // Fallback to original text drawing - var title = [[_dragType componentsSeparatedByString:@"DragType"] objectAtIndex:0]; - var textAttributes = @{ - CPFontAttributeName: [CPFont systemFontOfSize:10], - CPForegroundColorAttributeName: [CPColor blackColor] - }; - var titleSize = [title sizeWithAttributes:textAttributes]; - var titlePoint = CGPointMake( - (bounds.size.width - titleSize.width) / 2.0, - (bounds.size.height - titleSize.height) / 2.0 - ); - [title drawAtPoint:titlePoint withAttributes:textAttributes]; - } -} - -@end - -@implementation AppController : CPObject -{ - CPWindow _window; - CPPanel _palette; - UIBuilderController _builderController; - UICanvasView _canvasView; - InspectorController _inspectorController; -} - -- (void)applicationDidFinishLaunching:(CPNotification)aNotification -{ - // 1. Create the main window and canvas - _window = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask]; - [_window setTitle:@"Cappuccino UI Builder"]; - [_window setAcceptsMouseMovedEvents:YES]; - - _canvasView = [[UICanvasView alloc] initWithFrame:[[_window contentView] bounds]]; - [_canvasView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; - [[_window contentView] addSubview:_canvasView]; - - // 2. Create the controllers - _builderController = [[UIBuilderController alloc] init]; - - // 3. Wire everything together - [_canvasView setDelegate:_builderController]; - - // Bind the canvas to the controller's data model. This is the core of the architecture. - [_canvasView bind:"dataObjects" toObject:_builderController withKeyPath:@"elementsController.arrangedObjects" options:nil]; - [_canvasView bind:"selectionIndexes" toObject:_builderController withKeyPath:@"elementsController.selectionIndexes" options:nil]; - [_canvasView bind:"connections" toObject:_builderController withKeyPath:@"connectionsController.arrangedObjects" options:nil]; - [_canvasView bind:"selectedConnections" toObject:_builderController withKeyPath:@"connectionsController.selectedObjects" options:nil]; - - [self createPalette]; - [self createInspector]; - - // 5. Create the main menu - var mainMenuBar = [[CPMenu alloc] initWithTitle:@"MainMenu"]; - var editMenuItem = [[CPMenuItem alloc] initWithTitle:@"Edit" action:nil keyEquivalent:@""]; - - - var editMenu = [[CPMenu alloc] initWithTitle:@"Edit"]; - [editMenu addItemWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"]; - [editMenu addItemWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"Z"]; - [editMenu addItem:[CPMenuItem separatorItem]]; - [editMenu addItemWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"]; - [editMenu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"]; - [editMenu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"]; - [editMenu addItemWithTitle:@"Delete" action:@selector(delete:) keyEquivalent:@""]; - - [editMenuItem setSubmenu:editMenu]; - - var fileMenuItem = [[CPMenuItem alloc] initWithTitle:@"File" action:nil keyEquivalent:@""]; - var fileMenu = [[CPMenu alloc] initWithTitle:@"File"]; - [fileMenu addItemWithTitle:@"Run" action:@selector(run:) keyEquivalent:@"r"]; - [fileMenuItem setSubmenu:fileMenu]; - [mainMenuBar addItem:fileMenuItem]; - - [mainMenuBar addItem:editMenuItem]; - - [CPApp setMainMenu:mainMenuBar]; - [CPMenu setMenuBarVisible:YES]; - - [_window makeKeyAndOrderFront:self]; -} - -- (void)createPalette -{ - var screenWidth = window.innerWidth; - var paletteWidth = 220; - var paletteHeight = 60; - var paletteX = (screenWidth - paletteWidth) / 2; - var paletteY = 22; // Position near the top of the screen - - _palette = [[CPPanel alloc] initWithContentRect:CGRectMake(paletteX, paletteY, paletteWidth, paletteHeight) - styleMask:CPHUDBackgroundWindowMask | CPTitledWindowMask | CPClosableWindowMask]; - [_palette setTitle:@"Elements"]; - [_palette setFloatingPanel:YES]; - - var xPos = 10; - var types = [UIWindowDragType, UIButtonDragType, UISliderDragType, UITextFieldDragType]; - - // Create draggable symbols for each type - [_canvasView registerForDraggedTypes:types]; - - for (var i=0; i < [types count]; i++) { - var symbol = [[DraggableSymbolView alloc] initWithFrame:CGRectMake(xPos, 10, 40, 40)]; - symbol._dragType = types[i]; - - [[_palette contentView] addSubview:symbol]; - xPos += 50; - } - - [_palette orderFront:self]; -} - -- (void)createInspector -{ - var inspectorPanel = [[CPPanel alloc] initWithContentRect:CGRectMake(20, 200, 300, 150) - styleMask:CPTitledWindowMask | CPClosableWindowMask]; - [inspectorPanel setTitle:@"Inspector"]; - [inspectorPanel setFloatingPanel:YES]; - - var contentView = [inspectorPanel contentView]; - - _inspectorController = [[InspectorController alloc] init]; - [_inspectorController setBuilderController:_builderController]; - [_inspectorController setPanel:inspectorPanel]; - [_inspectorController setView:contentView]; - - [_inspectorController awakeFromMarkup]; // Manually call this - - [inspectorPanel orderFront:self]; -} - -- (void)run:(id)sender -{ - console.log("Run: Starting native UI generation..."); - var canvasSubviews = [_canvasView subviews]; - var nativeElementMap = [CPMutableDictionary dictionary]; - - // First pass: create all native elements and map them by their ID - console.log("Run: Creating native elements and building map..."); - for (var i = 0; i < [canvasSubviews count]; i++) - { - var view = [canvasSubviews objectAtIndex:i]; - if ([view isKindOfClass:[UIElementView class]]) - { - // This will now recursively build the map - [view nativeUIElementWithMap:nativeElementMap]; - } - } - - // Second pass: connect the native elements - console.log("Run: Processing connections..."); - var connections = [[_builderController connectionsController] content]; - for (var i = 0; i < [connections count]; i++) - { - var connection = [connections objectAtIndex:i]; - var sourceID = [connection valueForKey:@"sourceID"]; - var targetID = [connection valueForKey:@"targetID"]; - var action = [connection valueForKey:@"action"]; - - console.log(" - Connecting: " + sourceID + " -> " + targetID + " (Action: " + action + ")"); - - var nativeSource = [nativeElementMap objectForKey:sourceID]; - var nativeTarget = [nativeElementMap objectForKey:targetID]; - - if (nativeSource && nativeTarget && action) - { - console.log(" - Found native source and target. Applying connection."); - [nativeSource setTarget:nativeTarget]; - [nativeSource setAction:CPSelectorFromString(action)]; - } - else - { - console.log(" - WARNING: Could not find native source or target for connection."); - } - } - - // Third pass: show the windows - console.log("Run: Showing windows..."); - for (var i = 0; i < [canvasSubviews count]; i++) - { - var view = [canvasSubviews objectAtIndex:i]; - if ([view isKindOfClass:[UIWindowView class]]) - { - var elementID = [[view dataObject] valueForKey:@"id"]; - var nativeWindow = [nativeElementMap objectForKey:elementID]; - if (nativeWindow) - { - console.log(" - Showing window for ID: " + elementID); - [nativeWindow makeKeyAndOrderFront:self]; - } - } - } - console.log("Run: Finished."); -} - -@end diff --git a/Tests/Manual/UIBuilderDemo/ConnectionView.j b/Tests/Manual/UIBuilderDemo/ConnectionView.j deleted file mode 100644 index d77154cfb..000000000 --- a/Tests/Manual/UIBuilderDemo/ConnectionView.j +++ /dev/null @@ -1,97 +0,0 @@ - -@import - -function treshold(value, limit) -{ - return value > 0 ? Math.min(value, limit) : Math.max(value, -limit); -} - -@implementation ConnectionView : CPView -{ - CGPoint _startPoint; - CGPoint _endPoint; - CPColor _color; -} - -- (id)initWithFrame:(CGRect)aRect -{ - self = [super initWithFrame:aRect]; - if (self) - { - [self setBackgroundColor:[CPColor clearColor]]; - _color = [CPColor redColor]; - [self setHidden:YES]; // Hidden by default - } - return self; -} - -- (void)setStartPoint:(CGPoint)startPoint { _startPoint = startPoint; } -- (void)setEndPoint:(CGPoint)endPoint { _endPoint = endPoint; } -- (void)setColor:(CPColor)color { _color = color; } - -- (void)drawRect:(CGRect)rect -{ - console.log("ConnectionView: drawRect - Drawing connection from ", _startPoint, " to ", _endPoint); - if (_startPoint && _endPoint) - { - [self drawLinkFrom:_startPoint to:_endPoint color:_color]; - } -} - -- (void)drawLinkFrom:(CGPoint)startPoint to:(CGPoint)endPoint color:(CPColor)insideColor -{ - var dist = Math.sqrt(Math.pow(startPoint.x - endPoint.x, 2) + Math.pow(startPoint.y - endPoint.y, 2)); - var p0 = CGPointMake(startPoint.x, startPoint.y); - var p3 = CGPointMake(endPoint.x, endPoint.y); - var p1 = CGPointMake(startPoint.x + treshold((endPoint.x - startPoint.x) / 2, 50), startPoint.y); - var p2 = CGPointMake(endPoint.x - treshold((endPoint.x - startPoint.x) / 2, 50), endPoint.y); - var path = [CPBezierPath bezierPath]; - [path setLineWidth:0]; - [[CPColor grayColor] set]; - [path appendBezierPathWithOvalInRect:CGRectMake(startPoint.x-2.5,startPoint.y-2.5,5,5)]; - [path fill]; - path = [CPBezierPath bezierPath]; - [path setLineWidth:0]; - [insideColor set]; - [path appendBezierPathWithOvalInRect:CGRectMake(startPoint.x-1.5,startPoint.y-1.5,3,3)]; - [path fill]; - path = [CPBezierPath bezierPath]; - [path setLineWidth:0]; - [[CPColor grayColor] set]; - [path appendBezierPathWithOvalInRect:CGRectMake(endPoint.x-2.5,endPoint.y-2.5,5,5)]; - [path fill]; - path = [CPBezierPath bezierPath]; - [path setLineWidth:0]; - [insideColor set]; - [path appendBezierPathWithOvalInRect:CGRectMake(endPoint.x-1.5,endPoint.y-1.5,3,3)]; - [path fill]; - if (dist < 40) - { - path = [CPBezierPath bezierPath]; - [path setLineWidth:5]; - [path moveToPoint:startPoint]; - [path lineToPoint:endPoint]; - [[CPColor grayColor] set]; - [path stroke]; - path = [CPBezierPath bezierPath]; - [path setLineWidth:3]; - [path moveToPoint:startPoint]; - [path lineToPoint:endPoint]; - [insideColor set]; - [path stroke]; - return; - } - path = [CPBezierPath bezierPath]; - [path setLineWidth:5]; - [path moveToPoint:p0]; - [path curveToPoint:p3 controlPoint1:p1 controlPoint2:p2]; - [[CPColor grayColor] set]; - [path stroke]; - path = [CPBezierPath bezierPath]; - [path setLineWidth:3]; - [path moveToPoint:p0]; - [path curveToPoint:p3 controlPoint1:p1 controlPoint2:p2]; - [insideColor set]; - [path stroke]; -} -@end diff --git a/Tests/Manual/UIBuilderDemo/Info.plist b/Tests/Manual/UIBuilderDemo/Info.plist deleted file mode 100755 index 8bbc29491..000000000 --- a/Tests/Manual/UIBuilderDemo/Info.plist +++ /dev/null @@ -1,14 +0,0 @@ - - - - - CPApplicationDelegateClass - AppController - CPBundleName - UIBuilderDemo - CPPrincipalClass - CPApplication - CPDefaultTheme - Aristo2 - - diff --git a/Tests/Manual/UIBuilderDemo/InspectorController.j b/Tests/Manual/UIBuilderDemo/InspectorController.j deleted file mode 100644 index dd3d0af2f..000000000 --- a/Tests/Manual/UIBuilderDemo/InspectorController.j +++ /dev/null @@ -1,215 +0,0 @@ -@import - -@class UIBuilderController; - -@implementation InspectorController : CPViewController -{ - UIBuilderController _builderController @accessors(property=builderController); - CPPanel _panel @accessors(property=panel); - CPTableView _connectionsTableView; -} - -- (void)awakeFromMarkup -{ - [_builderController addObserver:self forKeyPath:@"elementsController.selectionIndexes" options:CPKeyValueObservingOptionNew context:nil]; - - // Create Tab View - var tabView = [[CPTabView alloc] initWithFrame:[[_panel contentView] bounds]]; - [tabView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; - [tabView setDelegate:self]; - - // Properties Tab - var propertiesView = [[CPView alloc] initWithFrame:CGRectMakeZero()]; - var propertiesTabItem = [[CPTabViewItem alloc] initWithIdentifier:@"properties"]; - [propertiesTabItem setLabel:@"Properties"]; - [propertiesTabItem setView:propertiesView]; - [tabView addTabViewItem:propertiesTabItem]; - - // Connections Tab - var connectionsView = [[CPView alloc] initWithFrame:CGRectMakeZero()]; - var connectionsTabItem = [[CPTabViewItem alloc] initWithIdentifier:@"connections"]; - [connectionsTabItem setLabel:@"Connections"]; - [connectionsTabItem setView:connectionsView]; - [tabView addTabViewItem:connectionsTabItem]; - - // Connections TableView - _connectionsTableView = [[CPTableView alloc] initWithFrame:[connectionsView bounds]]; - [_connectionsTableView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; - - var columns = [ - {identifier: "outlet", title: "Outlet", width: 80}, - {identifier: "action", title: "Action", width: 120} - ]; - - // Keep a reference to the array controller - var connectionsController = [_builderController connectionsController]; - - for (var i = 0; i < [columns count]; i++) { - var colInfo = columns[i]; - var column = [[CPTableColumn alloc] initWithIdentifier:colInfo.identifier]; - [[column headerView] setStringValue:colInfo.title]; - [column setWidth:colInfo.width]; - [_connectionsTableView addTableColumn:column]; - // Bind the value of each column to the corresponding key path on the arranged objects - [column bind:CPValueBinding toObject:connectionsController withKeyPath:("arrangedObjects." + colInfo.identifier) options:nil]; - } - - // Bind the table's selection to the array controller's selection - [_connectionsTableView bind:@"selectionIndexes" toObject:connectionsController withKeyPath:@"selectionIndexes" options:nil]; - - var connectionsViewBounds = [connectionsView bounds]; - var buttonBarHeight = 28; - var tableHeight = connectionsViewBounds.size.height - buttonBarHeight; - - var scrollViewFrame = CGRectMake(3, 3, connectionsViewBounds.size.width - 6, tableHeight - 6); - var buttonBarFrame = CGRectMake(0, tableHeight, connectionsViewBounds.size.width, buttonBarHeight); - - var scrollView = [[CPScrollView alloc] initWithFrame:scrollViewFrame]; - [scrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; - [scrollView setDocumentView:_connectionsTableView]; - [connectionsView addSubview:scrollView]; - - var buttonBar = [[CPView alloc] initWithFrame:buttonBarFrame]; - [buttonBar setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin]; // Stick to bottom - [connectionsView addSubview:buttonBar]; - - var deleteButton = [CPButtonBar minusButton]; - [deleteButton setAction:@selector(deleteSelectedConnection:)]; - [deleteButton setTarget:self]; - [buttonBar addSubview:deleteButton]; - - // Replace panel's content view with the tab view - [_panel setContentView:tabView]; - - [self updateInspector]; -} - -- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aTableColumn row:(int)aRow -{ - var connection = [[[_builderController connectionsController] arrangedObjects] objectAtIndex:aRow]; - var identifier = [aTableColumn identifier]; - - return [connection valueForKey:identifier]; -} - -- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context -{ - // We only observe selection changes now. - if (keyPath === @"elementsController.selectionIndexes") - { - [self updateInspector]; - [self _updateConnectionVisibility]; - } -} - -- (void)_updateConnectionVisibility -{ - var tabView = [[self panel] contentView]; - if (![tabView isKindOfClass:[CPTabView class]]) - return; - - var selectedTabViewItem = [tabView selectedTabViewItem]; - var connectionsController = [_builderController connectionsController]; - var selectedObjects = [[_builderController elementsController] selectedObjects]; - - // 1. Filter the connections based on the selected UI element. - if ([selectedObjects count] === 1) - { - var selectedID = [[selectedObjects objectAtIndex:0] valueForKey:@"id"]; - var predicate = [CPPredicate predicateWithFormat:@"sourceID == %@ OR targetID == %@", selectedID, selectedID]; - [connectionsController setFilterPredicate:predicate]; - } - else - { - [connectionsController setFilterPredicate:[CPPredicate predicateWithFormat:@"FALSEPREDICATE"]]; - } -} - -- (void)tabView:(CPTabView)aTabView didSelectTabViewItem:(CPTabViewItem)aTabViewItem -{ - [self _updateConnectionVisibility]; -} - -- (void)deleteSelectedConnection:(id)sender -{ - var selectedObjects = [[_builderController connectionsController] selectedObjects]; - if ([selectedObjects count] > 0) - [[_builderController connectionsController] removeObjects:selectedObjects]; -} - -- (void)updateInspector -{ - var selectedObjects = [[_builderController elementsController] selectedObjects]; - var propertiesView = [[[_panel contentView] tabViewItemAtIndex:0] view]; - - // Clear existing views from properties tab - var subviews = [propertiesView subviews]; - for (var i = [subviews count] - 1; i >= 0; i--) { - [subviews[i] removeFromSuperview]; - } - - if ([selectedObjects count] === 1) - { - var selectedObject = selectedObjects[0]; - var elementType = [selectedObject valueForKey:@"type"]; - var viewClass = [UIBuilderController classForElementType:elementType]; - var properties = [viewClass persistentProperties]; - - var yPos = 10; - - // Set panel title - [_panel setTitle:elementType]; - - for (var i = 0; i < [properties count]; i++) - { - var propertyName = properties[i]; - var value = [selectedObject valueForKey:propertyName]; - var propertyType = [[viewClass propertyTypes] valueForKey:propertyName]; - - // Create Label - var label = [[CPTextField alloc] initWithFrame:CGRectMake(10, yPos + 3, 100, 20)]; - [label setStringValue:propertyName]; - [label setBezeled:NO]; - [label setDrawsBackground:NO]; - [label setEditable:NO]; - [propertiesView addSubview:label]; - [label setTextColor:[CPColor grayColor]]; - - // Create Control based on property type - if (propertyType === UIBBoolean) { - var checkbox = [[CPCheckBox alloc] initWithFrame:CGRectMake(120, yPos, 100, 20)]; - [checkbox setTitle:@""]; - [checkbox bind:@"value" toObject:selectedObject withKeyPath:propertyName options:nil]; - [propertiesView addSubview:checkbox]; - } else if (propertyType === UIBString || propertyType === UIBNumber) { - var textField = [[CPTextField alloc] initWithFrame:CGRectMake(120, yPos, 150, 27)]; - [textField bind:@"value" toObject:selectedObject withKeyPath:propertyName options:nil]; - [textField setBezeled:YES]; - [textField setEditable:YES]; - [propertiesView addSubview:textField]; - } else { // Fallback for unknown types - var textField = [[CPTextField alloc] initWithFrame:CGRectMake(120, yPos, 150, 25)]; - [textField bind:@"value" toObject:selectedObject withKeyPath:propertyName options:nil]; - [textField setBezeled:YES]; - [textField setEditable:YES]; - [propertiesView addSubview:textField]; - } - - yPos += 30; - } - - [[self panel] orderFront:self]; - } - else - { - [[self panel] orderOut:self]; - } -} - -- (void)dealloc -{ - [_builderController removeObserver:self forKeyPath:@"elementsController.selectionIndexes"]; - [super dealloc]; -} - -@end diff --git a/Tests/Manual/UIBuilderDemo/Resources/spinner.gif b/Tests/Manual/UIBuilderDemo/Resources/spinner.gif deleted file mode 100755 index 06dbc2bc21dddcf0e09b566d5b211aee89570f52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1849 zcma*odr(tX9tZHtz31lM+(&Y`A`Ou`NeG&R#DrIfV%?hng21vs6v>ooRQQFbk7E_K!V( z{?5!fpZWgIZ%*d6t%i*j24bL}AZUJm9)h6R*;%L4IWse3G#VWaN1#$zSSXcBhlYlH zJ|D}n{r&w2f+Qp)SS*&n?*G4}{}!h?td_mjU6Kad-rW*QwWYCUk7d^e+sjpZAn6kT z5rM;`{~_}-wm+L@%+E;JphLm}C3WzQAQD0wBoY|rOTQX{(@pp-WAA8Iw0HKl|15j) zhGh(SFWu#5zUCw^T!}C~eC}~?{xG-r7ugQC>taC+|L^xr&a%{#cgUOZXIM&v?)k^2 z1i5%0!U00wq)r4vlAwXBA_m5J5WNeuBm-XJQ5pG4GD%vc|K){+TJ{AkHqN@SJKczX zU}QRd9n*_bf^h94)6Ctv{CUfz_;qs#RDuQ$DR%y1&5G-a6rhf*4Y zl+UOD1*GE{GQ7vv;c-tTIMkz0ovO>9j%b5hpa@a*CBJ#W!0@ndSwF|%hMkdFtXiU) z`BEp+yhkt8ZY>tg&uyH@%^%K4jDrbA;et&vnnzZ3z22@e6<27z2Vk13kb7OtISyxv z(XoO-LNQCZe3lfzc%*pNqU<9T+p~teThOEB+~e&U0mC|zS{Q)dJZ)!^1f7L(zg*L> z*vSW%?dR~@@0(}x6-V^A`~ImD;)NaBUnP8;RH=bS2@pk`wp>>8r&jGjmGPy%1BW}v zoPo}ca$~bze@efc3kapuEVW1!%teOZT3j2Tw5=hjiAf02d}7dL0oFC%@=RXp5Ow#% z@a>+AM|YWt$n&e`>sB-32gBcuTi>R>*|8@lp`y6thc7ydyqDsuUn~YzZf|{-R@-3y zL#wx{Ii}xxL_csiW*LBn0-A$>zp4WOmkf6=ilrjlW?-l9+bCd7|i?*b5NTmy<<6NZ0T8$SN@mwdC2f z6jqK=N@bS@!=YSj%>h0}*4p+%0HbTIrE$w7UMT6+AZZ&DAo*qZAAi(OtNbIfl#Dw^ zJWeiCp~zi#&t6x}m9)O^eR4HiLV3QA<<0>HZ8%$^lrSE94Wb}=+MM^!b>n#5&-JQR zkr-CEu9C;_F*7DqDisulV6Pmg$nFL0TPn%~*m^-`Z3^BgU(sNpnx%nW(!eV9A&FvI zHL3X3lw8Wji^6=8KbQDE-e%b?svdFxxdh$8r97@7$ojeI`6AL8Ob6QC$qh&>ZWVDX1F{0vJnT%%mE;GveKWR%C} zM&4d;Jf3s9|HAA)yVUPo`Aq;0do#)uHSXi5*QF*)x@MVVHr+cN)uMZ__F|&Ta#p8d z53TOKtce!PJUuie8UWol-S(`c2nH?UGqJP{!4RR4u$LCfn)q-hj0^f=h(VYy)T6eN zhRO!ja-aDBTcgeyP(8Ua4IdiOog^*CQa?R(cP#9AgL9`j>EX-6Yf1lzX(!~``M1XC zNmM<4<6d~wWZ$Xrk0K}UteTrq@LBBk#MsjkK;pbuVhe)NI7(7Pf(l?lxC7=1Z7Pzl zMbS;nV4NI5_N{1$P)&XC)huOGQ+h^zpYUZfb*1n6>!`#*b3y52LGmi+<4sY5jyD#- zw&$d}DTguLAfnRtjrM*JfqtHqUu6rQoU=g%1E9xk%;)TDm^2<8n-0IhTsQFaek)MY}>PK4;nBk=ow4pF>vzkO& gna%Org-kgQCf=+BeMi^RbuY;YE~rTjend;_cdi8t>i_@% diff --git a/Tests/Manual/UIBuilderDemo/UIBuilderConstants.j b/Tests/Manual/UIBuilderDemo/UIBuilderConstants.j deleted file mode 100644 index 1e5c5b5ec..000000000 --- a/Tests/Manual/UIBuilderDemo/UIBuilderConstants.j +++ /dev/null @@ -1,10 +0,0 @@ -// -// UIBuilderConstants.j -// Defines global constants for the UI Builder application. -// - -UIBuilderElementPboardType = "UIBuilderElementPboardType"; -UIWindowDragType = "UIWindowDragType"; -UIButtonDragType = "UIButtonDragType"; -UISliderDragType = "UISliderDragType"; -UITextFieldDragType = "UITextFieldDragType"; diff --git a/Tests/Manual/UIBuilderDemo/UIBuilderController.j b/Tests/Manual/UIBuilderDemo/UIBuilderController.j deleted file mode 100644 index cb4ba5c95..000000000 --- a/Tests/Manual/UIBuilderDemo/UIBuilderController.j +++ /dev/null @@ -1,645 +0,0 @@ -// -// UIBuilderController.j -// This is the main controller for the UI Builder application. -// It manages the data model for all elements on the canvas and acts -// as a delegate for the UICanvasView to respond to user interactions. -// -// By Daniel Boehringer in 2025. -// - -@import -@import "UIElementView.j" -@import "UICanvasView.j" -@import "UIBuilderConstants.j"; - -// This is a simple data model. In a real app, it might have more properties. -// We use a custom dictionary to ensure KVO compatibility and proper value setting. -@implementation CPConservativeDictionary : CPDictionary -{ } - -- (id)init -{ - self = [super init]; - if (self) { - // Rely on superclass to initialize _buckets - } - return self; -} - -+ (id)dictionary -{ - return [[self alloc] init]; -} - -- (void)setValue:(id)aVal forKey:(CPString)aKey -{ - // Only set the value if it's different from the current value - var currentValue = [super valueForKey:aKey]; - - - // Always set the value if the current value is null or undefined - if (currentValue == null || currentValue == undefined || currentValue != aVal) { - [super setValue:aVal forKey:aKey]; - } -} - -- (BOOL)isEqual:(id)otherObject -{ - return [self valueForKey:'id'] == [otherObject valueForKey:'id']; -} - -- (id)initWithCoder:(CPCoder)aCoder -{ - self = [super initWithCoder:aCoder]; - if (self) - { - var allKeys = [aCoder decodeObjectForKey:@"CPConservativeDictionaryKeys"]; - if (allKeys) - { - for (var i = 0; i < [allKeys count]; i++) - { - var key = allKeys[i]; - var value = [aCoder decodeObjectForKey:key]; - [self setObject:value forKey:key]; - } - } - } - return self; -} - -- (void)encodeWithCoder:(CPCoder)aCoder -{ - [super encodeWithCoder:aCoder]; - var allKeys = [self allKeys]; - [aCoder encodeObject:allKeys forKey:@"CPConservativeDictionaryKeys"]; - for (var i = 0; i < [allKeys count]; i++) - { - var key = allKeys[i]; - [aCoder encodeObject:[self objectForKey:key] forKey:key]; - } -} - -@end - - -@implementation UIBuilderController : CPViewController -{ - CPArrayController _elementsController @accessors(property=elementsController); - CPArrayController _connectionsController @accessors(property=connectionsController); - CPMutableArray _connections; - int _elementCounter; // To generate unique IDs -} - -+ (Class)classForElementType:(CPString)elementType -{ - if (elementType === "window") return UIWindowView; - if (elementType === "button") return UIButtonView; - if (elementType === "slider") return UISliderView; - if (elementType === "textfield") return UITextFieldView; - return UIElementView; -} - -- (id)init -{ - self = [super init]; - if (self) { - _elementsController = [[CPArrayController alloc] init]; - _connectionsController = [[CPArrayController alloc] init]; - _elementCounter = 0; - } - return self; -} - -#pragma mark - -#pragma mark Data Management - -- (CPDictionary)_containerDataAtPoint:(CGPoint)aPoint -{ - var allElements = [_elementsController arrangedObjects]; - for (var i = [allElements count] - 1; i >= 0; i--) - { - var elementData = allElements[i]; - var type = [elementData valueForKey:@"type"]; - if (type === "window") - { - var frame = CGRectMake([elementData valueForKey:@"originX"], [elementData valueForKey:@"originY"], [elementData valueForKey:@"width"], [elementData valueForKey:@"height"]); - if (CGRectContainsPoint(frame, aPoint)) - return elementData; - } - } - return nil; -} - -- (void)addNewElementOfType:(CPString)elementType atPoint:(CGPoint)aPoint -{ - var newElementData = [CPConservativeDictionary dictionary]; - var containerData = [self _containerDataAtPoint:aPoint]; - var viewClass = [UIBuilderController classForElementType:elementType]; - - // Set default properties based on type - [newElementData setValue:elementType forKey:@"type"]; - [newElementData setValue:@"id_" + _elementCounter++ forKey:@"id"]; - - // Set default values from the view class - var defaultValues = [viewClass defaultValues]; - for (var key in defaultValues) { - [newElementData setValue:defaultValues[key] forKey:key]; - } - - // Set default sizes - if (elementType === "window") { - [newElementData setValue:250 forKey:@"width"]; - [newElementData setValue:200 forKey:@"height"]; - [newElementData setValue:[] forKey:@"children"]; - } else if (elementType === "button") { - [newElementData setValue:100 forKey:@"width"]; - [newElementData setValue:24 forKey:@"height"]; - } else if (elementType === "slider") { - [newElementData setValue:150 forKey:@"width"]; - [newElementData setValue:20 forKey:@"height"]; - } else { // textfield - [newElementData setValue:150 forKey:@"width"]; - [newElementData setValue:22 forKey:@"height"]; - } - - // Calculate centered position - var elementWidth = [newElementData valueForKey:@"width"]; - var elementHeight = [newElementData valueForKey:@"height"]; - var centeredX = aPoint.x - (elementWidth / 2); - var centeredY = aPoint.y - (elementHeight / 2); - [newElementData setValue:centeredX forKey:@"originX"]; - [newElementData setValue:centeredY forKey:@"originY"]; - - if (containerData && elementType !== "window") - { - // Convert point to be relative to the container and center the element - var elementWidth = [newElementData valueForKey:@"width"]; - var elementHeight = [newElementData valueForKey:@"height"]; - var relativeX = (aPoint.x - [containerData valueForKey:@"originX"]) - (elementWidth / 2); - var relativeY = (aPoint.y - [containerData valueForKey:@"originY"]) - (elementHeight / 2); - [newElementData setValue:relativeX forKey:@"originX"]; - [newElementData setValue:relativeY forKey:@"originY"]; - - // Add as a child to the container - [newElementData setValue:[containerData valueForKey:@"id"] forKey:@"parentID"]; - [[containerData mutableArrayValueForKey:@"children"] addObject:newElementData]; - } - - // Add to the main controller regardless, so selection works. - [[[[CPApp keyWindow] undoManager] prepareWithInvocationTarget:_elementsController] removeObject:newElementData]; - [[[CPApp keyWindow] undoManager] setActionName:@"Add Element"]; - [_elementsController addObject:newElementData]; - - [_elementsController setSelectedObjects:[CPArray arrayWithObject:newElementData]]; -} - -- (void)removeSelectedElementsWithActionName:(CPString)actionName -{ - var selectedObjects = [[_elementsController selectedObjects] copy]; - if ([selectedObjects count] === 0) return; - - [[[[CPApp keyWindow] undoManager] prepareWithInvocationTarget:_elementsController] addObjects:selectedObjects]; - [[[CPApp keyWindow] undoManager] setActionName:actionName]; - [_elementsController removeObjects:selectedObjects]; -} - -- (void)removeSelectedElements -{ - [self removeSelectedElementsWithActionName:@"Delete"]; -} - -- (void)cut:(id)sender -{ - [self copy:sender]; - [self removeSelectedElementsWithActionName:@"Cut"]; -} - -#pragma mark - -#pragma mark Keyboard Movement - -- (void)moveSelectedElementsByDeltaX:(int)deltaX deltaY:(int)deltaY -{ - var selectedDataObjects = [_elementsController selectedObjects]; - var changes = [CPMutableArray array]; - for (var i = 0; i < [selectedDataObjects count]; i++) - { - var data = selectedDataObjects[i]; - var newFrame = { - origin: { - x: [data valueForKey:@"originX"] + deltaX, - y: [data valueForKey:@"originY"] + deltaY - } - }; - [changes addObject:{ data: data, frame: newFrame }]; - } - [self applyFrameChanges:changes withActionName:@"Move"]; -} - -- (void)moveLeft:(id)sender -{ - [self moveSelectedElementsByDeltaX:-1 deltaY:0]; -} - -- (void)moveRight:(id)sender -{ - [self moveSelectedElementsByDeltaX:1 deltaY:0]; -} - -- (void)moveUp:(id)sender -{ - [self moveSelectedElementsByDeltaX:0 deltaY:-1]; -} - -- (void)moveDown:(id)sender -{ - [self moveSelectedElementsByDeltaX:0 deltaY:1]; -} - -#pragma mark - -#pragma mark Copy & Paste - -- (void)copy:(id)sender -{ - var selectedData = [_elementsController selectedObjects]; - - if ([selectedData count] > 0) - { - var pboard = [CPPasteboard generalPasteboard]; - var data = [CPKeyedArchiver archivedDataWithRootObject:selectedData]; - - // 1. Declare that you are providing BOTH a custom type and a standard string type. - [pboard declareTypes:[UIBuilderElementPboardType, CPStringPboardType] owner:nil]; - - // 2. Set the data for your custom type, for your app's internal 'paste' to use. - [pboard setData:data forType:UIBuilderElementPboardType]; - - // 3. Set a string representation for the browser and other applications. - // This can be a simple description or a more complex JSON representation. - var description = [selectedData count] + " UI element(s) copied."; - [pboard setString:description forType:CPStringPboardType]; - } -} - -- (void)_assignNewIDsToElement:(CPMutableDictionary)elementData -{ - [elementData setValue:@"id_" + _elementCounter++ forKey:@"id"]; - - var children = [elementData valueForKey:@"children"]; - if (children) - { - var newChildren = [CPMutableArray array]; - for (var i = 0; i < [children count]; i++) - { - var child = children[i]; - // Deep copy child before modifying - var newChild = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:child]]; - [newChild setValue:[elementData valueForKey:@"id"] forKey:@"parentID"]; - [self _assignNewIDsToElement:newChild]; - [newChildren addObject:newChild]; - } - [elementData setValue:newChildren forKey:@"children"]; - } -} - -- (void)paste:(id)sender -{ - var pboard = [CPPasteboard generalPasteboard]; - var types = [pboard types]; - - if ([types containsObject:UIBuilderElementPboardType]) - { - var data = [pboard dataForType:UIBuilderElementPboardType]; - var pastedElements = [CPKeyedUnarchiver unarchiveObjectWithData:data]; - var newSelection = [CPMutableArray array]; - - // Determine the target container - var targetContainer = nil; - var selectedObjects = [_elementsController selectedObjects]; - if ([selectedObjects count] > 0) - { - var firstSelected = selectedObjects[0]; - var parentID = [firstSelected valueForKey:@"parentID"]; - if (parentID) - { - // Find the parent container in the elements controller - var allElements = [_elementsController arrangedObjects]; - for (var i = 0; i < [allElements count]; i++) - { - if ([[allElements[i] valueForKey:@"id"] isEqualToString:parentID]) - { - targetContainer = allElements[i]; - break; - } - } - } - else - { - // If the selected object has no parent, it must be a window - targetContainer = firstSelected; - } - } - else - { - // If no selection, find the first window - var allElements = [_elementsController arrangedObjects]; - for (var i = 0; i < [allElements count]; i++) - { - if ([[allElements[i] valueForKey:@"type"] isEqualToString:@"window"]) - { - targetContainer = allElements[i]; - break; - } - } - } - - for (var i = 0; i < [pastedElements count]; i++) - { - var newElement = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:pastedElements[i]]]; - - [newElement setValue:[newElement valueForKey:@"originX"] + 10 forKey:@"originX"]; - [newElement setValue:[newElement valueForKey:@"originY"] + 10 forKey:@"originY"]; - - [self _assignNewIDsToElement:newElement]; - - if (targetContainer && [newElement valueForKey:@"type"] !== @"window") - { - [newElement setValue:[targetContainer valueForKey:@"id"] forKey:@"parentID"]; - [[targetContainer mutableArrayValueForKey:@"children"] addObject:newElement]; - } - else - { - [newElement removeObjectForKey:@"parentID"]; - } - - [_elementsController addObject:newElement]; - - if ([newElement valueForKey:@"children"]) - [_elementsController addObjects:[newElement valueForKey:@"children"]]; - - [newSelection addObject:newElement]; - } - - [[[[CPApp keyWindow] undoManager] prepareWithInvocationTarget:_elementsController] removeObjects:newSelection]; - [[[CPApp keyWindow] undoManager] setActionName:@"Paste"]; - [_elementsController setSelectedObjects:newSelection]; - } -} - -- (void)addNewElementOfType:(CPString)elementType inNewWindowAtPoint:(CGPoint)aPoint -{ - // 1. Create the new element to be placed in the window - var newElementData = [CPConservativeDictionary dictionary]; - var viewClass = [UIBuilderController classForElementType:elementType]; - [newElementData setValue:elementType forKey:@"type"]; - [newElementData setValue:@"id_" + _elementCounter++ forKey:@"id"]; - - var defaultValues = [viewClass defaultValues]; - for (var key in defaultValues) - [newElementData setValue:defaultValues[key] forKey:key]; - - var elementWidth, elementHeight; - - if (elementType === "button") { - elementWidth = 100; - elementHeight = 24; - } else if (elementType === "slider") { - elementWidth = 150; - elementHeight = 20; - } else { // textfield - elementWidth = 150; - elementHeight = 22; - } - [newElementData setValue:elementWidth forKey:@"width"]; - [newElementData setValue:elementHeight forKey:@"height"]; - - // 2. Create the window that will contain the new element - var windowData = [CPConservativeDictionary dictionary]; - var windowClass = [UIBuilderController classForElementType:"window"]; - var windowWidth = 250, windowHeight = 200; - [windowData setValue:@"window" forKey:@"type"]; - [windowData setValue:@"id_" + _elementCounter++ forKey:@"id"]; - [windowData setValue:windowWidth forKey:@"width"]; - [windowData setValue:windowHeight forKey:@"height"]; - [windowData setValue:[] forKey:@"children"]; - - defaultValues = [windowClass defaultValues]; - for (var key in defaultValues) { - [windowData setValue:defaultValues[key] forKey:key]; - } - - // 3. Position the new element in the center of the window - var elementX = (windowWidth - elementWidth) / 2; - var elementY = (windowHeight - elementHeight) / 2; - [newElementData setValue:elementX forKey:@"originX"]; - [newElementData setValue:elementY forKey:@"originY"]; - - // 4. Position the window so the element is at the drop point - var windowX = aPoint.x - elementX; - var windowY = aPoint.y - elementY; - [windowData setValue:windowX forKey:@"originX"]; - [windowData setValue:windowY forKey:@"originY"]; - - // 5. Add the element to the window's children - [newElementData setValue:[windowData valueForKey:@"id"] forKey:@"parentID"]; - [[windowData mutableArrayValueForKey:@"children"] addObject:newElementData]; - - console.log("UIBuilderController: addNewElementOfType:inNewWindowAtPoint: - Adding new element to window's children:", newElementData); - - // 6. Add both to the elements controller - var undoManager = [[CPApp keyWindow] undoManager]; - [undoManager beginUndoGrouping]; - [[undoManager prepareWithInvocationTarget:_elementsController] removeObject:newElementData]; - [[undoManager prepareWithInvocationTarget:_elementsController] removeObject:windowData]; - [undoManager setActionName:@"Add Element in New Window"]; - [_elementsController addObject:windowData]; - [_elementsController addObject:newElementData]; - [undoManager endUndoGrouping]; - - // 7. Select the new element - [_elementsController setSelectedObjects:[CPArray arrayWithObject:newElementData]]; -} - -- (void)addNewElementOfType:(CPString)elementType inWindow:(CPDictionary)windowData atPoint:(CGPoint)aPoint -{ - console.log("UIBuilderController: addNewElementOfType:inWindow:atPoint: - Adding element ", elementType, " to window ", windowData, " at point ", aPoint); - var newElementData = [CPConservativeDictionary dictionary]; - var viewClass = [UIBuilderController classForElementType:elementType]; - - [newElementData setValue:elementType forKey:@"type"]; - [newElementData setValue:@"id_" + _elementCounter++ forKey:@"id"]; - - var defaultValues = [viewClass defaultValues]; - for (var key in defaultValues) - [newElementData setValue:defaultValues[key] forKey:key]; - - var elementWidth, elementHeight; - - if (elementType === "button") { - elementWidth = 100; - elementHeight = 24; - } else if (elementType === "slider") { - elementWidth = 150; - elementHeight = 20; - } else { // textfield - elementWidth = 150; - elementHeight = 22; - } - [newElementData setValue:elementWidth forKey:@"width"]; - [newElementData setValue:elementHeight forKey:@"height"]; - - // Position the new element relative to the window's origin - [newElementData setValue:aPoint.x forKey:@"originX"]; - [newElementData setValue:aPoint.y forKey:@"originY"]; - - // Add as a child to the container window - [newElementData setValue:[windowData valueForKey:@"id"] forKey:@"parentID"]; - [[windowData mutableArrayValueForKey:@"children"] addObject:newElementData]; - - // Add to the main controller - var undoManager = [[CPApp keyWindow] undoManager]; - [undoManager beginUndoGrouping]; - [[undoManager prepareWithInvocationTarget:_elementsController] removeObject:newElementData]; - [undoManager setActionName:@"Add Element to Window"]; - [_elementsController addObject:newElementData]; - [undoManager endUndoGrouping]; - - [_elementsController setSelectedObjects:[CPArray arrayWithObject:newElementData]]; -} - -- (void)addConnectionFrom:(CPDictionary)sourceData to:(CPDictionary)targetData atPoint:(CGPoint)atPoint outlet:(CPString)outlet action:(CPString)action -{ - var newConnection = [CPConservativeDictionary dictionary]; - [newConnection setValue:[sourceData valueForKey:@"id"] forKey:@"sourceID"]; - [newConnection setValue:[targetData valueForKey:@"id"] forKey:@"targetID"]; - [newConnection setValue:outlet forKey:@"outlet"]; - [newConnection setValue:action forKey:@"action"]; - [newConnection setValue:@"connection_" + _elementCounter++ forKey:@"id"]; - - if (atPoint) - [newConnection setValue:{x: atPoint.x, y: atPoint.y} forKey:@"atPoint"]; - - [[[[CPApp keyWindow] undoManager] prepareWithInvocationTarget:_connectionsController] removeObject:newConnection]; - [[[CPApp keyWindow] undoManager] setActionName:@"Add Connection"]; - - [_connectionsController addObject:newConnection]; - - console.log("UIBuilderController: addConnectionFrom:to: - Added connection: ", newConnection); - console.log("Connections controller count after add: " + [[_connectionsController arrangedObjects] count]); -} - -- (void)removeConnection:(CPDictionary)connection -{ - [[[[CPApp keyWindow] undoManager] prepareWithInvocationTarget:_connectionsController] addObject:connection]; - [[[CPApp keyWindow] undoManager] setActionName:@"Remove Connection"]; - - [_connectionsController removeObject:connection]; -} - -#pragma mark - -#pragma mark UICanvasView Delegate Methods - -- (void)applyFrameChanges:(CPArray)changes withActionName:(CPString)actionName -{ - var undoManager = [[CPApp keyWindow] undoManager]; - var undoChanges = [CPMutableArray array]; - - [undoManager beginUndoGrouping]; - [undoManager setActionName:actionName]; - - for (var i = 0; i < [changes count]; i++) - { - var change = changes[i]; - var data = change.data; - var newFrame = change.frame; - var oldValues = { data: data, frame: {} }; - - if (newFrame.origin) - { - oldValues.frame.origin = { - x: [data valueForKey:@"originX"], - y: [data valueForKey:@"originY"] - }; - [data setValue:newFrame.origin.x forKey:@"originX"]; - [data setValue:newFrame.origin.y forKey:@"originY"]; - } - - if (newFrame.size) - { - oldValues.frame.size = { - width: [data valueForKey:@"width"], - height: [data valueForKey:@"height"] - }; - [data setValue:newFrame.size.width forKey:@"width"]; - [data setValue:newFrame.size.height forKey:@"height"]; - } - [undoChanges addObject:oldValues]; - } - - [[undoManager prepareWithInvocationTarget:self] applyFrameChanges:undoChanges withActionName:actionName]; - [undoManager endUndoGrouping]; -} - -- (void)canvasView:(UICanvasView)aCanvas didMoveElement:(UIElementView)anElement -{ - var selectedViews = [aCanvas selectedSubViews]; - var changes = [CPMutableArray array]; - for (var i = 0; i < [selectedViews count]; i++) - { - var view = selectedViews[i]; - [changes addObject:{ data: [view dataObject], frame: { origin: [view frame].origin } }]; - } - [self applyFrameChanges:changes withActionName:@"Move"]; -} - -- (void)canvasView:(UICanvasView)aCanvas didResizeElement:(UIElementView)anElement -{ - var changes = [CPMutableArray array]; - var frame = [anElement frame]; - [changes addObject:{ data: [anElement dataObject], frame: { origin: frame.origin, size: frame.size } }]; - [self applyFrameChanges:changes withActionName:@"Resize"]; -} - -- (void)canvasView:(UICanvasView)aCanvas didConnectElement:(UIElementView)sourceElement toElement:(UIElementView)targetElement asTargetAction:(CPString)actionName -{ - var sourceData = [sourceElement dataObject]; - var targetData = [targetElement dataObject]; - - // For a target-action, the outlet is typically 'target' - var outletName = @"target"; - - [self addConnectionFrom:sourceData to:targetData atPoint:nil outlet:outletName action:actionName]; -} - -- (void)canvasView:(UICanvasView)aCanvas didConnectElement:(UIElementView)sourceElement toElement:(UIElementView)targetElement asOutlet:(CPString)outletName -{ - var sourceData = [sourceElement dataObject]; - var targetData = [targetElement dataObject]; - - // For a simple outlet connection, there is no action. - var actionName = nil; - - [self addConnectionFrom:sourceData to:targetData atPoint:nil outlet:outletName action:actionName]; -} - -- (void)changeValue:(id)newValue forObject:(id)dataObject -{ - var oldValue = [dataObject valueForKey:@"value"]; - if (oldValue != newValue) - { - var undoManager = [[CPApp keyWindow] undoManager]; - [[undoManager prepareWithInvocationTarget:self] changeValue:oldValue forObject:dataObject]; - [undoManager setActionName:@"Change Value"]; - [dataObject setValue:newValue forKey:@"value"]; - } -} - -- (void)changeValueForSelectedObject:(id)newValue -{ - var selectedObjects = [[self elementsController] selectedObjects]; - if ([selectedObjects count] === 1) - { - [self changeValue:newValue forObject:selectedObjects[0]]; - } -} - -@end diff --git a/Tests/Manual/UIBuilderDemo/UICanvasView.j b/Tests/Manual/UIBuilderDemo/UICanvasView.j deleted file mode 100644 index c374e9f42..000000000 --- a/Tests/Manual/UIBuilderDemo/UICanvasView.j +++ /dev/null @@ -1,996 +0,0 @@ -// -// UICanvasView.j -// A full-window canvas for the UI Builder. -// -// By Daniel Boehringer in 2025. -// - It acts as a drag-and-drop destination for new UI elements from the palette. -// - It correctly instantiates different UIElementView subclasses based on the data model. -// - -@import "UIBuilderConstants.j"; -@import "UIElementView.j"; -@import "ConnectionView.j"; -@import "UIBuilderConstants.j"; - -function treshold(value, limit) -{ - return value > 0 ? Math.min(value, limit) : Math.max(value, -limit); -} - -@implementation UICanvasView : CPView -{ - // Data binding ivars - id _dataObjectsContainer; - CPString _dataObjectsKeyPath; - id _selectionIndexesContainer; - CPString _selectionIndexesKeyPath; - CPArray _oldDataObjects; - - // Connections ivars - id _connectionsContainer; - CPString _connectionsKeyPath; - CPArray _oldConnections; - id _selectedConnectionsContainer; - CPString _selectedConnectionsKeyPath; - - // Rubber-band selection ivars - CGPoint _rubberStart; - CGPoint _rubberEnd; - BOOL _isRubbing; - - ConnectionView _connectionView; - - id _delegate; - - // Connection Menu ivars - UIElementView _connectionSource; - UIElementView _connectionTarget; - BOOL _connectionMade; -} - --(BOOL)acceptsFirstMouse:(CPEvent)aEvent -{ - return YES; -} - -// KVO contexts -var _propertyObservationContext = 1091; -var _dataObjectsObservationContext = 1092; -var _selectionIndexesObservationContext = 1093; -var _connectionsObservationContext = 1094; -var _selectedConnectionsObservationContext = 1095; - -- (id)initWithFrame:(CGRect)aRect -{ - self = [super initWithFrame:aRect]; - - if (self) - { - // Register to accept drops from the palette - [self registerForDraggedTypes:[ - UIWindowDragType, - UIButtonDragType, - UISliderDragType, - UITextFieldDragType - ]]; - - _connectionView = [[ConnectionView alloc] initWithFrame:[self bounds]]; - [_connectionView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; - [self addSubview:_connectionView]; - } - return self; -} - -#pragma mark - Bindings & KVO (Largely from EFLaceView) - -+ (void)initialize -{ - [self exposeBinding:"dataObjects"]; - [self exposeBinding:@"selectionIndexes"]; - [self exposeBinding:@"connections"]; - [self exposeBinding:@"selectedConnections"]; -} - -- (void)bind:(CPString)bindingName toObject:(id)observableObject withKeyPath:(CPString)observableKeyPath options:(CPDictionary)options -{ - if ([bindingName isEqualToString:@"dataObjects"]) - { - _dataObjectsContainer = observableObject; - _dataObjectsKeyPath = observableKeyPath; - [_dataObjectsContainer addObserver:self forKeyPath:_dataObjectsKeyPath options:(CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld) context:_dataObjectsObservationContext]; - [self startObservingDataObjects:[self dataObjects]]; - _oldDataObjects = [[self dataObjects] copy] || @[]; - } - else if ([bindingName isEqualToString:@"selectionIndexes"]) - { - _selectionIndexesContainer = observableObject; - _selectionIndexesKeyPath = observableKeyPath; - [_selectionIndexesContainer addObserver:self forKeyPath:_selectionIndexesKeyPath options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:_selectionIndexesObservationContext]; - } - else if ([bindingName isEqualToString:@"connections"]) - { - _connectionsContainer = observableObject; - _connectionsKeyPath = observableKeyPath; - [_connectionsContainer addObserver:self forKeyPath:_connectionsKeyPath options:(CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld) context:_connectionsObservationContext]; - _oldConnections = [[self connections] copy] || @[]; - } - else if ([bindingName isEqualToString:@"selectedConnections"]) - { - _selectedConnectionsContainer = observableObject; - _selectedConnectionsKeyPath = observableKeyPath; - [_selectedConnectionsContainer addObserver:self forKeyPath:_selectedConnectionsKeyPath options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:_selectedConnectionsObservationContext]; - } - else { [super bind:bindingName toObject:observableObject withKeyPath:observableKeyPath options:options]; } - - [self setNeedsDisplay:YES]; -} - -- (void)unbind:(CPString)bindingName -{ - if ([bindingName isEqualToString:@"dataObjects"]) { - [self stopObservingDataObjects:[self dataObjects]]; - [_dataObjectsContainer removeObserver:self forKeyPath:_dataObjectsKeyPath]; - _dataObjectsContainer = nil; _dataObjectsKeyPath = nil; - } else if ([bindingName isEqualToString:@"selectionIndexes"]) { - [_selectionIndexesContainer removeObserver:self forKeyPath:_selectionIndexesKeyPath]; - _selectionIndexesContainer = nil; _selectionIndexesKeyPath = nil; - } else if ([bindingName isEqualToString:@"connections"]) { - [_connectionsContainer removeObserver:self forKeyPath:_connectionsKeyPath]; - _connectionsContainer = nil; _connectionsKeyPath = nil; - } else if ([bindingName isEqualToString:@"selectedConnections"]) { - [_selectedConnectionsContainer removeObserver:self forKeyPath:_selectedConnectionsKeyPath]; - _selectedConnectionsContainer = nil; _selectedConnectionsKeyPath = nil; - } else { [super unbind:bindingName]; } - [self setNeedsDisplay:YES]; -} - -- (CPArray)dataObjects -{ - var result = [_dataObjectsContainer valueForKeyPath:_dataObjectsKeyPath]; - return (result == [CPNull null]) ? @[] : result; -} - -- (CPIndexSet)selectionIndexes -{ - return [_selectionIndexesContainer valueForKeyPath:_selectionIndexesKeyPath]; -} - -- (CPArray)connections -{ - var result = [_connectionsContainer valueForKeyPath:_connectionsKeyPath]; - return (result == [CPNull null]) ? @[] : result; -} - -- (CPArray)selectedConnections -{ - var result = [_selectedConnectionsContainer valueForKeyPath:_selectedConnectionsKeyPath]; - return (result == [CPNull null]) ? @[] : result; -} - -- (void)setSelectionIndexes:(CPIndexSet)indexes -{ - [_selectionIndexesContainer setValue:indexes forKeyPath:_selectionIndexesKeyPath]; -} - -- (void)startObservingDataObjects:(CPArray)dataObjects -{ - if (!dataObjects || dataObjects == [CPNull null]) - return; - - for (var i = 0; i < [dataObjects count]; i++) - { - var newDataObject = dataObjects[i]; - // Only create views for top-level objects. Children are handled by their parents. - if (![newDataObject valueForKey:@"parentID"]) - [self _createViewForDataObject:newDataObject superview:self]; - } -} - -- (void)_createViewForDataObject:(CPDictionary)dataObject superview:(CPView)superview -{ - var type = [dataObject valueForKey:@"type"]; - var newView; - - // Instantiate the correct view based on the data model's 'type' - if (type === "window") - newView = [[UIWindowView alloc] init]; - else if (type === "button") - newView = [[UIButtonView alloc] init]; - else if (type === "slider") - newView = [[UISliderView alloc] init]; - else if (type === "textfield") - newView = [[UITextFieldView alloc] init]; - else - newView = [[UIElementView alloc] init]; // Fallback - - [newView setDataObject:dataObject]; - - // Bind view properties to the data model - [newView bind:@"originX" toObject:dataObject withKeyPath:@"originX" options:nil]; - [newView bind:@"originY" toObject:dataObject withKeyPath:@"originY" options:nil]; - [newView bind:@"width" toObject:dataObject withKeyPath:@"width" options:nil]; - [newView bind:@"height" toObject:dataObject withKeyPath:@"height" options:nil]; - - if (type === "window") - { - var children = [dataObject valueForKey:@"children"]; - for (var j = 0; j < [children count]; j++) - { - [self _createViewForDataObject:children[j] superview:newView]; - } - } - - [superview addSubview:newView]; - // i have no idea why this is needed, but it is to make the initial click work - [CPApp._delegate._window makeKeyAndOrderFront:self]; -} - -- (void)stopObservingDataObjects:(CPArray)dataObjects -{ - if (!dataObjects || dataObjects == [CPNull null]) return; - - var viewsToRemove = [CPMutableArray array]; - [self _findViewsForDataObjects:dataObjects inView:self foundViews:viewsToRemove]; - - for (var i = 0; i < [viewsToRemove count]; i++) { - var viewToRemove = viewsToRemove[i]; - [self _removeViewAndChildren:viewToRemove]; - } -} - -- (void)_removeViewAndChildren:(UIElementView)viewToRemove -{ - // Unbind everything before removing - [viewToRemove unbind:@"value"]; - [viewToRemove unbind:@"originX"]; - [viewToRemove unbind:@"originY"]; - [viewToRemove unbind:@"width"]; - [viewToRemove unbind:@"height"]; - - var subviews = [[viewToRemove subviews] copy]; - for (var i = 0; i < [subviews count]; i++) - { - [self _removeViewAndChildren:subviews[i]]; - } - - [viewToRemove removeFromSuperview]; -} - -- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context -{ - if (context == _dataObjectsObservationContext) - { - var newDataObjects = [object valueForKeyPath:_dataObjectsKeyPath]; - var oldDataObjects = _oldDataObjects; - - var added = [newDataObjects mutableCopy]; - [added removeObjectsInArray:oldDataObjects]; - [self startObservingDataObjects:added]; - - var removed = [oldDataObjects mutableCopy]; - [removed removeObjectsInArray:newDataObjects]; - [self stopObservingDataObjects:removed]; - - _oldDataObjects = [newDataObjects copy]; - [self setNeedsDisplay:YES]; - } - else if (context == _selectionIndexesObservationContext) - { - var allDataObjects = [self dataObjects]; - var newIndexes = [change objectForKey:CPKeyValueChangeNewKey] || [CPIndexSet indexSet]; - var oldIndexes = [change objectForKey:CPKeyValueChangeOldKey] || [CPIndexSet indexSet]; - - // Find views for newly selected objects and redraw them - var newSelectedDataObjects = [allDataObjects objectsAtIndexes:newIndexes]; - var newlySelectedViews = [CPMutableArray array]; - [self _findViewsForDataObjects:newSelectedDataObjects inView:self foundViews:newlySelectedViews]; - [newlySelectedViews makeObjectsPerformSelector:@selector(setNeedsDisplay:) withObject:YES]; - - // Find views for deselected objects and redraw them, but only if those objects still exist. - var previouslySelectedViews = [CPMutableArray array]; - var oldSelectedDataObjects = [CPMutableArray array]; - var lastIndex = [oldIndexes lastIndex]; - - if (lastIndex != CPNotFound && lastIndex < [allDataObjects count]) - { - oldSelectedDataObjects = [allDataObjects objectsAtIndexes:oldIndexes]; - } - else - { - // If the indexes are out of bounds, it likely means the objects were deleted. - // We need to find the views that were associated with the old indexes another way. - // This is a tricky state to recover from. For now, we will just redraw all views. - // A more sophisticated solution might involve caching view-data relationships. - [[self subviews] makeObjectsPerformSelector:@selector(setNeedsDisplay:) withObject:YES]; - return; - } - - [self _findViewsForDataObjects:oldSelectedDataObjects inView:self foundViews:previouslySelectedViews]; - [previouslySelectedViews makeObjectsPerformSelector:@selector(setNeedsDisplay:) withObject:YES]; - } - else if (context == _connectionsObservationContext) - { - var newConnections = [object valueForKeyPath:_connectionsKeyPath]; - var oldConnections = _oldConnections; - - // For now, simply redraw all connections. A more optimized approach would be to only redraw changed connections. - [self setNeedsDisplay:YES]; - _oldConnections = [newConnections copy]; - } - else if (context == _selectedConnectionsObservationContext) - { - [self setNeedsDisplay:YES]; - } -} - -#pragma mark - Drawing & Mouse - -- (void)drawRect:(CPRect)rect -{ - // === START: Infographic Drawing === - - var bounds = [self bounds]; - - // 1. Define text attributes for the infographic - var titleFont = [CPFont fontWithName:@"Helvetica-Bold" size:36]; - var subtitleFont = [CPFont fontWithName:@"Helvetica" size:18]; - var featureFont = [CPFont fontWithName:@"Helvetica" size:14]; - var watermarkColor = [CPColor colorWithCalibratedWhite:0.85 alpha:1.0]; // A light gray for the watermark effect - - var titleAttributes = @{ - CPFontAttributeName: titleFont, - CPForegroundColorAttributeName: watermarkColor - }; - var subtitleAttributes = @{ - CPFontAttributeName: subtitleFont, - CPForegroundColorAttributeName: watermarkColor - }; - var featureAttributes = @{ - CPFontAttributeName: featureFont, - CPForegroundColorAttributeName: watermarkColor - }; - - // 2. Prepare the text content - var title = @"Cappuccino JS"; - var subtitle = @"Desktop-Quality Applications in the Browser"; - var features = [ - @"• Drag-and-Drop UI Creation", - @"• Direct Manipulation: Move & Resize (Keyboard / Mouse)", - @"• Undo/Redo & Keyboard Navigation", - @"• Control-Draggin -> Target-Action & Outlet Connections", - @"• Context sensitive inspector panel", - @"• Run the 'real thing' in a separate native window", - @"• Source: https://github.com/daboe01/UIBuilder" - - ]; - - // 3. Calculate positions and draw the text, centering it on the canvas - var titleSize = [title sizeWithAttributes:titleAttributes]; - var subtitleSize = [subtitle sizeWithAttributes:subtitleAttributes]; - var totalHeight = titleSize.height + subtitleSize.height + ([features count] * 20) + 40; // Approximate total height - var currentY = (bounds.size.height - totalHeight) / 2.0; - - // Draw Title - var titlePoint = CGPointMake((bounds.size.width - titleSize.width) / 2.0, currentY); - [title drawAtPoint:titlePoint withAttributes:titleAttributes]; - currentY += titleSize.height + 10; - - // Draw Subtitle - var subtitlePoint = CGPointMake((bounds.size.width - subtitleSize.width) / 2.0, currentY); - [subtitle drawAtPoint:subtitlePoint withAttributes:subtitleAttributes]; - currentY += subtitleSize.height + 30; - - // Draw Feature List - for (var i = 0; i < [features count]; i++) { - var feature = features[i]; - var featureSize = [feature sizeWithAttributes:featureAttributes]; - var featurePoint = CGPointMake((bounds.size.width - featureSize.width) / 2.0, currentY); - [feature drawAtPoint:featurePoint withAttributes:featureAttributes]; - currentY += featureSize.height + 5; - } - - // === END: Infographic Drawing === - - // The background is drawn by the window. We only draw the rubber-band. - if (_isRubbing) - { - var rubber = CGRectUnion(CGRectMake(_rubberStart.x, _rubberStart.y, 0.1, 0.1), CGRectMake(_rubberEnd.x, _rubberEnd.y, 0.1, 0.1)); - [[[[CPColor alternateSelectedControlColor] colorWithAlphaComponent:0.2] setFill]]; - [CPBezierPath fillRect:rubber]; - [[CPColor alternateSelectedControlColor] setStroke]; - [CPBezierPath setDefaultLineWidth:1.0]; - [CPBezierPath strokeRect:rubber]; - } - - // Draw existing connections that are selected in the connections controller. - var selectedConnections = [self selectedConnections]; - - if (selectedConnections && [selectedConnections count] > 0) - { - for (var i = 0; i < [selectedConnections count]; i++) - { - var connection = [selectedConnections objectAtIndex:i]; - var sourceID = [connection valueForKey:@"sourceID"]; - var targetID = [connection valueForKey:@"targetID"]; - var sourceView = [self viewForElementWithID:sourceID]; - var targetView = [self viewForElementWithID:targetID]; - - if (sourceView && targetView) - { - var startPoint = [sourceView convertPoint:CGPointMake(CGRectGetMidX([sourceView bounds]), CGRectGetMidY([sourceView bounds])) toView:self]; - var endPoint; - var connectionPoint = [connection valueForKey:@"atPoint"]; - - if (connectionPoint) { - endPoint = CGPointMake(connectionPoint.x, connectionPoint.y); - } else { - endPoint = [targetView convertPoint:CGPointMake(CGRectGetMidX([targetView bounds]), CGRectGetMidY([targetView bounds])) toView:self]; - } - - // Draw the link with a distinct color, like blue. - [self drawLinkFrom:startPoint to:endPoint color:[CPColor blueColor]]; - } - } - } -} - -- (void)drawLinkFrom:(CGPoint)startPoint to:(CGPoint)endPoint color:(CPColor)insideColor -{ - - var dist = Math.sqrt(Math.pow(startPoint.x - endPoint.x, 2) + Math.pow(startPoint.y - endPoint.y, 2)); - - // a lace is made of an outside gray line of width 5, and a inside insideColor(ed) line of width 3 - var p0 = CGPointMake(startPoint.x, startPoint.y); - var p3 = CGPointMake(endPoint.x, endPoint.y); - - var p1 = CGPointMake(startPoint.x + treshold((endPoint.x - startPoint.x) / 2, 50), startPoint.y); - var p2 = CGPointMake(endPoint.x - treshold((endPoint.x - startPoint.x) / 2, 50), endPoint.y); - - // p0 and p1 are on the same horizontal line - // distance between p0 and p1 is set with the treshold fuction - // the same holds for p2 and p3 - - var path = [CPBezierPath bezierPath]; - [path setLineWidth:0]; - [[CPColor grayColor] set]; - [path appendBezierPathWithOvalInRect:CGRectMake(startPoint.x-2.5,startPoint.y-2.5,5,5)]; - [path fill]; - - path = [CPBezierPath bezierPath]; - [path setLineWidth:0]; - [insideColor set]; - [path appendBezierPathWithOvalInRect:CGRectMake(startPoint.x-1.5,startPoint.y-1.5,3,3)]; - [path fill]; - - path = [CPBezierPath bezierPath]; - [path setLineWidth:0]; - [[CPColor grayColor] set]; - [path appendBezierPathWithOvalInRect:CGRectMake(endPoint.x-2.5,endPoint.y-2.5,5,5)]; - [path fill]; - - path = [CPBezierPath bezierPath]; - [path setLineWidth:0]; - [insideColor set]; - [path appendBezierPathWithOvalInRect:CGRectMake(endPoint.x-1.5,endPoint.y-1.5,3,3)]; - [path fill]; - - // if the line is rather short, draw a straight line. the curve would look rather odd in this case. - if (dist < 40) - { - path = [CPBezierPath bezierPath]; - [path setLineWidth:5]; - [path moveToPoint:startPoint]; - [path lineToPoint:endPoint]; - [[CPColor grayColor] set]; - [path stroke]; - - path = [CPBezierPath bezierPath]; - [path setLineWidth:3]; - [path moveToPoint:startPoint]; - [path lineToPoint:endPoint]; - [insideColor set]; - [path stroke]; - - return; - } - - path = [CPBezierPath bezierPath]; - [path setLineWidth:5]; - [path moveToPoint:p0]; - [path curveToPoint:p3 controlPoint1:p1 controlPoint2:p2]; - [[CPColor grayColor] set]; - [path stroke]; - - path = [CPBezierPath bezierPath]; - [path setLineWidth:3]; - [path moveToPoint:p0]; - [path curveToPoint:p3 controlPoint1:p1 controlPoint2:p2]; - [insideColor set]; - [path stroke]; -} - -#pragma mark - View Lookup - -// Private recursive helper method to search the entire view hierarchy. -- (UIElementView)_findViewForElementWithID:(CPString)elementID inView:(CPView)aView -{ - // Iterate through all subviews of the current view - var subviews = [aView subviews]; - for (var i = 0; i < [subviews count]; i++) - { - var subview = subviews[i]; - - // We are only interested in UIElementView subclasses - if (![subview isKindOfClass:[UIElementView class]]) - continue; - - // 1. Check if the current subview is the one we are looking for. - if ([[subview dataObject] valueForKey:@"id"] === elementID) - { - return subview; // Found it! - } - - // 2. If not, and this subview has children, recurse into it. - // This is the key step to search inside containers like UIWindowView. - if ([[subview subviews] count] > 0) - { - var foundView = [self _findViewForElementWithID:elementID inView:subview]; - if (foundView) - { - return foundView; // Found it in a nested hierarchy. - } - } - } - - // If we've searched this entire branch and found nothing, return nil. - return nil; -} - -// Public method to start the search from the canvas itself. -- (UIElementView)viewForElementWithID:(CPString)elementID -{ - if (!elementID) - return nil; - - // Start the recursive search from the top-level canvas view. - return [self _findViewForElementWithID:elementID inView:self]; -} - -- (void)drawConnectionFrom:(CGPoint)startPoint to:(CGPoint)endPoint -{ - [_connectionView setStartPoint:startPoint]; - [_connectionView setEndPoint:endPoint]; - [_connectionView setHidden:NO]; // Ensure it's visible when drawing - [self addSubview:_connectionView]; // Bring to front - [_connectionView setNeedsDisplay:YES]; -} - -- (void)clearConnection -{ - [_connectionView setHidden:YES]; - [_connectionView setNeedsDisplay:YES]; // Request redraw to clear old line -} - -- (UIElementView)viewAtPoint:(CGPoint)aPoint -{ - return [self _findDeepestUIElementViewAtPoint:aPoint inView:self]; -} - -- (UIElementView)_findDeepestUIElementViewAtPoint:(CGPoint)aPoint inView:(CPView)currentView -{ - // Iterate through subviews in reverse order to get the topmost view - for (var i = [[currentView subviews] count] - 1; i >= 0; i--) - { - var subview = [[currentView subviews] objectAtIndex:i]; - - // Convert the point to the subview's coordinate system - var localPoint = [subview convertPoint:aPoint fromView:currentView]; - - if ([subview isKindOfClass:[UIElementView class]]) - { - if (CGRectContainsPoint([subview bounds], localPoint)) - { - // If this is a container view, recursively search its subviews - if (subview._isContainer) - { - var deepestView = [self _findDeepestUIElementViewAtPoint:localPoint inView:subview]; - - if (deepestView) - return deepestView; - } - // If not a container, or no deeper view found, return this view - return subview; - } - } - } - - return nil; -} - -- (void)mouseDown:(CPEvent)theEvent -{ - if (_connectionSource) - { - [self menuDidEndTracking:nil]; - return; - } - // A click on the canvas background starts a rubber-band selection. - [self deselectViews]; - _isRubbing = YES; - _rubberStart = [self convertPoint:[theEvent locationInWindow] fromView:nil]; - _rubberEnd = _rubberStart; - - [CPApp setTarget:self selector:@selector(_dragOpenSpaceWithEvent:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES]; -} - -- (void)_dragOpenSpaceWithEvent:(CPEvent)theEvent -{ - var mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; - _rubberEnd = mouseLoc; - var rubberRect = CGRectUnion(CGRectMake(_rubberStart.x, _rubberStart.y, 1, 1), CGRectMake(_rubberEnd.x, _rubberEnd.y, 1, 1)); - - switch ([theEvent type]) - { - case CPLeftMouseDragged: - var indexesToSelect = [CPMutableIndexSet indexSet]; - var allDataObjects = [self dataObjects]; - for (var i = 0; i < [[self subviews] count]; i++) { - var aView = [self subviews][i]; - if ([aView isKindOfClass:[UIElementView class]] && CGRectIntersectsRect([aView frame], rubberRect)) { - var dataIndex = [allDataObjects indexOfObject:[aView dataObject]]; - if (dataIndex != CPNotFound) { - [indexesToSelect addIndex:dataIndex]; - } - } - } - [_selectionIndexesContainer setValue:indexesToSelect forKeyPath:_selectionIndexesKeyPath]; - [self setNeedsDisplay:YES]; - [CPApp setTarget:self selector:@selector(_dragOpenSpaceWithEvent:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES]; - break; - - case CPLeftMouseUp: - _isRubbing = NO; - [self setNeedsDisplay:YES]; - break; - } -} - -- (void)delete:(id)sender -{ - // Forward the delete action to the delegate/controller - if (_delegate && [_delegate respondsToSelector:@selector(removeSelectedElements)]) { - [_delegate removeSelectedElements]; - } -} - -- (void)cut:(id)sender -{ - if (_delegate && [_delegate respondsToSelector:@selector(cut:)]) { - [_delegate cut:sender]; - } -} - -- (void)copy:(id)sender -{ - if (_delegate && [_delegate respondsToSelector:@selector(copy:)]) { - [_delegate copy:sender]; - } -} - -- (void)paste:(id)sender -{ - if (_delegate && [_delegate respondsToSelector:@selector(paste:)]) { - [_delegate paste:sender]; - } -} - -- (void)viewDidMoveToWindow -{ - [super viewDidMoveToWindow]; - - if ([self window]) - { - [[self window] makeFirstResponder:self]; - } -} - -- (BOOL)acceptsFirstResponder -{ - return YES; -} - -/* -- (BOOL)validateMenuItem:(CPMenuItem)aMenuItem -{ - var action = [aMenuItem action]; - - if (action == @selector(copy:) || action == @selector(cut:) || action == @selector(delete:)) - { - return [[self selectionIndexes] count] > 0; - } - - if (action == @selector(paste:)) - { - return [[[CPPasteboard generalPasteboard] types] containsObject:UIBuilderElementPboardType]; - } - - if (action == @selector(createTargetActionConnection:) || action == @selector(createOutletConnection:)) - { - return YES; - } - - var undoManager = [[self window] undoManager]; - - if (action == @selector(undo:)) - { - return [undoManager canUndo]; - } - - if (action == @selector(redo:)) - { - return [undoManager canRedo]; - } - - return [super validateMenuItem:aMenuItem]; -} -*/ - -- (void)keyDown:(CPEvent)theEvent -{ - var characters = [theEvent characters]; - var flags = [theEvent modifierFlags]; - var selectors = [CPKeyBinding selectorsForKey:characters modifierFlags:flags]; - var delegate = [self delegate]; - var handled = NO; - - if (selectors && delegate) - { - for (var i = 0; i < [selectors count]; i++) - { - var selectorName = selectors[i]; - if ([delegate respondsToSelector:selectorName]) - { - [delegate performSelector:selectorName withObject:self]; - handled = YES; - break; - } - } - } - - if (!handled) - [super keyDown:theEvent]; -} - -#pragma mark - Drag and Drop Destination - -- (CPDragOperation)draggingEntered:(CPDraggingInfo)sender -{ - // We accept any of the registered types - return CPDragOperationCopy; -} - -- (BOOL)performDragOperation:(CPDraggingInfo)sender -{ - var dropPoint = [self convertPoint:[sender draggingLocation] fromView:nil]; - var pasteboard = [sender draggingPasteboard]; - var types = [pasteboard types]; - var draggedType = types[0]; // Assuming only one type is being dragged - var elementType; - - if (draggedType === UIWindowDragType) elementType = "window"; - else if (draggedType === UIButtonDragType) elementType = "button"; - else if (draggedType === UISliderDragType) elementType = "slider"; - else if (draggedType === UITextFieldDragType) elementType = "textfield"; - - if (elementType && _delegate) - { - if (elementType === "window") { - if ([_delegate respondsToSelector:@selector(addNewElementOfType:atPoint:)]) - { - [_delegate addNewElementOfType:elementType atPoint:dropPoint]; - [self setNeedsDisplay:YES]; - return YES; - } - } else { - if ([_delegate respondsToSelector:@selector(addNewElementOfType:inNewWindowAtPoint:)]) - { - [_delegate addNewElementOfType:elementType inNewWindowAtPoint:dropPoint]; - [self setNeedsDisplay:YES]; - return YES; - } - } - } - - return NO; -} - -#pragma mark - Delegate & Selection Management - -- (id)delegate { return _delegate; } -- (void)setDelegate:(id)newDelegate { _delegate = newDelegate; } - -- (void)deselectViews -{ - [_selectionIndexesContainer setValue:nil forKeyPath:_selectionIndexesKeyPath]; -} - -- (void)selectView:(UIElementView)aView state:(BOOL)select -{ - var selection = [[self selectionIndexes] mutableCopy] || [CPMutableIndexSet indexSet]; - var dataObjectIndex = [[self dataObjects] indexOfObject:[aView dataObject]]; - - - - if (dataObjectIndex != CPNotFound) - { - if (select) - [selection addIndex:dataObjectIndex]; - - else [selection removeIndex:dataObjectIndex]; - } - - [_selectionIndexesContainer setValue:selection forKeyPath:_selectionIndexesKeyPath]; -} - -- (CPArray)selectedSubViews -{ - var selectedDataObjects = [[self dataObjects] objectsAtIndexes:[self selectionIndexes]]; - var selectedViews = [CPMutableArray array]; - - [self _findViewsForDataObjects:selectedDataObjects inView:self foundViews:selectedViews]; - - return selectedViews; -} - -- (BOOL)isViewSelected:(CPView)aView -{ - var selected = [self selectedSubViews]; - - return [selected containsObject:aView]; -} - -- (void)_findViewsForDataObjects:(CPArray)dataObjects inView:(CPView)aView foundViews:(CPMutableArray)foundViews -{ - var subviews = [aView subviews]; - - for (var i = 0; i < [subviews count]; i++) - { - var subview = subviews[i]; - - // Skip the connection view and any other non-UIElementView instances - if (![subview isKindOfClass:[UIElementView class]]) - continue; - - var contains = [dataObjects containsObject:[subview dataObject]]; - - if (contains) - { - [foundViews addObject:subview]; - } - - // Recurse into subviews - [self _findViewsForDataObjects:dataObjects inView:subview foundViews:foundViews]; - } -} - -// These methods are called by the UIElementView children to notify the controller -- (void)elementDidMove:(UIElementView)anElement -{ - if (_delegate && [_delegate respondsToSelector:@selector(canvasView:didMoveElement:)]) { - [_delegate canvasView:self didMoveElement:anElement]; - } -} - -- (void)elementDidResize:(UIElementView)anElement -{ - if (_delegate && [_delegate respondsToSelector:@selector(canvasView:didResizeElement:)]) { - [_delegate canvasView:self didResizeElement:anElement]; - } -} - -- (void)elementDidConnect:(UIElementView)sourceElement to:(UIElementView)targetElement atPoint:(CGPoint)aPoint -{ - if (_delegate && [_delegate respondsToSelector:@selector(canvasView:didConnectElement:toElement:atPoint:)]) { - [_delegate canvasView:self didConnectElement:sourceElement toElement:targetElement atPoint:aPoint]; - } -} - -#pragma mark - Connection Menu - -- (void)showConnectionMenuForSource:(UIElementView)sourceView target:(UIElementView)targetView at:(CGPoint)aPoint -{ - _connectionSource = sourceView; - _connectionTarget = targetView; - _connectionMade = NO; - - var menu = [[CPMenu alloc] initWithTitle:@"Connection Menu"]; - [menu setDelegate:self]; - - // 1. Add Target's Actions - var targetActions = [[_connectionTarget dataObject] valueForKey:@"actions"]; - if (targetActions && [targetActions length] > 0) - { - var actionsArray = [targetActions componentsSeparatedByString:@", "]; - for (var i = 0; i < [actionsArray count]; i++) - { - var actionName = actionsArray[i]; - var menuItem = [[CPMenuItem alloc] initWithTitle:actionName action:@selector(createTargetActionConnection:) keyEquivalent:@""]; - [menu addItem:menuItem]; - } - } - - // 2. Add Separator - if ([menu numberOfItems] > 0) - [menu addItem:[CPMenuItem separatorItem]]; - - // 3. Add Source's Outlets - var sourceOutlets = [[_connectionSource dataObject] valueForKey:@"outlets"]; - if (sourceOutlets && [sourceOutlets length] > 0) - { - var outletsArray = [sourceOutlets componentsSeparatedByString:@", "]; - for (var i = 0; i < [outletsArray count]; i++) - { - var outletName = outletsArray[i]; - if (outletName === @"target") continue; // Skip 'target' outlet as requested - var menuItem = [[CPMenuItem alloc] initWithTitle:outletName action:@selector(createOutletConnection:) keyEquivalent:@""]; - [menu addItem:menuItem]; - } - } - - if ([menu numberOfItems] > 0) - { - [CPMenu popUpContextMenu:menu withEvent:[CPApp currentEvent] forView:self]; - } - else - { - [self menuDidEndTracking:menu]; // No items, so clean up immediately - } -} - -- (void)createTargetActionConnection:(CPMenuItem)sender -{ - var actionName = [sender title]; - if (_delegate && [_delegate respondsToSelector:@selector(canvasView:didConnectElement:toElement:asTargetAction:)]) - { - _connectionMade = YES; - [self clearConnection]; - if (_connectionTarget) - [_connectionTarget setAsDropTarget:NO]; - - [_delegate canvasView:self didConnectElement:_connectionSource toElement:_connectionTarget asTargetAction:actionName]; - } -} - -- (void)createOutletConnection:(CPMenuItem)sender -{ - var outletName = [sender title]; - if (_delegate && [_delegate respondsToSelector:@selector(canvasView:didConnectElement:toElement:asOutlet:)]) - { - _connectionMade = YES; - [self clearConnection]; - if (_connectionTarget) - [_connectionTarget setAsDropTarget:NO]; - - [_delegate canvasView:self didConnectElement:_connectionSource toElement:_connectionTarget asOutlet:outletName]; - } -} - -- (void)menuDidEndTracking:(CPMenu)aMenu -{ - // This delegate method is called after a menu item is selected OR the menu is cancelled. - if (!_connectionMade) - { - [self clearConnection]; - if (_connectionTarget) - [_connectionTarget setAsDropTarget:NO]; - } - - // Reset state - _connectionSource = nil; - _connectionTarget = nil; - _connectionMade = NO; - - [self setNeedsDisplay:YES]; -} - -@end diff --git a/Tests/Manual/UIBuilderDemo/UIElementView.j b/Tests/Manual/UIBuilderDemo/UIElementView.j deleted file mode 100644 index d220fc7fd..000000000 --- a/Tests/Manual/UIBuilderDemo/UIElementView.j +++ /dev/null @@ -1,1349 +0,0 @@ -// -// UIElementView.j by Daniel Böhringer in 2025 - -// This file is a drawing engine for a UI builder, with features such as: -// - Skeleton drawing for common UI elements (Window, Button, Slider, TextField). -// - Selection highlights. -// - Resize handles ("dimples") on selected views. -// - Mouse logic for moving and resizing elements. -// - Visual hints for drop targets (e.g., a Window accepting a Button). -// -// - -@import "UIBuilderConstants.j"; - -// --- Property Types --- -UIBString = "UIBString"; -UIBNumber = "UIBNumber"; -UIBBoolean = "UIBBoolean"; - -// --- Constants for Resizing --- -var kUIElementHandleSize = 8.0; -var kUIElementNoHandle = 0; -var kUIElementTopLeftHandle = 1; -var kUIElementTopMiddleHandle = 2; -var kUIElementTopRightHandle = 3; -var kUIElementMiddleLeftHandle = 4; -var kUIElementMiddleRightHandle = 5; -var kUIElementBottomLeftHandle = 6; -var kUIElementBottomMiddleHandle = 7; -var kUIElementBottomRightHandle = 8; - - -@class UIWindowView -@class UIButtonView -@class UISliderView -@class UITextFieldView; - -@implementation UIElementView : CPView -{ - CPMutableDictionary _stringAttributes; - id _dataObject @accessors(property=dataObject); - - // State for dragging and resizing - CGPoint _lastMouseLoc; - int _activeHandle; - BOOL _isDragTarget; // Used by subclasses (e.g. UIWindowView) - CPTrackingArea _trackingArea; - BOOL _isContainer; - BOOL _isConnecting; -} - -#pragma mark - -#pragma mark *** Class Methods *** - -+ (CPArray)persistentProperties -{ - return ["value"]; -} - -+ (CPDictionary)defaultValues -{ - return {value: "Element"}; -} - -+ (CPDictionary)propertyTypes -{ - return [CPDictionary dictionaryWithObjects:[UIBString] forKeys:["value"]]; -} - -- (id)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self) - { - _stringAttributes = [[CPMutableDictionary alloc] init]; - [_stringAttributes setObject:[CPFont boldSystemFontOfSize:12] forKey:CPFontAttributeName]; - [_stringAttributes setObject:[CPColor blackColor] forKey:CPForegroundColorAttributeName]; - - - _activeHandle = kUIElementNoHandle; - - if ([self frame].size.width < 50 || [self frame].size.height < 20) - [self setFrameSize:CGSizeMake(MAX(50, [self frame].size.width), MAX(20, [self frame].size.height))]; - - [self setNeedsDisplay:YES]; - - _trackingArea = [[CPTrackingArea alloc] initWithRect:CGRectMakeZero() - options:CPTrackingMouseMoved | CPTrackingActiveInKeyWindow | CPTrackingInVisibleRect | CPTrackingMouseEnteredAndExited - owner:self - userInfo:nil]; - [self addTrackingArea:_trackingArea]; - _isContainer = NO; - _isConnecting = NO; - } - return self; -} - -- (void)dealloc -{ - [self setDataObject:nil]; - [super dealloc]; -} - -- (void)setDataObject:(id)newDataObject -{ - var oldDataObject = [self dataObject]; - if (newDataObject != oldDataObject) - { - var properties = [[self class] persistentProperties]; - if (oldDataObject) - for (var i = 0; i < [properties count]; i++) - [oldDataObject removeObserver:self forKeyPath:properties[i]]; - - _dataObject = newDataObject; - - if (newDataObject) - { - for (var i = 0; i < [properties count]; i++) - { - var propertyName = properties[i]; - [newDataObject addObserver:self forKeyPath:propertyName options:CPKeyValueObservingOptionNew context:self]; - } - } - } -} - -- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context -{ - if (context == self) - { - // When a property on the dataObject changes, simply tell the view to redraw itself. - [self setNeedsDisplay:YES]; - } - else - { - [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; - } -} - -- (BOOL)acceptsFirstMouse -{ - // This view should accept first mouse events for interaction. - return YES; -} - -- (void)removeFromSuperview -{ - // This is the correct place to clean up view-related resources. - // When the view is removed from its superview, we no longer need to track - // mouse events within its bounds. - [self removeTrackingArea:_trackingArea]; - - // It's crucial to call the superclass's implementation at the end. - [super removeFromSuperview]; -} - -#pragma mark - -#pragma mark *** Geometry Accessors (for KVC Binding) *** - -- (float)originX -{ - return [self frame].origin.x; -} - -- (void)setOriginX:(float)aFloat -{ - // Only update if the value has actually changed. - if (aFloat !== [self originX]) - { - var frame = [self frame]; - frame.origin.x = aFloat; - [self setFrame:frame]; - - // Notify the superview (the canvas) that it might need to redraw - // if anything depends on this view's position. - [[self superview] setNeedsDisplay:YES]; - } -} - -- (float)originY -{ - return [self frame].origin.y; -} - -- (void)setOriginY:(float)aFloat -{ - if (aFloat !== [self originY]) - { - var frame = [self frame]; - frame.origin.y = aFloat; - [self setFrame:frame]; - [[self superview] setNeedsDisplay:YES]; - } -} - -- (float)width -{ - return [self frame].size.width; -} - -- (void)setWidth:(float)aFloat -{ - if (aFloat !== [self width]) - { - var frame = [self frame]; - // Enforce a minimum width to prevent rendering issues. - frame.size.width = MAX(aFloat, 20.0); - [self setFrame:frame]; - [[self superview] setNeedsDisplay:YES]; - } -} - -- (float)height -{ - return [self frame].size.height; -} - -- (void)setHeight:(float)aFloat -{ - if (aFloat !== [self height]) - { - var frame = [self frame]; - // Enforce a minimum height. - frame.size.height = MAX(aFloat, 20.0); - [self setFrame:frame]; - [[self superview] setNeedsDisplay:YES]; - } -} - -#pragma mark - -#pragma mark *** Accessors *** - -- (id)value -{ - return ([self dataObject] == nil) ? @"" : [[self dataObject] valueForKey:@"value"]; -} - -// You will need a way to get a reference to the canvas. -// This is often done by walking up the superview chain. -- (UICanvasView)canvas -{ - var aView = self; - while (aView = [aView superview]) { - if ([aView isKindOfClass:[UICanvasView class]]) - return aView; - } - return nil; -} - -#pragma mark - -#pragma mark *** Drawing *** - -- (void)drawRect:(CGRect)rect -{ - // 1. Draw the specific skeleton for the element subclass - [self drawSkeleton:rect]; - - // 2. If this view is a drop target, draw a highlight - if (_isDragTarget) - { - [[[CPColor redColor] colorWithAlphaComponent:0.8] setStroke]; - var highlightPath = [CPBezierPath bezierPathWithRect:CGRectInset([self bounds], 1, 1)]; - [highlightPath setLineWidth:2.0]; - [highlightPath stroke]; - } - - // 3. If selected, draw selection outline and resize handles - if ([self isSelected]) - { - // Draw selection highlight - [[CPColor keyboardFocusIndicatorColor] setStroke]; - var selectionPath = [CPBezierPath bezierPathWithRect:CGRectInset([self bounds], -2, -2)]; - [selectionPath setLineWidth:1.0]; - [selectionPath stroke]; - - // Draw resize handles ("dimples") - [self drawHandles]; - } -} - -- (void)drawSkeleton:(CGRect)rect -{ - // Base implementation: a simple placeholder box. - // Subclasses should override this to draw their specific look. - var bounds = [self bounds]; - [[CPColor lightGrayColor] setFill]; - [CPBezierPath fillRect:bounds]; - [[CPColor darkGrayColor] setStroke]; - [CPBezierPath strokeRect:bounds]; - - var valueSize = [[self value] sizeWithAttributes:_stringAttributes]; - [[self value] drawAtPoint:CGPointMake((bounds.size.width - valueSize.width) / 2.0, (bounds.size.height - valueSize.height) / 2.0) withAttributes:_stringAttributes]; -} - -- (CGRect)rectForHandle:(int)handle -{ - var bounds = [self bounds]; - var x, y; - - // Top Row - if (handle >= kUIElementTopLeftHandle && handle <= kUIElementTopRightHandle) - y = bounds.origin.y - kUIElementHandleSize / 2.0; - // Middle Row - if (handle === kUIElementMiddleLeftHandle || handle === kUIElementMiddleRightHandle) - y = bounds.origin.y + bounds.size.height / 2.0 - kUIElementHandleSize / 2.0; - // Bottom Row - if (handle >= kUIElementBottomLeftHandle && handle <= kUIElementBottomRightHandle) - y = bounds.origin.y + bounds.size.height - kUIElementHandleSize / 2.0; - - // Left Column - if (handle === kUIElementTopLeftHandle || handle === kUIElementMiddleLeftHandle || handle === kUIElementBottomLeftHandle) - x = bounds.origin.x - kUIElementHandleSize / 2.0; - // Center Column - if (handle === kUIElementTopMiddleHandle || handle === kUIElementBottomMiddleHandle) - x = bounds.origin.x + bounds.size.width / 2.0 - kUIElementHandleSize / 2.0; - // Right Column - if (handle === kUIElementTopRightHandle || handle === kUIElementMiddleRightHandle || handle === kUIElementBottomRightHandle) - x = bounds.origin.x + bounds.size.width - kUIElementHandleSize / 2.0; - - return CGRectMake(x, y, kUIElementHandleSize, kUIElementHandleSize); -} - -- (void)drawHandles -{ - [[CPColor controlDarkShadowColor] setFill]; - for (var i = 1; i <= 8; i++) - { - [CPBezierPath fillRect:[self rectForHandle:i]]; - } -} - -- (BOOL)isSelected -{ - return [[self canvas] isViewSelected:self]; -} - -#pragma mark - -#pragma mark *** Mouse Handling & Resizing *** - -- (int)handleAtPoint:(CGPoint)aPoint -{ - if (![self isSelected]) return kUIElementNoHandle; - - for (var i = 1; i <= 8; i++) - { - if (CGRectContainsPoint([self rectForHandle:i], aPoint)) - return i; - } - return kUIElementNoHandle; -} - -- (void)rightMouseDown:(CPEvent)theEvent -{ - [self mouseDown:theEvent]; -} -- (void)rightMouseUp:(CPEvent)theEvent -{ - [self mouseUp:theEvent]; -} - -- (void)mouseDown:(CPEvent)theEvent -{ - var canvas = [self canvas]; - var localPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil]; - - _lastMouseLoc = [[self canvas] convertPoint:[theEvent locationInWindow] fromView:nil]; - - // First, check if we clicked a resize handle - _activeHandle = [self handleAtPoint:localPoint]; - - // No handle was clicked, proceed with selection and movement logic - if ([theEvent modifierFlags] & CPShiftKeyMask) - { - [canvas selectView:self state:YES]; - } - else if ([theEvent modifierFlags] & CPCommandKeyMask) - { - [canvas selectView:self state:![self isSelected]]; - } - else if (![self isSelected]) - { - [canvas deselectViews]; - [canvas selectView:self state:YES]; - } -} - - - -- (void)mouseDragged:(CPEvent)theEvent -{ - var canvas = [self canvas]; - var mouseLoc = [canvas convertPoint:[theEvent locationInWindow] fromView:nil]; - - // If _lastMouseLoc is null, it means the drag started outside this view, - // so we initialize it with the current mouse location to prevent errors. - if (!_lastMouseLoc) { - _lastMouseLoc = mouseLoc; - } - - if ([theEvent modifierFlags] & CPControlKeyMask) - { - _isConnecting = YES; - // If control key is pressed, handle connection drawing - var startPointInView = CGPointMake(CGRectGetMidX([self bounds]), CGRectGetMidY([self bounds])); - var startPointInCanvas = [self convertPoint:startPointInView toView:canvas]; - - var canvasSubviews = [canvas subviews]; - for (var k = 0; k < [canvasSubviews count]; k++) { - var subview = [canvasSubviews objectAtIndex:k]; - if ([subview isKindOfClass:[UIElementView class]]) { - [subview setAsDropTarget:NO]; - } - } - var targetView = [canvas viewAtPoint:mouseLoc]; - - if (targetView && targetView != self) - { - var localPoint = [targetView convertPoint:mouseLoc fromView:canvas]; - if ([targetView canAcceptConnectionAtPoint:localPoint]) - { - var endPointInView = CGPointMake(CGRectGetMidX([targetView bounds]), CGRectGetMidY([targetView bounds])); - var endPointInCanvas = [targetView convertPoint:endPointInView toView:canvas]; - [canvas drawConnectionFrom:startPointInCanvas to:endPointInCanvas]; - [targetView setAsDropTarget:YES]; - } - else - { - [canvas drawConnectionFrom:startPointInCanvas to:mouseLoc]; - } - } - else - { - [canvas drawConnectionFrom:startPointInCanvas to:mouseLoc]; - } - } - else if (_activeHandle != kUIElementNoHandle) - { - // Resize logic - var sView = [self superview]; - var deltaX = mouseLoc.x - _lastMouseLoc.x; - var deltaY = mouseLoc.y - _lastMouseLoc.y; - - var frame = [self frame]; - var minSize = CGSizeMake(2 * kUIElementHandleSize, 2 * kUIElementHandleSize); - - // Left handles - if (_activeHandle === kUIElementTopLeftHandle || _activeHandle === kUIElementMiddleLeftHandle || _activeHandle === kUIElementBottomLeftHandle) { - if (frame.size.width - deltaX > minSize.width) { - frame.origin.x += deltaX; - frame.size.width -= deltaX; - } - } - // Right handles - if (_activeHandle === kUIElementTopRightHandle || _activeHandle === kUIElementMiddleRightHandle || _activeHandle === kUIElementBottomRightHandle) { - if (frame.size.width + deltaX > minSize.width) { - frame.size.width += deltaX; - } - } - // Top handles - if (_activeHandle === kUIElementTopLeftHandle || _activeHandle === kUIElementTopMiddleHandle || _activeHandle === kUIElementTopRightHandle) { - if (frame.size.height - deltaY > minSize.height) { - frame.origin.y += deltaY; - frame.size.height -= deltaY; - } - } - // Bottom handles - if (_activeHandle === kUIElementBottomLeftHandle || _activeHandle === kUIElementBottomMiddleHandle || _activeHandle === kUIElementBottomRightHandle) { - if (frame.size.height + deltaY > minSize.height) { - frame.size.height += deltaY; - } - } - - [self setFrame:frame]; - - _lastMouseLoc = mouseLoc; - [canvas setNeedsDisplay:YES]; - } - else - { - // This is the move logic, largely from the original EFView. - [[CPCursor closedHandCursor] set]; - var deltaX = mouseLoc.x - _lastMouseLoc.x; - var deltaY = mouseLoc.y - _lastMouseLoc.y; - - for (var i = 0; i < [[canvas selectedSubViews] count]; i++) - { - var view = [canvas selectedSubViews][i]; - var newOrigin = CGPointMake([view frame].origin.x + deltaX, [view frame].origin.y + deltaY); - - var parentView = [view superview]; - if ([parentView isKindOfClass:[UIWindowView class]]) - { - var parentBounds = [parentView bounds]; - var viewFrame = [view frame]; - newOrigin.x = MAX(0, MIN(newOrigin.x, parentBounds.size.width - viewFrame.size.width)); - newOrigin.y = MAX(0, MIN(newOrigin.y, parentBounds.size.height - viewFrame.size.height)); - } - - [view setFrameOrigin:newOrigin]; - } - - _lastMouseLoc = mouseLoc; - [canvas setNeedsDisplay:YES]; - } -} - -- (void)mouseUp:(CPEvent)theEvent -{ - var canvas = [self canvas]; - var mouseLoc = [canvas convertPoint:[theEvent locationInWindow] fromView:nil]; - - if (_isConnecting) - { - // Handle mouse up for connection - var targetView = [canvas viewAtPoint:mouseLoc]; - - if (targetView && targetView != self) - { - var localPoint = [targetView convertPoint:mouseLoc fromView:canvas]; - if ([targetView canAcceptConnectionAtPoint:localPoint]) - { - [canvas showConnectionMenuForSource:self target:targetView at:mouseLoc]; - } - else - { - [canvas clearConnection]; - } - } - else - { - [canvas clearConnection]; - } - - var canvasSubviews = [canvas subviews]; - - for (var k = 0; k < [canvasSubviews count]; k++) { - var subview = [canvasSubviews objectAtIndex:k]; - if ([subview isKindOfClass:[UIElementView class]] && subview != targetView) { - [subview setAsDropTarget:NO]; - } - } - [canvas setNeedsDisplay:YES]; - _isConnecting = NO; - } - else if (_activeHandle != kUIElementNoHandle) - { - // Handle mouse up for resize - [[CPCursor arrowCursor] set]; - _activeHandle = kUIElementNoHandle; - _lastMouseLoc = null; - [canvas setNeedsDisplay:YES]; - [canvas elementDidResize:self]; - } - else - { - // Handle mouse up for move - [[CPCursor openHandCursor] set]; - _lastMouseLoc = null; - [canvas setNeedsDisplay:YES]; - [canvas elementDidMove:self]; - } -} - -- (void)_resizeWithEvent:(CPEvent)theEvent -{ - var sView = [self superview]; - var canvas = [self canvas]; - var mouseLoc; - - switch ([theEvent type]) - { - case CPLeftMouseDragged: - [[CPCursor crosshairCursor] set]; // A generic resize cursor - mouseLoc = [sView convertPoint:[theEvent locationInWindow] fromView:nil]; - var deltaX = mouseLoc.x - _lastMouseLoc.x; - var deltaY = mouseLoc.y - _lastMouseLoc.y; - - var frame = [self frame]; - var minSize = CGSizeMake(2 * kUIElementHandleSize, 2 * kUIElementHandleSize); - - // Left handles - if (_activeHandle === kUIElementTopLeftHandle || _activeHandle === kUIElementMiddleLeftHandle || _activeHandle === kUIElementBottomLeftHandle) { - if (frame.size.width - deltaX > minSize.width) { - frame.origin.x += deltaX; - frame.size.width -= deltaX; - } - } - // Right handles - if (_activeHandle === kUIElementTopRightHandle || _activeHandle === kUIElementMiddleRightHandle || _activeHandle === kUIElementBottomRightHandle) { - if (frame.size.width + deltaX > minSize.width) { - frame.size.width += deltaX; - } - } - // Top handles - if (_activeHandle === kUIElementTopLeftHandle || _activeHandle === kUIElementTopMiddleHandle || _activeHandle === kUIElementTopRightHandle) { - if (frame.size.height - deltaY > minSize.height) { - frame.origin.y += deltaY; - frame.size.height -= deltaY; - } - } - // Bottom handles - if (_activeHandle === kUIElementBottomLeftHandle || _activeHandle === kUIElementBottomMiddleHandle || _activeHandle === kUIElementBottomRightHandle) { - if (frame.size.height + deltaY > minSize.height) { - frame.size.height += deltaY; - } - } - - [self setFrame:frame]; - - _lastMouseLoc = mouseLoc; - [canvas setNeedsDisplay:YES]; - [CPApp setTarget:self selector:@selector(_resizeWithEvent:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES]; - break; - case CPLeftMouseUp: - [[CPCursor arrowCursor] set]; - _activeHandle = kUIElementNoHandle; - _lastMouseLoc = null; - [canvas setNeedsDisplay:YES]; - [canvas elementDidResize:self]; - break; - } -} - -- (void)setAsDropTarget:(BOOL)isTarget -{ - if (_isDragTarget !== isTarget) - { - _isDragTarget = isTarget; - [self setNeedsDisplay:YES]; - } -} - -- (void)_connectWithEvent:(CPEvent)theEvent -{ - var canvas = [self canvas]; - var mouseLoc = [canvas convertPoint:[theEvent locationInWindow] fromView:nil]; - - // Convert the start point (center of the view) to the canvas's coordinate system - var startPointInView = CGPointMake(CGRectGetMidX([self bounds]), CGRectGetMidY([self bounds])); - var startPointInCanvas = [self convertPoint:startPointInView toView:canvas]; - - var canvasSubviews = [canvas subviews]; - for (var k = 0; k < [canvasSubviews count]; k++) { - var subview = [canvasSubviews objectAtIndex:k]; - if ([subview isKindOfClass:[UIElementView class]]) { - [subview setAsDropTarget:NO]; - } - } - var targetView = [canvas viewAtPoint:mouseLoc]; - var validTargetFound = NO; - var endPointForDrawing = mouseLoc; // Default to follow mouse - - if (targetView && targetView != self) - { - if ([targetView isKindOfClass:[UIWindowView class]]) - { - validTargetFound = YES; - // Snap to center of the window for drawing feedback - endPointForDrawing = CGPointMake(CGRectGetMidX([targetView bounds]), CGRectGetMidY([targetView bounds])); - endPointForDrawing = [targetView convertPoint:endPointForDrawing toView:canvas]; - } - else - { - // For non-window elements, allow connection anywhere on their bounds - validTargetFound = YES; - endPointForDrawing = mouseLoc; // Follow mouse for other elements during drag - } - } - - if ([theEvent type] == CPLeftMouseDragged) - { - if (validTargetFound) - { - [canvas drawConnectionFrom:startPointInCanvas to:endPointForDrawing]; - [targetView setAsDropTarget:YES]; - } - else - { - [canvas drawConnectionFrom:startPointInCanvas to:mouseLoc]; - } - } - else if ([theEvent type] == CPLeftMouseUp) - { - // For final connection, snap to center of non-window elements, or title bar for windows - var finalEndPoint = mouseLoc; - var currentValidTarget = validTargetFound; // Store the initial state - - if (targetView && targetView != self) { - finalEndPoint = CGPointMake(CGRectGetMidX([targetView bounds]), CGRectGetMidY([targetView bounds])); - finalEndPoint = [targetView convertPoint:finalEndPoint toView:canvas]; - } else { - currentValidTarget = NO; // No valid target or target is self - } - - if (currentValidTarget) { - [[self canvas] elementDidConnect:self to:targetView atPoint:finalEndPoint]; // Pass finalEndPoint - } - [[self canvas] clearConnection]; - var canvasSubviews = [canvas subviews]; - for (var k = 0; k < [canvasSubviews count]; k++) { - var subview = [canvasSubviews objectAtIndex:k]; - if ([subview isKindOfClass:[UIElementView class]]) { - [subview setAsDropTarget:NO]; - } - } - } -} - -- (void)mouseEntered:(CPEvent)theEvent -{ - [[CPCursor openHandCursor] set]; -} - -- (void)mouseExited:(CPEvent)theEvent -{ - [[CPCursor arrowCursor] set]; -} - -- (void)mouseMoved:(CPEvent)theEvent -{ - var localPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil]; - var handle = [self handleAtPoint:localPoint]; - - if (handle != kUIElementNoHandle) { - // In a full implementation, you could return a specific two-headed arrow cursor - // based on the handle. For now, we use a generic one. - [[CPCursor crosshairCursor] set]; - } else { - [[CPCursor openHandCursor] set]; - } -} - -- (id)nativeUIElement -{ - return [self nativeUIElementWithMap:nil]; -} - -- (id)nativeUIElementWithMap:(CPMutableDictionary)aMap -{ - // Base implementation returns a generic view with a red background to indicate it's not a real UI element. - var view = [[CPView alloc] initWithFrame:[self frame]]; - [view setBackgroundColor:[CPColor redColor]]; - - if (aMap) - { - var elementID = [[self dataObject] valueForKey:@"id"]; - [aMap setObject:view forKey:elementID]; - } - - return view; -} - -- (BOOL)canAcceptConnectionAtPoint:(CGPoint)aPoint -{ - // By default, any part of the view can be a connection target. - return YES; -} - -@end - - -#pragma mark - -#pragma mark *** UI Element Subclasses *** - -// ================================================================================================= -// UIWindowView -// A skeleton that looks like a window, and can act as a drop target. -// ================================================================================================= - -var _windowChildrenObservationContext = 1094; - -@implementation UIWindowView : UIElementView -{ - CGPoint _rubberStart; - CGPoint _rubberEnd; - BOOL _isRubbing; -} - -+ (CPDictionary)propertyTypes -{ - var types = [super propertyTypes]; - [types setObject:UIBBoolean forKey:@"CPHUDBackgroundWindowMask"]; - [types setObject:UIBBoolean forKey:@"CPTitledWindowMask"]; - [types setObject:UIBBoolean forKey:@"CPClosableWindowMask"]; - return types; -} - -+ (CPArray)persistentProperties -{ - return [super persistentProperties].concat(["CPHUDBackgroundWindowMask", "CPTitledWindowMask", "CPClosableWindowMask"]); -} - -+ (CPDictionary)defaultValues -{ - return { - value: "Untitled Window", - CPHUDBackgroundWindowMask: true, - CPTitledWindowMask: true, - CPClosableWindowMask: true, - outlets: "delegate", - actions: "makeKeyAndOrderFront:, orderOut:" - }; -} - -- (void)drawRect:(CGRect)rect -{ - [super drawRect:rect]; - - if (_isRubbing) - { - var rubber = CGRectUnion(CGRectMake(_rubberStart.x, _rubberStart.y, 0.1, 0.1), CGRectMake(_rubberEnd.x, _rubberEnd.y, 0.1, 0.1)); - [[[[CPColor alternateSelectedControlColor] colorWithAlphaComponent:0.2] setFill]]; - [CPBezierPath fillRect:rubber]; - [[CPColor alternateSelectedControlColor] setStroke]; - [CPBezierPath setDefaultLineWidth:1.0]; - [CPBezierPath strokeRect:rubber]; - } -} - -- (void)mouseDown:(CPEvent)theEvent -{ - var localPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil]; - var titleBarHeight = 30.0; - - // 1. Check for resize handle click first. - if ([self handleAtPoint:localPoint] != kUIElementNoHandle) { - [super mouseDown:theEvent]; - return; - } - - // 2. Check if the click is within the title bar area. - if (localPoint.y <= titleBarHeight) { - // Click is in the title bar. Allow the superclass to handle moving the window. - [super mouseDown:theEvent]; - return; - } - - // On a click into the window's content area, deselect all elements. - [[self canvas] deselectViews]; - - _rubberStart = localPoint; - _rubberEnd = _rubberStart; - _isRubbing = YES; - [CPApp setTarget:self selector:@selector(_dragOpenSpaceWithEvent:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES]; -} - -- (void)_dragOpenSpaceWithEvent:(CPEvent)theEvent -{ - var canvas = [self canvas]; - var mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; - _rubberEnd = mouseLoc; - var rubberRect = CGRectUnion(CGRectMake(_rubberStart.x, _rubberStart.y, 1, 1), CGRectMake(_rubberEnd.x, _rubberEnd.y, 1, 1)); - - switch ([theEvent type]) - { - case CPLeftMouseDragged: - var indexesToSelect = [CPMutableIndexSet indexSet]; - var allDataObjects = [canvas dataObjects]; - - for (var i = 0; i < [[self subviews] count]; i++) { - var aView = [self subviews][i]; - if (CGRectIntersectsRect([aView frame], rubberRect)) { - var dataIndex = [allDataObjects indexOfObject:[aView dataObject]]; - if (dataIndex != CPNotFound) { - [indexesToSelect addIndex:dataIndex]; - } - } - } - [canvas setSelectionIndexes:indexesToSelect]; - [self setNeedsDisplay:YES]; - [CPApp setTarget:self selector:@selector(_dragOpenSpaceWithEvent:) forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask untilDate:nil inMode:nil dequeue:YES]; - break; - - case CPLeftMouseUp: - _isRubbing = NO; - [self setNeedsDisplay:YES]; - break; - } -} - -- (void)dealloc -{ - [self setDataObject:nil]; - [super dealloc]; -} - -- (void)setDataObject:(id)newDataObject -{ - var oldDataObject = [self dataObject]; - - if (newDataObject != oldDataObject) - { - if (oldDataObject) - [oldDataObject removeObserver:self forKeyPath:@"children" context:_windowChildrenObservationContext]; - - [super setDataObject:newDataObject]; - - if (newDataObject) - { - [newDataObject addObserver:self forKeyPath:@"children" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:_windowChildrenObservationContext]; - [self _addChildrenViews:[newDataObject valueForKey:@"children"]]; - } - } -} - -- (void)_addChildrenViews:(CPArray)childDataObjects -{ - if (!childDataObjects) return; - - var canvas = [self superview]; - - for (var i = 0; i < [childDataObjects count]; i++) - { - var childData = childDataObjects[i]; - // This is a bit of a hack. We are reaching into the canvas's private method. - // A better solution would be a dedicated ViewFactory or similar. - if ([canvas respondsToSelector:@selector(_createViewForDataObject:superview:)]) - [canvas _createViewForDataObject:childData superview:self]; - } -} - -- (void)_removeChildrenViews:(CPArray)childDataObjects -{ - if (!childDataObjects) return; - - var canvas = [self superview]; - var viewsToRemove = []; - var subviews = [self subviews]; - - for (var i = 0; i < [subviews count]; i++) - { - var subview = subviews[i]; - if ([childDataObjects containsObject:[subview dataObject]]) - [viewsToRemove addObject:subview]; - } - - for (i = 0; i < [viewsToRemove count]; i++) - { - if ([canvas respondsToSelector:@selector(_removeViewAndChildren:)]) - [canvas _removeViewAndChildren:viewsToRemove[i]]; - } -} - - -- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context -{ - if (context == _windowChildrenObservationContext) - { - var oldChildren = [change objectForKey:CPKeyValueChangeOldKey]; - var newChildren = [change objectForKey:CPKeyValueChangeNewKey]; - - var added = [newChildren mutableCopy]; - [added removeObjectsInArray:oldChildren]; - [self _addChildrenViews:added]; - - var removed = [oldChildren mutableCopy]; - [removed removeObjectsInArray:newChildren]; - [self _removeChildrenViews:removed]; - } - else - { - [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; - } -} - - -- (id)initWithFrame:(CGRect)aRect -{ - self = [super initWithFrame:aRect]; - if (self) { - - if (CGRectIsEmpty(aRect)) { - [self setFrameSize:CGSizeMake(250, 200)]; - } - _isContainer = YES; - - // This view can accept drops of other elements. - [self registerForDraggedTypes:[ - UIButtonDragType, - UISliderDragType, - UITextFieldDragType - ]]; - } - return self; -} - -- (void)drawSkeleton:(CGRect)rect -{ - var bounds = [self bounds]; - var titleBarHeight = 22.0; - - // Main window background - [[[CPColor windowBackgroundColor] colorWithAlphaComponent:0.9] setFill]; - var bgPath = [CPBezierPath bezierPathWithRoundedRect:bounds radius:6.0]; - [bgPath fill]; - - // Title bar - var titleBarRect = CGRectMake(bounds.origin.x, bounds.origin.y, bounds.size.width, titleBarHeight); - var titleBarPath = [CPBezierPath bezierPathWithRoundedRect:titleBarRect xRadius:6.0 yRadius:6.0]; - [[[CPColor secondarySelectedControlColor] colorWithAlphaComponent:0.6] setFill]; - [titleBarPath fill]; - - // Window border - [[CPColor darkGrayColor] setStroke]; - [bgPath setLineWidth:1.0]; - [bgPath stroke]; - - // Value text - [_stringAttributes setObject:[CPColor whiteColor] forKey:CPForegroundColorAttributeName]; - var valueSize = [[self value] sizeWithAttributes:_stringAttributes]; - [[self value] drawAtPoint:CGPointMake((bounds.size.width - valueSize.width) / 2.0, (titleBarHeight - valueSize.height) / 2.0 - 4) withAttributes:_stringAttributes]; - [_stringAttributes setObject:[CPColor blackColor] forKey:CPForegroundColorAttributeName]; // reset color - - // Traffic light buttons - var circleRadius = 5.0; - var startX = 10.0; - var startY = titleBarHeight / 2.0; - [[CPColor redColor] setFill]; - [CPBezierPath fillRect:CGRectMake(startX, startY - circleRadius, circleRadius*2, circleRadius*2)]; - [[CPColor orangeColor] setFill]; - [CPBezierPath fillRect:CGRectMake(startX + 18, startY - circleRadius, circleRadius*2, circleRadius*2)]; - [[CPColor greenColor] setFill]; - [CPBezierPath fillRect:CGRectMake(startX + 36, startY - circleRadius, circleRadius*2, circleRadius*2)]; -} - -// --- Drag Destination Methods --- - -- (CPDragOperation)draggingEntered:(CPDraggingInfo)sender -{ - var pasteboard = [sender draggingPasteboard]; - var acceptedTypes = [self registeredDraggedTypes]; - var localPoint = [self convertPoint:[sender draggingLocation] fromView:nil]; - var titleBarHeight = 30.0; - - // Check if the dragged type is a new UI element (from the palette) - if ([acceptedTypes containsObject:UIWindowDragType] || [acceptedTypes containsObject:UIButtonDragType] || [acceptedTypes containsObject:UISliderDragType] || [acceptedTypes containsObject:UITextFieldDragType]) - { - _isDragTarget = YES; - [self setNeedsDisplay:YES]; - return CPDragOperationGeneric; - } - // Check if it's a connection drag (control key is pressed) - else if ([sender draggingSourceOperationMask] & CPControlKeyMask && localPoint.y <= titleBarHeight) - - { - debugger - _isDragTarget = YES; - [self setNeedsDisplay:YES]; - return CPDragOperationGeneric; - } - - return CPDragOperationNone; -} - -- (CPDragOperation)draggingUpdated:(CPDraggingInfo)sender -{ - var localPoint = [self convertPoint:[sender draggingLocation] fromView:nil]; - var titleBarHeight = 30.0; - var acceptedTypes = [self registeredDraggedTypes]; - - // Check if the dragged type is a new UI element (from the palette) - if ([acceptedTypes containsObject:UIButtonDragType] || [acceptedTypes containsObject:UISliderDragType] || [acceptedTypes containsObject:UITextFieldDragType]) - { - _isDragTarget = YES; - [self setNeedsDisplay:YES]; - return CPDragOperationGeneric; - } - // Check if it's a connection drag (control key is pressed) - else if ([sender draggingSourceOperationMask] & CPControlKeyMask && localPoint.y <= titleBarHeight) - { - _isDragTarget = YES; - [self setNeedsDisplay:YES]; - return CPDragOperationGeneric; - } - else - { - _isDragTarget = NO; - [self setNeedsDisplay:YES]; - return CPDragOperationNone; - } -} - -- (void)draggingExited:(CPDraggingInfo)sender -{ - _isDragTarget = NO; - [self setNeedsDisplay:YES]; -} - -- (BOOL)performDragOperation:(CPDraggingInfo)sender -{ - var dropPoint = [self convertPoint:[sender draggingLocation] fromView:nil]; - var pasteboard = [sender draggingPasteboard]; - var types = [pasteboard types]; - var draggedType = types[0]; - var elementType; - - // Determine if it's a new UI element drop - if (draggedType === UIButtonDragType) elementType = "button"; - else if (draggedType === UISliderDragType) elementType = "slider"; - else if (draggedType === UITextFieldDragType) elementType = "textfield"; - - if (elementType) - { - // We need to find the canvas and then the delegate - var canvas = [self superview]; - var delegate = [canvas delegate]; - if (delegate && [delegate respondsToSelector:@selector(addNewElementOfType:atPoint:)]) - { - var canvasPoint = [self convertPoint:dropPoint toView:canvas]; - [delegate addNewElementOfType:elementType atPoint:canvasPoint]; - } - } - // If it's a connection drag, the logic is handled in _connectWithEvent: in UIElementView - - _isDragTarget = NO; - [self setNeedsDisplay:YES]; - - return YES; -} - -- (id)nativeUIElementWithMap:(CPMutableDictionary)aMap -{ - var newPlatformWindow = [[CPPlatformWindow alloc] initWithContentRect:[self frame]]; - - var styleMask = 0; - if ([[self dataObject] valueForKey:@"CPHUDBackgroundWindowMask"]) styleMask |= CPHUDBackgroundWindowMask; - if ([[self dataObject] valueForKey:@"CPTitledWindowMask"]) styleMask |= CPTitledWindowMask; - if ([[self dataObject] valueForKey:@"CPClosableWindowMask"]) styleMask |= CPClosableWindowMask; - - var theNewWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0, 0, [self frame].size.width, [self frame].size.height) styleMask:styleMask]; - [theNewWindow setPlatformWindow:newPlatformWindow]; - - if (aMap) - { - var elementID = [[self dataObject] valueForKey:@"id"]; - [aMap setObject:theNewWindow forKey:elementID]; - } - - var contentView = [theNewWindow contentView]; - var subviews = [self subviews]; - for (var i = 0; i < [subviews count]; i++) - { - var subview = subviews[i]; - var nativeSubview = [subview nativeUIElementWithMap:aMap]; - [contentView addSubview:nativeSubview]; - } - - return theNewWindow; -} - -- (BOOL)canAcceptConnectionAtPoint:(CGPoint)aPoint -{ - var titleBarHeight = 22.0; - return aPoint.y <= titleBarHeight; -} - -@end - - -// ================================================================================================= -// UIButtonView -// A skeleton that looks like a push button. -// ================================================================================================= -@implementation UIButtonView : UIElementView - -+ (CPDictionary)defaultValues -{ - return {value: "Button", outlets: "target, delegate", actions: "takeValueFrom:"}; -} - -+ (CPDictionary)propertyTypes -{ - return [super propertyTypes].copy({value: UIBString}); -} -- (id)initWithFrame:(CGRect)aRect -{ - self = [super initWithFrame:aRect]; - if (self) { - if (CGRectIsEmpty(aRect)) { - [self setFrameSize:CGSizeMake(100, 24)]; - } - } - return self; -} - -- (void)drawSkeleton:(CGRect)rect -{ - var bounds = CGRectInset([self bounds], 1, 1); - - // Draw button shape with gradient - var buttonPath = [CPBezierPath bezierPathWithRoundedRect:bounds radius:5.0]; - var gradient = [[CPGradient alloc] initWithStartingColor:[CPColor whiteColor] - endingColor:[CPColor controlColor]]; - [gradient drawInBezierPath:buttonPath angle:90]; - - // Draw button border - [[CPColor grayColor] setStroke]; - [buttonPath setLineWidth:1.0]; - [buttonPath stroke]; - - // Draw value - var valueSize = [[self value] sizeWithAttributes:_stringAttributes]; - [[self value] drawAtPoint:CGPointMake((bounds.size.width - valueSize.width) / 2.0 + 1, (bounds.size.height - valueSize.height) / 2.0 - 2) withAttributes:_stringAttributes]; -} - -- (id)nativeUIElementWithMap:(CPMutableDictionary)aMap -{ - var button = [[CPButton alloc] initWithFrame:[self frame]]; - [button setTitle:[self value]]; - - if (aMap) - { - var elementID = [[self dataObject] valueForKey:@"id"]; - [aMap setObject:button forKey:elementID]; - } - - return button; -} - -@end - -// ================================================================================================= -// UISliderView -// A skeleton that looks like a slider. -// ================================================================================================= -@implementation UISliderView : UIElementView - -+ (CPDictionary)defaultValues -{ - return {value: 0.5, outlets: "target, delegate", actions: "takeFloatValueFrom:, takeIntegerValueFrom:"}; -} - -+ (CPDictionary)propertyTypes -{ - return [super propertyTypes].copy({value: UIBNumber}); -} -- (id)initWithFrame:(CGRect)aRect -{ - self = [super initWithFrame:aRect]; - if (self) { - if (CGRectIsEmpty(aRect)) { - [self setFrameSize:CGSizeMake(150, 20)]; - } - } - return self; -} - -- (void)drawSkeleton:(CGRect)rect -{ - var bounds = CGRectInset([self bounds], 8, 0); - var midY = bounds.size.height / 2.0; - - // Draw track - [[CPColor grayColor] setStroke]; - var trackPath = [CPBezierPath bezierPath]; - [trackPath setLineWidth:3.0]; - [trackPath moveToPoint:CGPointMake(bounds.origin.x, midY)]; - [trackPath lineToPoint:CGPointMake(bounds.origin.x + bounds.size.width, midY)]; - [trackPath stroke]; - - // Draw knob - var knobX = bounds.origin.x + bounds.size.width * [self value]; - var knobRect = CGRectMake(knobX - 8, midY - 8, 16, 16); - var knobPath = [CPBezierPath bezierPathWithOvalInRect:knobRect]; - [[CPColor whiteColor] setFill]; - [knobPath fill]; - [[CPColor darkGrayColor] setStroke]; - [knobPath setLineWidth:1.0]; - [knobPath stroke]; -} - -- (id)nativeUIElementWithMap:(CPMutableDictionary)aMap -{ - var slider = [[CPSlider alloc] initWithFrame:[self frame]]; - [slider setFloatValue:[self value]]; - - if (aMap) - { - var elementID = [[self dataObject] valueForKey:@"id"]; - [aMap setObject:slider forKey:elementID]; - } - - return slider; -} - -@end - -// ================================================================================================= -// UITextFieldView -// A skeleton that looks like a text field. -// ================================================================================================= -@implementation UITextFieldView : UIElementView - -+ (CPDictionary)defaultValues -{ - return {value: "Text Field", outlets: "target, delegate", actions: "takeStringValueFrom:, takeIntegerValueFrom:"}; -} - -+ (CPDictionary)propertyTypes -{ - return [super propertyTypes].copy({value: UIBString}); -} -- (id)initWithFrame:(CGRect)aRect -{ - self = [super initWithFrame:aRect]; - if (self) - { - if (CGRectIsEmpty(aRect)) - { - [self setFrameSize:CGSizeMake(150, 22)]; - } - [_stringAttributes setObject:[CPFont systemFontOfSize:12] forKey:CPFontAttributeName]; - [_stringAttributes setObject:[CPColor grayColor] forKey:CPForegroundColorAttributeName]; - } - return self; -} - -- (void)drawSkeleton:(CGRect)rect -{ - var bounds = CGRectInset([self bounds], 1, 1); - - // Background - [[CPColor textBackgroundColor] setFill]; - [CPBezierPath fillRect:bounds]; - - // Inset border - [[CPColor grayColor] setStroke]; - [CPBezierPath strokeRect:bounds]; - - // Draw placeholder value - var valueSize = [[self value] sizeWithAttributes:_stringAttributes]; - [[self value] drawAtPoint:CGPointMake(5, (bounds.size.height - valueSize.height) / 2.0 - 2) withAttributes:_stringAttributes]; -} - -- (id)nativeUIElementWithMap:(CPMutableDictionary)aMap -{ - var textField = [[CPTextField alloc] initWithFrame:[self frame]]; - [textField setStringValue:[self value]]; - - if (aMap) - { - var elementID = [[self dataObject] valueForKey:@"id"]; - [aMap setObject:textField forKey:elementID]; - } - - return textField; -} - -@end diff --git a/Tests/Manual/UIBuilderDemo/index.html b/Tests/Manual/UIBuilderDemo/index.html deleted file mode 100644 index cea729b96..000000000 --- a/Tests/Manual/UIBuilderDemo/index.html +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - UIBuilderDemo - - - - - - - - - - - - -
-
-
- -
-
- -
- - diff --git a/Tests/Manual/UIBuilderDemo/main.j b/Tests/Manual/UIBuilderDemo/main.j deleted file mode 100755 index 993c34a35..000000000 --- a/Tests/Manual/UIBuilderDemo/main.j +++ /dev/null @@ -1,10 +0,0 @@ -@import -@import - -@import "UIElementView.j" -@import "AppController.j" - -function main(args, namedArgs) -{ - CPApplicationMain(); -} From e3fd7a295c2c8652af0498be16554ff419c60a9c Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 17 Jul 2025 19:41:48 +0200 Subject: [PATCH 029/103] fixed: ipad keyboard does not show --- AppKit/CPTextView/CPTextView.j | 3 +++ dist/cappuccino/bin/flatten | 2 +- dist/cappuccino/bin/fontinfo | Bin 69888 -> 70736 bytes dist/cappuccino/bin/imagesize | Bin 69536 -> 0 bytes dist/cappuccino/bin/objj2objcskeleton | 22 ++++++++++++++++------ 5 files changed, 20 insertions(+), 7 deletions(-) delete mode 100755 dist/cappuccino/bin/imagesize diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 0b0f00253..2763faac4 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -1100,6 +1100,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 focusForTextView:self]; + [_CPNativeInputManager cancelCurrentInputSessionIfNeeded]; [_caret setVisibility:NO]; diff --git a/dist/cappuccino/bin/flatten b/dist/cappuccino/bin/flatten index 1df033252..80b9ecf8e 100755 --- a/dist/cappuccino/bin/flatten +++ b/dist/cappuccino/bin/flatten @@ -233,7 +233,7 @@ ObjectiveJFlattener.prototype.serializeFunctions = function() { var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK var relative = this.rootPath.relative(path).toString(); - this.functionsBuffer.push("ObjectiveJ.StaticResource._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); + this.functionsBuffer.push("ObjectiveJ.FileExecutable._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); } var bundle = this.context.global.CFBundle.bundleContainingURL(path); diff --git a/dist/cappuccino/bin/fontinfo b/dist/cappuccino/bin/fontinfo index f1ff06158a2525d34541aff240713c0067bd8633..1a91ba1c8040bbcfe787719cb7e44c0e7578d180 100755 GIT binary patch literal 70736 zcmeI5eQ;FO6~OQACJ;h^1yK+YmgU3nk-PvA!-B9OAAH3mLl%j&?3ZpyJRPbmx_ioUv5-(a^H?;YI6xLpX%D8sm06b*U1!3|K^`Od)H zF<&b#1`67|YBdzFq*bwgMNv26em&ikoo`jKl@A$UK|3>F5zhw<%_54be@kE>X?DH? zJYO$|hBmG*U2^6Z&<#zAw($*M)O^CW0z zmfU>tXonIFib^!3LdWd+ofyTh2Ty`_c8<*axKFBpjVRVu`>Ms7rp6X)w|My`1#Olr z?SRIVIH4|zZED6g88Tmr8-sippDMWDQhN^DL@^M`yb#&*+wQgIm&DFc(9X!3uWHW|@n=j(%b+#*Fh2JL*@_CsGQ4-Z0>%;E;NS-h4?G2`12;;sEFhc!6_x5ZHT z(%h1R*9m(W@u$nc`O&btacy1g>eRj8!{?RcUBSlE%>6i@)SR%r5>{d+?g0ZpEKJ!i zFy^wF*hJ8!V9x>V`ZHrEU~wa%-VA~T=YoQHCxHx_DA<|h@ShmFJhOqej%6$2f@a26$sAb@c8auexlAlz4OWG$#=rDN*6$z3k1hzg=0*?y17RR_ZHxs zGv`wZ>%%;Feq|OMw-p3^)_%a6jR`FY-H;s8L<0wp&MgVQXy-obGzVT%opvzA`IA~Q0ZQ{mDvaMG526{qT0Y^`E^`C2R= z%mwqdgAC^Mz*pt1Q8jsh#h2yB3z69e^3MXn{osQAjAy`XZsY#Oay1D0oi8!wU5hOj zS1$a~e&mC>1(B1}3nS;+hIRFc!@EA+P}Fs{X++og9mQSeciKD8I&9saI9NCA!F{aI z+rO8w`#%QzP%+!~asl)09_H=8(&k;f{5iJm4~+SW3cZV`{>HZLT?g}RWXW}~FUNa8 zUkCBMqrLsD!`aw7Hr~?r3FP?{@-U}7Wvb)yt*{hda^v>CZO~ri+C1g7jrm$fva#m4 z^S%A=Ij-nF489&aJBV=@^N^kSFy|c*_b&K8gZ8m7UL}mz>$t4@5oqgmjPE|bV-2in zHLPtFtZ`-6xwiVQlhf-WAIzt80y7lzlq%|q5d4q-R!s6+mE`a%-ip< zS!=^}&ADdQS>kE~9=$Mtw>~ z-JMaNW7V^FaW&+f#=QMz&Pi4u7jwOl64PIh$6u7kUy{e~%;O)Q$3HQTe{vqbE02Fh z9)D>b|GYf@iah?xJpQVo{X5(tDWZfEjqtc~d)za#)L8Be(w=qMD%Pb^?b(s~jnL1nVtFLOYUaX~9 z46<5=ho#0~K{+JF!$w0oX}zk|t5L%P9zHG$Dgk`{!2@@LVzf8;x5xp*6H>Lcass+( zk`je6Q_Eu^h^~cLO^F$*W||m=OZVu?EwYCxQN_R}J{ie?8T#Xfta})CVAdNb_JGMm zB$&%U%fO1DH4y=ZO27wGFw8%0Y;a>u<~LWSwnH1fV|YQM;NLe`yyow~55A|Az;o9L zf@O?@!4X7-2oM1xKm>>Y5g-CYfCvx)B0vO)01+SpM1Tko0U|&IhyW2F0z`la5CI}U z1c(3;AOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)01+SpMBu+ez?rnZ>To9Udl2}lFlm05 z3flU}!$6m6vWWK|!L{}Ih%<@b-C`d6UJ^BYOloTUeim)`0v3uZ zdDu$Ctd1_$X12rkC0YIxW+GSmqmyj^Y3{e?nH&GHH4*;xiZzIrk41_I5CI}U1c(3; zAOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)01+SpM1Tko0U|&IhyW2F0z`la5CI}U1c(3; zAOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)01+SpM1Tko0U|&Ih`={Z!20*yoy=i=pzma3 zxt_#zCD+Ti{ub8~*I}-AaJ`4?F0S!kk%sU6Sr6AoP5n);n#LyrM1Tko0U|&IhyW2F z0z`la5CI}U1c(3;AOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)01+SpM1Tko0U|&IhyW2F z0z`la5CI}U1c(3;AOb{y2oM1xKm>>Y5g-CYfCvx)BJjUQzy(uk2YCX-31S0DmBLg7 z^hA&fZeyK!hk@`0P#BkPPY!Ok+ZnUhg4_wx!VgWOVMh8UGQxW&h3qGYHc zCdOc*2I$6?dNpdKSI{U$FbzCTls~S|wm8YE;q^OcceM`WiK= z8&cG;)>0FebbXy1k~KLRkkePkCW?`EQQj^GfTh?jOF_td6}kaBV`s+TFJmFNxKlvR z47aXx7;Oh*2Z}9w7q@%3{WEU&a{JfZW+Scsz1((jyN}ymZcm5NG5=<6U&rkvx0|_r zfZGvn_i%d;w|lw$9Jg7CHU3|@?c(++ZhN^s8pbxq=Qd1_1@VWMx!@0`4A3`a*+H;F z8Fvzfmcvhq-#g%+kzu2M21qN+0}I-XS^muNvD+9M-mO;OD=eM9UFTXh&&F6omL0Zi zxPh&5w`Hf}AI!3UYuV|1f6KBzvg~xeu`qY|uK=3OGQqN8)mE9FWmjg|E3)k7EPIn> zr}IyTw8(EJ$Tc9PAhSVmCr<`(flLEQ^_d0rR6mTxzLP+vfJ_Cs8U%|AZl&D&op*W94StjjoQ0QkE~ZYzh@?aZpQkg{`|7I(ox=Q}ogu|y zaNb%jPganm2f&ACqdCD;NKg(*@vzaLMCDbo($;R&s~RHA4iCyh#HUZz4T{m;QkdrnM@vC7a;DaR_Iuq9GvZf0`$*;;4^95ZC z2m!cO@jyU{s)7%m!McEJQlcSMV5wSp}Kmi>19z|Vpx#0 z$fC-DJ}MKSRkHD!Ud+<>Jc9K-K(8N%GGRUAri>aK0R@ zS||k;EUc;s1{VbuELu>-zUW+X=SbVD1#dm_{L~ZD_=Am!Blnd&`TE*tMpr$xe7Enz zRO+Ghvkp11>g2=AtBQWV<9GFr!|c1Ko?W@^?O?~Tr|-G!X5-VL;x4v*f5Nq|@#pgB z`1T(bt~v0`y2dTZLoZHxxUBE>>mHj>(|=dKJWF=|DtIRT&ZE6=9$vch+pTvu-22M+ zl)vwp)B594V@D;&H9U3ulh3xQe>oZIDR}e9k*)To*1`+79=!FIU2i@AgH_+3_xPpy zXWPp6?LE5h*pf}7KHoI?#s@!hANc&iSLU+}i`#YJ0=c`nrLdF6gjaCW0{ z&utyWk3QP=qw|M6a3{cX+1lynj&IurcfyjT%eP0uuAAX#fOpgD-Sf*U+%9+nRfF)- ze!Uw`m$EAN@@2(Kr`0yq_-<;hbH&1nZn&D)uV`3Z<8qe?LUk+_mIa~KSLKa2tr3kM>!ldVYxh@MzEtUG;3;1){I00BrAh}@}LoP!!TC%LDPZO zARO?^ibpKnDkqjf3Z4v8Dgg;!s>_t13`bW;k+r1)_JYpV8M$n!z$-cNI=obCijgR9 zlZ`S%(zll3UlJYZ1cnxu1sGkhhF>a}lUr5{=rq9iomh+AW_zZwVn&H8-=s=d#jKKP z1HFbA+Ge|O?)XT&Mn(DVHS!YyB0vO)01+SpM1Tko0U|&IhyW2F0z`la5CI}U1c(3; zAOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)01+SpM1Tko0U|&IhyW2F0z`la5CI}U1c(3; hAOb{y2oM1xKm>>Y5g-CYfCvx)B0vO)!2c70e*+qW#^L|~ delta 3394 zcmZ`*3s6+o89w`DsqFGx+h670NB67 z0BER_-s?-eqvet#M>G=>)tO2(O5#IV%?yIz_H@(>o1IN<&UUZ~LaljSEpq}>J}u!b zVr7xn4nNt^c`Qus7M+>LN3Ns}%e^# zxB<8vC=Ez+f{-lWvAl8B06|2vfltV9N$Vy>N(-^UM2r-KiqFN|))av-q#`Yxy;N9bap; zbl)tk_TTZ}vjob6WVpw18Q|(jmB3tzoS61`(Bi_#J{o&92UPh z1kMEhI4Gn~<~Ls&Pedbs907*Sm!KzryabTV!B&|iP}1kEA|#@JLkYP4ll)U(?U-NX zAFB>59U7~hoVp=??dgX2CkHG6b144R477I-%gz1=@l%E%bP;IYh+YSQ<68b0W_{yhNVO;gS%)DY9|CFu^Cw_= zYy98vk0JR4z<0VfKmeuBn;21b1(;^GjCA5h9;5Oo%! z21?$Q5G{eau*LtS6tcp9AL}t6+`C0LOp)vbxba+Y#jpKaRba;@D5&``bSwIxt2;VA z7m6Rkl)~s0(;9<9JrV}$1}Ip05ODcBLvl+{l?1>wLG3sMukeR!;;Ta7kO9ZS>T&*{ z{zEzOLM8vvr;O+WxQhNk^R7t8bcndWOpKTit&nIF3W1UdG-GYnqaG}e!0VceCM^D; zqR<$gLFfq+Os>atD9(nKXg?6aV+ z31%YzZ;ceFP(~ny?u;8q%gFHMF*P z*19~KSK6ALWp3At&N8}5o0%b<<}Q0}yUpcs6Pw%aY;nLP>2T5{?Mzys&7!~17U$a9 z+Q4r0w7Xgw%Sc`AGo_MD95AkhP+wdpy{^rjho{UUm=x(7xTUe73@bt6;B-ll&Mk6b zok~V|ozWY{djeYYs%w5>{$De{>i_urm>(8vvT=m;M>tsp975~7 z#q3f}VLXR%BjZaMk6zvlOz&cR7vq%i1C0NM@!vCkj`5p}$4H6gkT{lM4&(C}UyNKl zEc;jK_Gyz6^9p^6wAC}orWEl_k~sS8sTu0k?y#(fqfMs_THkqCezBqDpP?MD&)CBW z&n;VCDOA?*R;!RNthBNPoEn_WNf&anyU_~EP1-duOuVAoolQ=g+evy-I7e5LL;9bp zcZpl2N6!J=GqAsS4{VV74v!Nz8S!0>rjZSKJUTtb6L%kGxNjgw(UZ#2PX;pOpvDj7 zZEQD$6lpz#xJe2p*gUNbq=&dQ(fn*pO|5pcB>cc4xLRBuG^a()t!*Bm#km!21^{Y< z?dt(3`fwo4I~_hNsom+2_*U2)IY80>&K)4@bVU41z^&d{@~((mC%WL|DnN|GIIl{@Jw!dTys4T{x9RzodJaZ z4tRN|17!j|7So@7$|alo*un|T`0c_^6Rs%KW{b_^Bnv#vZFED}sE~LjJrZ7`>?}12 z^j0`ibN5PI2(t0gw9_+V&CZvc1KBrsh$+yI+k6k&Di|x%8J~FIq82mvzXg|eCJcY(|mto zMx}1;LO?SZmK-?9EuXVz>5=5^X}>L7k@!{fg5Ehp)(=xh-#lb|+cpY89Bmol413={ z-q`FoHlKWPG2x=~EpMW(JNwJmf~muwCO=3yz5WY&c7)^fYphqZ_NR{CzQ7&xoUOR< zAJ@MQUj86?*7l0)Tdq}e?|z(>fA3J|=hi;mdw(7(Fm6j396lJl*4(&vgpTzG?tKj) zn+%nU^QL|O$r072fIIj3og;U;LIdSB8QV|n`;}GwcKVJL7_=y2daz~N;7i#ZKlc9g ziFc;uy>z+dxtB-kDrb~^yD_)tRtD4(7aR|Ea`(y$F95qo>p%S?-EgKtQCwuCBWGsE Q%I;p7hEba639vimze2ENzW@LL diff --git a/dist/cappuccino/bin/imagesize b/dist/cappuccino/bin/imagesize deleted file mode 100755 index 5d67a1ce0a9a1e20577f82ccdfe0d3b3e94143b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69536 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 Z009sH0T2KI5C8!X009sHfo}nU{{v@rhV}pe diff --git a/dist/cappuccino/bin/objj2objcskeleton b/dist/cappuccino/bin/objj2objcskeleton index b7972dbb3..51ecab38d 100755 --- a/dist/cappuccino/bin/objj2objcskeleton +++ b/dist/cappuccino/bin/objj2objcskeleton @@ -4,14 +4,11 @@ var fs = require("fs"), acorn = require("objj-parser"), - walk = require("objj-parser/util/walk"), + walk = require("acorn-walk"), stream = ObjectiveJ.term; - debugger; - function main(args) { - debugger; args.shift(); if (args.length < 1) @@ -40,6 +37,8 @@ function raise(pos, message) throw syntaxError; } +function ignore(_node, _st, _c) {} + var errors = [], xcc = walk.make( { @@ -115,7 +114,18 @@ var errors = [], else raise(node.loc.start, "Action methods must have exactly one parameter"); } - } + }, + TypeDefStatement: ignore, + ClassStatement: ignore, + MessageSendExpression: ignore, + GlobalStatement: ignore, + ProtocolDeclarationStatement: ignore, + ArrayLiteral: ignore, + Reference: ignore, + DictionaryLiteral: ignore, + Dereference: ignore, + ImportStatement: ignore, + SelectorLiteralExpression: ignore } ); @@ -154,7 +164,7 @@ function parser(args) outputHeaderURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".h"]), outputImplementationURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".m"]), source = fs.readFileSync(sourcePath, { encoding: "utf8" }), - tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath }), + tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath, ecmaVersion: 2022 }), classesInformation = [], ObjectiveCSource = "", ObjectiveCHeader = "", From 134688ef5c5cab102206efc3bba3c0d3c2ce86e8 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 18 Jul 2025 21:48:09 +0200 Subject: [PATCH 030/103] improved: double click emulation by double tapping --- AppKit/Platform/DOM/CPPlatformWindow+DOM.j | 31 +++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index 28246375f..49de64861 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -1271,6 +1271,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 +1386,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 +1433,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 +1455,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 +1956,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. From bda5594f0cabb0cf625143f16128f7dfe876ec51 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 18 Jul 2025 22:05:12 +0200 Subject: [PATCH 031/103] fixed: ipad softkeyboard trigger --- AppKit/CPTextView/CPTextView.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 2763faac4..c0e54f77e 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -1101,7 +1101,7 @@ Sets the selection to a range of characters in response to user action. return; // this is for the ipad-keyboard - [_CPNativeInputManager focusForTextView:self]; + [_CPNativeInputManager focusForClipboardOfTextView:self]; [_CPNativeInputManager cancelCurrentInputSessionIfNeeded]; [_caret setVisibility:NO]; From 2a32934f864a156b69639ae2be3e86b2ca83ec07 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 19 Jul 2025 09:40:14 +0200 Subject: [PATCH 032/103] fixed: ipad backspace issue --- AppKit/CPTextView/CPTextView.j | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index c0e54f77e..1719695df 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2680,12 +2680,21 @@ var _CPCopyPlaceholder = '-'; // Fires for simple key presses (a, b, 1, 2) _CPNativeInputField.addEventListener('input', function(e) { - // If we are in a composition (e.g., IME), we do nothing. + // If we are in a composition (e.g., IME), do nothing. // We wait for 'compositionend' to get the final, complete text. - if (_isComposing) { + if (_isComposing) + return; + + // The 'input' event fires for deletions too. On iPad, repeatedly + // backspacing on an empty field can insert strange content (like
). + // We only want to handle this event for actual insertions. + // Deletions are handled by the standard key binding mechanism (deleteBackward:). + if (e.inputType && e.inputType.startsWith('delete')) + { + _CPNativeInputField.innerHTML = ''; return; } - // If not composing, this is a simple character. Handle it immediately. + handleInput(e.target.innerHTML); }); From 46bab97b496854a4750492598ee23c15da1cdba6 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 19 Jul 2025 13:51:58 +0200 Subject: [PATCH 033/103] fixed: backspace issue on ipad --- AppKit/CPTextView/CPTextView.j | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 1719695df..74afe64ea 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2677,7 +2677,20 @@ var _CPCopyPlaceholder = '-'; _CPNativeInputField.innerHTML = ''; }; - // Fires for simple key presses (a, b, 1, 2) + // Proactively prevent the backspace bug on iPad + // Intercept the keydown event before the browser acts on it. + _CPNativeInputField.addEventListener('keydown', function(e) { + // This bug occurs when backspace is pressed on an *empty* contentEditable div. + if (e.key === 'Backspace' && _CPNativeInputField.innerHTML === '') { + // Prevent the browser's default action (which is to insert a junk character). + // This stops an 'input' event from firing, solving the problem at the source. + // The keydown event will still be handled by Cappuccino's responder chain. + e.preventDefault(); + } + }); + + // Reactively handle input events as a fallback + // Fires for simple key presses, deletions, etc. _CPNativeInputField.addEventListener('input', function(e) { // If we are in a composition (e.g., IME), do nothing. @@ -2685,12 +2698,6 @@ var _CPCopyPlaceholder = '-'; if (_isComposing) return; - // The 'input' event fires for deletions too. On iPad, repeatedly - // backspacing on an empty field can insert strange content (like
). - // We only want to handle this event for actual insertions. - // Deletions are handled by the standard key binding mechanism (deleteBackward:). - if (e.inputType && e.inputType.startsWith('delete')) - { _CPNativeInputField.innerHTML = ''; return; } From 76ba6a4e64b70926912f18aeba9f64e56f59b51a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 19 Jul 2025 13:53:28 +0200 Subject: [PATCH 034/103] fixed: ipad issue with backspace --- AppKit/CPTextView/CPTextView.j | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 74afe64ea..51ee6d424 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2698,6 +2698,11 @@ var _CPCopyPlaceholder = '-'; if (_isComposing) return; + // This is a safety net. The 'input' event fires for deletions too. + // We only want to handle this event for actual insertions. Deletions are + // handled by the standard key binding mechanism (deleteBackward:). + if (e.inputType && e.inputType.startsWith('delete')) { + // It was a deletion. Ensure the native field is empty and stop processing. _CPNativeInputField.innerHTML = ''; return; } From 0b4d156b5b1bb7aadeafb9a6db7a4d8f1b75e7f2 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 19 Jul 2025 14:19:46 +0200 Subject: [PATCH 035/103] fixed: keyboard issues on ipad --- AppKit/CPTextView/CPTextView.j | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 51ee6d424..d27ec4cdf 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2677,37 +2677,35 @@ var _CPCopyPlaceholder = '-'; _CPNativeInputField.innerHTML = ''; }; - // Proactively prevent the backspace bug on iPad - // Intercept the keydown event before the browser acts on it. + // Intercept problematic keys before the browser acts. _CPNativeInputField.addEventListener('keydown', function(e) { - // This bug occurs when backspace is pressed on an *empty* contentEditable div. - if (e.key === 'Backspace' && _CPNativeInputField.innerHTML === '') { - // Prevent the browser's default action (which is to insert a junk character). - // This stops an 'input' event from firing, solving the problem at the source. - // The keydown event will still be handled by Cappuccino's responder chain. + + if (e.key === 'Enter' || (e.key === 'Backspace' && _CPNativeInputField.innerHTML === '')) { + // Prevent browser default action: + // - 'Enter': Prevents inserting

. + // - 'Backspace' on empty: Prevents inserting junk characters on iPadOS. e.preventDefault(); } }); - // Reactively handle input events as a fallback - // Fires for simple key presses, deletions, etc. + // This listener handles all other character input. _CPNativeInputField.addEventListener('input', function(e) { - // If we are in a composition (e.g., IME), do nothing. - // We wait for 'compositionend' to get the final, complete text. + // If we are in a composition (e.g., IME), do nothing yet. if (_isComposing) return; - // This is a safety net. The 'input' event fires for deletions too. - // We only want to handle this event for actual insertions. Deletions are - // handled by the standard key binding mechanism (deleteBackward:). - if (e.inputType && e.inputType.startsWith('delete')) { - // It was a deletion. Ensure the native field is empty and stop processing. + // Safety net: ignore deletion events, as they are handled by keydown. + if (e.inputType && e.inputType.startsWith('delete')) + { _CPNativeInputField.innerHTML = ''; return; } - - handleInput(e.target.innerHTML); + + // 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). From 7e99e3ca20f48fde3b91c2b2486b33658f48cb9c Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 19 Jul 2025 14:21:10 +0200 Subject: [PATCH 036/103] removed: unwanted files --- dist/cappuccino/bin/flatten | 368 --------------- dist/cappuccino/bin/fontinfo | Bin 70736 -> 0 bytes dist/cappuccino/bin/objj2objcskeleton | 623 -------------------------- 3 files changed, 991 deletions(-) delete mode 100755 dist/cappuccino/bin/flatten delete mode 100755 dist/cappuccino/bin/fontinfo delete mode 100755 dist/cappuccino/bin/objj2objcskeleton diff --git a/dist/cappuccino/bin/flatten b/dist/cappuccino/bin/flatten deleted file mode 100755 index 80b9ecf8e..000000000 --- a/dist/cappuccino/bin/flatten +++ /dev/null @@ -1,368 +0,0 @@ -#!/usr/bin/env objj - -require("narwhal").ensureEngine("rhino"); - -@import - -@import "../lib/cappuccino/objj-analysis-tools.j" - -var FILE = require("file"); -var OS = require("os"); -var UTIL = require("narwhal/util"); - -var CACHEMANIFEST = require("objective-j/cache-manifest"); - -var stream = require("narwhal/term").stream; -var parser = new (require("narwhal/args").Parser)(); - -parser.usage("INPUT_PROJECT OUTPUT_PROJECT"); -parser.help("Combine a Cappuccino application into a single JavaScript file."); - -parser.option("-m", "--main", "main") - .def("main.j") - .set() - .help("The relative path (from INPUT_PROJECT) to the main file (default: 'main.j')"); - -parser.option("-F", "--framework", "frameworks") - .push() - .help("Add a frameworks directory, relative to INPUT_PROJECT (default: ['Frameworks'])"); - -parser.option("-P", "--path", "paths") - .push() - .help("Add a path (relative to the application root) to inline."); - -parser.option("-f", "--force", "force") - .def(false) - .set(true) - .help("Force overwriting OUTPUT_PROJECT if it exists"); - -parser.option("--index", "index") - .def("index.html") - .set() - .help("The root HTML file to modify (default: index.html)"); - -parser.option("-s", "--split", "number", "split") - .natural() - .def(0) - .help("Split into multiple files"); - -parser.option("-c", "--compressor", "compressor") - .def("shrinksafe") - .set() - .help("Select a compressor to use (closure-compiler, yuicompressor, shrinksafe), or \"none\" (default: shrinksafe)"); - -parser.option("--manifest", "manifest") - .set(true) - .help("Generate HTML5 cache manifest."); - -parser.option("-v", "--verbose", "verbose") - .def(false) - .set(true) - .help("Verbose logging"); - -parser.helpful(); - -function main(args) -{ - var options = parser.parse(args); - - if (options.args.length < 2) { - parser.printUsage(options); - return; - } - - var rootPath = FILE.path(options.args[0]).join("").absolute(); - var outputPath = FILE.path(options.args[1]).join("").absolute(); - - if (outputPath.exists()) { - if (options.force) { - // FIXME: why doesn't this work?! - //outputPath.rmtree(); - OS.system(["rm", "-rf", outputPath]); - } else { - stream.print("\0red(OUTPUT_PROJECT " + outputPath + " exists. Use -f to overwrite.\0)"); - OS.exit(1); - } - } - - options.frameworks.push("Frameworks"); - - var mainPath = String(rootPath.join(options.main)); - var frameworks = options.frameworks.map(function(framework) { return rootPath.join(framework); }); - var environment = "Browser"; - - stream.print("\0yellow("+Array(81).join("=")+"\0)"); - stream.print("Application root: \0green(" + rootPath + "\0)"); - stream.print("Output directory: \0green(" + outputPath + "\0)"); - - stream.print("\0yellow("+Array(81).join("=")+"\0)"); - stream.print("Main file: \0green(" + mainPath + "\0)"); - stream.print("Frameworks: \0green(" + frameworks + "\0)"); - stream.print("Environment: \0green(" + environment + "\0)"); - - var flattener = new ObjectiveJFlattener(rootPath); - - flattener.options = options; - - flattener.setIncludePaths(frameworks); - flattener.setEnvironments([environment, "ObjJ"]); - - print("Loading application."); - flattener.load(mainPath); - - print("Loading default theme."); - flattener.require("objective-j").objj_eval("("+(function() { - - var defaultThemeName = [CPApplication defaultThemeName], - bundle = nil; - - if (defaultThemeName === @"Aristo" || defaultThemeName === @"Aristo2") - bundle = [CPBundle bundleForClass:[CPApplication class]]; - else - bundle = [CPBundle mainBundle]; - - var blend = [[CPThemeBlend alloc] initWithContentsOfURL:[bundle pathForResource:defaultThemeName + @".blend"]]; - [blend loadWithDelegate:nil]; - - })+")")(); - - var applicationJSs = flattener.buildApplicationJS(); - - FILE.copyTree(rootPath, outputPath); - - applicationJSs.forEach(function(applicationJS, n) { - var name = "Application"+(n||"")+".js"; - if (options.compressor === "none") { - print("skipping compression: " + name); - } else { - print("compressing: " + name); - applicationJS = require("minify/"+options.compressor).compress(applicationJS, { charset : "UTF-8", useServer : true }); - } - outputPath.join(name).write(applicationJS, { charset : "UTF-8" }); - }); - - rewriteMainHTML(outputPath.join(options.index)); - - if (options.manifest) { - CACHEMANIFEST.generateManifest(outputPath, { - index : outputPath.join(options.index), - exclude : Object.keys(flattener.filesToCache).map(function(path) { return outputPath.join(path).toString(); }) - }); - } -} - -// ObjectiveJFlattener inherits from ObjectiveJRuntimeAnalyzer -function ObjectiveJFlattener(rootPath) { - ObjectiveJRuntimeAnalyzer.apply(this, arguments); - - this.filesToCache = {}; - this.fileCacheBuffer = []; - this.functionsBuffer = []; -} - -ObjectiveJFlattener.prototype = Object.create(ObjectiveJRuntimeAnalyzer.prototype); - -ObjectiveJFlattener.prototype.buildApplicationJS = function() { - - this.setupFileCache(); - this.serializeFunctions(); - this.serializeFileCache(); - - var additions = FILE.read(FILE.join(FILE.dirname(module.path), "..", "..", "cappuccino", "lib", "cappuccino", "objj-flatten-additions.js"), { charset:"UTF-8" }); - - var applicationJSs = []; - - if (this.options.split === 0) { - var buffer = []; - buffer.push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);"); - buffer.push(additions); - buffer.push(this.fileCacheBuffer.join("\n")); - buffer.push(this.functionsBuffer.join("\n")); - buffer.push("ObjectiveJ.bootstrap();"); - applicationJSs.push(buffer.join("\n")); - } else { - var appFilesCount = this.options.split; - - var buffers = []; - for (var i = 0; i <= appFilesCount; i++) - buffers.push([]); - - var chunks = this.fileCacheBuffer.concat(this.functionsBuffer).sort(function(chunkA, chunkB) { - return chunkA.length - chunkB.length; - }); - - // try to equally distribute the chunks. could be better but good enough for now. - var n = 0; - while (chunks.length) { - buffers[(n++ % appFilesCount) + 1].push(chunks.pop()); - } - - buffers[0].push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);"); - buffers[0].push(additions); - - buffers[0].push("var appFilesCount = " + appFilesCount +";"); - buffers[0].push("for (var i = 1; i <= appFilesCount; i++) {"); - buffers[0].push(" var script = document.createElement(\"script\");"); - buffers[0].push(" script.src = \"Application\"+i+\".js\";"); - buffers[0].push(" script.charset = \"UTF-8\";"); - buffers[0].push(" script.onload = function() { if (--appFilesCount === 0) ObjectiveJ.bootstrap(); };"); - buffers[0].push(" document.getElementsByTagName(\"head\")[0].appendChild(script);"); - buffers[0].push("}"); - - buffers.forEach(function(buffer) { - applicationJSs.push(buffer.join("\n")); - }); - } - - return applicationJSs; -} - -ObjectiveJFlattener.prototype.serializeFunctions = function() { - var inlineFunctions = true;//this.options.inlineFunctions; - - var outputFiles = {}; - - var _cachedExecutableFunctions = {}; - - this.require("objective-j").FileExecutable.allFileExecutables().forEach(function(executable) { - var path = executable.path(); - - if (inlineFunctions) - { - // stringify the function, replacing arguments - var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK - - var relative = this.rootPath.relative(path).toString(); - this.functionsBuffer.push("ObjectiveJ.FileExecutable._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); - } - - var bundle = this.context.global.CFBundle.bundleContainingURL(path); - if (bundle && bundle.infoDictionary()) - { - var executablePath = bundle.executablePath(), - relativeToBundle = FILE.relative(FILE.join(bundle.path(), ""), path); - - if (executablePath) - { - if (inlineFunctions) - { - // remove the code since we're inlining the functions - executable._code = "alert("+JSON.stringify(relativeToBundle)+");"; - } - - if (!outputFiles[executablePath]) - { - outputFiles[executablePath] = []; - outputFiles[executablePath].push("@STATIC;1.0;"); - } - - var fileContents = executable.toMarkedString(); - - outputFiles[executablePath].push("p;" + relativeToBundle.length + ";" + relativeToBundle); - outputFiles[executablePath].push("t;" + fileContents.length + ";" + fileContents); - - // stream.print("Adding \0green(" + this.rootPath.relative(path) + "\0) to \0cyan(" + this.rootPath.relative(executablePath) + "\0)"); - } - } - else - CPLog.warn("No bundle (or info dictionary for) " + rootPath.relative(path)); - }, this); - - for (var executablePath in outputFiles) - { - var relative = this.rootPath.relative(executablePath).toString(); - var contents = outputFiles[executablePath].join(""); - this.filesToCache[relative] = contents; - } -} - -ObjectiveJFlattener.prototype.serializeFileCache = function() { - for (var relative in this.filesToCache) { - var contents = this.filesToCache[relative]; - print("caching: " + relative + " => " + (contents == null ? 404 : 200)); - if (contents == null) - this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 404);"); - else - this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 200, {}, "+JSON.stringify(contents)+");"); - } -} - -ObjectiveJFlattener.prototype.setupFileCache = function() { - var paths = {}; - - UTIL.update(paths, this.requestedURLs); - - this.options.paths.forEach(function(relativePath) { - paths[this.rootPath.join(relativePath)] = true; - }, this); - - Object.keys(paths).forEach(function(absolute) { - var relative = this.rootPath.relative(absolute).toString(); - if (relative.indexOf("..") === 0) - { - print("skipping (parent of app root): " + absolute); - return; - } - - if (FILE.isFile(absolute)) - { - // if (this.options.maxCachedSize && FILE.size(absolute) > this.options.maxCachedSize) - // { - // print("skipping (larger than "+this.options.maxCachedSize+" bytes): " + absolute); - // return; - // } - - var contents = FILE.read(absolute, { charset : "UTF-8" }); - this.filesToCache[relative] = contents; - } else { - this.filesToCache[relative] = null; - } - }, this); -} - -// "$1" is the matching indentation -var scriptTagsBefore = - '$1'; - -var scriptTagsAfter = - '$1'; - -// enable CPLog: -// scriptTagsAfter = '$1\n' + scriptTagsAfter; - -function rewriteMainHTML(indexHTMLPath) { - if (indexHTMLPath.isFile()) { - var indexHTML = indexHTMLPath.read({ charset : "UTF-8" }); - - // inline the Application.js if it's smallish - var applicationJSPath = indexHTMLPath.dirname().join("Application.js"); - if (applicationJSPath.size() < 10*1024) { - // escape any dollar signs by replacing them with two - // then indent by splitting/joining on newlines - scriptTagsAfter = - '$1'; - } - - // attempt to find Objective-J script tag and add ours - var newIndexHTML = indexHTML.replace(/([ \t]+)]+Objective-J\.js[^>]+>(?:\s*<\/script>)?/, - scriptTagsBefore+'\n$&\n'+scriptTagsAfter); - - if (newIndexHTML !== indexHTML) { - stream.print("\0green(Modified: "+indexHTMLPath+".\0)"); - indexHTMLPath.write(newIndexHTML, { charset : "UTF-8" }); - return; - } - } else { - stream.print("\0yellow(Warning: "+indexHTMLPath+" does not exist. Specify an alternate index HTML file with the --index option.\0)"); - } - - stream.print("\0yellow(Warning: Unable to automatically modify "+indexHTMLPath + ".\0)"); - stream.print("\nAdd the following before the Objective-J script tag:"); - stream.print(scriptTagsBefore.replace(/\$1/g, " ")); - stream.print("\nAdd the following after the Objective-J script tag:"); - stream.print(scriptTagsAfter.replace(/\$1/g, " ")); -} diff --git a/dist/cappuccino/bin/fontinfo b/dist/cappuccino/bin/fontinfo deleted file mode 100755 index 1a91ba1c8040bbcfe787719cb7e44c0e7578d180..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70736 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 diff --git a/dist/cappuccino/bin/objj2objcskeleton b/dist/cappuccino/bin/objj2objcskeleton deleted file mode 100755 index 51ecab38d..000000000 --- a/dist/cappuccino/bin/objj2objcskeleton +++ /dev/null @@ -1,623 +0,0 @@ -#!/usr/bin/env objj - -@import - -var fs = require("fs"), - acorn = require("objj-parser"), - walk = require("acorn-walk"), - stream = ObjectiveJ.term; - -function main(args) -{ - args.shift(); - - if (args.length < 1) - return printUsage(); - - parser(args); -} - -function printUsage() -{ - console.log("objj2objskeleton [FILE] [DESTINATION]"); - console.log("Convert a objective-j file to an objective-c files skeleton (.h and .m)") -} - -// Debug function to print some JS objects -function dump(obj) -{ - console.log(JSON.stringify(obj)); -} - -function raise(pos, message) -{ - var syntaxError = new SyntaxError(message); - syntaxError.line = pos.line; - - throw syntaxError; -} - -function ignore(_node, _st, _c) {} - -var errors = [], - xcc = walk.make( - { - ClassDeclarationStatement: function(node, st, c) - { - var className = node.classname.name, - superclassname = node.superclassname ? node.superclassname.name : "", - declaredOutletsName = [], - classInfo = { - "name": className, - "category": node.categoryname ? node.categoryname.name : "", - "superClass": superclassname, - "outlets": [], - "actions": [], - "actionNames": [] - }; - - if (node.ivardeclarations) - { - for (var i = 0; i < node.ivardeclarations.length; ++i) - { - var ivarDecl = node.ivardeclarations[i], - ivarType = ivarDecl.ivartype ? ivarDecl.ivartype.name : null, - ivarName = ivarDecl.id.name, - ivarHasOutlet = ivarDecl.outlet ? "@outlet" : null; - - if (ivarHasOutlet) - { - if (declaredOutletsName.indexOf(ivarName) !== -1) - raise(ivarDecl.loc.start, "Outlet '" + ivarName + "' declared more than once"); - - declaredOutletsName.push(ivarName); - classInfo.outlets.push({"type": ivarType, "name": ivarName}); - } - } - } - - st.push(classInfo) - - for (var i = 0; i < node.body.length; ++i) - c(node.body[i], classInfo, "Statement"); - }, - - MethodDeclarationStatement: function(node, st, c) - { - var selectors = node.selectors, - arguments = node.arguments, - //methodReturnType = [node.returntype ? node.returntype.name : "id"], - methodHasAction = node.action ? "IBAction" : null, - selector = selectors[0].name, - actionInfo = {"name": selector, "arguments":[]}; - - if (methodHasAction) - { - if (arguments.length == 1) - { - if (st.actionNames.indexOf(selector) !== -1) - raise(node.loc.start, "Action '" + selector + "' declared more than once"); - - st.actionNames.push(selector); - - for (var i = 0; i < arguments.length; i++) - { - var argument = arguments[i], - argumentName = argument.identifier.name, - argumentType = argument.type ? argument.type.name : null; - - actionInfo.arguments.push({"type": argumentType, "name": argumentName}); - } - - st.actions.push(actionInfo) - } - else - raise(node.loc.start, "Action methods must have exactly one parameter"); - } - }, - TypeDefStatement: ignore, - ClassStatement: ignore, - MessageSendExpression: ignore, - GlobalStatement: ignore, - ProtocolDeclarationStatement: ignore, - ArrayLiteral: ignore, - Reference: ignore, - DictionaryLiteral: ignore, - Dereference: ignore, - ImportStatement: ignore, - SelectorLiteralExpression: ignore - } -); - -function compile(node, state, visitor) -{ - function c(node, st, override) - { - visitor[override || node.type](node, st, c); - } - - c(node, state); -}; - -function removeLastSlashIfNecessary(path) -{ - if (path[path.length - 1] == "/") - return path.substring(0, path.length - 1); - - return path; -} - -/* - $1 Full project source path - $2 Destination - $-n name of the cocoa files -*/ -function parser(args) -{ - try - { - var sourcePath = args.shift(), - projectBasePath = removeLastSlashIfNecessary(args.shift()), - outputDirectory = projectBasePath, - baseFilename = [sourcePath lastPathComponent], - baseFilenameWithNoExtension = args.shift() == "-n" ? args.shift() : baseFilename.substring(0, baseFilename.length - 2), - outputHeaderURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".h"]), - outputImplementationURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".m"]), - source = fs.readFileSync(sourcePath, { encoding: "utf8" }), - tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath, ecmaVersion: 2022 }), - classesInformation = [], - ObjectiveCSource = "", - ObjectiveCHeader = "", - hasErrors = NO; - - compile(tokens, classesInformation, xcc); - - // dump(classesInformation) - - ObjectiveCHeader += - "#import \n" + - '#import "xcc_general_include.h"\n'; - - ObjectiveCSource += "#import \"" + outputHeaderURL.lastPathComponent() + "\"\n"; - - // Traverse each found classes - classesInformation.forEach(function(aClass) - { - // add new class definition - if (aClass.superClass) - ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ : %@", aClass.name, NSCompatibleClassName(aClass.superClass)]; - else - ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ (%@)", NSCompatibleClassName(aClass.name, NO), aClass.category]; - - // add each outlet in header - if (aClass.outlets.length > 0) - ObjectiveCHeader += "\n"; - - aClass.outlets.forEach(function(anOutlet) - { - ObjectiveCHeader += [CPString stringWithFormat:@"\n@property (assign) IBOutlet %@ %@;", NSCompatibleClassName(anOutlet.type, YES), anOutlet.name]; - }); - - if (aClass.actions.length > 0) - ObjectiveCHeader += "\n"; - - // add each action in header - aClass.actions.forEach(function(anAction) - { - ObjectiveCHeader += [CPString stringWithFormat:@"\n- (IBAction)%@:(%@)%@;", anAction.name, anAction.arguments[0].type, anAction.arguments[0].name]; - }); - - if (aClass.outlets.length > 0 || aClass.actions.length > 0) - ObjectiveCHeader += "\n"; - - ObjectiveCHeader += "\n@end\n"; - - // fill up the implementation file - ObjectiveCSource += "\n@implementation " + NSCompatibleClassName(aClass.name, NO) + "\n@end\n"; - }); - - if (ObjectiveCSource.length) - fs.writeFileSync(outputImplementationURL.absoluteString(), ObjectiveCSource, 'utf8'); - - if (ObjectiveCHeader.length) - fs.writeFileSync(outputHeaderURL.absoluteString(), ObjectiveCHeader, 'utf8'); - } - catch (e) - { - [errors addObject:@{ - @"message": e.message, - @"sourcePath": sourcePath, - @"line": e.line - }]; - - hasErrors = YES; - } - - if ([errors count]) - { - var plist = [CPPropertyListSerialization dataFromPropertyList:errors format:CPPropertyListXMLFormat_v1_0]; - - stream.printError([plist rawString]); - - // If there were category warnings, hasErrors is NO, so return a warning status - process.exit(hasErrors ? 1 : 2); - } -} - -function NSCompatibleClassName(aClassName, asPointer) -{ - if (aClassName === "var" || aClassName === "id") - return "id"; - - var prefix = aClassName.substr(0, 2), - asterisk = asPointer ? "*" : ""; - - if (prefix !== "CP") - return aClassName + asterisk; - - var NSClassName = "NS" + aClassName.substr(2); - - if (NSClasses[NSClassName]) - return NSClassName + asterisk; - - if (ReplacementClasses[aClassName]) - return ReplacementClasses[aClassName] + asterisk; - - return aClassName + asterisk; -} - -var ReplacementClasses = { - "CPWebView": "WebView", - "CPRadio": "NSButtonCell", - "CPRadioGroup": "NSMatrix" - }; - -var NSClasses = { - "NSAffineTransform" : YES, - "NSAppleEventDescriptor" : YES, - "NSAppleEventManager" : YES, - "NSAppleScript" : YES, - "NSArchiver" : YES, - "NSArray" : YES, - "NSAssertionHandler" : YES, - "NSAttributedString" : YES, - "NSAutoreleasePool" : YES, - "NSBlockOperation" : YES, - "NSBundle" : YES, - "NSCache" : YES, - "NSCachedURLResponse" : YES, - "NSCalendar" : YES, - "NSCharacterSet" : YES, - "NSClassDescription" : YES, - "NSCloneCommand" : YES, - "NSCloseCommand" : YES, - "NSCoder" : YES, - "NSComparisonPredicate" : YES, - "NSCompoundPredicate" : YES, - "NSCondition" : YES, - "NSConditionLock" : YES, - "NSConnection" : YES, - "NSCountCommand" : YES, - "NSCountedSet" : YES, - "NSCreateCommand" : YES, - "NSData" : YES, - "NSDate" : YES, - "NSDateComponents" : YES, - "NSDateFormatter" : YES, - "NSDecimalNumber" : YES, - "NSDecimalNumberHandler" : YES, - "NSDeleteCommand" : YES, - "NSDeserializer" : YES, - "NSDictionary" : YES, - "NSDirectoryEnumerator" : YES, - "NSDistantObject" : YES, - "NSDistantObjectRequest" : YES, - "NSDistributedLock" : YES, - "NSDistributedNotificationCenter" : YES, - "NSEnumerator" : YES, - "NSError" : YES, - "NSException" : YES, - "NSExistsCommand" : YES, - "NSExpression" : YES, - "NSFileHandle" : YES, - "NSFileManager" : YES, - "NSFileWrapper" : YES, - "NSFormatter" : YES, - "NSGarbageCollector" : YES, - "NSGetCommand" : YES, - "NSHashTable" : YES, - "NSHost" : YES, - "NSHTTPCookie" : YES, - "NSHTTPCookieStorage" : YES, - "NSHTTPURLResponse" : YES, - "NSIndexPath" : YES, - "NSIndexSet" : YES, - "NSIndexSpecifier" : YES, - "NSInputStream" : YES, - "NSInvocation" : YES, - "NSInvocationOperation" : YES, - "NSKeyedArchiver" : YES, - "NSKeyedUnarchiver" : YES, - "NSLocale" : YES, - "NSLock" : YES, - "NSLogicalTest" : YES, - "NSMachBootstrapServer" : YES, - "NSMachPort" : YES, - "NSMapTable" : YES, - "NSMessagePort" : YES, - "NSMessagePortNameServer" : YES, - "NSMetadataItem" : YES, - "NSMetadataQuery" : YES, - "NSMetadataQueryAttributeValueTuple" : YES, - "NSMetadataQueryResultGroup" : YES, - "NSMethodSignature" : YES, - "NSMiddleSpecifier" : YES, - "NSMoveCommand" : YES, - "NSMutableArray" : YES, - "NSMutableAttributedString" : YES, - "NSMutableCharacterSet" : YES, - "NSMutableData" : YES, - "NSMutableDictionary" : YES, - "NSMutableIndexSet" : YES, - "NSMutableSet" : YES, - "NSMutableString" : YES, - "NSMutableURLRequest" : YES, - "NSNameSpecifier" : YES, - "NSNetService" : YES, - "NSNetServiceBrowser" : YES, - "NSNotification" : YES, - "NSNotificationCenter" : YES, - "NSNotificationQueue" : YES, - "NSNull" : YES, - "NSNumber" : YES, - "NSNumberFormatter" : YES, - "NSObject" : YES, - "NSOperation" : YES, - "NSOperationQueue" : YES, - "NSOrthography" : YES, - "NSOutputStream" : YES, - "NSPipe" : YES, - "NSPointerArray" : YES, - "NSPointerFunctions" : YES, - "NSPort" : YES, - "NSPortCoder" : YES, - "NSPortMessage" : YES, - "NSPortNameServer" : YES, - "NSPositionalSpecifier" : YES, - "NSPredicate" : YES, - "NSProcessInfo" : YES, - "NSPropertyListSerialization" : YES, - "NSPropertySpecifier" : YES, - "NSProtocolChecker" : YES, - "NSProxy" : YES, - "NSPurgeableData" : YES, - "NSQuitCommand" : YES, - "NSRandomSpecifier" : YES, - "NSRangeSpecifier" : YES, - "NSRecursiveLock" : YES, - "NSRelativeSpecifier" : YES, - "NSRunLoop" : YES, - "NSScanner" : YES, - "NSScriptClassDescription" : YES, - "NSScriptCoercionHandler" : YES, - "NSScriptCommand" : YES, - "NSScriptCommandDescription" : YES, - "NSScriptExecutionContext" : YES, - "NSScriptObjectSpecifier" : YES, - "NSScriptSuiteRegistry" : YES, - "NSScriptWhoseTest" : YES, - "NSSerializer" : YES, - "NSSet" : YES, - "NSSetCommand" : YES, - "NSSocketPort" : YES, - "NSSocketPortNameServer" : YES, - "NSSortDescriptor" : YES, - "NSSpecifierTest" : YES, - "NSSpellServer" : YES, - "NSStream" : YES, - "NSString" : YES, - "NSTask" : YES, - "NSTextCheckingResult" : YES, - "NSThread" : YES, - "NSTimer" : YES, - "NSTimeZone" : YES, - "NSUnarchiver" : YES, - "NSUndoManager" : YES, - "NSUniqueIDSpecifier" : YES, - "NSURL" : YES, - "NSURLAuthenticationChallenge" : YES, - "NSURLCache" : YES, - "NSURLConnection" : YES, - "NSURLCredential" : YES, - "NSURLCredentialStorage" : YES, - "NSURLDownload" : YES, - "NSURLHandle" : YES, - "NSURLProtectionSpace" : YES, - "NSURLProtocol" : YES, - "NSURLRequest" : YES, - "NSURLResponse" : YES, - "NSUserDefaults" : YES, - "NSValue" : YES, - "NSValueTransformer" : YES, - "NSWhoseSpecifier" : YES, - "NSXMLDocument" : YES, - "NSXMLDTD" : YES, - "NSXMLDTDNode" : YES, - "NSXMLElement" : YES, - "NSXMLNode" : YES, - "NSXMLParser" : YES, - "NSActionCell" : YES, - "NSAffineTransform Additions" : YES, - "NSAlert" : YES, - "NSAnimation" : YES, - "NSAnimationContext" : YES, - "NSAppleScript Additions" : YES, - "NSApplication" : YES, - "NSArrayController" : YES, - "NSATSTypesetter" : YES, - "NSAttributedString Application Kit Additions" : YES, - "NSBezierPath" : YES, - "NSBitmapImageRep" : YES, - "NSBox" : YES, - "NSBrowser" : YES, - "NSBrowserCell" : YES, - "NSBundle Additions" : YES, - "NSButton" : YES, - "NSButtonCell" : YES, - "NSCachedImageRep" : YES, - "NSCell" : YES, - "NSCIImageRep" : YES, - "NSClipView" : YES, - "NSCoder Application Kit Additions" : YES, - "NSCollectionView" : YES, - "NSCollectionViewItem" : YES, - "NSColor" : YES, - "NSColorList" : YES, - "NSColorPanel" : YES, - "NSColorPicker" : YES, - "NSColorSpace" : YES, - "NSColorWell" : YES, - "NSComboBox" : YES, - "NSComboBoxCell" : YES, - "NSControl" : YES, - "NSController" : YES, - "NSCursor" : YES, - "NSCustomImageRep" : YES, - "NSDatePicker" : YES, - "NSDatePickerCell" : YES, - "NSDictionaryController" : YES, - "NSDockTile" : YES, - "NSDocument" : YES, - "NSDocumentController" : YES, - "NSDrawer" : YES, - "NSEPSImageRep" : YES, - "NSEvent" : YES, - "NSFileWrapper" : YES, - "NSFont" : YES, - "NSFontDescriptor" : YES, - "NSFontManager" : YES, - "NSFontPanel" : YES, - "NSForm" : YES, - "NSFormCell" : YES, - "NSGlyphGenerator" : YES, - "NSGlyphInfo" : YES, - "NSGradient" : YES, - "NSGraphicsContext" : YES, - "NSHelpManager" : YES, - "NSImage" : YES, - "NSImageCell" : YES, - "NSImageRep" : YES, - "NSImageView" : YES, - "NSLayoutManager" : YES, - "NSLevelIndicator" : YES, - "NSLevelIndicatorCell" : YES, - "NSMatrix" : YES, - "NSMenu" : YES, - "NSMenuItem" : YES, - "NSMenuItemCell" : YES, - "NSMenuView" : YES, - "NSMutableAttributedString Additions" : YES, - "NSMutableParagraphStyle" : YES, - "NSNib" : YES, - "NSNibConnector" : YES, - "NSNibControlConnector" : YES, - "NSNibOutletConnector" : YES, - "NSObjectController" : YES, - "NSOpenGLContext" : YES, - "NSOpenGLLayer" : YES, - "NSOpenGLPixelBuffer" : YES, - "NSOpenGLPixelFormat" : YES, - "NSOpenGLView" : YES, - "NSOpenPanel" : YES, - "NSOutlineView" : YES, - "NSPageLayout" : YES, - "NSPanel" : YES, - "NSParagraphStyle" : YES, - "NSPasteboard" : YES, - "NSPasteboardItem" : YES, - "NSPathCell" : YES, - "NSPathComponentCell" : YES, - "NSPathControl" : YES, - "NSPDFImageRep" : YES, - "NSPersistentDocument" : YES, - "NSPICTImageRep" : YES, - "NSPopUpButton" : YES, - "NSPopUpButtonCell" : YES, - "NSPredicateEditor" : YES, - "NSPredicateEditorRowTemplate" : YES, - "NSPrinter" : YES, - "NSPrintInfo" : YES, - "NSPrintOperation" : YES, - "NSPrintPanel" : YES, - "NSProgressIndicator" : YES, - "NSResponder" : YES, - "NSRuleEditor" : YES, - "NSRulerMarker" : YES, - "NSRulerView" : YES, - "NSRunningApplication" : YES, - "NSSavePanel" : YES, - "NSScreen" : YES, - "NSScroller" : YES, - "NSScrollView" : YES, - "NSSearchField" : YES, - "NSSearchFieldCell" : YES, - "NSSecureTextField" : YES, - "NSSecureTextFieldCell" : YES, - "NSSegmentedCell" : YES, - "NSSegmentedControl" : YES, - "NSShadow" : YES, - "NSSlider" : YES, - "NSSliderCell" : YES, - "NSSound" : YES, - "NSSpeechRecognizer" : YES, - "NSSpeechSynthesizer" : YES, - "NSSpellChecker" : YES, - "NSSplitView" : YES, - "NSStatusBar" : YES, - "NSStatusItem" : YES, - "NSStepper" : YES, - "NSStepperCell" : YES, - "NSString Application Kit Additions" : YES, - "NSTableCellView" : YES, - "NSTableColumn" : YES, - "NSTableHeaderCell" : YES, - "NSTableHeaderView" : YES, - "NSTableView" : YES, - "NSTabView" : YES, - "NSTabViewItem" : YES, - "NSText" : YES, - "NSTextAttachment" : YES, - "NSTextAttachmentCell" : YES, - "NSTextBlock" : YES, - "NSTextContainer" : YES, - "NSTextField" : YES, - "NSTextFieldCell" : YES, - "NSTextInputContext" : YES, - "NSTextList" : YES, - "NSTextStorage" : YES, - "NSTextTab" : YES, - "NSTextTable" : YES, - "NSTextTableBlock" : YES, - "NSTextView" : YES, - "NSTokenField" : YES, - "NSTokenFieldCell" : YES, - "NSToolbar" : YES, - "NSToolbarItem" : YES, - "NSToolbarItemGroup" : YES, - "NSTouch" : YES, - "NSTrackingArea" : YES, - "NSTreeController" : YES, - "NSTreeNode" : YES, - "NSTypesetter" : YES, - "NSURL Additions" : YES, - "NSUserDefaultsController" : YES, - "NSView" : YES, - "NSViewAnimation" : YES, - "NSViewController" : YES, - "NSWindow" : YES, - "NSWindowController" : YES, - "NSWorkspace" : YES, - "NSPopover": YES, - "NSAppearance" : YES, - "NSVisualEffectView" : YES, - }; From 49e5cb64ad5555f0540fc368402934e633246643 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 19 Jul 2025 14:23:52 +0200 Subject: [PATCH 037/103] Revert "removed: unwanted files" This reverts commit 7e99e3ca20f48fde3b91c2b2486b33658f48cb9c. --- dist/cappuccino/bin/flatten | 368 +++++++++++++++ dist/cappuccino/bin/fontinfo | Bin 0 -> 70736 bytes dist/cappuccino/bin/objj2objcskeleton | 623 ++++++++++++++++++++++++++ 3 files changed, 991 insertions(+) create mode 100755 dist/cappuccino/bin/flatten create mode 100755 dist/cappuccino/bin/fontinfo create mode 100755 dist/cappuccino/bin/objj2objcskeleton diff --git a/dist/cappuccino/bin/flatten b/dist/cappuccino/bin/flatten new file mode 100755 index 000000000..80b9ecf8e --- /dev/null +++ b/dist/cappuccino/bin/flatten @@ -0,0 +1,368 @@ +#!/usr/bin/env objj + +require("narwhal").ensureEngine("rhino"); + +@import + +@import "../lib/cappuccino/objj-analysis-tools.j" + +var FILE = require("file"); +var OS = require("os"); +var UTIL = require("narwhal/util"); + +var CACHEMANIFEST = require("objective-j/cache-manifest"); + +var stream = require("narwhal/term").stream; +var parser = new (require("narwhal/args").Parser)(); + +parser.usage("INPUT_PROJECT OUTPUT_PROJECT"); +parser.help("Combine a Cappuccino application into a single JavaScript file."); + +parser.option("-m", "--main", "main") + .def("main.j") + .set() + .help("The relative path (from INPUT_PROJECT) to the main file (default: 'main.j')"); + +parser.option("-F", "--framework", "frameworks") + .push() + .help("Add a frameworks directory, relative to INPUT_PROJECT (default: ['Frameworks'])"); + +parser.option("-P", "--path", "paths") + .push() + .help("Add a path (relative to the application root) to inline."); + +parser.option("-f", "--force", "force") + .def(false) + .set(true) + .help("Force overwriting OUTPUT_PROJECT if it exists"); + +parser.option("--index", "index") + .def("index.html") + .set() + .help("The root HTML file to modify (default: index.html)"); + +parser.option("-s", "--split", "number", "split") + .natural() + .def(0) + .help("Split into multiple files"); + +parser.option("-c", "--compressor", "compressor") + .def("shrinksafe") + .set() + .help("Select a compressor to use (closure-compiler, yuicompressor, shrinksafe), or \"none\" (default: shrinksafe)"); + +parser.option("--manifest", "manifest") + .set(true) + .help("Generate HTML5 cache manifest."); + +parser.option("-v", "--verbose", "verbose") + .def(false) + .set(true) + .help("Verbose logging"); + +parser.helpful(); + +function main(args) +{ + var options = parser.parse(args); + + if (options.args.length < 2) { + parser.printUsage(options); + return; + } + + var rootPath = FILE.path(options.args[0]).join("").absolute(); + var outputPath = FILE.path(options.args[1]).join("").absolute(); + + if (outputPath.exists()) { + if (options.force) { + // FIXME: why doesn't this work?! + //outputPath.rmtree(); + OS.system(["rm", "-rf", outputPath]); + } else { + stream.print("\0red(OUTPUT_PROJECT " + outputPath + " exists. Use -f to overwrite.\0)"); + OS.exit(1); + } + } + + options.frameworks.push("Frameworks"); + + var mainPath = String(rootPath.join(options.main)); + var frameworks = options.frameworks.map(function(framework) { return rootPath.join(framework); }); + var environment = "Browser"; + + stream.print("\0yellow("+Array(81).join("=")+"\0)"); + stream.print("Application root: \0green(" + rootPath + "\0)"); + stream.print("Output directory: \0green(" + outputPath + "\0)"); + + stream.print("\0yellow("+Array(81).join("=")+"\0)"); + stream.print("Main file: \0green(" + mainPath + "\0)"); + stream.print("Frameworks: \0green(" + frameworks + "\0)"); + stream.print("Environment: \0green(" + environment + "\0)"); + + var flattener = new ObjectiveJFlattener(rootPath); + + flattener.options = options; + + flattener.setIncludePaths(frameworks); + flattener.setEnvironments([environment, "ObjJ"]); + + print("Loading application."); + flattener.load(mainPath); + + print("Loading default theme."); + flattener.require("objective-j").objj_eval("("+(function() { + + var defaultThemeName = [CPApplication defaultThemeName], + bundle = nil; + + if (defaultThemeName === @"Aristo" || defaultThemeName === @"Aristo2") + bundle = [CPBundle bundleForClass:[CPApplication class]]; + else + bundle = [CPBundle mainBundle]; + + var blend = [[CPThemeBlend alloc] initWithContentsOfURL:[bundle pathForResource:defaultThemeName + @".blend"]]; + [blend loadWithDelegate:nil]; + + })+")")(); + + var applicationJSs = flattener.buildApplicationJS(); + + FILE.copyTree(rootPath, outputPath); + + applicationJSs.forEach(function(applicationJS, n) { + var name = "Application"+(n||"")+".js"; + if (options.compressor === "none") { + print("skipping compression: " + name); + } else { + print("compressing: " + name); + applicationJS = require("minify/"+options.compressor).compress(applicationJS, { charset : "UTF-8", useServer : true }); + } + outputPath.join(name).write(applicationJS, { charset : "UTF-8" }); + }); + + rewriteMainHTML(outputPath.join(options.index)); + + if (options.manifest) { + CACHEMANIFEST.generateManifest(outputPath, { + index : outputPath.join(options.index), + exclude : Object.keys(flattener.filesToCache).map(function(path) { return outputPath.join(path).toString(); }) + }); + } +} + +// ObjectiveJFlattener inherits from ObjectiveJRuntimeAnalyzer +function ObjectiveJFlattener(rootPath) { + ObjectiveJRuntimeAnalyzer.apply(this, arguments); + + this.filesToCache = {}; + this.fileCacheBuffer = []; + this.functionsBuffer = []; +} + +ObjectiveJFlattener.prototype = Object.create(ObjectiveJRuntimeAnalyzer.prototype); + +ObjectiveJFlattener.prototype.buildApplicationJS = function() { + + this.setupFileCache(); + this.serializeFunctions(); + this.serializeFileCache(); + + var additions = FILE.read(FILE.join(FILE.dirname(module.path), "..", "..", "cappuccino", "lib", "cappuccino", "objj-flatten-additions.js"), { charset:"UTF-8" }); + + var applicationJSs = []; + + if (this.options.split === 0) { + var buffer = []; + buffer.push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);"); + buffer.push(additions); + buffer.push(this.fileCacheBuffer.join("\n")); + buffer.push(this.functionsBuffer.join("\n")); + buffer.push("ObjectiveJ.bootstrap();"); + applicationJSs.push(buffer.join("\n")); + } else { + var appFilesCount = this.options.split; + + var buffers = []; + for (var i = 0; i <= appFilesCount; i++) + buffers.push([]); + + var chunks = this.fileCacheBuffer.concat(this.functionsBuffer).sort(function(chunkA, chunkB) { + return chunkA.length - chunkB.length; + }); + + // try to equally distribute the chunks. could be better but good enough for now. + var n = 0; + while (chunks.length) { + buffers[(n++ % appFilesCount) + 1].push(chunks.pop()); + } + + buffers[0].push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);"); + buffers[0].push(additions); + + buffers[0].push("var appFilesCount = " + appFilesCount +";"); + buffers[0].push("for (var i = 1; i <= appFilesCount; i++) {"); + buffers[0].push(" var script = document.createElement(\"script\");"); + buffers[0].push(" script.src = \"Application\"+i+\".js\";"); + buffers[0].push(" script.charset = \"UTF-8\";"); + buffers[0].push(" script.onload = function() { if (--appFilesCount === 0) ObjectiveJ.bootstrap(); };"); + buffers[0].push(" document.getElementsByTagName(\"head\")[0].appendChild(script);"); + buffers[0].push("}"); + + buffers.forEach(function(buffer) { + applicationJSs.push(buffer.join("\n")); + }); + } + + return applicationJSs; +} + +ObjectiveJFlattener.prototype.serializeFunctions = function() { + var inlineFunctions = true;//this.options.inlineFunctions; + + var outputFiles = {}; + + var _cachedExecutableFunctions = {}; + + this.require("objective-j").FileExecutable.allFileExecutables().forEach(function(executable) { + var path = executable.path(); + + if (inlineFunctions) + { + // stringify the function, replacing arguments + var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK + + var relative = this.rootPath.relative(path).toString(); + this.functionsBuffer.push("ObjectiveJ.FileExecutable._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); + } + + var bundle = this.context.global.CFBundle.bundleContainingURL(path); + if (bundle && bundle.infoDictionary()) + { + var executablePath = bundle.executablePath(), + relativeToBundle = FILE.relative(FILE.join(bundle.path(), ""), path); + + if (executablePath) + { + if (inlineFunctions) + { + // remove the code since we're inlining the functions + executable._code = "alert("+JSON.stringify(relativeToBundle)+");"; + } + + if (!outputFiles[executablePath]) + { + outputFiles[executablePath] = []; + outputFiles[executablePath].push("@STATIC;1.0;"); + } + + var fileContents = executable.toMarkedString(); + + outputFiles[executablePath].push("p;" + relativeToBundle.length + ";" + relativeToBundle); + outputFiles[executablePath].push("t;" + fileContents.length + ";" + fileContents); + + // stream.print("Adding \0green(" + this.rootPath.relative(path) + "\0) to \0cyan(" + this.rootPath.relative(executablePath) + "\0)"); + } + } + else + CPLog.warn("No bundle (or info dictionary for) " + rootPath.relative(path)); + }, this); + + for (var executablePath in outputFiles) + { + var relative = this.rootPath.relative(executablePath).toString(); + var contents = outputFiles[executablePath].join(""); + this.filesToCache[relative] = contents; + } +} + +ObjectiveJFlattener.prototype.serializeFileCache = function() { + for (var relative in this.filesToCache) { + var contents = this.filesToCache[relative]; + print("caching: " + relative + " => " + (contents == null ? 404 : 200)); + if (contents == null) + this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 404);"); + else + this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 200, {}, "+JSON.stringify(contents)+");"); + } +} + +ObjectiveJFlattener.prototype.setupFileCache = function() { + var paths = {}; + + UTIL.update(paths, this.requestedURLs); + + this.options.paths.forEach(function(relativePath) { + paths[this.rootPath.join(relativePath)] = true; + }, this); + + Object.keys(paths).forEach(function(absolute) { + var relative = this.rootPath.relative(absolute).toString(); + if (relative.indexOf("..") === 0) + { + print("skipping (parent of app root): " + absolute); + return; + } + + if (FILE.isFile(absolute)) + { + // if (this.options.maxCachedSize && FILE.size(absolute) > this.options.maxCachedSize) + // { + // print("skipping (larger than "+this.options.maxCachedSize+" bytes): " + absolute); + // return; + // } + + var contents = FILE.read(absolute, { charset : "UTF-8" }); + this.filesToCache[relative] = contents; + } else { + this.filesToCache[relative] = null; + } + }, this); +} + +// "$1" is the matching indentation +var scriptTagsBefore = + '$1'; + +var scriptTagsAfter = + '$1'; + +// enable CPLog: +// scriptTagsAfter = '$1\n' + scriptTagsAfter; + +function rewriteMainHTML(indexHTMLPath) { + if (indexHTMLPath.isFile()) { + var indexHTML = indexHTMLPath.read({ charset : "UTF-8" }); + + // inline the Application.js if it's smallish + var applicationJSPath = indexHTMLPath.dirname().join("Application.js"); + if (applicationJSPath.size() < 10*1024) { + // escape any dollar signs by replacing them with two + // then indent by splitting/joining on newlines + scriptTagsAfter = + '$1'; + } + + // attempt to find Objective-J script tag and add ours + var newIndexHTML = indexHTML.replace(/([ \t]+)]+Objective-J\.js[^>]+>(?:\s*<\/script>)?/, + scriptTagsBefore+'\n$&\n'+scriptTagsAfter); + + if (newIndexHTML !== indexHTML) { + stream.print("\0green(Modified: "+indexHTMLPath+".\0)"); + indexHTMLPath.write(newIndexHTML, { charset : "UTF-8" }); + return; + } + } else { + stream.print("\0yellow(Warning: "+indexHTMLPath+" does not exist. Specify an alternate index HTML file with the --index option.\0)"); + } + + stream.print("\0yellow(Warning: Unable to automatically modify "+indexHTMLPath + ".\0)"); + stream.print("\nAdd the following before the Objective-J script tag:"); + stream.print(scriptTagsBefore.replace(/\$1/g, " ")); + stream.print("\nAdd the following after the Objective-J script tag:"); + stream.print(scriptTagsAfter.replace(/\$1/g, " ")); +} diff --git a/dist/cappuccino/bin/fontinfo b/dist/cappuccino/bin/fontinfo new file mode 100755 index 0000000000000000000000000000000000000000..1a91ba1c8040bbcfe787719cb7e44c0e7578d180 GIT binary patch literal 70736 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 literal 0 HcmV?d00001 diff --git a/dist/cappuccino/bin/objj2objcskeleton b/dist/cappuccino/bin/objj2objcskeleton new file mode 100755 index 000000000..51ecab38d --- /dev/null +++ b/dist/cappuccino/bin/objj2objcskeleton @@ -0,0 +1,623 @@ +#!/usr/bin/env objj + +@import + +var fs = require("fs"), + acorn = require("objj-parser"), + walk = require("acorn-walk"), + stream = ObjectiveJ.term; + +function main(args) +{ + args.shift(); + + if (args.length < 1) + return printUsage(); + + parser(args); +} + +function printUsage() +{ + console.log("objj2objskeleton [FILE] [DESTINATION]"); + console.log("Convert a objective-j file to an objective-c files skeleton (.h and .m)") +} + +// Debug function to print some JS objects +function dump(obj) +{ + console.log(JSON.stringify(obj)); +} + +function raise(pos, message) +{ + var syntaxError = new SyntaxError(message); + syntaxError.line = pos.line; + + throw syntaxError; +} + +function ignore(_node, _st, _c) {} + +var errors = [], + xcc = walk.make( + { + ClassDeclarationStatement: function(node, st, c) + { + var className = node.classname.name, + superclassname = node.superclassname ? node.superclassname.name : "", + declaredOutletsName = [], + classInfo = { + "name": className, + "category": node.categoryname ? node.categoryname.name : "", + "superClass": superclassname, + "outlets": [], + "actions": [], + "actionNames": [] + }; + + if (node.ivardeclarations) + { + for (var i = 0; i < node.ivardeclarations.length; ++i) + { + var ivarDecl = node.ivardeclarations[i], + ivarType = ivarDecl.ivartype ? ivarDecl.ivartype.name : null, + ivarName = ivarDecl.id.name, + ivarHasOutlet = ivarDecl.outlet ? "@outlet" : null; + + if (ivarHasOutlet) + { + if (declaredOutletsName.indexOf(ivarName) !== -1) + raise(ivarDecl.loc.start, "Outlet '" + ivarName + "' declared more than once"); + + declaredOutletsName.push(ivarName); + classInfo.outlets.push({"type": ivarType, "name": ivarName}); + } + } + } + + st.push(classInfo) + + for (var i = 0; i < node.body.length; ++i) + c(node.body[i], classInfo, "Statement"); + }, + + MethodDeclarationStatement: function(node, st, c) + { + var selectors = node.selectors, + arguments = node.arguments, + //methodReturnType = [node.returntype ? node.returntype.name : "id"], + methodHasAction = node.action ? "IBAction" : null, + selector = selectors[0].name, + actionInfo = {"name": selector, "arguments":[]}; + + if (methodHasAction) + { + if (arguments.length == 1) + { + if (st.actionNames.indexOf(selector) !== -1) + raise(node.loc.start, "Action '" + selector + "' declared more than once"); + + st.actionNames.push(selector); + + for (var i = 0; i < arguments.length; i++) + { + var argument = arguments[i], + argumentName = argument.identifier.name, + argumentType = argument.type ? argument.type.name : null; + + actionInfo.arguments.push({"type": argumentType, "name": argumentName}); + } + + st.actions.push(actionInfo) + } + else + raise(node.loc.start, "Action methods must have exactly one parameter"); + } + }, + TypeDefStatement: ignore, + ClassStatement: ignore, + MessageSendExpression: ignore, + GlobalStatement: ignore, + ProtocolDeclarationStatement: ignore, + ArrayLiteral: ignore, + Reference: ignore, + DictionaryLiteral: ignore, + Dereference: ignore, + ImportStatement: ignore, + SelectorLiteralExpression: ignore + } +); + +function compile(node, state, visitor) +{ + function c(node, st, override) + { + visitor[override || node.type](node, st, c); + } + + c(node, state); +}; + +function removeLastSlashIfNecessary(path) +{ + if (path[path.length - 1] == "/") + return path.substring(0, path.length - 1); + + return path; +} + +/* + $1 Full project source path + $2 Destination + $-n name of the cocoa files +*/ +function parser(args) +{ + try + { + var sourcePath = args.shift(), + projectBasePath = removeLastSlashIfNecessary(args.shift()), + outputDirectory = projectBasePath, + baseFilename = [sourcePath lastPathComponent], + baseFilenameWithNoExtension = args.shift() == "-n" ? args.shift() : baseFilename.substring(0, baseFilename.length - 2), + outputHeaderURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".h"]), + outputImplementationURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".m"]), + source = fs.readFileSync(sourcePath, { encoding: "utf8" }), + tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath, ecmaVersion: 2022 }), + classesInformation = [], + ObjectiveCSource = "", + ObjectiveCHeader = "", + hasErrors = NO; + + compile(tokens, classesInformation, xcc); + + // dump(classesInformation) + + ObjectiveCHeader += + "#import \n" + + '#import "xcc_general_include.h"\n'; + + ObjectiveCSource += "#import \"" + outputHeaderURL.lastPathComponent() + "\"\n"; + + // Traverse each found classes + classesInformation.forEach(function(aClass) + { + // add new class definition + if (aClass.superClass) + ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ : %@", aClass.name, NSCompatibleClassName(aClass.superClass)]; + else + ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ (%@)", NSCompatibleClassName(aClass.name, NO), aClass.category]; + + // add each outlet in header + if (aClass.outlets.length > 0) + ObjectiveCHeader += "\n"; + + aClass.outlets.forEach(function(anOutlet) + { + ObjectiveCHeader += [CPString stringWithFormat:@"\n@property (assign) IBOutlet %@ %@;", NSCompatibleClassName(anOutlet.type, YES), anOutlet.name]; + }); + + if (aClass.actions.length > 0) + ObjectiveCHeader += "\n"; + + // add each action in header + aClass.actions.forEach(function(anAction) + { + ObjectiveCHeader += [CPString stringWithFormat:@"\n- (IBAction)%@:(%@)%@;", anAction.name, anAction.arguments[0].type, anAction.arguments[0].name]; + }); + + if (aClass.outlets.length > 0 || aClass.actions.length > 0) + ObjectiveCHeader += "\n"; + + ObjectiveCHeader += "\n@end\n"; + + // fill up the implementation file + ObjectiveCSource += "\n@implementation " + NSCompatibleClassName(aClass.name, NO) + "\n@end\n"; + }); + + if (ObjectiveCSource.length) + fs.writeFileSync(outputImplementationURL.absoluteString(), ObjectiveCSource, 'utf8'); + + if (ObjectiveCHeader.length) + fs.writeFileSync(outputHeaderURL.absoluteString(), ObjectiveCHeader, 'utf8'); + } + catch (e) + { + [errors addObject:@{ + @"message": e.message, + @"sourcePath": sourcePath, + @"line": e.line + }]; + + hasErrors = YES; + } + + if ([errors count]) + { + var plist = [CPPropertyListSerialization dataFromPropertyList:errors format:CPPropertyListXMLFormat_v1_0]; + + stream.printError([plist rawString]); + + // If there were category warnings, hasErrors is NO, so return a warning status + process.exit(hasErrors ? 1 : 2); + } +} + +function NSCompatibleClassName(aClassName, asPointer) +{ + if (aClassName === "var" || aClassName === "id") + return "id"; + + var prefix = aClassName.substr(0, 2), + asterisk = asPointer ? "*" : ""; + + if (prefix !== "CP") + return aClassName + asterisk; + + var NSClassName = "NS" + aClassName.substr(2); + + if (NSClasses[NSClassName]) + return NSClassName + asterisk; + + if (ReplacementClasses[aClassName]) + return ReplacementClasses[aClassName] + asterisk; + + return aClassName + asterisk; +} + +var ReplacementClasses = { + "CPWebView": "WebView", + "CPRadio": "NSButtonCell", + "CPRadioGroup": "NSMatrix" + }; + +var NSClasses = { + "NSAffineTransform" : YES, + "NSAppleEventDescriptor" : YES, + "NSAppleEventManager" : YES, + "NSAppleScript" : YES, + "NSArchiver" : YES, + "NSArray" : YES, + "NSAssertionHandler" : YES, + "NSAttributedString" : YES, + "NSAutoreleasePool" : YES, + "NSBlockOperation" : YES, + "NSBundle" : YES, + "NSCache" : YES, + "NSCachedURLResponse" : YES, + "NSCalendar" : YES, + "NSCharacterSet" : YES, + "NSClassDescription" : YES, + "NSCloneCommand" : YES, + "NSCloseCommand" : YES, + "NSCoder" : YES, + "NSComparisonPredicate" : YES, + "NSCompoundPredicate" : YES, + "NSCondition" : YES, + "NSConditionLock" : YES, + "NSConnection" : YES, + "NSCountCommand" : YES, + "NSCountedSet" : YES, + "NSCreateCommand" : YES, + "NSData" : YES, + "NSDate" : YES, + "NSDateComponents" : YES, + "NSDateFormatter" : YES, + "NSDecimalNumber" : YES, + "NSDecimalNumberHandler" : YES, + "NSDeleteCommand" : YES, + "NSDeserializer" : YES, + "NSDictionary" : YES, + "NSDirectoryEnumerator" : YES, + "NSDistantObject" : YES, + "NSDistantObjectRequest" : YES, + "NSDistributedLock" : YES, + "NSDistributedNotificationCenter" : YES, + "NSEnumerator" : YES, + "NSError" : YES, + "NSException" : YES, + "NSExistsCommand" : YES, + "NSExpression" : YES, + "NSFileHandle" : YES, + "NSFileManager" : YES, + "NSFileWrapper" : YES, + "NSFormatter" : YES, + "NSGarbageCollector" : YES, + "NSGetCommand" : YES, + "NSHashTable" : YES, + "NSHost" : YES, + "NSHTTPCookie" : YES, + "NSHTTPCookieStorage" : YES, + "NSHTTPURLResponse" : YES, + "NSIndexPath" : YES, + "NSIndexSet" : YES, + "NSIndexSpecifier" : YES, + "NSInputStream" : YES, + "NSInvocation" : YES, + "NSInvocationOperation" : YES, + "NSKeyedArchiver" : YES, + "NSKeyedUnarchiver" : YES, + "NSLocale" : YES, + "NSLock" : YES, + "NSLogicalTest" : YES, + "NSMachBootstrapServer" : YES, + "NSMachPort" : YES, + "NSMapTable" : YES, + "NSMessagePort" : YES, + "NSMessagePortNameServer" : YES, + "NSMetadataItem" : YES, + "NSMetadataQuery" : YES, + "NSMetadataQueryAttributeValueTuple" : YES, + "NSMetadataQueryResultGroup" : YES, + "NSMethodSignature" : YES, + "NSMiddleSpecifier" : YES, + "NSMoveCommand" : YES, + "NSMutableArray" : YES, + "NSMutableAttributedString" : YES, + "NSMutableCharacterSet" : YES, + "NSMutableData" : YES, + "NSMutableDictionary" : YES, + "NSMutableIndexSet" : YES, + "NSMutableSet" : YES, + "NSMutableString" : YES, + "NSMutableURLRequest" : YES, + "NSNameSpecifier" : YES, + "NSNetService" : YES, + "NSNetServiceBrowser" : YES, + "NSNotification" : YES, + "NSNotificationCenter" : YES, + "NSNotificationQueue" : YES, + "NSNull" : YES, + "NSNumber" : YES, + "NSNumberFormatter" : YES, + "NSObject" : YES, + "NSOperation" : YES, + "NSOperationQueue" : YES, + "NSOrthography" : YES, + "NSOutputStream" : YES, + "NSPipe" : YES, + "NSPointerArray" : YES, + "NSPointerFunctions" : YES, + "NSPort" : YES, + "NSPortCoder" : YES, + "NSPortMessage" : YES, + "NSPortNameServer" : YES, + "NSPositionalSpecifier" : YES, + "NSPredicate" : YES, + "NSProcessInfo" : YES, + "NSPropertyListSerialization" : YES, + "NSPropertySpecifier" : YES, + "NSProtocolChecker" : YES, + "NSProxy" : YES, + "NSPurgeableData" : YES, + "NSQuitCommand" : YES, + "NSRandomSpecifier" : YES, + "NSRangeSpecifier" : YES, + "NSRecursiveLock" : YES, + "NSRelativeSpecifier" : YES, + "NSRunLoop" : YES, + "NSScanner" : YES, + "NSScriptClassDescription" : YES, + "NSScriptCoercionHandler" : YES, + "NSScriptCommand" : YES, + "NSScriptCommandDescription" : YES, + "NSScriptExecutionContext" : YES, + "NSScriptObjectSpecifier" : YES, + "NSScriptSuiteRegistry" : YES, + "NSScriptWhoseTest" : YES, + "NSSerializer" : YES, + "NSSet" : YES, + "NSSetCommand" : YES, + "NSSocketPort" : YES, + "NSSocketPortNameServer" : YES, + "NSSortDescriptor" : YES, + "NSSpecifierTest" : YES, + "NSSpellServer" : YES, + "NSStream" : YES, + "NSString" : YES, + "NSTask" : YES, + "NSTextCheckingResult" : YES, + "NSThread" : YES, + "NSTimer" : YES, + "NSTimeZone" : YES, + "NSUnarchiver" : YES, + "NSUndoManager" : YES, + "NSUniqueIDSpecifier" : YES, + "NSURL" : YES, + "NSURLAuthenticationChallenge" : YES, + "NSURLCache" : YES, + "NSURLConnection" : YES, + "NSURLCredential" : YES, + "NSURLCredentialStorage" : YES, + "NSURLDownload" : YES, + "NSURLHandle" : YES, + "NSURLProtectionSpace" : YES, + "NSURLProtocol" : YES, + "NSURLRequest" : YES, + "NSURLResponse" : YES, + "NSUserDefaults" : YES, + "NSValue" : YES, + "NSValueTransformer" : YES, + "NSWhoseSpecifier" : YES, + "NSXMLDocument" : YES, + "NSXMLDTD" : YES, + "NSXMLDTDNode" : YES, + "NSXMLElement" : YES, + "NSXMLNode" : YES, + "NSXMLParser" : YES, + "NSActionCell" : YES, + "NSAffineTransform Additions" : YES, + "NSAlert" : YES, + "NSAnimation" : YES, + "NSAnimationContext" : YES, + "NSAppleScript Additions" : YES, + "NSApplication" : YES, + "NSArrayController" : YES, + "NSATSTypesetter" : YES, + "NSAttributedString Application Kit Additions" : YES, + "NSBezierPath" : YES, + "NSBitmapImageRep" : YES, + "NSBox" : YES, + "NSBrowser" : YES, + "NSBrowserCell" : YES, + "NSBundle Additions" : YES, + "NSButton" : YES, + "NSButtonCell" : YES, + "NSCachedImageRep" : YES, + "NSCell" : YES, + "NSCIImageRep" : YES, + "NSClipView" : YES, + "NSCoder Application Kit Additions" : YES, + "NSCollectionView" : YES, + "NSCollectionViewItem" : YES, + "NSColor" : YES, + "NSColorList" : YES, + "NSColorPanel" : YES, + "NSColorPicker" : YES, + "NSColorSpace" : YES, + "NSColorWell" : YES, + "NSComboBox" : YES, + "NSComboBoxCell" : YES, + "NSControl" : YES, + "NSController" : YES, + "NSCursor" : YES, + "NSCustomImageRep" : YES, + "NSDatePicker" : YES, + "NSDatePickerCell" : YES, + "NSDictionaryController" : YES, + "NSDockTile" : YES, + "NSDocument" : YES, + "NSDocumentController" : YES, + "NSDrawer" : YES, + "NSEPSImageRep" : YES, + "NSEvent" : YES, + "NSFileWrapper" : YES, + "NSFont" : YES, + "NSFontDescriptor" : YES, + "NSFontManager" : YES, + "NSFontPanel" : YES, + "NSForm" : YES, + "NSFormCell" : YES, + "NSGlyphGenerator" : YES, + "NSGlyphInfo" : YES, + "NSGradient" : YES, + "NSGraphicsContext" : YES, + "NSHelpManager" : YES, + "NSImage" : YES, + "NSImageCell" : YES, + "NSImageRep" : YES, + "NSImageView" : YES, + "NSLayoutManager" : YES, + "NSLevelIndicator" : YES, + "NSLevelIndicatorCell" : YES, + "NSMatrix" : YES, + "NSMenu" : YES, + "NSMenuItem" : YES, + "NSMenuItemCell" : YES, + "NSMenuView" : YES, + "NSMutableAttributedString Additions" : YES, + "NSMutableParagraphStyle" : YES, + "NSNib" : YES, + "NSNibConnector" : YES, + "NSNibControlConnector" : YES, + "NSNibOutletConnector" : YES, + "NSObjectController" : YES, + "NSOpenGLContext" : YES, + "NSOpenGLLayer" : YES, + "NSOpenGLPixelBuffer" : YES, + "NSOpenGLPixelFormat" : YES, + "NSOpenGLView" : YES, + "NSOpenPanel" : YES, + "NSOutlineView" : YES, + "NSPageLayout" : YES, + "NSPanel" : YES, + "NSParagraphStyle" : YES, + "NSPasteboard" : YES, + "NSPasteboardItem" : YES, + "NSPathCell" : YES, + "NSPathComponentCell" : YES, + "NSPathControl" : YES, + "NSPDFImageRep" : YES, + "NSPersistentDocument" : YES, + "NSPICTImageRep" : YES, + "NSPopUpButton" : YES, + "NSPopUpButtonCell" : YES, + "NSPredicateEditor" : YES, + "NSPredicateEditorRowTemplate" : YES, + "NSPrinter" : YES, + "NSPrintInfo" : YES, + "NSPrintOperation" : YES, + "NSPrintPanel" : YES, + "NSProgressIndicator" : YES, + "NSResponder" : YES, + "NSRuleEditor" : YES, + "NSRulerMarker" : YES, + "NSRulerView" : YES, + "NSRunningApplication" : YES, + "NSSavePanel" : YES, + "NSScreen" : YES, + "NSScroller" : YES, + "NSScrollView" : YES, + "NSSearchField" : YES, + "NSSearchFieldCell" : YES, + "NSSecureTextField" : YES, + "NSSecureTextFieldCell" : YES, + "NSSegmentedCell" : YES, + "NSSegmentedControl" : YES, + "NSShadow" : YES, + "NSSlider" : YES, + "NSSliderCell" : YES, + "NSSound" : YES, + "NSSpeechRecognizer" : YES, + "NSSpeechSynthesizer" : YES, + "NSSpellChecker" : YES, + "NSSplitView" : YES, + "NSStatusBar" : YES, + "NSStatusItem" : YES, + "NSStepper" : YES, + "NSStepperCell" : YES, + "NSString Application Kit Additions" : YES, + "NSTableCellView" : YES, + "NSTableColumn" : YES, + "NSTableHeaderCell" : YES, + "NSTableHeaderView" : YES, + "NSTableView" : YES, + "NSTabView" : YES, + "NSTabViewItem" : YES, + "NSText" : YES, + "NSTextAttachment" : YES, + "NSTextAttachmentCell" : YES, + "NSTextBlock" : YES, + "NSTextContainer" : YES, + "NSTextField" : YES, + "NSTextFieldCell" : YES, + "NSTextInputContext" : YES, + "NSTextList" : YES, + "NSTextStorage" : YES, + "NSTextTab" : YES, + "NSTextTable" : YES, + "NSTextTableBlock" : YES, + "NSTextView" : YES, + "NSTokenField" : YES, + "NSTokenFieldCell" : YES, + "NSToolbar" : YES, + "NSToolbarItem" : YES, + "NSToolbarItemGroup" : YES, + "NSTouch" : YES, + "NSTrackingArea" : YES, + "NSTreeController" : YES, + "NSTreeNode" : YES, + "NSTypesetter" : YES, + "NSURL Additions" : YES, + "NSUserDefaultsController" : YES, + "NSView" : YES, + "NSViewAnimation" : YES, + "NSViewController" : YES, + "NSWindow" : YES, + "NSWindowController" : YES, + "NSWorkspace" : YES, + "NSPopover": YES, + "NSAppearance" : YES, + "NSVisualEffectView" : YES, + }; From c077863e7af86a2d67b226e1d85bab87ab14f585 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 19 Jul 2025 14:29:46 +0200 Subject: [PATCH 038/103] fixed: accidental changes --- dist/cappuccino/bin/flatten | 2 +- dist/cappuccino/bin/fontinfo | Bin 70736 -> 69888 bytes dist/cappuccino/bin/objj2objcskeleton | 22 ++++++---------------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/dist/cappuccino/bin/flatten b/dist/cappuccino/bin/flatten index 80b9ecf8e..1df033252 100755 --- a/dist/cappuccino/bin/flatten +++ b/dist/cappuccino/bin/flatten @@ -233,7 +233,7 @@ ObjectiveJFlattener.prototype.serializeFunctions = function() { var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK var relative = this.rootPath.relative(path).toString(); - this.functionsBuffer.push("ObjectiveJ.FileExecutable._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); + this.functionsBuffer.push("ObjectiveJ.StaticResource._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); } var bundle = this.context.global.CFBundle.bundleContainingURL(path); diff --git a/dist/cappuccino/bin/fontinfo b/dist/cappuccino/bin/fontinfo index 1a91ba1c8040bbcfe787719cb7e44c0e7578d180..f1ff06158a2525d34541aff240713c0067bd8633 100755 GIT binary patch delta 3394 zcmZ`*3s6+o89w`DsqFGx+h670NB67 z0BER_-s?-eqvet#M>G=>)tO2(O5#IV%?yIz_H@(>o1IN<&UUZ~LaljSEpq}>J}u!b zVr7xn4nNt^c`Qus7M+>LN3Ns}%e^# zxB<8vC=Ez+f{-lWvAl8B06|2vfltV9N$Vy>N(-^UM2r-KiqFN|))av-q#`Yxy;N9bap; zbl)tk_TTZ}vjob6WVpw18Q|(jmB3tzoS61`(Bi_#J{o&92UPh z1kMEhI4Gn~<~Ls&Pedbs907*Sm!KzryabTV!B&|iP}1kEA|#@JLkYP4ll)U(?U-NX zAFB>59U7~hoVp=??dgX2CkHG6b144R477I-%gz1=@l%E%bP;IYh+YSQ<68b0W_{yhNVO;gS%)DY9|CFu^Cw_= zYy98vk0JR4z<0VfKmeuBn;21b1(;^GjCA5h9;5Oo%! z21?$Q5G{eau*LtS6tcp9AL}t6+`C0LOp)vbxba+Y#jpKaRba;@D5&``bSwIxt2;VA z7m6Rkl)~s0(;9<9JrV}$1}Ip05ODcBLvl+{l?1>wLG3sMukeR!;;Ta7kO9ZS>T&*{ z{zEzOLM8vvr;O+WxQhNk^R7t8bcndWOpKTit&nIF3W1UdG-GYnqaG}e!0VceCM^D; zqR<$gLFfq+Os>atD9(nKXg?6aV+ z31%YzZ;ceFP(~ny?u;8q%gFHMF*P z*19~KSK6ALWp3At&N8}5o0%b<<}Q0}yUpcs6Pw%aY;nLP>2T5{?Mzys&7!~17U$a9 z+Q4r0w7Xgw%Sc`AGo_MD95AkhP+wdpy{^rjho{UUm=x(7xTUe73@bt6;B-ll&Mk6b zok~V|ozWY{djeYYs%w5>{$De{>i_urm>(8vvT=m;M>tsp975~7 z#q3f}VLXR%BjZaMk6zvlOz&cR7vq%i1C0NM@!vCkj`5p}$4H6gkT{lM4&(C}UyNKl zEc;jK_Gyz6^9p^6wAC}orWEl_k~sS8sTu0k?y#(fqfMs_THkqCezBqDpP?MD&)CBW z&n;VCDOA?*R;!RNthBNPoEn_WNf&anyU_~EP1-duOuVAoolQ=g+evy-I7e5LL;9bp zcZpl2N6!J=GqAsS4{VV74v!Nz8S!0>rjZSKJUTtb6L%kGxNjgw(UZ#2PX;pOpvDj7 zZEQD$6lpz#xJe2p*gUNbq=&dQ(fn*pO|5pcB>cc4xLRBuG^a()t!*Bm#km!21^{Y< z?dt(3`fwo4I~_hNsom+2_*U2)IY80>&K)4@bVU41z^&d{@~((mC%WL|DnN|GIIl{@Jw!dTys4T{x9RzodJaZ z4tRN|17!j|7So@7$|alo*un|T`0c_^6Rs%KW{b_^Bnv#vZFED}sE~LjJrZ7`>?}12 z^j0`ibN5PI2(t0gw9_+V&CZvc1KBrsh$+yI+k6k&Di|x%8J~FIq82mvzXg|eCJcY(|mto zMx}1;LO?SZmK-?9EuXVz>5=5^X}>L7k@!{fg5Ehp)(=xh-#lb|+cpY89Bmol413={ z-q`FoHlKWPG2x=~EpMW(JNwJmf~muwCO=3yz5WY&c7)^fYphqZ_NR{CzQ7&xoUOR< zAJ@MQUj86?*7l0)Tdq}e?|z(>fA3J|=hi;mdw(7(Fm6j396lJl*4(&vgpTzG?tKj) zn+%nU^QL|O$r072fIIj3og;U;LIdSB8QV|n`;}GwcKVJL7_=y2daz~N;7i#ZKlc9g ziFc;uy>z+dxtB-kDrb~^yD_)tRtD4(7aR|Ea`(y$F95qo>p%S?-EgKtQCwuCBWGsE Q%I;p7hEba639vimze2ENzW@LL literal 70736 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 diff --git a/dist/cappuccino/bin/objj2objcskeleton b/dist/cappuccino/bin/objj2objcskeleton index 51ecab38d..b7972dbb3 100755 --- a/dist/cappuccino/bin/objj2objcskeleton +++ b/dist/cappuccino/bin/objj2objcskeleton @@ -4,11 +4,14 @@ var fs = require("fs"), acorn = require("objj-parser"), - walk = require("acorn-walk"), + walk = require("objj-parser/util/walk"), stream = ObjectiveJ.term; + debugger; + function main(args) { + debugger; args.shift(); if (args.length < 1) @@ -37,8 +40,6 @@ function raise(pos, message) throw syntaxError; } -function ignore(_node, _st, _c) {} - var errors = [], xcc = walk.make( { @@ -114,18 +115,7 @@ var errors = [], else raise(node.loc.start, "Action methods must have exactly one parameter"); } - }, - TypeDefStatement: ignore, - ClassStatement: ignore, - MessageSendExpression: ignore, - GlobalStatement: ignore, - ProtocolDeclarationStatement: ignore, - ArrayLiteral: ignore, - Reference: ignore, - DictionaryLiteral: ignore, - Dereference: ignore, - ImportStatement: ignore, - SelectorLiteralExpression: ignore + } } ); @@ -164,7 +154,7 @@ function parser(args) outputHeaderURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".h"]), outputImplementationURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".m"]), source = fs.readFileSync(sourcePath, { encoding: "utf8" }), - tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath, ecmaVersion: 2022 }), + tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath }), classesInformation = [], ObjectiveCSource = "", ObjectiveCHeader = "", From 56eb096903022f410d1b55bcedaab86b058a41e1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 19 Jul 2025 14:44:38 +0200 Subject: [PATCH 039/103] fixed: accidental delete --- dist/cappuccino/bin/imagesize | Bin 0 -> 69536 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100755 dist/cappuccino/bin/imagesize diff --git a/dist/cappuccino/bin/imagesize b/dist/cappuccino/bin/imagesize new file mode 100755 index 0000000000000000000000000000000000000000..5d67a1ce0a9a1e20577f82ccdfe0d3b3e94143b4 GIT binary patch literal 69536 zcmeI5e{fXQ702%;q(+D&L6Bcs*R65Dl6?dcNKyj33_G?93N5|S^|7AZqlV*Zf+pqaePQXj zEdh?1`-qKOUo5z>Q;w)cPgIGhZkW3*{NmOFJLp+&o`NxAF$sik+h zHO_Iijn_*?d$njt)}mc8nwYD%&(iZ-A{^&xWb5-jsitQq%MIRkuUy~O8ZdW@$GlHL zZN_q1-oLh&P+yk2V_Dneym~x0V;-~KiZc6+^=u<P584{%=@%Qg_uS4e4o`;isNiOdiO^j z5l^x7I>U;tvtq8EpXyo9NzTS`wjQN&^g@cE@J!Qk_2T`ep8Xs+&e5|f&1Up_+0xv) zrm>;9-5MS;b(mkqhRkNS^*!Y^v)kq((hE5lG8bGz?Rym3c6!KUHt&#mue>+-nfNK5 zLv6`nDkZ@woFiu|ww$lS5GIqK&rUx(y7#i@Or^j{UPLY>^@J4Gf>Nk29HN+QE;+Ab z{`>xtn-?q^tD_~X2*sh|B|=F$DL&QAJ1W*c9( zgPh0sp6^?5oVHxKkOF3PMHnN{p>DZ{58zyOQgA$j9E#&%cUOeJr7luK8T|qQ=V__ue5tp2 zc-N4ziISI^NzM$^t>R#ys)U=nXPl&Sdi(NBsm41#HD=P#S#M_O6b<=O@1|!w&Ygj( zQ=H~_FUNP+`KYP*yDxRLV(7$OboCDlp30*p^*7(`M+!H1Z}Q4F9=319+a736eU>^y z*MFI0POPPg&8ef_r%6NHekN~+t9jto!hMeZ73ImI{#zZ-oH%(r zolf^m|ynzXQ5D|Bs} zDn?71Vu^@i)CwgYSEC`jcvMJ)5{ecT+g-g{$mns^mUS<1x_VTtyT{<*W;&}`mN}&E zpygaj&X(OWRzB-qNiF+_*11XU^BKz`IxRWfJVESuf#T9BlN^d8pXJVJ&+|cEGW$F@ zpKHxFd;B-n`R;5XPxeIgK>!3m00ck)1V8`;KmY_l00ck)1V8`;KmY_l00ck)1V8`; zKmY_l00ck)1V8`;KmY_l00ck)1V8`;KmY_l00ck)1V8`;KmY_l00ck)1V8`;KmY_l z00ck)1STXt!`moq-=+V6=QtdUZW+_rLcyxRMCn$=wR*?kv~gqJq!CM~VO7ype=HUj{e^?$=3J9Blwep57zr)fE&5NV zEpv>YYR@?_I_F}K91Cvjlp}g~K#hh(LJUs#g!;lESvQhFIjBWLD&=N)WLd6ns*go= zLx~!uP<>d@^|fl3noy&is`%p|-J>pSMI{`Lb&5fu7mmruT2wPQT{7Jx$7#(*7ezui zWI56!t6SC1q@l_^s>0WunP&q%u>A!G_y0xyUFsdEqY~Y3Wb|~Z2gC*e`Qw8<5!ZuK+eW-EB$20TyL-Eaq4ru z9aA?`-^0aZ9CLkk9O9&944~Lil8HEd2|2YRT$Y&eNJUG|_{N<0HZv}uCSwoMFBRuo zp?D-G9?FSdGUG|qWbCi>OZ6l142pS;v&d(X&nC~#ImNl2bM8BlyYF+y&37g@JN8Y; z9kn+9#QWHMA)^7Unt>R{u*j8pgB#M%`GNv zr5YZeBJwiZg!+z6G^fLyC+KZG@`^K06jA&&%wx}Si~uUHBCD*atXx#1R#q*nSXiYl zUbMKTOQ~5Ltf{D2QW08Q5mc%|A+hM=XD+M0Xim?X))kk3`Q6!n`pZ(!?t{0~>@WQ9 zvsctlUBB4YK41HTJ-_m-o`3JMM=#zn>#^EvO5ce{KbwCv@bD$a@7p`?e&zW2>b`FN z7k$pb*59d9lUsjMuzL5twXGZb4?Z{N!SesTw)EFC>p#D9BAplSExx1nZ1KxWU)nP; z<;eQd;ywR-Yxf(eQ^UI+`12QYU%&njxBmXIj@Z$YT|<-JeD&2$d2Jm9=^gubY}@tr zGXuWVf`=v=Z@F;urH9Tg{;aX!^X*sP^Vrcs5 zzhC^@r|Syde!lRjF6Hst4}MeqiR#a8f1>YD`sNLftg5b_bf5jPH#*~CO*fqWbt_w%>z%H0N%F?yVO5eE+8dnymgYdalQbl$vDM{t z^%zFHR+4&qd)@S@39IhTScE6)l0OlPs|lm8g~ZCKz#TF|F1n1VK4v+T4QZXm^2rx2 z+obj_CkabTr@z(-&FFK}=dRpP^iAdb+3Z~=@idxkB$BE`)sk7hOv;EapG>!&pj%&5 zF>m2jMORFy=WsZNk4<54h2$_0009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI z5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X Z009sH0T2KI5C8!X009sHfo}nU{{v@rhV}pe literal 0 HcmV?d00001 From 568d0faef6a69d7ca20a1c12b73573efc2eea86e Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 6 Aug 2025 17:19:14 +0200 Subject: [PATCH 040/103] fixed: missing parameter to CPEvent initializer --- AppKit/CPTextField.j | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AppKit/CPTextField.j b/AppKit/CPTextField.j index 4c66ddb3f..933f2b9c0 100644 --- a/AppKit/CPTextField.j +++ b/AppKit/CPTextField.j @@ -332,7 +332,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); characters:nil charactersIgnoringModifiers:nil isARepeat:NO - keyCode:nil]; + keyCode:nil + isActionKey:NO]; [CPTextFieldInputOwner keyUp:cappEvent]; From 13688507dc4715bcd3394edbc16efafb6a94d25b Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 7 Aug 2025 10:01:03 +0200 Subject: [PATCH 041/103] new: restore old CPEvent factory method for backwards compatibility --- AppKit/CPEvent.j | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/AppKit/CPEvent.j b/AppKit/CPEvent.j index 41d1cefef..1d486521a 100644 --- a/AppKit/CPEvent.j +++ b/AppKit/CPEvent.j @@ -125,6 +125,16 @@ var _CPEventPeriodicEventPeriod = 0, 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 +{ + 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]; +} + /*! Creates a new mouse event. From 25d186bf5c263ed66a2e499d1963fe267697cab8 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 20 Nov 2025 19:58:24 +0100 Subject: [PATCH 042/103] New: CAAnimationGroup and implement timer-based animations in CALayer --- AppKit/CoreAnimation/CAAnimationGroup.j | 66 ++++++++++ AppKit/CoreAnimation/CALayer.j | 166 ++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 AppKit/CoreAnimation/CAAnimationGroup.j diff --git a/AppKit/CoreAnimation/CAAnimationGroup.j b/AppKit/CoreAnimation/CAAnimationGroup.j new file mode 100644 index 000000000..e84c5f57d --- /dev/null +++ b/AppKit/CoreAnimation/CAAnimationGroup.j @@ -0,0 +1,66 @@ +/* + * CAAnimationGroup.j + * AppKit + * + * Implements grouping for Core Animation. + */ + +@import +@import "CAAnimation.j" + +@implementation CAAnimationGroup : CAAnimation +{ + CPArray _animations; +} + ++ (id)group +{ + return [[self alloc] init]; +} + +- (id)init +{ + if (self = [super init]) + { + _animations = []; + } + return self; +} + +- (void)setAnimations:(CPArray)anArray +{ + if (_animations === anArray) + return; + + _animations = anArray; +} + +- (CPArray)animations +{ + return _animations; +} + +/* + Iterates through children and executes them recursively. + This effectively runs all grouped animations concurrently. +*/ +- (void)runActionForKey:(CPString)aKey object:(id)anObject arguments:(CPDictionary)arguments +{ + var count = [_animations count], + i = 0; + + for (; i < count; i++) + { + var animation = [_animations objectAtIndex:i]; + + // Recursively call runActionForKey on the child. + // If the child is a CABasicAnimation, it will call [anObject addAnimation:...] + // If the child is another Group, it will recurse here. + if ([animation respondsToSelector:@selector(runActionForKey:object:arguments:)]) + { + [animation runActionForKey:aKey object:anObject arguments:arguments]; + } + } +} + +@end diff --git a/AppKit/CoreAnimation/CALayer.j b/AppKit/CoreAnimation/CALayer.j index 20d2dae66..14bd97ca8 100644 --- a/AppKit/CoreAnimation/CALayer.j +++ b/AppKit/CoreAnimation/CALayer.j @@ -118,6 +118,8 @@ var CALayerRegisteredRunLoopUpdates = nil; CGAffineTransform _transformToLayer; CGAffineTransform _transformFromLayer; + + CPMutableDictionary _activeAnimations; } @global document @@ -160,6 +162,8 @@ var CALayerRegisteredRunLoopUpdates = nil; _sublayers = []; + _activeAnimations = [CPMutableDictionary dictionary]; + #if PLATFORM(DOM) _DOMElement = document.createElement("div"); @@ -977,6 +981,168 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) return _delegate; } +/* + Adds an animation to the layer. + Supports CABasicAnimation for Numbers (opacity) and Points (position/anchorPoint). +*/ +- (void)addAnimation:(CAAnimation)anim forKey:(CPString)key +{ + if (!anim) return; + + // Remove existing animation for this key + [self removeAnimationForKey:key]; + + // Determine KeyPath + var keyPath = key; + if ([anim respondsToSelector:@selector(keyPath)] && [anim keyPath]) + keyPath = [anim keyPath]; + + // Determine Start Value + var startValue = nil; + if ([anim respondsToSelector:@selector(fromValue)]) + startValue = [anim fromValue]; + + // Fallback to current layer value + if (startValue == nil) + startValue = [self valueForKey:keyPath]; + + // Determine End Value + var endValue = nil; + if ([anim respondsToSelector:@selector(toValue)]) + endValue = [anim toValue]; + + if (endValue == nil) + return; + + // Determine Duration + var duration = 0.25; + if ([anim respondsToSelector:@selector(duration)]) + duration = [anim duration]; + + // Create a JS context object to hold state (lightweight) + var context = { + "animation": anim, + "keyPath": keyPath, + "startValue": startValue, + "endValue": endValue, + "startTime": [CPDate date], + "duration": duration, + "timer": nil + }; + + // Schedule Timer (approx 60fps) + var timer = [CPTimer scheduledTimerWithTimeInterval:1.0/60.0 + target:self + selector:@selector(_animationTick:) + userInfo:context + repeats:YES]; + + context.timer = timer; + + [_activeAnimations setObject:context forKey:key]; +} + +- (void)removeAnimationForKey:(CPString)key +{ + var context = [_activeAnimations objectForKey:key]; + if (context) + { + var timer = context.timer; + if (timer) + [timer invalidate]; + + [_activeAnimations removeObjectForKey:key]; + } +} + +- (void)removeAllAnimations +{ + var keys = [_activeAnimations allKeys], + count = [keys count]; + + while (count--) + { + [self removeAnimationForKey:[keys objectAtIndex:count]]; + } +} + +- (void)_animationTick:(CPTimer)timer +{ + var context = [timer userInfo], + anim = context.animation, + startTime = context.startTime, + duration = context.duration, + now = [CPDate date]; + + // Calculate Progress + var elapsed = [now timeIntervalSinceDate:startTime], + progress = elapsed / duration; + + if (progress > 1.0) progress = 1.0; + + // Interpolate + var start = context.startValue, + end = context.endValue, + current = nil; + + // Interpolation Logic + if (typeof start === "number") + { + current = start + (end - start) * progress; + } + // Check for CGPoint (Simple JS Objects in Cappuccino) + else if (start && start.x !== undefined && start.y !== undefined) + { + var x = start.x + (end.x - start.x) * progress, + y = start.y + (end.y - start.y) * progress; + current = CGPointMake(x, y); + } + // Check for CGSize + else if (start && start.width !== undefined && start.height !== undefined) + { + var w = start.width + (end.width - start.width) * progress, + h = start.height + (end.height - start.height) * progress; + current = CGSizeMake(w, h); + } + // Check for CGRect + else if (start && start.origin !== undefined && start.size !== undefined) + { + var x = start.origin.x + (end.origin.x - start.origin.x) * progress, + y = start.origin.y + (end.origin.y - start.origin.y) * progress, + w = start.size.width + (end.size.width - start.size.width) * progress, + h = start.size.height + (end.size.height - start.size.height) * progress; + current = CGRectMake(x, y, w, h); + } + + // Apply Value + if (current !== nil) + [self setValue:current forKey:context.keyPath]; + + // Completion + if (progress >= 1.0) + { + [timer invalidate]; + + // Check removedOnCompletion + var shouldRemove = YES; + if ([anim respondsToSelector:@selector(isRemovedOnCompletion)]) + shouldRemove = [anim isRemovedOnCompletion]; + + if (shouldRemove) + { + // Remove from dictionary by finding the key for this context + var allKeys = [_activeAnimations allKeysForObject:context]; + if ([allKeys count] > 0) + [_activeAnimations removeObjectForKey:[allKeys objectAtIndex:0]]; + } + + // Notify Delegate + var delegate = [anim delegate]; + if (delegate && [delegate respondsToSelector:@selector(animationDidStop:finished:)]) + [delegate animationDidStop:anim finished:YES]; + } +} + /* @ignore */ - (void)_setOwningView:(CPView)anOwningView { From ed50f3b654f017bfd25d7de4d390dca48a3a8da8 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 20 Nov 2025 22:07:44 +0100 Subject: [PATCH 043/103] add test --- .../CPAnimationContextTest/AppController.j | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/Tests/Manual/CPAnimationContextTest/AppController.j b/Tests/Manual/CPAnimationContextTest/AppController.j index 7ae29e0eb..961cdf07a 100644 --- a/Tests/Manual/CPAnimationContextTest/AppController.j +++ b/Tests/Manual/CPAnimationContextTest/AppController.j @@ -403,6 +403,61 @@ [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 + + 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]; + 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]; +} + @end var unCamelCase = function(aString) From 3d0639b6dcff20704e10f6b8bc45e020d499edbb Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 21 Nov 2025 08:18:41 +0100 Subject: [PATCH 044/103] fixed: use requestAnimationFrame instead of setTimeout --- AppKit/CoreAnimation/CAAnimationGroup.j | 4 +- AppKit/CoreAnimation/CALayer.j | 125 +++++++++++++----------- 2 files changed, 70 insertions(+), 59 deletions(-) diff --git a/AppKit/CoreAnimation/CAAnimationGroup.j b/AppKit/CoreAnimation/CAAnimationGroup.j index e84c5f57d..936af3b44 100644 --- a/AppKit/CoreAnimation/CAAnimationGroup.j +++ b/AppKit/CoreAnimation/CAAnimationGroup.j @@ -1,7 +1,9 @@ /* * CAAnimationGroup.j * AppKit - * + * Created by Daniel Boehringer. + * Copyright 2025. + * * Implements grouping for Core Animation. */ diff --git a/AppKit/CoreAnimation/CALayer.j b/AppKit/CoreAnimation/CALayer.j index 14bd97ca8..a09d8c3df 100644 --- a/AppKit/CoreAnimation/CALayer.j +++ b/AppKit/CoreAnimation/CALayer.j @@ -989,67 +989,73 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) { if (!anim) return; - // Remove existing animation for this key + // 1. Remove existing animation for this key [self removeAnimationForKey:key]; - // Determine KeyPath + // 2. Determine Properties var keyPath = key; if ([anim respondsToSelector:@selector(keyPath)] && [anim keyPath]) keyPath = [anim keyPath]; - // Determine Start Value - var startValue = nil; - if ([anim respondsToSelector:@selector(fromValue)]) - startValue = [anim fromValue]; - - // Fallback to current layer value + var startValue = ([anim respondsToSelector:@selector(fromValue)]) ? [anim fromValue] : nil; + if (startValue == nil) startValue = [self valueForKey:keyPath]; - // Determine End Value - var endValue = nil; - if ([anim respondsToSelector:@selector(toValue)]) - endValue = [anim toValue]; + var endValue = ([anim respondsToSelector:@selector(toValue)]) ? [anim toValue] : nil; if (endValue == nil) return; - // Determine Duration - var duration = 0.25; - if ([anim respondsToSelector:@selector(duration)]) - duration = [anim duration]; - - // Create a JS context object to hold state (lightweight) + var duration = ([anim respondsToSelector:@selector(duration)]) ? [anim duration] : 0.25; + // Convert seconds to milliseconds for rAF math + var durationMS = duration * 1000.0; + + // 3. Create Context var context = { "animation": anim, "keyPath": keyPath, "startValue": startValue, "endValue": endValue, - "startTime": [CPDate date], - "duration": duration, - "timer": nil + "duration": durationMS, + "startTime": null, // Will be set on first frame + "requestId": null // To cancel if needed }; - // Schedule Timer (approx 60fps) - var timer = [CPTimer scheduledTimerWithTimeInterval:1.0/60.0 - target:self - selector:@selector(_animationTick:) - userInfo:context - repeats:YES]; + // 4. Define the Render Loop + // We use a JavaScript closure to capture 'self' and 'context' + var _self = self; - context.timer = timer; + var renderLoop = function(timestamp) { + // Pass control back to Objective-J to handle the logic + // Returns YES if animation should continue, NO if finished. + var shouldContinue = [_self _renderAnimationStep:context timestamp:timestamp]; + if (shouldContinue) + context.requestId = window.requestAnimationFrame(renderLoop); + else + context.requestId = null; + // Cleanup is handled inside _renderAnimationStep: when it returns NO + }; + + // 5. Kick off the loop + context.requestId = window.requestAnimationFrame(renderLoop); + + // 6. Store context [_activeAnimations setObject:context forKey:key]; } +/* + Cancels the specific animation frame and removes it from the dictionary. +*/ - (void)removeAnimationForKey:(CPString)key { var context = [_activeAnimations objectForKey:key]; + if (context) { - var timer = context.timer; - if (timer) - [timer invalidate]; + if (context.requestId !== null) + window.cancelAnimationFrame(context.requestId); [_activeAnimations removeObjectForKey:key]; } @@ -1061,51 +1067,48 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) count = [keys count]; while (count--) - { [self removeAnimationForKey:[keys objectAtIndex:count]]; - } } -- (void)_animationTick:(CPTimer)timer +/* + Internal method called every frame by requestAnimationFrame. + Returns YES to continue, NO to stop. +*/ +- (BOOL)_renderAnimationStep:(JSObject)context timestamp:(double)timestamp { - var context = [timer userInfo], - anim = context.animation, - startTime = context.startTime, - duration = context.duration, - now = [CPDate date]; + // 1. Initialize Start Time on first frame + if (context.startTime === null) + context.startTime = timestamp; - // Calculate Progress - var elapsed = [now timeIntervalSinceDate:startTime], - progress = elapsed / duration; + // 2. Calculate Progress + var elapsed = timestamp - context.startTime, + progress = elapsed / context.duration; + // Clamp to 1.0 if (progress > 1.0) progress = 1.0; - // Interpolate + // 3. Interpolate Values var start = context.startValue, end = context.endValue, current = nil; - // Interpolation Logic if (typeof start === "number") { current = start + (end - start) * progress; } - // Check for CGPoint (Simple JS Objects in Cappuccino) - else if (start && start.x !== undefined && start.y !== undefined) + else if (start && start.x !== undefined && start.y !== undefined) // CGPoint { var x = start.x + (end.x - start.x) * progress, y = start.y + (end.y - start.y) * progress; current = CGPointMake(x, y); } - // Check for CGSize - else if (start && start.width !== undefined && start.height !== undefined) + else if (start && start.width !== undefined && start.height !== undefined) // CGSize { var w = start.width + (end.width - start.width) * progress, h = start.height + (end.height - start.height) * progress; current = CGSizeMake(w, h); } - // Check for CGRect - else if (start && start.origin !== undefined && start.size !== undefined) + else if (start && start.origin !== undefined && start.size !== undefined) // CGRect { var x = start.origin.x + (end.origin.x - start.origin.x) * progress, y = start.origin.y + (end.origin.y - start.origin.y) * progress, @@ -1114,33 +1117,39 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) current = CGRectMake(x, y, w, h); } - // Apply Value + // 4. Apply Value if (current !== nil) [self setValue:current forKey:context.keyPath]; - // Completion + // 5. Check for Completion if (progress >= 1.0) { - [timer invalidate]; - - // Check removedOnCompletion + var anim = context.animation; + + // Handle removedOnCompletion var shouldRemove = YES; if ([anim respondsToSelector:@selector(isRemovedOnCompletion)]) shouldRemove = [anim isRemovedOnCompletion]; - + if (shouldRemove) { - // Remove from dictionary by finding the key for this context + // Remove from _activeAnimations + // We search by object equality to ensure we delete the right key var allKeys = [_activeAnimations allKeysForObject:context]; if ([allKeys count] > 0) [_activeAnimations removeObjectForKey:[allKeys objectAtIndex:0]]; } - + // Notify Delegate var delegate = [anim delegate]; + if (delegate && [delegate respondsToSelector:@selector(animationDidStop:finished:)]) [delegate animationDidStop:anim finished:YES]; + + return NO; // Stop the loop } + + return YES; // Continue the loop } /* @ignore */ From bf0f0ab509cb59cf25dd15b4a93bfb99e909df46 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 6 Dec 2025 19:09:55 +0100 Subject: [PATCH 045/103] fixed: Update CPMenu immediately when a CPMenuItem changes --- AppKit/CPMenu/CPMenu.j | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AppKit/CPMenu/CPMenu.j b/AppKit/CPMenu/CPMenu.j index 6c23e1408..a50abce34 100644 --- a/AppKit/CPMenu/CPMenu.j +++ b/AppKit/CPMenu/CPMenu.j @@ -390,6 +390,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] From 3c02cf5358efef0831517d529892f0fb2eb20f08 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 6 Dec 2025 19:15:59 +0100 Subject: [PATCH 046/103] new: Highlight top-level menu item when performing key equivalent --- AppKit/CPMenu/CPMenu.j | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/AppKit/CPMenu/CPMenu.j b/AppKit/CPMenu/CPMenu.j index 6c23e1408..380d3e2d0 100644 --- a/AppKit/CPMenu/CPMenu.j +++ b/AppKit/CPMenu/CPMenu.j @@ -1062,7 +1062,13 @@ 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]; + [self performActionForItemAtIndex:index]; + } else { //beep? @@ -1072,7 +1078,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 +1164,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 From dd24de5daff2ae031836747a07436d9e30f1e0dd Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 6 Dec 2025 19:32:52 +0100 Subject: [PATCH 047/103] new: Left-align menu bar icon and title --- AppKit/CPMenu/_CPMenuBarWindow.j | 52 ++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/AppKit/CPMenu/_CPMenuBarWindow.j b/AppKit/CPMenu/_CPMenuBarWindow.j index 513ec2049..5be5f231c 100644 --- a/AppKit/CPMenu/_CPMenuBarWindow.j +++ b/AppKit/CPMenu/_CPMenuBarWindow.j @@ -370,12 +370,41 @@ - (void)tile { + var bounds = [[self contentView] bounds], + height = CGRectGetHeight(bounds), + x = [[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-window-left-margin" forClass:_CPMenuView]; + + // 1. Layout the Icon (if present) + if (_iconImageView && ![_iconImageView isHidden]) + { + var iconFrame = [_iconImageView frame]; + + iconFrame.origin.x = x; + // Vertically center + iconFrame.origin.y = (height - CGRectGetHeight(iconFrame)) / 2.0; + + [_iconImageView setFrame:iconFrame]; + + x = CGRectGetMaxX(iconFrame) + 6.0; // Spacing between icon and title + } + + // 2. Layout the Title (if present) + if (_titleField && [_titleField stringValue] && [[_titleField stringValue] length] > 0) + { + var titleFrame = [_titleField frame]; + + titleFrame.origin.x = x; + titleFrame.origin.y = (height - CGRectGetHeight(titleFrame)) / 2.0; + + [_titleField setFrame:titleFrame]; + + x = CGRectGetMaxX(titleFrame) + 12.0; // Spacing between title and menu items + } + + // 3. Layout the Menu Items var items = [_menu itemArray], index = 0, - count = items.length, - - x = [[CPTheme defaultTheme] valueForAttributeWithName:@"menu-bar-window-left-margin" forClass:_CPMenuView], - y = 0.0, + count = items ? items.length : 0, isLeftAligned = YES; for (; index < count; ++index) @@ -409,21 +438,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 From 395022d65284ea95187a99a192efc1fb8b33fffc Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 6 Dec 2025 19:39:53 +0100 Subject: [PATCH 048/103] new: Hide main menu items that do not have submenus --- AppKit/CPMenu/_CPMenuBarWindow.j | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/AppKit/CPMenu/_CPMenuBarWindow.j b/AppKit/CPMenu/_CPMenuBarWindow.j index 513ec2049..d401b16fd 100644 --- a/AppKit/CPMenu/_CPMenuBarWindow.j +++ b/AppKit/CPMenu/_CPMenuBarWindow.j @@ -390,11 +390,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) { @@ -464,7 +471,7 @@ { var item = items[index]; - if ([item isHidden] || [item isSeparatorItem]) + if ([item isHidden] || [item isSeparatorItem] || ![item submenu]) continue; if (CGRectContainsPoint([self rectForItemAtIndex:index], aPoint)) From a88002861bc17e14d7ce31b340f7c665e2fc069e Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 6 Dec 2025 19:48:18 +0100 Subject: [PATCH 049/103] Fixed: CPMenu items appearing on the wrong side of the menu bar --- AppKit/CPMenu/CPMenu.j | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/AppKit/CPMenu/CPMenu.j b/AppKit/CPMenu/CPMenu.j index 6c23e1408..297171d12 100644 --- a/AppKit/CPMenu/CPMenu.j +++ b/AppKit/CPMenu/CPMenu.j @@ -269,7 +269,8 @@ var _CPMenuBarVisible = NO, if (self) { _title = aTitle; - _items = []; + // Use CPMutableArray instead of raw JS array for consistency with removeAllItems + _items = [CPMutableArray array]; _autoenablesItems = YES; _showsStateColumn = YES; @@ -374,6 +375,10 @@ var _CPMenuBarVisible = NO, [self willChangeValueForKey:@"items"]; _items = [CPMutableArray array]; [self didChangeValueForKey:@"items"]; + + // Ensure the main menu updates if cleared + if (self === [CPApp mainMenu] && _CPMenuBarSharedWindow) + [_CPMenuBarSharedWindow setMenu:self]; } /*! @@ -1223,6 +1228,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 +1248,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 +1345,3 @@ var CPMenuTitleKey = @"CPMenuTitleKey", @import "_CPMenuBarWindow.j" @import "_CPMenuWindow.j" - From c7f3dd5b0fe2e953b322af6fdbc6a95567de1ea3 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 7 Dec 2025 11:21:11 +0100 Subject: [PATCH 050/103] Fixed: [CPView addSubview:] was ignoring bounds origin for DOM positioning --- AppKit/CPView.j | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AppKit/CPView.j b/AppKit/CPView.j index adfd25a1f..31b26f017 100644 --- a/AppKit/CPView.j +++ b/AppKit/CPView.j @@ -662,6 +662,11 @@ var CPViewHighDPIDrawingEnabled = YES; #endif } +#if PLATFORM(DOM) + var origin = aSubview._frame.origin; + CPDOMDisplayServerSetStyleLeftTop(aSubview._DOMElement, _boundsTransform, origin.x, origin.y); +#endif + [aSubview setNextResponder:self]; [aSubview _scaleSizeUnitSquareToSize:[self _hierarchyScaleSize]]; From f3fe384df176d0d5195fe929454a1b1e09ea5ca1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 7 Dec 2025 11:28:53 +0100 Subject: [PATCH 051/103] fixed: CPDocument sends windowWillClose: message twice --- AppKit/CPDocument.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPDocument.j b/AppKit/CPDocument.j index 8770d57aa..e4a9d696b 100644 --- a/AppKit/CPDocument.j +++ b/AppKit/CPDocument.j @@ -874,7 +874,7 @@ var CPDocumentUntitledCount = 0; if (aDocument === self && shouldClose) [self close]; - if (theDelegate != null) + else if (theDelegate != null) theDelegate.isa.objj_msgSend3(theDelegate, context.selector, aDocument, shouldClose, context.context); } From 99ec1af3017baea47bd7a3da71d01514b6b7da90 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 7 Dec 2025 18:14:52 +0100 Subject: [PATCH 052/103] new: CPStackView --- AppKit/CPStackView.j | 700 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 700 insertions(+) create mode 100644 AppKit/CPStackView.j diff --git a/AppKit/CPStackView.j b/AppKit/CPStackView.j new file mode 100644 index 000000000..dbd3a7d88 --- /dev/null +++ b/AppKit/CPStackView.j @@ -0,0 +1,700 @@ +/* + * CPStackView.j + * AppKit + * + * Created by Daniel Boehringer. + * Copyright 2025, Cappuccino Project. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "../Foundation/Foundation.h" + +@import "CPView.j" + +// Gravity Areas +@typedef CPStackViewGravity + CPStackViewGravityTop = 1; + CPStackViewGravityLeading = 1; + CPStackViewGravityCenter = 2; + CPStackViewGravityBottom = 3; + CPStackViewGravityTrailing = 3; + +// Distribution (Deprecated in modern macOS, but kept for compatibility/logic) +@typedef CPStackViewDistribution + CPStackViewDistributionGravityAreas = 0; + CPStackViewDistributionFill = 1; + CPStackViewDistributionFillEqually = 2; + CPStackViewDistributionFillProportionally = 3; + CPStackViewDistributionEqualSpacing = 4; + CPStackViewDistributionEqualCentering = 5; + +// Visibility Priority +@typedef CPStackViewVisibilityPriority + CPStackViewVisibilityPriorityMustHold = 1000.0; + CPStackViewVisibilityPriorityNotVisible = 0.0; + +var CPStackViewSpacingUseDefault = 3.40282347e+38; // FLT_MAX + +/*! + @ingroup appkit + @class CPStackView + + CPStackView arranges an array of views horizontally or vertically and updates + their placement and sizing when the window size changes. + + Unlike a simple list, CPStackView supports "Gravity Areas" (Leading, Center, Trailing), + allowing you to pin groups of views to specific sections of the layout. +*/ +@implementation CPStackView : CPView +{ + CPUserInterfaceLayoutOrientation _orientation; + CPLayoutAttribute _alignment; + float _spacing; + CPEdgeInsets _edgeInsets; + + BOOL _detachesHiddenViews; + + // View Storage by Gravity + CPMutableArray _viewsLeading; + CPMutableArray _viewsCenter; + CPMutableArray _viewsTrailing; + + // Internal cache of all arranged subviews to maintain order for hittesting/iterating + CPMutableArray _arrangedSubviews; + + // Custom Spacing storage + CPMapTable _customSpacings; + + // Visibility Priorities + CPMapTable _visibilityPriorities; +} + +#pragma mark - +#pragma mark Initialization + ++ (CPStackView)stackViewWithViews:(CPArray)views +{ + var stackView = [[CPStackView alloc] initWithFrame:CGRectMakeZero()]; + + for (var i = 0, count = [views count]; i < count; i++) + [stackView addView:views[i] inGravity:CPStackViewGravityLeading]; + + return stackView; +} + +- (id)initWithFrame:(CGRect)aFrame +{ + if (self = [super initWithFrame:aFrame]) + { + _orientation = CPUserInterfaceLayoutOrientationHorizontal; + _alignment = CPLayoutAttributeCenterY; // Default alignment + _spacing = 8.0; // Default Cocoa spacing + _edgeInsets = CPEdgeInsetsMake(0, 0, 0, 0); + _detachesHiddenViews = YES; + + _viewsLeading = [[CPMutableArray alloc] init]; + _viewsCenter = [[CPMutableArray alloc] init]; + _viewsTrailing = [[CPMutableArray alloc] init]; + _arrangedSubviews = [[CPMutableArray alloc] init]; + + _customSpacings = [[CPMapTable alloc] init]; + _visibilityPriorities = [[CPMapTable alloc] init]; + } + return self; +} + +#pragma mark - +#pragma mark Configuration + +/*! + The horizontal or vertical layout direction of the stack view. +*/ +- (CPUserInterfaceLayoutOrientation)orientation +{ + return _orientation; +} + +- (void)setOrientation:(CPUserInterfaceLayoutOrientation)anOrientation +{ + if (_orientation === anOrientation) + return; + + _orientation = anOrientation; + + // Reset default alignment based on new orientation if needed, + // though usually developer sets alignment explicitly. + // If switching to Vertical, CenterY makes less sense, usually CenterX. + if (_orientation === CPUserInterfaceLayoutOrientationVertical) + { + if (_alignment === CPLayoutAttributeCenterY) + _alignment = CPLayoutAttributeCenterX; + } + else + { + if (_alignment === CPLayoutAttributeCenterX) + _alignment = CPLayoutAttributeCenterY; + } + + [self setNeedsLayout:YES]; +} + +/*! + The view alignment within the stack view. + Common values: + Horizontal: CPLayoutAttributeTop, CPLayoutAttributeBottom, CPLayoutAttributeCenterY, CPLayoutAttributeHeight (fill) + Vertical: CPLayoutAttributeLeading, CPLayoutAttributeTrailing, CPLayoutAttributeCenterX, CPLayoutAttributeWidth (fill) +*/ +- (CPLayoutAttribute)alignment +{ + return _alignment; +} + +- (void)setAlignment:(CPLayoutAttribute)anAlignment +{ + if (_alignment === anAlignment) + return; + + _alignment = anAlignment; + [self setNeedsLayout:YES]; +} + +/*! + The minimum spacing, in points, between adjacent views in the stack view. +*/ +- (float)spacing +{ + return _spacing; +} + +- (void)setSpacing:(float)aSpacing +{ + if (_spacing === aSpacing) + return; + + _spacing = aSpacing; + [self setNeedsLayout:YES]; +} + +/*! + The geometric padding, in points, inside the stack view, surrounding its views. +*/ +- (CPEdgeInsets)edgeInsets +{ + return _edgeInsets; +} + +- (void)setEdgeInsets:(CPEdgeInsets)insets +{ + if (CPEdgeInsetsEqualToEdgeInsets(_edgeInsets, insets)) + return; + + _edgeInsets = insets; + [self setNeedsLayout:YES]; +} + +/*! + A Boolean value that indicates whether the stack view removes hidden views from its view hierarchy. +*/ +- (BOOL)detachesHiddenViews +{ + return _detachesHiddenViews; +} + +- (void)setDetachesHiddenViews:(BOOL)shouldDetach +{ + if (_detachesHiddenViews === shouldDetach) + return; + + _detachesHiddenViews = shouldDetach; + [self setNeedsLayout:YES]; +} + +#pragma mark - +#pragma mark Managing Views in Gravity Areas + +- (CPArray)_containerForGravity:(CPStackViewGravity)gravity +{ + if (gravity === CPStackViewGravityCenter) + return _viewsCenter; + else if (gravity === CPStackViewGravityTrailing) // or Bottom + return _viewsTrailing; + + return _viewsLeading; // Leading or Top +} + +/*! + Adds a view to the end of the stack view gravity area. +*/ +- (void)addView:(CPView)aView inGravity:(CPStackViewGravity)gravity +{ + var container = [self _containerForGravity:gravity]; + + // Check if view is already in a container + if ([_arrangedSubviews containsObject:aView]) + [self removeView:aView]; + + [container addObject:aView]; + [_arrangedSubviews addObject:aView]; + + // Add as actual subview + if ([aView superview] !== self) + [self addSubview:aView]; + + [self setNeedsLayout:YES]; +} + +/*! + Adds a view to a stack view gravity area at a specified index position. +*/ +- (void)insertView:(CPView)aView atIndex:(CPInteger)index inGravity:(CPStackViewGravity)gravity +{ + var container = [self _containerForGravity:gravity]; + + if ([_arrangedSubviews containsObject:aView]) + [self removeView:aView]; + + if (index >= [container count]) + [container addObject:aView]; + else + [container insertObject:aView atIndex:index]; + + [_arrangedSubviews addObject:aView]; + + if ([aView superview] !== self) + [self addSubview:aView]; + + [self setNeedsLayout:YES]; +} + +/*! + Specifies an array of views for a specified gravity area in the stack view, replacing any previous views in that area. +*/ +- (void)setViews:(CPArray)views inGravity:(CPStackViewGravity)gravity +{ + var container = [self _containerForGravity:gravity]; + + // Remove old views from arranged list and superview + for (var i = 0; i < [container count]; i++) + { + var oldView = container[i]; + [oldView removeFromSuperview]; + [_arrangedSubviews removeObject:oldView]; + } + + [container removeAllObjects]; + + for (var i = 0; i < [views count]; i++) + { + var newView = views[i]; + [container addObject:newView]; + [_arrangedSubviews addObject:newView]; + [self addSubview:newView]; + } + + [self setNeedsLayout:YES]; +} + +/*! + Removes a specified view from the stack view. +*/ +- (void)removeView:(CPView)aView +{ + if (![_arrangedSubviews containsObject:aView]) + return; + + [_viewsLeading removeObject:aView]; + [_viewsCenter removeObject:aView]; + [_viewsTrailing removeObject:aView]; + [_arrangedSubviews removeObject:aView]; + + [aView removeFromSuperview]; + + [self setNeedsLayout:YES]; +} + +/*! + Returns the array of views in the specified gravity area in the stack view. +*/ +- (CPArray)viewsInGravity:(CPStackViewGravity)gravity +{ + return [[self _containerForGravity:gravity] copy]; +} + +/*! + The array of views arranged by the stack view. +*/ +- (CPArray)arrangedSubviews +{ + return [_arrangedSubviews copy]; +} + +/*! + Adds the specified view to the end of the arranged subviews list. + (Defaults to Leading gravity if not specified). +*/ +- (void)addArrangedSubview:(CPView)view +{ + [self addView:view inGravity:CPStackViewGravityLeading]; +} + +/*! + Removes the provided view from the stack’s array of arranged subviews. +*/ +- (void)removeArrangedSubview:(CPView)view +{ + [self removeView:view]; +} + +#pragma mark - +#pragma mark Custom Spacing + +- (float)customSpacingAfterView:(CPView)aView +{ + var val = [_customSpacings objectForKey:aView]; + if (val) + return [val floatValue]; + + return CPStackViewSpacingUseDefault; +} + +- (void)setCustomSpacing:(float)spacing afterView:(CPView)aView +{ + if (spacing === CPStackViewSpacingUseDefault) + [_customSpacings removeObjectForKey:aView]; + else + [_customSpacings setObject:spacing forKey:aView]; + + [self setNeedsLayout:YES]; +} + +- (float)_spacingAfterView:(CPView)aView +{ + var custom = [self customSpacingAfterView:aView]; + if (custom !== CPStackViewSpacingUseDefault) + return custom; + return _spacing; +} + +#pragma mark - +#pragma mark Visibility Priority + +- (void)setVisibilityPriority:(float)priority forView:(CPView)aView +{ + [_visibilityPriorities setObject:priority forKey:aView]; + + if (priority === CPStackViewVisibilityPriorityNotVisible) + { + [aView setHidden:YES]; + } + else if (priority === CPStackViewVisibilityPriorityMustHold) + { + [aView setHidden:NO]; + } + // Note: Intermediate priorities require complex constraint logic + // or a multi-pass layout system to determine fitting, which is + // simplified here to basic Hidden/Visible states. + + [self setNeedsLayout:YES]; +} + +- (float)visibilityPriorityForView:(CPView)aView +{ + var val = [_visibilityPriorities objectForKey:aView]; + if (val) + return [val floatValue]; + return CPStackViewVisibilityPriorityMustHold; +} + +#pragma mark - +#pragma mark Layout + +- (void)resizeSubviewsWithOldSize:(CGSize)oldSize +{ + [self layoutSubviews]; +} + +- (void)layoutSubviews +{ + if (_orientation === CPUserInterfaceLayoutOrientationVertical) + [self _layoutVertical]; + else + [self _layoutHorizontal]; +} + +- (void)_layoutHorizontal +{ + var bounds = [self bounds], + availWidth = CGRectGetWidth(bounds) - _edgeInsets.left - _edgeInsets.right, + availHeight = CGRectGetHeight(bounds) - _edgeInsets.top - _edgeInsets.bottom, + currentX = _edgeInsets.left; + + // 1. Layout Leading Views + currentX = [self _layoutViews:_viewsLeading startOffset:currentX availableOrthogonalSize:availHeight direction:1]; + + // 2. Layout Trailing Views + // We layout backwards from the right + var startRight = CGRectGetWidth(bounds) - _edgeInsets.right; + [self _layoutViews:_viewsTrailing startOffset:startRight availableOrthogonalSize:availHeight direction:-1]; + + // 3. Layout Center Views + if ([_viewsCenter count] > 0) + { + // Calculate total width of center stack + var centerStackWidth = 0.0; + for (var i = 0; i < [_viewsCenter count]; i++) + { + var view = _viewsCenter[i]; + if (_detachesHiddenViews && [view isHidden]) continue; + + centerStackWidth += CGRectGetWidth([view frame]); + if (i < [_viewsCenter count] - 1) + centerStackWidth += [self _spacingAfterView:view]; + } + + var centerStart = (CGRectGetWidth(bounds) / 2.0) - (centerStackWidth / 2.0); + + // Clamp to prevent overlap with Leading (simplified collision logic) + // ideally stack view compresses views, but here we just shift/clip + if (centerStart < currentX) + centerStart = currentX; + + [self _layoutViews:_viewsCenter startOffset:centerStart availableOrthogonalSize:availHeight direction:1]; + } +} + +- (void)_layoutVertical +{ + var bounds = [self bounds], + availWidth = CGRectGetWidth(bounds) - _edgeInsets.left - _edgeInsets.right, + availHeight = CGRectGetHeight(bounds) - _edgeInsets.top - _edgeInsets.bottom, + currentY = _edgeInsets.top; + + // 1. Layout Top (Leading) Views + currentY = [self _layoutViews:_viewsLeading startOffset:currentY availableOrthogonalSize:availWidth direction:1]; + + // 2. Layout Bottom (Trailing) Views + var startBottom = CGRectGetHeight(bounds) - _edgeInsets.bottom; + [self _layoutViews:_viewsTrailing startOffset:startBottom availableOrthogonalSize:availWidth direction:-1]; + + // 3. Layout Center Views + if ([_viewsCenter count] > 0) + { + var centerStackHeight = 0.0; + for (var i = 0; i < [_viewsCenter count]; i++) + { + var view = _viewsCenter[i]; + if (_detachesHiddenViews && [view isHidden]) continue; + + centerStackHeight += CGRectGetHeight([view frame]); + if (i < [_viewsCenter count] - 1) + centerStackHeight += [self _spacingAfterView:view]; + } + + var centerStart = (CGRectGetHeight(bounds) / 2.0) - (centerStackHeight / 2.0); + + if (centerStart < currentY) + centerStart = currentY; + + [self _layoutViews:_viewsCenter startOffset:centerStart availableOrthogonalSize:availWidth direction:1]; + } +} + +// Helper to layout a specific array of views in one direction +// Returns the ending offset +- (float)_layoutViews:(CPArray)views startOffset:(float)offset availableOrthogonalSize:(float)orthoSize direction:(int)dir +{ + var cursor = offset; + var isVert = (_orientation === CPUserInterfaceLayoutOrientationVertical); + + // If direction is -1 (Trailing/Bottom), we iterate backwards + // However, the standard behavior for trailing gravity is that the *last* view added is at the *end*. + // Leading: [A] [B] -> + // Trailing: -> [C] [D] (where D is rightmost) + // To support Trailing logic: We start at Right Edge, move left by Width(D), place D, move left by Spacing... + + var count = [views count]; + if (count === 0) return cursor; + + // If direction is negative (Trailing), we process list in reverse order to stack them from edge inwards + var i = (dir === 1) ? 0 : count - 1; + var limit = (dir === 1) ? count : -1; + var step = (dir === 1) ? 1 : -1; + + for (; i !== limit; i += step) + { + var view = views[i]; + + if (_detachesHiddenViews && [view isHidden]) + continue; + + var viewFrame = [view frame]; + var viewSizePrimary = isVert ? CGRectGetHeight(viewFrame) : CGRectGetWidth(viewFrame); + + // Handle Alignment (Orthogonal Axis) + var orthoPos = 0.0; + var viewOrthoSize = isVert ? CGRectGetWidth(viewFrame) : CGRectGetHeight(viewFrame); + + // Apply Stretch/Fill Alignment + if (isVert) + { + // Vertical Stack, dealing with Width + if (_alignment === CPLayoutAttributeWidth || _alignment === CPLayoutAttributeLeading || _alignment === CPLayoutAttributeTrailing) + { + // Note: CPLayoutAttributeLeading/Trailing in this context implies filling width usually, + // or aligning to edges. Let's assume Width/Fill for Leading/Trailing/Left/Right + // in this simplified implementation, or strictly left/right. + + if (_alignment === CPLayoutAttributeWidth || _alignment === CPLayoutAttributeLeft || _alignment === CPLayoutAttributeLeading) + { + // Fill width if explicit, or just align left + if (_alignment === CPLayoutAttributeWidth) viewOrthoSize = orthoSize; + orthoPos = _edgeInsets.left; + } + else if (_alignment === CPLayoutAttributeRight || _alignment === CPLayoutAttributeTrailing) + { + orthoPos = _edgeInsets.left + (orthoSize - viewOrthoSize); + } + else // CenterX + { + orthoPos = _edgeInsets.left + (orthoSize - viewOrthoSize) / 2.0; + } + } + else // Default CenterX + { + orthoPos = _edgeInsets.left + (orthoSize - viewOrthoSize) / 2.0; + } + } + else + { + // Horizontal Stack, dealing with Height + if (_alignment === CPLayoutAttributeHeight || _alignment === CPLayoutAttributeTop || _alignment === CPLayoutAttributeBottom) + { + if (_alignment === CPLayoutAttributeHeight) + { + viewOrthoSize = orthoSize; + orthoPos = _edgeInsets.top; + } + else if (_alignment === CPLayoutAttributeTop) + { + orthoPos = _edgeInsets.top; + } + else if (_alignment === CPLayoutAttributeBottom) + { + orthoPos = _edgeInsets.top + (orthoSize - viewOrthoSize); + } + else // CenterY + { + orthoPos = _edgeInsets.top + (orthoSize - viewOrthoSize) / 2.0; + } + } + else // Default CenterY + { + orthoPos = _edgeInsets.top + (orthoSize - viewOrthoSize) / 2.0; + } + } + + // Calculate Position + var originX = 0.0, originY = 0.0; + var sizeW = 0.0, sizeH = 0.0; + + if (isVert) + { + // Vertical + sizeH = viewSizePrimary; + sizeW = viewOrthoSize; + originX = orthoPos; + + if (dir === 1) { + originY = cursor; + cursor += sizeH + [self _spacingAfterView:view]; + } else { + cursor -= sizeH; + originY = cursor; + cursor -= [self _spacingAfterView:view]; + } + } + else + { + // Horizontal + sizeW = viewSizePrimary; + sizeH = viewOrthoSize; + originY = orthoPos; + + if (dir === 1) { + originX = cursor; + cursor += sizeW + [self _spacingAfterView:view]; + } else { + cursor -= sizeW; + originX = cursor; + cursor -= [self _spacingAfterView:view]; + } + } + + [view setFrame:CGRectMake(originX, originY, sizeW, sizeH)]; + } + + return cursor; +} + +#pragma mark - +#pragma mark CPCoding + +- (id)initWithCoder:(CPCoder)aCoder +{ + self = [super initWithCoder:aCoder]; + if (self) + { + _orientation = [aCoder decodeIntForKey:@"CPStackViewOrientation"]; + _alignment = [aCoder decodeIntForKey:@"CPStackViewAlignment"]; + _spacing = [aCoder decodeFloatForKey:@"CPStackViewSpacing"]; + _edgeInsets = [aCoder decodeObjectForKey:@"CPStackViewEdgeInsets"]; // Assuming CPEdgeInsets supports obj coding or manual decode + if (!_edgeInsets) _edgeInsets = CPEdgeInsetsMake(0,0,0,0); + + _detachesHiddenViews = [aCoder decodeBoolForKey:@"CPStackViewDetachesHiddenViews"]; + + _viewsLeading = [aCoder decodeObjectForKey:@"CPStackViewViewsLeading"] || []; + _viewsCenter = [aCoder decodeObjectForKey:@"CPStackViewViewsCenter"] || []; + _viewsTrailing = [aCoder decodeObjectForKey:@"CPStackViewViewsTrailing"] || []; + + // Rebuild arranged subviews cache + _arrangedSubviews = [[CPMutableArray alloc] init]; + [_arrangedSubviews addObjectsFromArray:_viewsLeading]; + [_arrangedSubviews addObjectsFromArray:_viewsCenter]; + [_arrangedSubviews addObjectsFromArray:_viewsTrailing]; + + _customSpacings = [aCoder decodeObjectForKey:@"CPStackViewCustomSpacings"] || [[CPMapTable alloc] init]; + _visibilityPriorities = [[CPMapTable alloc] init]; // usually not persisted + } + return self; +} + +- (void)encodeWithCoder:(CPCoder)aCoder +{ + [super encodeWithCoder:aCoder]; + [aCoder encodeInt:_orientation forKey:@"CPStackViewOrientation"]; + [aCoder encodeInt:_alignment forKey:@"CPStackViewAlignment"]; + [aCoder encodeFloat:_spacing forKey:@"CPStackViewSpacing"]; + [aCoder encodeObject:_edgeInsets forKey:@"CPStackViewEdgeInsets"]; + [aCoder encodeBool:_detachesHiddenViews forKey:@"CPStackViewDetachesHiddenViews"]; + + [aCoder encodeObject:_viewsLeading forKey:@"CPStackViewViewsLeading"]; + [aCoder encodeObject:_viewsCenter forKey:@"CPStackViewViewsCenter"]; + [aCoder encodeObject:_viewsTrailing forKey:@"CPStackViewViewsTrailing"]; + + [aCoder encodeObject:_customSpacings forKey:@"CPStackViewCustomSpacings"]; +} + +@end From 9a9c9b84814ce482aa3ec84760f6812ec8a365c2 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 7 Dec 2025 19:46:11 +0100 Subject: [PATCH 053/103] Fixed: CPRuleEditor rows did not resize after being shrunken --- AppKit/CPRuleEditor/CPRuleEditor.j | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j index f3b3e0726..1910d6153 100644 --- a/AppKit/CPRuleEditor/CPRuleEditor.j +++ b/AppKit/CPRuleEditor/CPRuleEditor.j @@ -1774,7 +1774,12 @@ TODO: implement - (_CPRuleEditorViewSliceRow)_createNewSliceWithFrame:(CGRect)frame ruleEditorView:(CPRuleEditor)editor { - return [[_CPRuleEditorViewSliceRow alloc] initWithFrame:frame ruleEditorView:editor]; + var slice = [[_CPRuleEditorViewSliceRow alloc] initWithFrame:frame ruleEditorView:editor]; + + // Ensure the slice resizes with the editor + [slice setAutoresizingMask:CPViewWidthSizable]; + + return slice; } - (void)_reconfigureSubviewsAnimate:(BOOL)animate From a2595093f8141d248c7984fa3aaa53d6ccc58183 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 7 Dec 2025 21:23:07 +0100 Subject: [PATCH 054/103] Fixed: Toolbar items show now visual feedback when clicked --- AppKit/CPToolbar.j | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/AppKit/CPToolbar.j b/AppKit/CPToolbar.j index bcfec1aee..c60276b64 100644 --- a/AppKit/CPToolbar.j +++ b/AppKit/CPToolbar.j @@ -1014,6 +1014,7 @@ var LABEL_MARGIN = 2.0; CPImageView _imageView; CPView _view; + CPView _highlightView; CPTextField _labelField; @@ -1243,6 +1244,18 @@ var LABEL_MARGIN = 2.0; if (alternateImage) [_imageView setImage:alternateImage]; + else + { + if (!_highlightView) + { + _highlightView = [[CPView alloc] initWithFrame:[_imageView bounds]]; + [_highlightView setBackgroundColor:[CPColor blackColor]]; + [_highlightView setAlphaValue:0.3]; + [_highlightView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + } + + [_imageView addSubview:_highlightView]; + } [_labelField setTextShadowOffset:CGSizeMakeZero()]; } @@ -1253,6 +1266,8 @@ var LABEL_MARGIN = 2.0; if (image) [_imageView setImage:image]; + [_highlightView removeFromSuperview]; + [_labelField setTextShadowOffset:CGSizeMake(0.0, 1.0)]; } From 2045d1eaebb98ddf624cd91c26bf93e721af6858 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 9 Dec 2025 17:56:47 +0100 Subject: [PATCH 055/103] Add cappuccino bin directory to .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fdcc77f81..36beffcd1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ node_modules /dist/objective-j/lib /dist/cappuccino/package.json /dist/cappuccino/lib - +/dist/cappuccino/bin From 569f93549e3257af6c3439fb07b508c16d23883f Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 25 Dec 2025 17:23:54 +0100 Subject: [PATCH 056/103] new: slideback animation after failed drag new: slideback animation after failed drag --- AppKit/CPDragServer.j | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/AppKit/CPDragServer.j b/AppKit/CPDragServer.j index a26189618..85d1fce37 100644 --- a/AppKit/CPDragServer.j +++ b/AppKit/CPDragServer.j @@ -132,6 +132,12 @@ var CPDraggingSource_draggedImage_movedTo_ = 1 << 0, unsigned _dragOperation; CPTimer _draggingUpdateTimer; + + // Animation Support + CGPoint _animationStartOrigin; + CGPoint _animationTargetOrigin; + CGPoint _pendingEndLocation; + CPDragOperation _pendingEndOperation; } /* @@ -325,6 +331,49 @@ var CPDraggingSource_draggedImage_movedTo_ = 1 << 0, [_draggingUpdateTimer invalidate]; _draggingUpdateTimer = nil; + // Check if we should slide back. + // Logic: + // 1. It must be an emulated drag (controls _draggedWindow). + // 2. SlideBack was requested in dragView:... + // 3. The operation failed (CPDragOperationNone) or was cancelled. + if (![CPPlatform supportsDragAndDrop] && _shouldSlideBack && anOperation === CPDragOperationNone) + { + // Save these for the final cleanup after animation + _pendingEndLocation = aLocation; + _pendingEndOperation = anOperation; + + _animationStartOrigin = [_draggedWindow frame].origin; + _animationTargetOrigin = _startDragLocation; // Captured when drag started + + var animation = [[CPAnimation alloc] initWithDuration:0.25 animationCurve:CPAnimationEaseOut]; + [animation setDelegate:self]; + [animation startAnimation]; + + // Return early. We will call _performFinalCleanup in animationDidEnd: + return; + } + + // Normal path (Success or no slide back) + [self _performFinalCleanupWithLocation:aLocation operation:anOperation]; +} + +// Helper to interpolate the window movement manually +- (void)animation:(CPAnimation)anAnimation valueForProgress:(float)aProgress +{ + var x = _animationStartOrigin.x + (_animationTargetOrigin.x - _animationStartOrigin.x) * aProgress, + y = _animationStartOrigin.y + (_animationTargetOrigin.y - _animationStartOrigin.y) * aProgress; + + [_draggedWindow setFrameOrigin:CGPointMake(x, y)]; +} + +- (void)animationDidEnd:(CPAnimation)anAnimation +{ + [self _performFinalCleanupWithLocation:_pendingEndLocation operation:_pendingEndOperation]; +} + +// Consolidate cleanup logic to avoid duplication +- (void)_performFinalCleanupWithLocation:(CGPoint)aLocation operation:(CPDragOperation)anOperation +{ [_draggedView removeFromSuperview]; if (![CPPlatform supportsDragAndDrop]) From bc64d147a874d79e8acd48911e5172938a677013 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 25 Dec 2025 19:56:15 +0100 Subject: [PATCH 057/103] fixed: used wrong approach --- AppKit/CPDragServer.j | 48 ++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/AppKit/CPDragServer.j b/AppKit/CPDragServer.j index 85d1fce37..bd2b09a74 100644 --- a/AppKit/CPDragServer.j +++ b/AppKit/CPDragServer.j @@ -26,6 +26,7 @@ @import "CPPasteboard.j" @import "CPView.j" @import "CPWindow_Constants.j" +@import "CPViewAnimation.j" @class CPWindow // This file is imported by CPWindow.j @class _CPDOMDataTransferPasteboard @@ -132,10 +133,8 @@ var CPDraggingSource_draggedImage_movedTo_ = 1 << 0, unsigned _dragOperation; CPTimer _draggingUpdateTimer; - - // Animation Support - CGPoint _animationStartOrigin; - CGPoint _animationTargetOrigin; + + // Animation State CGPoint _pendingEndLocation; CPDragOperation _pendingEndOperation; } @@ -331,47 +330,44 @@ var CPDraggingSource_draggedImage_movedTo_ = 1 << 0, [_draggingUpdateTimer invalidate]; _draggingUpdateTimer = nil; - // Check if we should slide back. - // Logic: - // 1. It must be an emulated drag (controls _draggedWindow). - // 2. SlideBack was requested in dragView:... - // 3. The operation failed (CPDragOperationNone) or was cancelled. + // Check if we should slide back (drag failed + slideBack requested) if (![CPPlatform supportsDragAndDrop] && _shouldSlideBack && anOperation === CPDragOperationNone) { - // Save these for the final cleanup after animation + // Store state to finalize drag after animation completes _pendingEndLocation = aLocation; _pendingEndOperation = anOperation; - _animationStartOrigin = [_draggedWindow frame].origin; - _animationTargetOrigin = _startDragLocation; // Captured when drag started + var currentFrame = [_draggedWindow frame], + targetFrame = CGRectMake(_startDragLocation.x, _startDragLocation.y, currentFrame.size.width, currentFrame.size.height); - var animation = [[CPAnimation alloc] initWithDuration:0.25 animationCurve:CPAnimationEaseOut]; + // 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 early. We will call _performFinalCleanup in animationDidEnd: + return; } - // Normal path (Success or no slide back) [self _performFinalCleanupWithLocation:aLocation operation:anOperation]; } -// Helper to interpolate the window movement manually -- (void)animation:(CPAnimation)anAnimation valueForProgress:(float)aProgress -{ - var x = _animationStartOrigin.x + (_animationTargetOrigin.x - _animationStartOrigin.x) * aProgress, - y = _animationStartOrigin.y + (_animationTargetOrigin.y - _animationStartOrigin.y) * aProgress; - - [_draggedWindow setFrameOrigin:CGPointMake(x, y)]; -} - - (void)animationDidEnd:(CPAnimation)anAnimation { [self _performFinalCleanupWithLocation:_pendingEndLocation operation:_pendingEndOperation]; } -// Consolidate cleanup logic to avoid duplication +- (void)animationDidStop:(CPAnimation)anAnimation +{ + [self _performFinalCleanupWithLocation:_pendingEndLocation operation:_pendingEndOperation]; +} + - (void)_performFinalCleanupWithLocation:(CGPoint)aLocation operation:(CPDragOperation)anOperation { [_draggedView removeFromSuperview]; From 99cf8e63874f6a668c032fa1e4cda19c8ac0523b Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 26 Dec 2025 10:42:41 +0100 Subject: [PATCH 058/103] improved logic --- AppKit/CPDocument.j | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/AppKit/CPDocument.j b/AppKit/CPDocument.j index e4a9d696b..70fa6ef00 100644 --- a/AppKit/CPDocument.j +++ b/AppKit/CPDocument.j @@ -871,10 +871,13 @@ var CPDocumentUntitledCount = 0; { var theDelegate = context.delegate; - if (aDocument === self && shouldClose) + // Only close the document explicitly if there is NO delegate to handle the action. + // If a delegate exists (e.g., the CPWindow), it is responsible for performing the close + // upon receiving the callback below. Calling [self close] here would cause a double-close. + if (aDocument === self && shouldClose && theDelegate == nil) [self close]; - else if (theDelegate != null) + if (theDelegate) theDelegate.isa.objj_msgSend3(theDelegate, context.selector, aDocument, shouldClose, context.context); } From 5ac90c163ab3be7472ee8f35fa5b2675c1f54c9c Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 26 Dec 2025 11:10:44 +0100 Subject: [PATCH 059/103] new: manual test --- Tests/Manual/CPStackViewTest/AppController.j | 208 +++++ Tests/Manual/CPStackViewTest/Info.plist | 10 + Tests/Manual/CPStackViewTest/Jakefile | 94 ++ .../CPStackViewTest/Resources/MainMenu.cib | 1 + .../CPStackViewTest/Resources/MainMenu.xib | 809 ++++++++++++++++++ .../CPStackViewTest/Resources/spinner.gif | Bin 0 -> 1434 bytes Tests/Manual/CPStackViewTest/index-debug.html | 204 +++++ Tests/Manual/CPStackViewTest/index.html | 166 ++++ Tests/Manual/CPStackViewTest/main.j | 18 + 9 files changed, 1510 insertions(+) create mode 100644 Tests/Manual/CPStackViewTest/AppController.j create mode 100644 Tests/Manual/CPStackViewTest/Info.plist create mode 100644 Tests/Manual/CPStackViewTest/Jakefile create mode 100644 Tests/Manual/CPStackViewTest/Resources/MainMenu.cib create mode 100644 Tests/Manual/CPStackViewTest/Resources/MainMenu.xib create mode 100644 Tests/Manual/CPStackViewTest/Resources/spinner.gif create mode 100644 Tests/Manual/CPStackViewTest/index-debug.html create mode 100644 Tests/Manual/CPStackViewTest/index.html create mode 100644 Tests/Manual/CPStackViewTest/main.j diff --git a/Tests/Manual/CPStackViewTest/AppController.j b/Tests/Manual/CPStackViewTest/AppController.j new file mode 100644 index 000000000..698f59ff3 --- /dev/null +++ b/Tests/Manual/CPStackViewTest/AppController.j @@ -0,0 +1,208 @@ +/* + * AppController.j + * CPStackViewTest + * + * Created by Daniel Boehringer. + * Copyright 2025, Cappuccino Project. + */ + +@import +@import + +// We import the class to be tested. +// Assuming CPStackView.j is in the same directory or properly included in the build. +@import "CPStackView.j" + +@implementation AppController : CPObject +{ + CPWindow theWindow; + + // We will construct these programmatically for the test + // to avoid needing a .cib file for a new class. + CPStackView stackViewHorizontal; + CPStackView stackViewVertical; + + // Control references + CPCheckBox detachHiddenCheckbox; + CPView toggleTargetView; +} + +- (void)applicationDidFinishLaunching:(CPNotification)aNotification +{ + theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask]; + var contentView = [theWindow contentView]; + [contentView setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]]; + + // 1. Create a Label + var label = [CPTextField labelWithTitle:@"CPStackView Manual Test"]; + [label setFont:[CPFont boldSystemFontOfSize:18]]; + [label setFrameOrigin:CGPointMake(20, 20)]; + [contentView addSubview:label]; + + // 2. Create Horizontal Stack View (The primary test subject) + // We frame it in the top half + stackViewHorizontal = [[CPStackView alloc] initWithFrame:CGRectMake(20, 60, 600, 150)]; + [stackViewHorizontal setBackgroundColor:[CPColor whiteColor]]; + [stackViewHorizontal setOrientation:CPUserInterfaceLayoutOrientationHorizontal]; + [stackViewHorizontal setEdgeInsets:CPEdgeInsetsMake(10, 10, 10, 10)]; + + // Add Views to Horizontal Stack + // Leading + [stackViewHorizontal addView:[self _createBoxColor:[CPColor redColor] size:CGSizeMake(40, 40) label:@"L1"] inGravity:CPStackViewGravityLeading]; + [stackViewHorizontal addView:[self _createBoxColor:[CPColor redColor] size:CGSizeMake(60, 80) label:@"L2"] inGravity:CPStackViewGravityLeading]; // Taller to test alignment + + // Center + toggleTargetView = [self _createBoxColor:[CPColor greenColor] size:CGSizeMake(50, 50) label:@"C1\n(Toggle)"]; + [stackViewHorizontal addView:toggleTargetView inGravity:CPStackViewGravityCenter]; + [stackViewHorizontal addView:[self _createBoxColor:[CPColor greenColor] size:CGSizeMake(50, 50) label:@"C2"] inGravity:CPStackViewGravityCenter]; + + // Trailing + [stackViewHorizontal addView:[self _createBoxColor:[CPColor blueColor] size:CGSizeMake(40, 40) label:@"T1"] inGravity:CPStackViewGravityTrailing]; + [stackViewHorizontal addView:[self _createBoxColor:[CPColor blueColor] size:CGSizeMake(40, 40) label:@"T2"] inGravity:CPStackViewGravityTrailing]; + + // Set Autoresizing to stick to width + [stackViewHorizontal setAutoresizingMask:CPViewWidthSizable]; + + // Visual border for the stack view itself + var borderView = [[CPView alloc] initWithFrame:CGRectInset([stackViewHorizontal frame], -1, -1)]; + [borderView setBackgroundColor:[CPColor grayColor]]; + [contentView addSubview:borderView]; + [contentView addSubview:stackViewHorizontal]; + + + // 3. Create Vertical Stack View (Secondary test) + stackViewVertical = [[CPStackView alloc] initWithFrame:CGRectMake(20, 230, 200, 300)]; + [stackViewVertical setBackgroundColor:[CPColor whiteColor]]; + [stackViewVertical setOrientation:CPUserInterfaceLayoutOrientationVertical]; + [stackViewVertical setEdgeInsets:CPEdgeInsetsMake(5, 5, 5, 5)]; + [stackViewVertical setAlignment:CPLayoutAttributeCenterX]; // Center items horizontally + + [stackViewVertical addView:[self _createBoxColor:[CPColor orangeColor] size:CGSizeMake(40, 30) label:@"Top"] inGravity:CPStackViewGravityTop]; + [stackViewVertical addView:[self _createBoxColor:[CPColor purpleColor] size:CGSizeMake(80, 40) label:@"Mid"] inGravity:CPStackViewGravityCenter]; + [stackViewVertical addView:[self _createBoxColor:[CPColor brownColor] size:CGSizeMake(40, 30) label:@"Bot"] inGravity:CPStackViewGravityBottom]; + + var borderViewVert = [[CPView alloc] initWithFrame:CGRectInset([stackViewVertical frame], -1, -1)]; + [borderViewVert setBackgroundColor:[CPColor grayColor]]; + [contentView addSubview:borderViewVert]; + [contentView addSubview:stackViewVertical]; + + + // 4. Controls Area + var controlsY = 230; + var controlsX = 250; + + var btnToggleHide = [CPButton buttonWithTitle:@"Toggle Center View Hidden"]; + [btnToggleHide setFrameOrigin:CGPointMake(controlsX, controlsY)]; + [btnToggleHide setTarget:self]; + [btnToggleHide setAction:@selector(toggleHidden:)]; + [contentView addSubview:btnToggleHide]; + + controlsY += 40; + detachHiddenCheckbox = [CPCheckBox checkBoxWithTitle:@"Detaches Hidden Views"]; + [detachHiddenCheckbox setFrameOrigin:CGPointMake(controlsX, controlsY)]; + [detachHiddenCheckbox setState:CPOnState]; + [detachHiddenCheckbox setTarget:self]; + [detachHiddenCheckbox setAction:@selector(toggleDetaches:)]; + [contentView addSubview:detachHiddenCheckbox]; + + controlsY += 40; + var btnAlignTop = [CPButton buttonWithTitle:@"Align Horizontal: Top"]; + [btnAlignTop setFrameOrigin:CGPointMake(controlsX, controlsY)]; + [btnAlignTop setTarget:self]; + [btnAlignTop setAction:@selector(setAlignmentTop:)]; + [contentView addSubview:btnAlignTop]; + + controlsY += 30; + var btnAlignCenter = [CPButton buttonWithTitle:@"Align Horizontal: CenterY"]; + [btnAlignCenter setFrameOrigin:CGPointMake(controlsX, controlsY)]; + [btnAlignCenter setTarget:self]; + [btnAlignCenter setAction:@selector(setAlignmentCenter:)]; + [contentView addSubview:btnAlignCenter]; + + controlsY += 30; + var btnAlignFill = [CPButton buttonWithTitle:@"Align Horizontal: Height (Fill)"]; + [btnAlignFill setFrameOrigin:CGPointMake(controlsX, controlsY)]; + [btnAlignFill setTarget:self]; + [btnAlignFill setAction:@selector(setAlignmentHeight:)]; + [contentView addSubview:btnAlignFill]; + + controlsY += 40; + var btnSpacing = [CPButton buttonWithTitle:@"Increase Spacing"]; + [btnSpacing setFrameOrigin:CGPointMake(controlsX, controlsY)]; + [btnSpacing setTarget:self]; + [btnSpacing setAction:@selector(changeSpacing:)]; + [contentView addSubview:btnSpacing]; + + [theWindow setFullPlatformWindow:YES]; + [theWindow orderFront:self]; +} + +- (void)awakeFromCib +{ + // If we were using a Cib, initialization would happen here. +} + +#pragma mark - +#pragma mark Actions + +- (@action)toggleHidden:(id)sender +{ + var isHidden = [toggleTargetView isHidden]; + [toggleTargetView setHidden:!isHidden]; + + // In standard Cocoa, hiding a view triggers layout if stackView is observing, + // but in this manual implementation, we might need to nudge it or ensure + // setHidden triggers needsLayout on superview. + // The CPStackView provided relies on 'layoutSubviews' being called. + + [stackViewHorizontal setNeedsLayout:YES]; +} + +- (@action)toggleDetaches:(id)sender +{ + [stackViewHorizontal setDetachesHiddenViews:([sender state] === CPOnState)]; +} + +- (@action)setAlignmentTop:(id)sender +{ + [stackViewHorizontal setAlignment:CPLayoutAttributeTop]; +} + +- (@action)setAlignmentCenter:(id)sender +{ + [stackViewHorizontal setAlignment:CPLayoutAttributeCenterY]; +} + +- (@action)setAlignmentHeight:(id)sender +{ + [stackViewHorizontal setAlignment:CPLayoutAttributeHeight]; +} + +- (@action)changeSpacing:(id)sender +{ + var current = [stackViewHorizontal spacing]; + [stackViewHorizontal setSpacing:(current >= 20.0 ? 8.0 : current + 4.0)]; +} + +#pragma mark - +#pragma mark Helpers + +- (CPView)_createBoxColor:(CPColor)aColor size:(CGSize)aSize label:(CPString)text +{ + var view = [[CPView alloc] initWithFrame:CGRectMake(0, 0, aSize.width, aSize.height)]; + [view setBackgroundColor:aColor]; + + var label = [[CPTextField alloc] initWithFrame:CGRectInset([view bounds], 2, 2)]; + [label setStringValue:text]; + [label setTextColor:[CPColor whiteColor]]; + [label setAlignment:CPCenterTextAlignment]; + [label setVerticalAlignment:CPCenterVerticalTextAlignment]; + [label setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [label setLineBreakMode:CPLineBreakByWordWrapping]; + + [view addSubview:label]; + + return view; +} + +@end diff --git a/Tests/Manual/CPStackViewTest/Info.plist b/Tests/Manual/CPStackViewTest/Info.plist new file mode 100644 index 000000000..7f51d542f --- /dev/null +++ b/Tests/Manual/CPStackViewTest/Info.plist @@ -0,0 +1,10 @@ + + + + + Main cib file base name + MainMenu.cib + CPBundleName + CPStackViewTest + + diff --git a/Tests/Manual/CPStackViewTest/Jakefile b/Tests/Manual/CPStackViewTest/Jakefile new file mode 100644 index 000000000..a4174055a --- /dev/null +++ b/Tests/Manual/CPStackViewTest/Jakefile @@ -0,0 +1,94 @@ +/* + * Jakefile + * CPSplitViewTest + * + * Created by Alexander Ljungberg on January 27, 2012. + * Copyright 2012, WireLoad All rights reserved. + */ + +var ENV = require("system").env, + FILE = require("file"), + JAKE = require("jake"), + task = JAKE.task, + FileList = JAKE.FileList, + app = require("cappuccino/jake").app, + configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug", + OS = require("os"); + +app ("CPSplitViewTest", function(task) +{ + task.setBuildIntermediatesPath(FILE.join("Build", "CPSplitViewTest.build", configuration)); + task.setBuildPath(FILE.join("Build", configuration)); + + task.setProductName("CPSplitViewTest"); + task.setIdentifier("com.yourcompany.CPSplitViewTest"); + task.setVersion("1.0"); + task.setAuthor("WireLoad"); + task.setEmail("feedback @nospam@ yourcompany.com"); + task.setSummary("CPSplitViewTest"); + task.setSources((new FileList("**/*.j")).exclude(FILE.join("Build", "**"))); + task.setResources(new FileList("Resources/**")); + task.setIndexFilePath("index.html"); + task.setInfoPlistPath("Info.plist"); + task.setNib2CibFlags("-R Resources/"); + + if (configuration === "Debug") + task.setCompilerFlags("-DDEBUG -g"); + else + task.setCompilerFlags("-O"); +}); + +task ("default", ["CPSplitViewTest"], function() +{ + printResults(configuration); +}); + +task ("build", ["default"]); + +task ("debug", function() +{ + ENV["CONFIGURATION"] = "Debug"; + JAKE.subjake(["."], "build", ENV); +}); + +task ("release", function() +{ + ENV["CONFIGURATION"] = "Release"; + JAKE.subjake(["."], "build", ENV); +}); + +task ("run", ["debug"], function() +{ + OS.system(["open", FILE.join("Build", "Debug", "CPSplitViewTest", "index.html")]); +}); + +task ("run-release", ["release"], function() +{ + OS.system(["open", FILE.join("Build", "Release", "CPSplitViewTest", "index.html")]); +}); + +task ("deploy", ["release"], function() +{ + FILE.mkdirs(FILE.join("Build", "Deployment", "CPSplitViewTest")); + OS.system(["press", "-f", FILE.join("Build", "Release", "CPSplitViewTest"), FILE.join("Build", "Deployment", "CPSplitViewTest")]); + printResults("Deployment") +}); + +task ("desktop", ["release"], function() +{ + FILE.mkdirs(FILE.join("Build", "Desktop", "CPSplitViewTest")); + require("cappuccino/nativehost").buildNativeHost(FILE.join("Build", "Release", "CPSplitViewTest"), FILE.join("Build", "Desktop", "CPSplitViewTest", "CPSplitViewTest.app")); + printResults("Desktop") +}); + +task ("run-desktop", ["desktop"], function() +{ + OS.system([FILE.join("Build", "Desktop", "CPSplitViewTest", "CPSplitViewTest.app", "Contents", "MacOS", "NativeHost"), "-i"]); +}); + +function printResults(configuration) +{ + print("----------------------------"); + print(configuration+" app built at path: "+FILE.join("Build", configuration, "CPSplitViewTest")); + print("----------------------------"); +} diff --git a/Tests/Manual/CPStackViewTest/Resources/MainMenu.cib b/Tests/Manual/CPStackViewTest/Resources/MainMenu.cib new file mode 100644 index 000000000..21dc1cbf9 --- /dev/null +++ b/Tests/Manual/CPStackViewTest/Resources/MainMenu.cib @@ -0,0 +1 @@ +280NPLIST;1.0;D;K;4;$topD;K;18;CPCibObjectDataKeyD;K;6;CP$UIDd;1;2E;E;K;8;$objectsA;S;5;$nullD;K;10;$classnameS;16;_CPCibObjectDataK;8;$classesA;S;16;_CPCibObjectDataS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;1E;K;28;_CPCibObjectDataNamesKeysKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataNamesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataClassesKeysKeyD;K;6;CP$UIDd;1;0E;K;32;_CPCibObjectDataClassesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataConnectionsKeyD;K;6;CP$UIDd;1;4E;K;28;_CPCibObjectDataFrameworkKeyD;K;6;CP$UIDd;1;0E;K;26;_CPCibObjectDataNextOidKeyD;K;6;CP$UIDd;1;5E;K;30;_CPCibObjectDataObjectsKeysKeyD;K;6;CP$UIDd;1;6E;K;32;_CPCibObjectDataObjectsValuesKeyD;K;6;CP$UIDd;1;7E;K;26;_CPCibObjectDataOidKeysKeyD;K;6;CP$UIDd;1;8E;K;28;_CPCibObjectDataOidValuesKeyD;K;6;CP$UIDd;1;9E;K;28;_CPCibObjectDataFileOwnerKeyD;K;6;CP$UIDd;2;11E;K;33;_CPCibObjectDataVisibleWindowsKeyD;K;6;CP$UIDd;2;13E;E;D;K;10;$classnameS;7;CPArrayK;8;$classesA;S;7;CPArrayS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;15E;D;K;6;CP$UIDd;2;16E;D;K;6;CP$UIDd;2;17E;D;K;6;CP$UIDd;2;18E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;20E;D;K;6;CP$UIDd;2;22E;D;K;6;CP$UIDd;2;23E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;24E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;28E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;34E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;36E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;39E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;40E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;41E;D;K;6;CP$UIDd;2;42E;D;K;6;CP$UIDd;2;43E;D;K;6;CP$UIDd;2;44E;D;K;6;CP$UIDd;2;45E;D;K;6;CP$UIDd;2;46E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;47E;D;K;6;CP$UIDd;2;48E;D;K;6;CP$UIDd;1;0E;D;K;6;CP$UIDd;2;49E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;28E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;34E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;36E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;39E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;40E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;41E;D;K;6;CP$UIDd;2;42E;D;K;6;CP$UIDd;2;43E;D;K;6;CP$UIDd;2;43E;D;K;6;CP$UIDd;2;45E;D;K;6;CP$UIDd;2;46E;D;K;6;CP$UIDd;2;41E;D;K;6;CP$UIDd;2;47E;D;K;6;CP$UIDd;2;48E;D;K;6;CP$UIDd;2;11E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;10;$classnameS;18;_CPCibCustomObjectK;8;$classesA;S;18;_CPCibCustomObjectS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;50E;E;D;K;10;$classnameS;5;CPSetK;8;$classesA;S;5;CPSetS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;12E;K;15;CPSetObjectsKeyD;K;6;CP$UIDd;2;51E;E;D;K;10;$classnameS;20;CPCibOutletConnectorK;8;$classesA;S;20;CPCibOutletConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;49E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;52E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;49E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;30E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;53E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;49E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;41E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;54E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;49E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;43E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;55E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;49E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;26E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;56E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;30E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;49E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;52E;E;D;K;10;$classnameS;21;CPCibControlConnectorK;8;$classesA;S;21;CPCibControlConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;21E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;39E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;36E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;57E;E;D;K;6;$classD;K;6;CP$UIDd;2;21E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;34E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;49E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;58E;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;50E;E;D;K;10;$classnameS;20;_CPCibWindowTemplateK;8;$classesA;S;20;_CPCibWindowTemplateS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;25E;K;30;_CPCibWindowTemplateMaxSizeKeyD;K;6;CP$UIDd;2;59E;K;32;_CPCibWindowTemplateViewClassKeyD;K;6;CP$UIDd;1;0E;K;34;_CPCibWindowTemplateWindowClassKeyD;K;6;CP$UIDd;2;60E;K;30;_CPCibWindowTemplateWTFlagsKeyD;K;6;CP$UIDd;2;61E;K;33;_CPCibWindowTemplateWindowRectKeyD;K;6;CP$UIDd;2;62E;K;33;_CPCibWindowTemplateScreenRectKeyD;K;6;CP$UIDd;2;63E;K;30;_CPCibWindowTempatStyleMaskKeyD;K;6;CP$UIDd;2;64E;K;34;_CPCibWindowTemplateWindowTitleKeyD;K;6;CP$UIDd;2;65E;K;33;_CPCibWindowTemplateWindowViewKeyD;K;6;CP$UIDd;2;28E;E;D;K;10;$classnameS;6;CPViewK;8;$classesA;S;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;27E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;67E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;67E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;2;68E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;73E;E;D;K;10;$classnameS;11;CPSplitViewK;8;$classesA;S;11;CPSplitViewS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;29E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;28E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;67E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;67E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;2;74E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;28E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;75E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;76E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;77E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;78E;K;22;CPSplitViewDelegateKeyD;K;6;CP$UIDd;1;0E;K;24;CPSplitViewIsVerticalKeyD;K;6;CP$UIDd;2;72E;K;26;CPSplitViewDividerStyleKeyD;K;6;CP$UIDd;2;79E;K;29;CPSplitViewDividerSubviewsKeyD;K;6;CP$UIDd;2;80E;K;30;CPSplitViewArrangedSubviewsKeyD;K;6;CP$UIDd;2;81E;K;26;CPSplitViewRealSubviewsKeyD;K;6;CP$UIDd;2;82E;K;33;CPSplitViewArrangesAllSubviewsKeyD;K;6;CP$UIDd;2;83E;K;26;CPSplitViewAutosaveNameKeyD;K;6;CP$UIDd;2;53E;E;D;K;10;$classnameS;16;_CPCibCustomViewK;8;$classesA;S;16;_CPCibCustomViewS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;31E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;30E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;84E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;85E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;2;86E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;30E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;88E;K;28;_CPCibCustomViewClassNameKeyD;K;6;CP$UIDd;2;89E;E;D;K;10;$classnameS;8;CPButtonK;8;$classesA;S;8;CPButtonS;9;CPControlS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;33E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;32E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;90E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;91E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;32E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;92E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;93E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;94E;K;15;$aimage-scalingD;K;6;CP$UIDd;2;66E;K;16;$aimage-positionD;K;6;CP$UIDd;2;66E;K;6;$afontD;K;6;CP$UIDd;2;96E;K;17;$aline-break-modeD;K;6;CP$UIDd;2;66E;K;11;$aalignmentD;K;6;CP$UIDd;2;97E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;98E;K;17;CPControlValueKeyD;K;6;CP$UIDd;2;66E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;2;99E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;2;66E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;3;100E;K;16;CPButtonTitleKeyD;K;6;CP$UIDd;3;101E;K;25;CPButtonAlternateTitleKeyD;K;6;CP$UIDd;3;102E;K;27;CPButtonAllowsMixedStateKeyD;K;6;CP$UIDd;2;72E;K;23;CPButtonHighlightsByKeyD;K;6;CP$UIDd;3;103E;K;23;CPButtonShowsStateByKeyD;K;6;CP$UIDd;2;66E;K;32;CPButtonImageDimsWhenDisabledKeyD;K;6;CP$UIDd;2;83E;K;24;CPButtonImagePositionKeyD;K;6;CP$UIDd;2;66E;K;28;CPButtonKeyEquivalentMaskKeyD;K;6;CP$UIDd;2;66E;K;24;CPButtonPeriodicDelayKeyD;K;6;CP$UIDd;1;0E;K;27;CPButtonPeriodicIntervalKeyD;K;6;CP$UIDd;1;0E;K;21;CPButtonIsBorderedKeyD;K;6;CP$UIDd;2;83E;K;21;CPButtonBezelStyleKeyD;K;6;CP$UIDd;3;104E;E;D;K;10;$classnameS;11;CPTextFieldK;8;$classesA;S;11;CPTextFieldS;9;CPControlS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;35E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;32E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;105E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;106E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;32E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;3;107E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;108E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;3;109E;K;6;$afontD;K;6;CP$UIDd;3;111E;K;17;$aline-break-modeD;K;6;CP$UIDd;3;112E;K;11;$aalignmentD;K;6;CP$UIDd;3;113E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;114E;K;35;CPControlSendsActionOnEndEditingKeyD;K;6;CP$UIDd;2;83E;K;17;CPControlValueKeyD;K;6;CP$UIDd;3;102E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;2;99E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;2;66E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;3;100E;K;24;CPTextFieldIsEditableKeyD;K;6;CP$UIDd;2;83E;K;26;CPTextFieldIsSelectableKeyD;K;6;CP$UIDd;2;83E;K;29;CPTextFieldDrawsBackgroundKeyD;K;6;CP$UIDd;2;83E;K;29;CPTextFieldBackgroundColorKeyD;K;6;CP$UIDd;3;116E;K;27;CPTextFieldLineBreakModeKeyD;K;6;CP$UIDd;2;97E;K;23;CPTextFieldAlignmentKeyD;K;6;CP$UIDd;2;99E;K;31;CPTextFieldPlaceholderStringKeyD;K;6;CP$UIDd;1;0E;K;29;CPTextFieldUsesSingleLineModeD;K;6;CP$UIDd;2;72E;K;16;CPTextFieldWrapsD;K;6;CP$UIDd;2;72E;K;18;CPTextFieldScrollsD;K;6;CP$UIDd;2;83E;E;D;K;6;$classD;K;6;CP$UIDd;2;31E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;30E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;117E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;118E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;3;119E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;30E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;120E;K;28;_CPCibCustomViewClassNameKeyD;K;6;CP$UIDd;2;89E;E;D;K;10;$classnameS;8;CPSliderK;8;$classesA;S;8;CPSliderS;9;CPControlS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;38E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;37E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;121E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;122E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;37E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;3;107E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;123E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;3;124E;K;6;$afontD;K;6;CP$UIDd;3;125E;K;17;$aline-break-modeD;K;6;CP$UIDd;2;66E;K;11;$aalignmentD;K;6;CP$UIDd;2;66E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;126E;K;17;CPControlValueKeyD;K;6;CP$UIDd;3;127E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;3;128E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;2;66E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;3;100E;K;19;CPSliderMinValueKeyD;K;6;CP$UIDd;2;66E;K;19;CPSliderMaxValueKeyD;K;6;CP$UIDd;3;129E;K;23;CPSliderAltIncrValueKeyD;K;6;CP$UIDd;2;66E;K;15;CPSliderTypeKeyD;K;6;CP$UIDd;2;66E;K;35;CPSliderAllowsTickMarkValuesOnlyKeyD;K;6;CP$UIDd;2;72E;K;27;CPSliderTickMarkPositionKeyD;K;6;CP$UIDd;3;104E;K;28;CPSliderNumberOfTickMarksKeyD;K;6;CP$UIDd;2;66E;E;D;K;6;$classD;K;6;CP$UIDd;2;35E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;30E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;130E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;131E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;30E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;92E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;108E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;3;124E;K;6;$afontD;K;6;CP$UIDd;3;132E;K;17;$aline-break-modeD;K;6;CP$UIDd;3;133E;K;11;$aalignmentD;K;6;CP$UIDd;3;134E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;135E;K;35;CPControlSendsActionOnEndEditingKeyD;K;6;CP$UIDd;2;83E;K;17;CPControlValueKeyD;K;6;CP$UIDd;3;136E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;2;99E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;2;66E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;3;100E;K;24;CPTextFieldIsEditableKeyD;K;6;CP$UIDd;2;72E;K;26;CPTextFieldIsSelectableKeyD;K;6;CP$UIDd;2;72E;K;29;CPTextFieldDrawsBackgroundKeyD;K;6;CP$UIDd;2;72E;K;29;CPTextFieldBackgroundColorKeyD;K;6;CP$UIDd;3;137E;K;27;CPTextFieldLineBreakModeKeyD;K;6;CP$UIDd;2;97E;K;23;CPTextFieldAlignmentKeyD;K;6;CP$UIDd;2;99E;K;31;CPTextFieldPlaceholderStringKeyD;K;6;CP$UIDd;1;0E;K;29;CPTextFieldUsesSingleLineModeD;K;6;CP$UIDd;2;72E;K;16;CPTextFieldWrapsD;K;6;CP$UIDd;2;72E;K;18;CPTextFieldScrollsD;K;6;CP$UIDd;2;83E;E;D;K;6;$classD;K;6;CP$UIDd;2;29E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;30E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;138E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;138E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;3;139E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;30E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;76E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;3;140E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;141E;K;22;CPSplitViewDelegateKeyD;K;6;CP$UIDd;1;0E;K;24;CPSplitViewIsVerticalKeyD;K;6;CP$UIDd;2;83E;K;26;CPSplitViewDividerStyleKeyD;K;6;CP$UIDd;2;97E;K;29;CPSplitViewDividerSubviewsKeyD;K;6;CP$UIDd;3;142E;K;30;CPSplitViewArrangedSubviewsKeyD;K;6;CP$UIDd;3;143E;K;26;CPSplitViewRealSubviewsKeyD;K;6;CP$UIDd;3;144E;K;33;CPSplitViewArrangesAllSubviewsKeyD;K;6;CP$UIDd;2;83E;K;26;CPSplitViewAutosaveNameKeyD;K;6;CP$UIDd;2;54E;E;D;K;6;$classD;K;6;CP$UIDd;2;31E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;41E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;145E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;145E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;3;146E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;41E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;147E;K;28;_CPCibCustomViewClassNameKeyD;K;6;CP$UIDd;2;89E;E;D;K;6;$classD;K;6;CP$UIDd;2;29E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;42E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;145E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;145E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;3;148E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;42E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;75E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;76E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;3;149E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;150E;K;22;CPSplitViewDelegateKeyD;K;6;CP$UIDd;1;0E;K;24;CPSplitViewIsVerticalKeyD;K;6;CP$UIDd;2;72E;K;26;CPSplitViewDividerStyleKeyD;K;6;CP$UIDd;3;104E;K;29;CPSplitViewDividerSubviewsKeyD;K;6;CP$UIDd;3;151E;K;30;CPSplitViewArrangedSubviewsKeyD;K;6;CP$UIDd;3;152E;K;26;CPSplitViewRealSubviewsKeyD;K;6;CP$UIDd;3;153E;K;33;CPSplitViewArrangesAllSubviewsKeyD;K;6;CP$UIDd;2;83E;K;26;CPSplitViewAutosaveNameKeyD;K;6;CP$UIDd;2;55E;E;D;K;6;$classD;K;6;CP$UIDd;2;31E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;43E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;154E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;155E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;43E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;156E;K;28;_CPCibCustomViewClassNameKeyD;K;6;CP$UIDd;2;89E;E;D;K;6;$classD;K;6;CP$UIDd;2;31E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;43E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;157E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;157E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;3;158E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;43E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;159E;K;28;_CPCibCustomViewClassNameKeyD;K;6;CP$UIDd;2;89E;E;D;K;6;$classD;K;6;CP$UIDd;2;33E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;45E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;160E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;161E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;45E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;93E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;94E;K;15;$aimage-scalingD;K;6;CP$UIDd;2;66E;K;16;$aimage-positionD;K;6;CP$UIDd;2;66E;K;6;$afontD;K;6;CP$UIDd;2;96E;K;17;$aline-break-modeD;K;6;CP$UIDd;2;66E;K;11;$aalignmentD;K;6;CP$UIDd;2;97E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;162E;K;17;CPControlValueKeyD;K;6;CP$UIDd;2;66E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;2;99E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;2;66E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;3;100E;K;16;CPButtonTitleKeyD;K;6;CP$UIDd;3;163E;K;25;CPButtonAlternateTitleKeyD;K;6;CP$UIDd;3;102E;K;27;CPButtonAllowsMixedStateKeyD;K;6;CP$UIDd;2;72E;K;23;CPButtonHighlightsByKeyD;K;6;CP$UIDd;3;103E;K;23;CPButtonShowsStateByKeyD;K;6;CP$UIDd;2;66E;K;32;CPButtonImageDimsWhenDisabledKeyD;K;6;CP$UIDd;2;83E;K;24;CPButtonImagePositionKeyD;K;6;CP$UIDd;2;66E;K;28;CPButtonKeyEquivalentMaskKeyD;K;6;CP$UIDd;2;66E;K;24;CPButtonPeriodicDelayKeyD;K;6;CP$UIDd;1;0E;K;27;CPButtonPeriodicIntervalKeyD;K;6;CP$UIDd;1;0E;K;21;CPButtonIsBorderedKeyD;K;6;CP$UIDd;2;83E;K;21;CPButtonBezelStyleKeyD;K;6;CP$UIDd;3;104E;E;D;K;6;$classD;K;6;CP$UIDd;2;31E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;41E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;164E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;165E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;3;166E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;41E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;69E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;167E;K;28;_CPCibCustomViewClassNameKeyD;K;6;CP$UIDd;2;89E;E;D;K;6;$classD;K;6;CP$UIDd;2;33E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;47E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;66E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;168E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;169E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;47E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;87E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;93E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;94E;K;15;$aimage-scalingD;K;6;CP$UIDd;2;66E;K;16;$aimage-positionD;K;6;CP$UIDd;2;66E;K;6;$afontD;K;6;CP$UIDd;2;96E;K;17;$aline-break-modeD;K;6;CP$UIDd;2;66E;K;11;$aalignmentD;K;6;CP$UIDd;2;97E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;71E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;71E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;170E;K;17;CPControlValueKeyD;K;6;CP$UIDd;2;66E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;2;99E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;2;66E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;3;100E;K;16;CPButtonTitleKeyD;K;6;CP$UIDd;3;171E;K;25;CPButtonAlternateTitleKeyD;K;6;CP$UIDd;3;102E;K;27;CPButtonAllowsMixedStateKeyD;K;6;CP$UIDd;2;72E;K;23;CPButtonHighlightsByKeyD;K;6;CP$UIDd;3;103E;K;23;CPButtonShowsStateByKeyD;K;6;CP$UIDd;2;66E;K;32;CPButtonImageDimsWhenDisabledKeyD;K;6;CP$UIDd;2;83E;K;24;CPButtonImagePositionKeyD;K;6;CP$UIDd;2;66E;K;28;CPButtonKeyEquivalentMaskKeyD;K;6;CP$UIDd;2;66E;K;24;CPButtonPeriodicDelayKeyD;K;6;CP$UIDd;1;0E;K;27;CPButtonPeriodicIntervalKeyD;K;6;CP$UIDd;1;0E;K;21;CPButtonIsBorderedKeyD;K;6;CP$UIDd;2;83E;K;21;CPButtonBezelStyleKeyD;K;6;CP$UIDd;3;104E;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;3;172E;E;S;13;CPApplicationD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;26E;E;E;S;8;delegateS;10;splitViewAS;10;splitViewBS;10;splitViewCS;9;theWindowS;20;takeDoubleValueFrom:S;15;deleteAutosave:S;32;{10000000000000, 10000000000000}S;8;CPWindowd;10;1946157056S;24;{{335, 668}, {480, 360}}S;22;{{0, 0}, {2560, 1418}}d;1;7S;6;Windowd;1;0S;20;{{0, 0}, {480, 360}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;30E;E;E;S;4;viewS;6;normalS;6;{1, 1}F;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;40E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;41E;E;E;d;2;18S;9;splitviewS;31;splitview-divider-pane-splitterD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;d;1;3D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;T;S;21;{{0, 231}, {480, 85}}S;19;{{0, 0}, {480, 85}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;36E;D;K;6;CP$UIDd;2;34E;E;E;d;1;8D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;6;CPViewS;21;{{20, 33}, {130, 25}}S;19;{{0, 0}, {130, 25}}d;2;36S;6;buttonS;35;bordered+controlSizeRegular+roundedD;K;10;$classnameS;6;CPFontK;8;$classesA;S;6;CPFontS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;95E;K;13;CPFontNameKeyD;K;6;CP$UIDd;3;173E;K;13;CPFontSizeKeyD;K;6;CP$UIDd;3;100E;K;15;CPFontIsBoldKeyD;K;6;CP$UIDd;2;83E;K;17;CPFontIsItalicKeyD;K;6;CP$UIDd;2;72E;K;17;CPFontIsSystemKeyD;K;6;CP$UIDd;2;83E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;174E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;E;d;1;2D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;d;1;4d;2;-1S;15;Delete AutosaveS;0;d;2;14d;1;1S;22;{{360, 30}, {104, 22}}S;19;{{0, 0}, {104, 22}}d;2;45S;9;textfieldS;47;bezeled+controlSizeRegular+editable+placeholderD;K;10;$classnameS;17;_CPThemeAttributeK;8;$classesA;S;17;_CPThemeAttributeS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;110E;K;4;nameD;K;6;CP$UIDd;3;174E;K;12;defaultValueD;K;6;CP$UIDd;3;176E;K;6;valuesD;K;6;CP$UIDd;3;178E;E;D;K;6;$classD;K;6;CP$UIDd;3;110E;K;4;nameD;K;6;CP$UIDd;3;179E;K;12;defaultValueD;K;6;CP$UIDd;2;97E;K;6;valuesD;K;6;CP$UIDd;3;180E;E;D;K;6;$classD;K;6;CP$UIDd;3;110E;K;4;nameD;K;6;CP$UIDd;3;181E;K;12;defaultValueD;K;6;CP$UIDd;2;66E;K;6;valuesD;K;6;CP$UIDd;3;182E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;184E;D;K;6;CP$UIDd;3;185E;E;E;D;K;10;$classnameS;7;CPColorK;8;$classesA;S;7;CPColorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;115E;K;20;CPColorComponentsKeyD;K;6;CP$UIDd;3;186E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;187E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;E;S;21;{{0, 142}, {480, 79}}S;19;{{0, 0}, {480, 79}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;39E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;21;{{194, 31}, {92, 21}}S;18;{{0, 0}, {92, 21}}S;6;sliderS;18;controlSizeRegularD;K;6;$classD;K;6;CP$UIDd;2;95E;K;13;CPFontNameKeyD;K;6;CP$UIDd;3;188E;K;13;CPFontSizeKeyD;K;6;CP$UIDd;3;189E;K;15;CPFontIsBoldKeyD;K;6;CP$UIDd;2;72E;K;17;CPFontIsItalicKeyD;K;6;CP$UIDd;2;72E;K;17;CPFontIsSystemKeyD;K;6;CP$UIDd;2;72E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;174E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;d;2;50d;2;68d;3;100S;21;{{2, 326}, {476, 34}}S;19;{{0, 0}, {476, 34}}D;K;6;$classD;K;6;CP$UIDd;3;110E;K;4;nameD;K;6;CP$UIDd;3;174E;K;12;defaultValueD;K;6;CP$UIDd;3;176E;K;6;valuesD;K;6;CP$UIDd;3;190E;E;D;K;6;$classD;K;6;CP$UIDd;3;110E;K;4;nameD;K;6;CP$UIDd;3;179E;K;12;defaultValueD;K;6;CP$UIDd;2;97E;K;6;valuesD;K;6;CP$UIDd;3;191E;E;D;K;6;$classD;K;6;CP$UIDd;3;110E;K;4;nameD;K;6;CP$UIDd;3;181E;K;12;defaultValueD;K;6;CP$UIDd;2;66E;K;6;valuesD;K;6;CP$UIDd;3;192E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;193E;E;E;S;5;LabelD;K;6;$classD;K;6;CP$UIDd;3;115E;K;20;CPColorComponentsKeyD;K;6;CP$UIDd;3;194E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;187E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;E;S;20;{{0, 0}, {480, 132}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;42E;D;K;6;CP$UIDd;2;47E;E;E;S;31;splitview-divider-thin+verticalD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;20;{{0, 0}, {246, 132}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;43E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;44E;D;K;6;CP$UIDd;2;45E;E;E;S;78;dummy one as CPSplitViewDividerStyle is not zero-based+splitview-divider-thickD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;20;{{0, 81}, {246, 51}}S;19;{{0, 0}, {246, 51}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;19;{{0, 0}, {246, 72}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;46E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;21;{{72, 26}, {102, 25}}S;19;{{0, 0}, {102, 25}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;11;Bottom LeftS;22;{{247, 0}, {233, 132}}S;20;{{0, 0}, {233, 132}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;48E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;20;{{86, 56}, {61, 25}}S;18;{{0, 0}, {61, 25}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;5;RightS;13;AppControllerS;28;_CPFontSystemFacePlaceholderS;4;fontD;K;10;$classnameS;6;CPNullK;8;$classesA;S;6;CPNullS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;175E;E;D;K;10;$classnameS;12;CPDictionaryK;8;$classesA;S;12;CPDictionaryS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;177E;K;10;CP.objectsD;K;13;tableDataViewD;K;6;CP$UIDd;3;195E;K;6;normalD;K;6;CP$UIDd;3;195E;E;E;S;15;line-break-modeD;K;6;$classD;K;6;CP$UIDd;3;177E;K;10;CP.objectsD;K;13;tableDataViewD;K;6;CP$UIDd;2;97E;K;6;normalD;K;6;CP$UIDd;2;97E;E;E;S;9;alignmentD;K;6;$classD;K;6;CP$UIDd;3;177E;K;10;CP.objectsD;K;13;tableDataViewD;K;6;CP$UIDd;2;99E;K;6;normalD;K;6;CP$UIDd;2;99E;E;E;D;K;10;$classnameS;14;CPTrackingAreaK;8;$classesA;S;14;CPTrackingAreaS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;183E;K;25;CPTrackinkAreaViewRectKeyD;K;6;CP$UIDd;3;197E;K;24;CPTrackingAreaOptionsKeyD;K;6;CP$UIDd;3;198E;K;22;CPTrackingAreaOwnerKeyD;K;6;CP$UIDd;2;36E;K;25;CPTrackingAreaUserInfoKeyD;K;6;CP$UIDd;1;0E;K;32;CPTrackingAreaReferencingViewKeyD;K;6;CP$UIDd;2;36E;K;24;CPTrackingAreaWindowRectD;K;6;CP$UIDd;3;199E;E;D;K;6;$classD;K;6;CP$UIDd;3;183E;K;25;CPTrackinkAreaViewRectKeyD;K;6;CP$UIDd;3;200E;K;24;CPTrackingAreaOptionsKeyD;K;6;CP$UIDd;3;201E;K;22;CPTrackingAreaOwnerKeyD;K;6;CP$UIDd;2;36E;K;25;CPTrackingAreaUserInfoKeyD;K;6;CP$UIDd;1;0E;K;32;CPTrackingAreaReferencingViewKeyD;K;6;CP$UIDd;2;36E;K;24;CPTrackingAreaWindowRectD;K;6;CP$UIDd;3;202E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;104E;D;K;6;CP$UIDd;3;104E;D;K;6;CP$UIDd;3;104E;D;K;6;CP$UIDd;3;104E;E;E;S;5;colorS;9;Helveticad;2;12D;K;6;$classD;K;6;CP$UIDd;3;177E;K;10;CP.objectsD;K;13;tableDataViewD;K;6;CP$UIDd;3;195E;K;6;normalD;K;6;CP$UIDd;3;195E;E;E;D;K;6;$classD;K;6;CP$UIDd;3;177E;K;10;CP.objectsD;K;13;tableDataViewD;K;6;CP$UIDd;2;97E;K;6;normalD;K;6;CP$UIDd;2;97E;E;E;D;K;6;$classD;K;6;CP$UIDd;3;177E;K;10;CP.objectsD;K;13;tableDataViewD;K;6;CP$UIDd;2;99E;K;6;normalD;K;6;CP$UIDd;2;99E;E;E;D;K;6;$classD;K;6;CP$UIDd;3;183E;K;25;CPTrackinkAreaViewRectKeyD;K;6;CP$UIDd;3;203E;K;24;CPTrackingAreaOptionsKeyD;K;6;CP$UIDd;3;201E;K;22;CPTrackingAreaOwnerKeyD;K;6;CP$UIDd;2;40E;K;25;CPTrackingAreaUserInfoKeyD;K;6;CP$UIDd;1;0E;K;32;CPTrackingAreaReferencingViewKeyD;K;6;CP$UIDd;2;40E;K;24;CPTrackingAreaWindowRectD;K;6;CP$UIDd;3;204E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;205E;D;K;6;CP$UIDd;3;205E;D;K;6;CP$UIDd;3;205E;D;K;6;CP$UIDd;3;104E;E;E;D;K;6;$classD;K;6;CP$UIDd;2;95E;K;13;CPFontNameKeyD;K;6;CP$UIDd;3;173E;K;13;CPFontSizeKeyD;K;6;CP$UIDd;3;100E;K;15;CPFontIsBoldKeyD;K;6;CP$UIDd;2;72E;K;17;CPFontIsItalicKeyD;K;6;CP$UIDd;2;72E;K;17;CPFontIsSystemKeyD;K;6;CP$UIDd;2;83E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;174E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;70E;E;D;K;10;$classnameS;21;_CPKeyedArchiverValueK;8;$classesA;S;21;_CPKeyedArchiverValueS;7;CPValueS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;196E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;206E;E;d;2;40D;K;6;$classD;K;6;CP$UIDd;3;196E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;207E;E;D;K;6;$classD;K;6;CP$UIDd;3;196E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;208E;E;d;3;546D;K;6;$classD;K;6;CP$UIDd;3;196E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;209E;E;D;K;6;$classD;K;6;CP$UIDd;3;196E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;208E;E;D;K;6;$classD;K;6;CP$UIDd;3;196E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;210E;E;f;18;0.6862745098039216S;56;{"origin":{"x":0,"y":1},"size":{"width":96,"height":21}}S;59;{"origin":{"x":364,"y":75},"size":{"width":96,"height":21}}S;54;{"origin":{"x":0,"y":0},"size":{"width":0,"height":0}}S;59;{"origin":{"x":364,"y":74},"size":{"width":96,"height":22}}S;57;{"origin":{"x":0,"y":0},"size":{"width":480,"height":34}}E;K;9;$archiverS;15;CPKeyedArchiverK;8;$versionS;6;100000E; \ No newline at end of file diff --git a/Tests/Manual/CPStackViewTest/Resources/MainMenu.xib b/Tests/Manual/CPStackViewTest/Resources/MainMenu.xib new file mode 100644 index 000000000..d497e2df5 --- /dev/null +++ b/Tests/Manual/CPStackViewTest/Resources/MainMenu.xib @@ -0,0 +1,809 @@ + + + + 1050 + 11E53 + 2182 + 1138.47 + 569.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 2182 + + + YES + NSTextField + NSView + NSWindowTemplate + NSSplitView + NSTextFieldCell + NSSliderCell + NSCustomView + NSSlider + NSButtonCell + NSButton + NSCustomObject + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + + + PluginDependencyRecalculationVersion + + + + YES + + NSApplication + + + FirstResponder + + + NSApplication + + + 7 + 2 + {{335, 390}, {480, 360}} + 1946157056 + Window + NSWindow + + + + + 256 + + YES + + + 274 + + YES + + + 268 + {480, 34} + + + + _NS:3944 + YES + + 68288064 + 272630784 + Label + + LucidaGrande + 13 + 1044 + + _NS:3944 + + + 6 + System + controlColor + + 3 + MC42NjY2NjY2NjY3AA + + + + 6 + System + controlTextColor + + 3 + MAA + + + + + + + 256 + + YES + + + 268 + {{14, 24}, {142, 32}} + + + + _NS:9 + YES + + 67239424 + 134217728 + Delete Autosave + + _NS:9 + + -2038284033 + 129 + + + 200 + 25 + + + + + 301 + {{364, 30}, {96, 22}} + + + + YES + + -1804468671 + 272630784 + + + + YES + + 6 + System + textBackgroundColor + + 3 + MQA + + + + 6 + System + textColor + + + + + + {{0, 44}, {480, 85}} + + + + _NS:1113 + NSView + + + + 256 + + YES + + + 301 + {{192, 27}, {96, 21}} + + + + YES + + -2079981824 + 0 + + + Helvetica + 12 + 16 + + + 100 + 0.0 + 50 + 0.0 + 0 + 1 + NO + NO + + + + {{0, 139}, {480, 79}} + + + + _NS:1116 + NSView + + + + 256 + + YES + + + 256 + + YES + + + 274 + + YES + + + 256 + {246, 51} + + + + _NS:11 + NSView + + + + 256 + + YES + + + 256 + {{66, 18}, {114, 32}} + + + + _NS:687 + YES + + 67239424 + 134217728 + Bottom Left + + _NS:687 + + -2038284033 + 129 + + + 200 + 25 + + + + {{0, 60}, {246, 72}} + + + + _NS:13 + NSView + + + {246, 132} + + + + _NS:9 + splitViewC + + + {246, 132} + + + + _NS:1165 + NSView + + + + 256 + + YES + + + 256 + {{80, 48}, {73, 32}} + + + + _NS:687 + YES + + 67239424 + 134217728 + Right + + _NS:687 + + -2038284033 + 129 + + + 200 + 25 + + + + {{247, 0}, {233, 132}} + + + + _NS:1167 + NSView + + + {{0, 228}, {480, 132}} + + + + _NS:1163 + YES + 2 + splitViewB + + + {480, 360} + + + + _NS:1111 + 3 + splitViewA + + + {480, 360} + + + + + {{0, 0}, {2560, 1418}} + {10000000000000, 10000000000000} + YES + + + AppController + + + + + YES + + + delegate + + + + 451 + + + + theWindow + + + + 459 + + + + splitViewA + + + + 479 + + + + splitViewB + + + + 480 + + + + splitViewC + + + + 481 + + + + deleteAutosave: + + + + 482 + + + + takeDoubleValueFrom: + + + + 458 + + + + delegate + + + + 473 + + + + + YES + + 0 + + YES + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 371 + + + YES + + + + + + 372 + + + YES + + + + + + 450 + + + + + 460 + + + YES + + + + + + + + + 461 + + + YES + + + + + + + 462 + + + YES + + + + + + 456 + + + YES + + + + + + 457 + + + + + 452 + + + YES + + + + + + 453 + + + + + 463 + + + YES + + + + + + 464 + + + + + 465 + + + YES + + + + + + + 466 + + + YES + + + + + + 467 + + + YES + + + + + + 470 + + + YES + + + + + + 471 + + + + + 474 + + + YES + + + + + + + 475 + + + + + 476 + + + YES + + + + + + 468 + + + YES + + + + + + 469 + + + + + 477 + + + YES + + + + + + 478 + + + + + + + YES + + YES + -1.IBPluginDependency + -2.IBPluginDependency + -3.IBPluginDependency + 371.IBPluginDependency + 371.IBWindowTemplateEditedContentRect + 371.NSWindowTemplate.visibleAtLaunch + 372.IBPluginDependency + 450.IBPluginDependency + 452.IBPluginDependency + 453.IBPluginDependency + 456.IBPluginDependency + 457.IBPluginDependency + 460.IBPluginDependency + 461.IBPluginDependency + 462.IBPluginDependency + 463.IBPluginDependency + 464.IBPluginDependency + 465.IBPluginDependency + 466.IBPluginDependency + 467.IBPluginDependency + 468.IBPluginDependency + 469.IBPluginDependency + 470.IBPluginDependency + 471.IBPluginDependency + 474.IBPluginDependency + 475.IBPluginDependency + 476.IBPluginDependency + 477.IBPluginDependency + 478.IBPluginDependency + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + {{303, 221}, {480, 360}} + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + + + YES + + + + + + YES + + + + + 482 + + + + YES + + AppController + NSObject + + deleteAutosave: + id + + + deleteAutosave: + + deleteAutosave: + id + + + + YES + + YES + splitViewA + splitViewB + splitViewC + + + YES + NSSplitView + NSSplitView + NSSplitView + + + + YES + + YES + splitViewA + splitViewB + splitViewC + + + YES + + splitViewA + NSSplitView + + + splitViewB + NSSplitView + + + splitViewC + NSSplitView + + + + + IBProjectSource + ./Classes/AppController.h + + + + + 0 + IBCocoaFramework + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 + + + YES + 3 + + diff --git a/Tests/Manual/CPStackViewTest/Resources/spinner.gif b/Tests/Manual/CPStackViewTest/Resources/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..a5e705f6cbdf914e5e714c35a8dfef807f19e3c2 GIT binary patch literal 1434 zcmZvbdrVVj7{t$-UNOu86#VHu^|s&3rWfwHBbPG_8{SrlB{Tv1uvvj4t6zP!)#d!Ogc zP^62J^$0+~?-ZcXXpBaq%jFsw8L`=H2?+@R02Yg-*Xw;gACBV^i3CMahr>Z667S!? zk3LDe>VLEsU;sWo$Py~o!gWuaIAnaG3Sv``&tvc+8DJO!| zt4fcbQ8tW}a&xZfgtQ9BnFYLvGG|PvCNlg&uh@Q^w9Pz}+I^hCj`vu9JQADPqdkyk zR40xycD9geUgJu1e;$L`Fpa)?Wl-2&6hO@U*KrPqK(I1H=1h?2fce}6GhhP1oBZC# z1e@gt-qFa6lZrl%s1>bdxg*W)Ebt__O(4sj6(*2X@ciUClwHH#U(NRM7XAjS z+scDZ32J*PCoslsgS~#ZlCCGo`r>S6F)Ghw5wVlqHioFaw?ucD(XoLJ0j;28oPoPV z9A}dR$0SKn>uExfkl#j09o;v$BZ909R=*p{ATNq8BJW;YduX2QMOU7$a$_L5e!G^g zpbkx5G4&FZ%7m14rTN}5YB(;x7$5XOc_hf3E|Hw$TVg)P#qf~}nOn03uqsp>UG>vi zWThIoO)-1Q#@u#O;fMC#WAf@Xj70-0g9YZ;dA$HH1fW1q=9-c>>_yA0S$7M*5?}vi z)8MU`Q%P!7sEBBnd$DrKgFQ2?O%6LpdaC%F8Pn-)EC2t=j~ zoaLamla$FX!GQqWi!%s>sj-#5SJX0IF!TP=r21Z}s)k#btN0?=I7uD9C)Gb$fZ#sm zaVq7g`LwZ%PfNpN^_N-IGLHj@AV`s#4&va7_{Hw63G6BkSGXHdogTZ%QOiRb bDpZ@qYcJKM>x%b%*HX74c8MnqfHi*u-bC?g literal 0 HcmV?d00001 diff --git a/Tests/Manual/CPStackViewTest/index-debug.html b/Tests/Manual/CPStackViewTest/index-debug.html new file mode 100644 index 000000000..a36b1d3b9 --- /dev/null +++ b/Tests/Manual/CPStackViewTest/index-debug.html @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + __project.name__ + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/CPStackViewTest/index.html b/Tests/Manual/CPStackViewTest/index.html new file mode 100644 index 000000000..ac42c98a7 --- /dev/null +++ b/Tests/Manual/CPStackViewTest/index.html @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + __project.name__ + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/CPStackViewTest/main.j b/Tests/Manual/CPStackViewTest/main.j new file mode 100644 index 000000000..bceec5f14 --- /dev/null +++ b/Tests/Manual/CPStackViewTest/main.j @@ -0,0 +1,18 @@ +/* + * AppController.j + * CPStackViewTest + * + * Created by Daniel Böhringer on December 26, 2025. + * Copyright 2025, All rights reserved. + */ + +@import +@import + +@import "AppController.j" + + +function main(args, namedArgs) +{ + CPApplicationMain(args, namedArgs); +} From b2ada8a76fce7fbf5ae53d09f2c3969e9c1d1914 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 26 Dec 2025 11:16:46 +0100 Subject: [PATCH 060/103] formatting --- Tests/Manual/CPStackViewTest/AppController.j | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/Manual/CPStackViewTest/AppController.j b/Tests/Manual/CPStackViewTest/AppController.j index 698f59ff3..05db03974 100644 --- a/Tests/Manual/CPStackViewTest/AppController.j +++ b/Tests/Manual/CPStackViewTest/AppController.j @@ -11,7 +11,6 @@ // We import the class to be tested. // Assuming CPStackView.j is in the same directory or properly included in the build. -@import "CPStackView.j" @implementation AppController : CPObject { From 42ea1a5972c4216a3abdcc40fa40f4d76e6fad49 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 26 Dec 2025 11:34:40 +0100 Subject: [PATCH 061/103] new: manual test --- .../AppController.j | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/Tests/Manual/CPMenuSubmenuPlacementConstraints/AppController.j b/Tests/Manual/CPMenuSubmenuPlacementConstraints/AppController.j index 3261959e8..5ba828a39 100644 --- a/Tests/Manual/CPMenuSubmenuPlacementConstraints/AppController.j +++ b/Tests/Manual/CPMenuSubmenuPlacementConstraints/AppController.j @@ -3,6 +3,7 @@ * CPMenuTest * * Created by Daniel Boehringer 2025 for submenu constraints on the rightmost end of the screen. + * Updated for Issue #3149 (Immediate menu updates). */ @@ -12,6 +13,11 @@ { CPWindow theWindow; BOOL _isEnabled; + + // Ivars for Live Update Test + CPMenuItem _changeTitleItem; + CPMenuItem _changeStateItem; + CPMenuItem _changeEnabledItem; } - (void)applicationDidFinishLaunching:(CPNotification)aNotification @@ -57,6 +63,28 @@ var dummyMenuItem2 = [mainMenu addItemWithTitle:@"Dummy Menu 2" action:nil keyEquivalent:@""]; [mainMenu setSubmenu:dummyMenu2 forItem:dummyMenuItem2]; + // ------------------------------------------------------------------------- + // 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]; + + [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. var rightTestMenu = [[CPMenu alloc] initWithTitle:@"Right-Side Test"], @@ -106,4 +134,32 @@ return YES; } +// ------------------------------------------------------------------------- +// Live Update Test Actions +// ------------------------------------------------------------------------- + +- (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)"]; +} + @end From 14a195592aa10486628497494bfc07fce6a872f6 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 26 Dec 2025 11:43:54 +0100 Subject: [PATCH 062/103] new manual test --- .../AppController.j | 141 ++++++++++++++---- 1 file changed, 109 insertions(+), 32 deletions(-) diff --git a/Tests/Manual/CPMenuSubmenuPlacementConstraints/AppController.j b/Tests/Manual/CPMenuSubmenuPlacementConstraints/AppController.j index 3261959e8..aca954a40 100644 --- a/Tests/Manual/CPMenuSubmenuPlacementConstraints/AppController.j +++ b/Tests/Manual/CPMenuSubmenuPlacementConstraints/AppController.j @@ -3,6 +3,8 @@ * CPMenuTest * * Created by Daniel Boehringer 2025 for submenu constraints on the rightmost end of the screen. + * Updated for Issue #3149 (Immediate menu updates). + * Updated for Issue #3153 (Hide main menu items without submenus). */ @@ -12,6 +14,15 @@ { 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; } - (void)applicationDidFinishLaunching:(CPNotification)aNotification @@ -21,13 +32,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 +46,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 +54,74 @@ [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]; - var dummyMenuItem2 = [mainMenu addItemWithTitle:@"Dummy Menu 2" action:nil keyEquivalent:@""]; - [mainMenu setSubmenu:dummyMenu2 forItem:dummyMenuItem2]; - - - // 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 +139,48 @@ return YES; } +// ------------------------------------------------------------------------- +// Live Update Test Actions (#3149) +// ------------------------------------------------------------------------- + +- (void)startUpdateTimer:(id)sender +{ + [_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"]; + + [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 From f0e29069a97b5ce417dd8d5d591bf53246e1d09a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 26 Dec 2025 12:06:18 +0100 Subject: [PATCH 063/103] fixed: sticky main menu selection issue --- AppKit/CPMenu/_CPMenuBarWindow.j | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AppKit/CPMenu/_CPMenuBarWindow.j b/AppKit/CPMenu/_CPMenuBarWindow.j index 513ec2049..614e2c9f7 100644 --- a/AppKit/CPMenu/_CPMenuBarWindow.j +++ b/AppKit/CPMenu/_CPMenuBarWindow.j @@ -471,6 +471,11 @@ 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 bounds], aPoint)) + [_menu _highlightItemAtIndex:CPNotFound]; + return CPNotFound; } From dda325b51b83a711b3f9c51cd0fe3300ee4d3c2f Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 26 Dec 2025 12:09:15 +0100 Subject: [PATCH 064/103] fixed: wrong target for bounds --- AppKit/CPMenu/_CPMenuBarWindow.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPMenu/_CPMenuBarWindow.j b/AppKit/CPMenu/_CPMenuBarWindow.j index 614e2c9f7..ce916d5cf 100644 --- a/AppKit/CPMenu/_CPMenuBarWindow.j +++ b/AppKit/CPMenu/_CPMenuBarWindow.j @@ -473,7 +473,7 @@ // 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 bounds], aPoint)) + if (CGRectContainsPoint([[self contentView] bounds], aPoint)) [_menu _highlightItemAtIndex:CPNotFound]; return CPNotFound; From 907450a63d9ac8e655242e0052256c357f22922a Mon Sep 17 00:00:00 2001 From: Michael Bach Date: Sat, 27 Dec 2025 21:38:14 +0100 Subject: [PATCH 065/103] Fix zero width/height for svg images --- Tools/imagesize/imagesize.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Tools/imagesize/imagesize.m b/Tools/imagesize/imagesize.m index 3d427ec65..87d35e4f0 100644 --- a/Tools/imagesize/imagesize.m +++ b/Tools/imagesize/imagesize.m @@ -30,6 +30,12 @@ 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]; From c05b900fe613f402c7fd0539b5826d6d59c88c47 Mon Sep 17 00:00:00 2001 From: Michael Bach Date: Sat, 27 Dec 2025 21:58:00 +0100 Subject: [PATCH 066/103] improve formatting of fox --- Tools/imagesize/imagesize.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Tools/imagesize/imagesize.m b/Tools/imagesize/imagesize.m index 87d35e4f0..6ca2eeeb4 100644 --- a/Tools/imagesize/imagesize.m +++ b/Tools/imagesize/imagesize.m @@ -31,6 +31,7 @@ int getImageSize(const char* utf8Path, BOOL appendLineFeed) NSInteger width = [representation pixelsWide]; NSInteger height = [representation pixelsHigh]; NSString *extension = [[path pathExtension] lowercaseString]; + if ([extension isEqualToString:@"svg"]) { width = [image size].width; From 8d61a5fbe1ec19a5f95b73a0ee23f8e359039285 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 31 Dec 2025 09:18:30 +0100 Subject: [PATCH 067/103] Correct release year in project status Update project status release year from 2025 to 2026. --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index b59bbc863..e3796c890 100644 --- a/README.markdown +++ b/README.markdown @@ -8,7 +8,7 @@ Cappuccino is an open-source framework that supports building powerful, desktop- Cappuccino faithfully implements the proven design patterns of NeXTSTEP/Apple's Cocoa frameworks, enabling the creation of incredibly complex and reliable applications with a fraction of the code. > **✨ Project Status: Active Development & Node.js Transition** -> Cappuccino has been under continuous development since 2008 and is actively maintained. A major transition to a modern, **Node.js-based toolchain** has recently been finalized. The current release is a production-ready Release Candidate, with a formal release scheduled for 2025. It is stable, fast, and ready for new projects. +> Cappuccino has been under continuous development since 2008 and is actively maintained. A major transition to a modern, **Node.js-based toolchain** has recently been finalized. The current release is a production-ready Release Candidate, with a formal release scheduled for 2026. It is stable, fast, and ready for new projects. --- From 261557717339e30322f68053ba04d2a368bc05b1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 19 Jan 2026 07:50:18 +0100 Subject: [PATCH 068/103] Fix Kitchen Sink demo link in README Updated the link for the Kitchen Sink demo in the README. --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index e3796c890..2544da7ed 100644 --- a/README.markdown +++ b/README.markdown @@ -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 [Demo application](https://ansb.uniklinik-freiburg.de/UIBuilder/index.html). Also take a look at the [Kitchen Sink demo](https://ansb.uniklinik-freiburg.de/ThemeKitchenSinkA3/). * **🚀 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. From 8433d4a1d1b20ba0927eb71014c4b3d48e57f4b3 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 25 Jan 2026 17:25:17 +0100 Subject: [PATCH 069/103] new: support for kCAMediaTimingFunctionEaseInEaseOut --- AppKit/CoreAnimation/CALayer.j | 150 +++++++++++++++++++-------------- 1 file changed, 87 insertions(+), 63 deletions(-) diff --git a/AppKit/CoreAnimation/CALayer.j b/AppKit/CoreAnimation/CALayer.j index a09d8c3df..4c0ad999e 100644 --- a/AppKit/CoreAnimation/CALayer.j +++ b/AppKit/CoreAnimation/CALayer.j @@ -998,18 +998,17 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) keyPath = [anim keyPath]; var startValue = ([anim respondsToSelector:@selector(fromValue)]) ? [anim fromValue] : nil; - if (startValue == nil) startValue = [self valueForKey:keyPath]; var endValue = ([anim respondsToSelector:@selector(toValue)]) ? [anim toValue] : nil; - if (endValue == nil) return; var duration = ([anim respondsToSelector:@selector(duration)]) ? [anim duration] : 0.25; - // Convert seconds to milliseconds for rAF math - var durationMS = duration * 1000.0; + + // Default to EaseInEaseOut if not specified + var timingFunction = ([anim respondsToSelector:@selector(timingFunction)]) ? [anim timingFunction] : [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; // 3. Create Context var context = { @@ -1017,46 +1016,34 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) "keyPath": keyPath, "startValue": startValue, "endValue": endValue, - "duration": durationMS, - "startTime": null, // Will be set on first frame - "requestId": null // To cancel if needed + "duration": duration * 1000.0, // ms + "timingFunction": timingFunction, + "startTime": null, + "requestId": null }; // 4. Define the Render Loop - // We use a JavaScript closure to capture 'self' and 'context' var _self = self; - - var renderLoop = function(timestamp) { - // Pass control back to Objective-J to handle the logic - // Returns YES if animation should continue, NO if finished. - var shouldContinue = [_self _renderAnimationStep:context timestamp:timestamp]; - if (shouldContinue) + var renderLoop = function(timestamp) { + if ([_self _renderAnimationStep:context timestamp:timestamp]) context.requestId = window.requestAnimationFrame(renderLoop); else context.requestId = null; - // Cleanup is handled inside _renderAnimationStep: when it returns NO }; - // 5. Kick off the loop + // 5. Kick off context.requestId = window.requestAnimationFrame(renderLoop); - - // 6. Store context [_activeAnimations setObject:context forKey:key]; } -/* - Cancels the specific animation frame and removes it from the dictionary. -*/ - (void)removeAnimationForKey:(CPString)key { var context = [_activeAnimations objectForKey:key]; - if (context) { if (context.requestId !== null) window.cancelAnimationFrame(context.requestId); - [_activeAnimations removeObjectForKey:key]; } } @@ -1065,91 +1052,128 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) { var keys = [_activeAnimations allKeys], count = [keys count]; - while (count--) [self removeAnimationForKey:[keys objectAtIndex:count]]; } /* - Internal method called every frame by requestAnimationFrame. - Returns YES to continue, NO to stop. + 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 { - // 1. Initialize Start Time on first frame if (context.startTime === null) context.startTime = timestamp; - // 2. Calculate Progress var elapsed = timestamp - context.startTime, - progress = elapsed / context.duration; + linearProgress = elapsed / context.duration; - // Clamp to 1.0 - if (progress > 1.0) progress = 1.0; + if (linearProgress > 1.0) linearProgress = 1.0; + + // Apply Timing Function + var progress = [self _solveBezier:linearProgress forTimingFunction:context.timingFunction]; - // 3. Interpolate Values 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 { - var x = start.x + (end.x - start.x) * progress, - y = start.y + (end.y - start.y) * progress; - current = CGPointMake(x, y); + 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 { - var w = start.width + (end.width - start.width) * progress, - h = start.height + (end.height - start.height) * progress; - current = CGSizeMake(w, h); + 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 { - var x = start.origin.x + (end.origin.x - start.origin.x) * progress, - y = start.origin.y + (end.origin.y - start.origin.y) * progress, - w = start.size.width + (end.size.width - start.size.width) * progress, - h = start.size.height + (end.size.height - start.size.height) * progress; - current = CGRectMake(x, y, w, h); + 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 + ); } - // 4. Apply Value if (current !== nil) [self setValue:current forKey:context.keyPath]; - // 5. Check for Completion - if (progress >= 1.0) + if (linearProgress >= 1.0) { var anim = context.animation; - - // Handle removedOnCompletion - var shouldRemove = YES; - if ([anim respondsToSelector:@selector(isRemovedOnCompletion)]) - shouldRemove = [anim isRemovedOnCompletion]; - - if (shouldRemove) - { - // Remove from _activeAnimations - // We search by object equality to ensure we delete the right key - var allKeys = [_activeAnimations allKeysForObject:context]; - if ([allKeys count] > 0) - [_activeAnimations removeObjectForKey:[allKeys objectAtIndex:0]]; + + // 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; + } + } } - // Notify Delegate + // Delegate var delegate = [anim delegate]; - if (delegate && [delegate respondsToSelector:@selector(animationDidStop:finished:)]) [delegate animationDidStop:anim finished:YES]; - return NO; // Stop the loop + return NO; } - return YES; // Continue the loop + return YES; } /* @ignore */ From 0fc2ed70811983425df2af29eb7d899bba4a5e18 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 25 Jan 2026 19:33:33 +0100 Subject: [PATCH 070/103] new: manual test --- AppKit/CoreAnimation/CALayer.j | 57 +++++- .../CPAnimationContextTest/AppController.j | 192 ++++++++++++++++++ 2 files changed, 241 insertions(+), 8 deletions(-) diff --git a/AppKit/CoreAnimation/CALayer.j b/AppKit/CoreAnimation/CALayer.j index 4c0ad999e..2f60cdccf 100644 --- a/AppKit/CoreAnimation/CALayer.j +++ b/AppKit/CoreAnimation/CALayer.j @@ -989,19 +989,54 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) { if (!anim) return; - // 1. Remove existing animation for this key - [self removeAnimationForKey:key]; + // --- 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; - // 2. Determine Properties + 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) - startValue = [self valueForKey:keyPath]; + { + try { + startValue = [self 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; @@ -1010,7 +1045,7 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) // Default to EaseInEaseOut if not specified var timingFunction = ([anim respondsToSelector:@selector(timingFunction)]) ? [anim timingFunction] : [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; - // 3. Create Context + // --- 4. Prepare Context --- var context = { "animation": anim, "keyPath": keyPath, @@ -1022,7 +1057,7 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) "requestId": null }; - // 4. Define the Render Loop + // --- 5. Render Loop --- var _self = self; var renderLoop = function(timestamp) { @@ -1032,9 +1067,15 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) context.requestId = null; }; - // 5. Kick off + // --- 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:key]; + [_activeAnimations setObject:context forKey:storageKey]; } - (void)removeAnimationForKey:(CPString)key diff --git a/Tests/Manual/CPAnimationContextTest/AppController.j b/Tests/Manual/CPAnimationContextTest/AppController.j index 961cdf07a..dbcac5947 100644 --- a/Tests/Manual/CPAnimationContextTest/AppController.j +++ b/Tests/Manual/CPAnimationContextTest/AppController.j @@ -9,6 +9,7 @@ @import @import @import +@import #define UIAssert(a) [self markTest:_cmd didPass:a]; @@ -410,11 +411,19 @@ [_testView setAlphaValue:1.0]; [_pathView setPath:nil]; // Clear the path view as we aren't using it here + // FIX: 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 @@ -458,6 +467,189 @@ [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]; + [layer setDelegate:_testView]; + + // 3. Define the Animation + 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 + +/* + Category to allow CALayer to drive the 'angle' property on the view. +*/ +@implementation CALayer (RotationTest) + +- (void)setAngle:(float)anAngle +{ + // Store it so [self valueForKey:@"angle"] works for animation start values + self._angle = anAngle; + + // Forward the value to the View (the layer's delegate) to trigger drawRect + if (_delegate && [_delegate respondsToSelector:@selector(setAngle:)]) + [_delegate setAngle:anAngle]; +} + +- (float)angle +{ + return self._angle || 0.0; +} + @end var unCamelCase = function(aString) From b8f63166cd85a6a5830dac52a531ed469511e437 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 26 Jan 2026 07:18:02 +0100 Subject: [PATCH 071/103] fixed: group animation semantics and test --- AppKit/CoreAnimation/CALayer.j | 8 ++++-- .../CPAnimationContextTest/AppController.j | 27 +++---------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/AppKit/CoreAnimation/CALayer.j b/AppKit/CoreAnimation/CALayer.j index 2f60cdccf..4cd003635 100644 --- a/AppKit/CoreAnimation/CALayer.j +++ b/AppKit/CoreAnimation/CALayer.j @@ -29,6 +29,8 @@ @import "CGGeometry.j" @import "CPColor.j" @import "CPView.j" +@import "CAMediaTimingFunction.j" + #define DOM(aLayer) aLayer._DOMElement @@ -984,6 +986,8 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) /* 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 { @@ -1027,7 +1031,7 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) if (startValue == nil) { try { - startValue = [self valueForKey:keyPath]; + startValue = [[self delegate] valueForKey:keyPath]; } catch (e) { // The keyPath was likely invalid (not KVC compliant), abort. @@ -1186,7 +1190,7 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) } if (current !== nil) - [self setValue:current forKey:context.keyPath]; + [[self delegate] setValue:current forKey:context.keyPath]; if (linearProgress >= 1.0) { diff --git a/Tests/Manual/CPAnimationContextTest/AppController.j b/Tests/Manual/CPAnimationContextTest/AppController.j index dbcac5947..a9d657859 100644 --- a/Tests/Manual/CPAnimationContextTest/AppController.j +++ b/Tests/Manual/CPAnimationContextTest/AppController.j @@ -411,7 +411,7 @@ [_testView setAlphaValue:1.0]; [_pathView setPath:nil]; // Clear the path view as we aren't using it here - // FIX: Ensure the view is layer-backed. + // Ensure the view is layer-backed. // Without this, [_testView layer] returns nil. [_testView setWantsLayer:YES]; @@ -483,9 +483,10 @@ [_testView setWantsLayer:YES]; var layer = [_testView layer]; - [layer setDelegate:_testView]; // 3. Define the Animation + // Animation is performed on _testView + [layer setDelegate:_testView]; var rotationAnim = [CABasicAnimation animationWithKeyPath:@"angle"]; // Rotate 360 degrees (2 * PI) @@ -630,28 +631,6 @@ @end -/* - Category to allow CALayer to drive the 'angle' property on the view. -*/ -@implementation CALayer (RotationTest) - -- (void)setAngle:(float)anAngle -{ - // Store it so [self valueForKey:@"angle"] works for animation start values - self._angle = anAngle; - - // Forward the value to the View (the layer's delegate) to trigger drawRect - if (_delegate && [_delegate respondsToSelector:@selector(setAngle:)]) - [_delegate setAngle:anAngle]; -} - -- (float)angle -{ - return self._angle || 0.0; -} - -@end - var unCamelCase = function(aString) { // insert a space before all caps From a5e9d65852f2a87b7c17dd9d4d3ea63d648519b2 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 27 Jan 2026 20:03:05 +0100 Subject: [PATCH 072/103] fixed: fontpanel was crashing --- AppKit/AppKit.j | 1 + AppKit/CPTextView/CPFontPanel.j | 3 +++ 2 files changed, 4 insertions(+) diff --git a/AppKit/AppKit.j b/AppKit/AppKit.j index 99f532131..a893e0204 100644 --- a/AppKit/AppKit.j +++ b/AppKit/AppKit.j @@ -115,3 +115,4 @@ @import "CPWindow.j" @import "CPWindowController.j" @import "CPWorkspace.j" +@import "CPFontPanel.j" diff --git a/AppKit/CPTextView/CPFontPanel.j b/AppKit/CPTextView/CPFontPanel.j index b3abeece7..4dbe710b2 100644 --- a/AppKit/CPTextView/CPFontPanel.j +++ b/AppKit/CPTextView/CPFontPanel.j @@ -197,6 +197,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]; From 41648b860579b8caad733344cfdef5b70bbc93c3 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 28 Jan 2026 07:26:02 +0100 Subject: [PATCH 073/103] Fixed: CPDatePicker tab navigation when elements are hidden --- AppKit/CPDatePicker/_CPDatePickerTextField.j | 54 +++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/AppKit/CPDatePicker/_CPDatePickerTextField.j b/AppKit/CPDatePicker/_CPDatePickerTextField.j index 9f53e7848..c78dc8155 100644 --- a/AppKit/CPDatePicker/_CPDatePickerTextField.j +++ b/AppKit/CPDatePicker/_CPDatePickerTextField.j @@ -221,13 +221,24 @@ 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) { if (flags & CPShiftKeyMask) - [self _selectTextField:_lastTextField]; + { + // If _lastTextField is hidden, find the one before it that is visible + if ([_lastTextField isHidden]) + [self _selectTextField:[self _previousVisibleTextFieldFrom:_lastTextField]]; + else + [self _selectTextField:_lastTextField]; + } else - [self _selectTextField:_firstTextField]; + { + // If _firstTextField is hidden, find the one after it that is visible + if ([_firstTextField isHidden]) + [self _selectTextField:[self _nextVisibleTextFieldFrom:_firstTextField]]; + else + [self _selectTextField:_firstTextField]; + } } } @@ -334,15 +345,40 @@ var CPZeroKeyCode = 48, return [super performKeyEquivalent:anEvent]; } +- (_CPDatePickerElementTextField)_nextVisibleTextFieldFrom:(_CPDatePickerElementTextField)aTextField +{ + var next = [aTextField nextTextField]; + // Keep looking while next exists AND it is hidden + while (next && [next isHidden]) + next = [next nextTextField]; + + return next; +} + +- (_CPDatePickerElementTextField)_previousVisibleTextFieldFrom:(_CPDatePickerElementTextField)aTextField +{ + var prev = [aTextField previousTextField]; + // Keep looking while prev exists AND it is hidden + while (prev && [prev isHidden]) + prev = [prev previousTextField]; + + return prev; +} + - (void)insertTab:(id)sender { if (!_currentTextField) return; - if (_currentTextField == _lastTextField) - [[self window] selectNextKeyView:self]; + // Determine what the actual next field is + var nextField = [self _nextVisibleTextFieldFrom:_currentTextField]; + + // If there is a visible field to go to, go there. + if (nextField) + [self _selectTextField:nextField]; else - [self moveRight:sender]; + // Otherwise, leave the DatePicker control + [[self window] selectNextKeyView:self]; } - (void)moveRight:(id)sender @@ -350,7 +386,11 @@ var CPZeroKeyCode = 48, if (!_currentTextField) return; - [self _selectTextField:[_currentTextField nextTextField]]; + // Use the helper to skip hidden fields + var nextField = [self _nextVisibleTextFieldFrom:_currentTextField]; + + if (nextField) + [self _selectTextField:nextField]; } - (void)insertBacktab:(id)sender From 45162a3fcbe557234440855a9bbc5b0a2f37f10f Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 28 Jan 2026 19:24:29 +0100 Subject: [PATCH 074/103] fixed: acceptsFirstMouse was missing --- AppKit/CPTextView/CPTextView.j | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index f51a0da11..9327db2a4 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -524,6 +524,11 @@ var kDelegateRespondsTo_textShouldBeginEditing return [self isSelectable]; // editable textviews are automatically selectable } +- (BOOL)acceptsFirstMouse:(CPEvent)anEvent +{ + return YES; +} + - (void)_becomeFirstResponder { [self updateInsertionPointStateAndRestartTimer:YES]; From 16d87808123eb44a9e38f5d0a3975de9b38394cc Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 28 Jan 2026 19:38:29 +0100 Subject: [PATCH 075/103] fixed: keyview loop trap --- AppKit/CPDatePicker/_CPDatePickerTextField.j | 151 +++++++++++++++---- 1 file changed, 118 insertions(+), 33 deletions(-) diff --git a/AppKit/CPDatePicker/_CPDatePickerTextField.j b/AppKit/CPDatePicker/_CPDatePickerTextField.j index c78dc8155..48a2ee031 100644 --- a/AppKit/CPDatePicker/_CPDatePickerTextField.j +++ b/AppKit/CPDatePicker/_CPDatePickerTextField.j @@ -223,22 +223,28 @@ var CPZeroKeyCode = 48, if (!_currentTextField) { + var targetField = nil; + if (flags & CPShiftKeyMask) { - // If _lastTextField is hidden, find the one before it that is visible + // Try last field; if hidden, find previous visible if ([_lastTextField isHidden]) - [self _selectTextField:[self _previousVisibleTextFieldFrom:_lastTextField]]; + targetField = [self _previousVisibleTextFieldFrom:_lastTextField]; else - [self _selectTextField:_lastTextField]; + targetField = _lastTextField; } else { - // If _firstTextField is hidden, find the one after it that is visible + // Try first field; if hidden, find next visible if ([_firstTextField isHidden]) - [self _selectTextField:[self _nextVisibleTextFieldFrom:_firstTextField]]; + targetField = [self _nextVisibleTextFieldFrom:_firstTextField]; else - [self _selectTextField:_firstTextField]; + targetField = _firstTextField; } + + // Only select if we actually found a valid visible field + if (targetField) + [self _selectTextField:targetField]; } } @@ -347,22 +353,52 @@ var CPZeroKeyCode = 48, - (_CPDatePickerElementTextField)_nextVisibleTextFieldFrom:(_CPDatePickerElementTextField)aTextField { - var next = [aTextField nextTextField]; - // Keep looking while next exists AND it is hidden - while (next && [next isHidden]) - next = [next nextTextField]; - - return next; + 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 prev = [aTextField previousTextField]; - // Keep looking while prev exists AND it is hidden - while (prev && [prev isHidden]) - prev = [prev previousTextField]; - - return prev; + 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 @@ -370,15 +406,71 @@ var CPZeroKeyCode = 48, if (!_currentTextField) return; - // Determine what the actual next field is + // Ensure boundaries are up to date + [_datePickerElementView _updateResponderTextField]; + var nextField = [self _nextVisibleTextFieldFrom:_currentTextField]; - // If there is a visible field to go to, go there. if (nextField) + { [self _selectTextField:nextField]; + } else - // Otherwise, leave the DatePicker control - [[self window] selectNextKeyView:self]; + { + // 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]; + + // 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]; + } + + if (nextView) + [[self window] makeFirstResponder:nextView]; + } +} + +- (void)insertBacktab:(id)sender +{ + if (!_currentTextField) + return; + + [_datePickerElementView _updateResponderTextField]; + + var prevField = [self _previousVisibleTextFieldFrom:_currentTextField]; + + if (prevField) + { + [self _selectTextField:prevField]; + } + else + { + // 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 @@ -386,6 +478,8 @@ var CPZeroKeyCode = 48, if (!_currentTextField) return; + [_datePickerElementView _updateResponderTextField]; + // Use the helper to skip hidden fields var nextField = [self _nextVisibleTextFieldFrom:_currentTextField]; @@ -393,22 +487,13 @@ var CPZeroKeyCode = 48, [self _selectTextField:nextField]; } -- (void)insertBacktab:(id)sender -{ - if (!_currentTextField) - return; - - if (_currentTextField == _firstTextField) - [[self window] selectPreviousKeyView:self]; - else - [self moveLeft:sender]; -} - - (void)moveLeft:(id)sender { if (!_currentTextField) return; + [_datePickerElementView _updateResponderTextField]; + [self _selectTextField:[_currentTextField previousTextField]]; } From f19901c214c3afb34b6da48cf9098edd2f49824a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 28 Jan 2026 21:23:03 +0100 Subject: [PATCH 076/103] new: action methods for format menu --- AppKit/CPTextView/CPTextView.j | 81 ++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 7e862f1ae..1baff4b49 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -1924,6 +1924,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]) From 886a4a422fd1b5946fbe09e522ecca2462939a1d Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 28 Jan 2026 21:41:15 +0100 Subject: [PATCH 077/103] fixed: addFontTrait --- AppKit/CPFontManager.j | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AppKit/CPFontManager.j b/AppKit/CPFontManager.j index f2159359d..d10a27e44 100644 --- a/AppKit/CPFontManager.j +++ b/AppKit/CPFontManager.j @@ -208,7 +208,11 @@ CPRemoveTraitFontAction = 7; - (@action)addFontTrait:(id)sender { - var tag = [sender tag]; + var tag = sender; + + if ([sender respondsToSelector:@selector(tag)]) + tag = [sender tag]; + _activeChange = tag == nil ? @{} : @{ @"addTraits": tag }; _fontAction = CPAddTraitFontAction; From fffcc854869f42327b2ff6694f595584b5df5e12 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 29 Jan 2026 19:06:35 +0100 Subject: [PATCH 078/103] fixed: cpdatepicker relied on deprecated keycode property --- .../_CPDatePickerElementTextField.j | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/AppKit/CPDatePicker/_CPDatePickerElementTextField.j b/AppKit/CPDatePicker/_CPDatePickerElementTextField.j index 8bdb0b1ac..f24d0d15d 100644 --- a/AppKit/CPDatePicker/_CPDatePickerElementTextField.j +++ b/AppKit/CPDatePicker/_CPDatePickerElementTextField.j @@ -27,12 +27,7 @@ CPDatePickerElementTextFieldBecomeFirstResponder = @"CPDatePickerElementTextFieldBecomeFirstResponder"; CPDatePickerElementTextFieldAMPMChangedNotification = @"CPDatePickerElementTextFieldAMPMChangedNotification"; -var CPZeroKeyCode = 48, - CPNineKeyCode = 57, - CPMajAKeyCode = 65, - CPMajPKeyCode = 80, - CPAKeyCode = 97, - CPPKeyCode = 112; +// Removed hardcoded KeyCodes (CPZeroKeyCode, etc) as they are unreliable across browsers/layouts. CPMonthDateType = 0; CPDayDateType = 1; @@ -190,23 +185,38 @@ CPAMPMDateType = 6; */ - (void)setValueForKeyEvent:(CPEvent)anEvent { - var keyCode = [anEvent keyCode]; + var keyCode = [anEvent keyCode], + characters = [anEvent characters]; - if (keyCode != CPDeleteKeyCode && keyCode != CPDeleteForwardKeyCode && keyCode < CPZeroKeyCode || keyCode > CPNineKeyCode) + // Check if the event is a deletion + var isDelete = (keyCode === CPDeleteKeyCode || keyCode === CPDeleteForwardKeyCode); + + // Check if the event is a numeric input. + // By testing the character string against a regex, we support num-pads and + // international keyboards correctly, rather than relying on keyCode ranges. + var isNumeric = (characters && [characters length] > 0 && /^[0-9]$/.test(characters)); + + // If it is neither a delete command nor a digit, we ignore it. + if (!isDelete && !isNumeric) return; var newValue = [self stringValue].replace(/\s/g, ''), - length = [newValue length], - eventKeyValue = parseInt([anEvent characters]).toString(); + length = [newValue length]; - if (keyCode == CPDeleteKeyCode || keyCode == CPDeleteForwardKeyCode) + if (isDelete) { [_timerEdition invalidate]; _timerEdition = nil; - newValue = [newValue substringToIndex:(length - 1)]; + + // Ensure we don't substring if length is 0 + if (length > 0) + newValue = [newValue substringToIndex:(length - 1)]; } else { + // Since isNumeric is true, characters is a valid digit string + var eventKeyValue = characters; + if (!_timerEdition) { _timerEdition = [CPTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(_timerKeyEvent:) userInfo:nil repeats:NO]; @@ -227,7 +237,12 @@ CPAMPMDateType = 6; } } - if (parseInt(newValue) > [self _maxNumberWithMaxDate] || ([_datePicker _isAmericanFormat] && _dateType == CPHourDateType && parseInt(newValue) > 12)) + // Safety check for NaN before comparison + var numericValue = parseInt(newValue); + if (isNaN(numericValue)) + numericValue = 0; + + if (numericValue > [self _maxNumberWithMaxDate] || ([_datePicker _isAmericanFormat] && _dateType == CPHourDateType && numericValue > 12)) return; _firstEvent = NO; From 9dfaf293f2ec8ee8f8224ee3ae303f7fb72a31f4 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 29 Jan 2026 19:12:17 +0100 Subject: [PATCH 079/103] Removed: hardcoded KeyCodes as they are unreliable --- AppKit/CPDatePicker/_CPDatePickerTextField.j | 40 +++++++++++--------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/AppKit/CPDatePicker/_CPDatePickerTextField.j b/AppKit/CPDatePicker/_CPDatePickerTextField.j index 48a2ee031..2fc284b3d 100644 --- a/AppKit/CPDatePicker/_CPDatePickerTextField.j +++ b/AppKit/CPDatePicker/_CPDatePickerTextField.j @@ -48,13 +48,6 @@ @global CPYearMonthDayDatePickerElementFlag @global CPEraDatePickerElementFlag -var CPZeroKeyCode = 48, - CPNineKeyCode = 57, - CPMajAKeyCode = 65, - CPMajPKeyCode = 80, - CPAKeyCode = 97, - CPPKeyCode = 112; - // This class is used to represente the datePicker with the CPTextFieldAndStepperDatePickerStyle/CPTextFieldDatePickerStyle mode @implementation _CPDatePickerTextField : CPControl { @@ -131,7 +124,7 @@ var CPZeroKeyCode = 48, // Don't forget to unbind, otherwise several steppers will increase or decrease [_currentTextField unbind:@"objectValue"]; [_currentTextField makeDeselectable]; - _currentTextField = nil + _currentTextField = nil; // This is usefull when clicking on the stepper when the datePicker is not selected [_stepper setObjectValue:0]; @@ -494,7 +487,11 @@ var CPZeroKeyCode = 48, [_datePickerElementView _updateResponderTextField]; - [self _selectTextField:[_currentTextField previousTextField]]; + // Use the helper to skip hidden fields to be safe + var prevField = [self _previousVisibleTextFieldFrom:_currentTextField]; + + if (prevField) + [self _selectTextField:prevField]; } - (void)moveDown:(id)sender @@ -526,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 { @@ -535,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]; } From 680047272572b3d9f9d590c17330c1a6800dde3c Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 30 Jan 2026 22:40:35 +0100 Subject: [PATCH 080/103] fixed: keystrokes from menu shortcuts are inserted into textviews --- AppKit/CPMenu/CPMenu.j | 1 + AppKit/CPTextView/CPTextView.j | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/AppKit/CPMenu/CPMenu.j b/AppKit/CPMenu/CPMenu.j index a0fbb3785..962307a66 100644 --- a/AppKit/CPMenu/CPMenu.j +++ b/AppKit/CPMenu/CPMenu.j @@ -1075,6 +1075,7 @@ var _CPMenuBarVisible = NO, if (self === [CPApp mainMenu]) [self _flashItemAtIndex:index]; + anEvent._isKeyEquivalent = YES; // prevent the menu keystroke from beeing inserted into textview [self performActionForItemAtIndex:index]; } else diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 1baff4b49..6c186de21 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -1867,7 +1867,7 @@ Sets the selection to a range of characters in response to user action. } else { - [_typingAttributes setObject:[sender selectedFont] forKey:CPFontAttributeName]; + [_typingAttributes setObject:[sender convertFont:oldFont] forKey:CPFontAttributeName]; } } else @@ -2754,12 +2754,14 @@ var _CPCopyPlaceholder = '-'; var currentFirstResponder = [[CPApp keyWindow] firstResponder]; if (currentFirstResponder && [currentFirstResponder respondsToSelector:@selector(insertText:)]) - // setTimeout to prevent flickering - setTimeout(function(){ - [currentFirstResponder insertText:textToInsert] - }, 20); + var event = [CPApp currentEvent]; - // CRUCIAL: Clear the field immediately after grabbing its content. + if (!event._isKeyEquivalent) + setTimeout(function(){ + [currentFirstResponder insertText:textToInsert] + }, 20); + + // Clear the field immediately after grabbing its content. _CPNativeInputField.innerHTML = ''; }; From 0c4d33dccc0518728ffd08bc04c8dcda67f6b23f Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 31 Jan 2026 18:21:41 +0100 Subject: [PATCH 081/103] new: font preview in fontpanel --- AppKit/CPTextView/CPFontPanel.j | 182 ++++++++++++++++++++++++++++---- 1 file changed, 164 insertions(+), 18 deletions(-) diff --git a/AppKit/CPTextView/CPFontPanel.j b/AppKit/CPTextView/CPFontPanel.j index 4dbe710b2..a92045947 100644 --- a/AppKit/CPTextView/CPFontPanel.j +++ b/AppKit/CPTextView/CPFontPanel.j @@ -2,11 +2,6 @@ * 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 @@ -29,7 +24,6 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ - @import "CPPanel.j" @import "CPColorWell.j" @import "CPColorPanel.j" @@ -51,6 +45,7 @@ var kTypefaceIndex_Normal = 0, kTypefaceIndex_Bold = 2, kTypefaceIndex_BoldItalic = 3, kToolbarHeight = 32, + kPreviewHeight = 70, kBorderSpacing = 6, kInnerSpacing = 2, kNothingChanged = 0, @@ -65,7 +60,7 @@ var kTypefaceIndex_Normal = 0, // FIXME Locale support var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], - _availableSizes = [@"9", @"10", @"11", @"12", @"13", @"14", @"18", @"24", @"36", @"48", @"72", @"96"]; + _availableSizes = [@"9", @"10", @"11", @"12", @"13", @"14", @"18", @"24", @"36", @"48", @"64", @"72", @"96", @"144", @"288"]; /*! @@ -77,6 +72,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], id _fontBrowser; id _traitBrowser; id _sizeBrowser; + + // Preview + _CPFontPanelPreviewView _previewView; + CPArray _availableFonts; id _textColorWell; CPColor _textColor; @@ -108,6 +107,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], return _sharedFontPanel; } +- (BOOL)acceptsFirstResponder +{ + return NO; +} #pragma mark - #pragma mark Init methods @@ -115,7 +118,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"]; @@ -166,15 +169,36 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], [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]; + + // Browser Layout Calculations + var browserY = previewY + kPreviewHeight + 10, + browserHeight = CGRectGetHeight(contentBounds) - browserY - 10, + availableWidth = CGRectGetWidth(contentBounds) - 20, // 10px padding L/R + + // Define Column Widths + sizeWidth = 50, + spacing = 5, + remainingWidth = availableWidth - sizeWidth - (spacing * 2), + // Split remaining roughly 60% font name, 40% trait + fontWidth = Math.floor(remainingWidth * 0.60), + traitWidth = remainingWidth - fontWidth; + + _fontBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(10, browserY, fontWidth, browserHeight)]; + _traitBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(10 + fontWidth + spacing, browserY, traitWidth, browserHeight)]; + _sizeBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(10 + fontWidth + traitWidth + (spacing * 2), browserY, sizeWidth, browserHeight)]; + + [_sizeBrowser setAutoresizingMask:CPViewHeightSizable | CPViewMinXMargin]; + [_traitBrowser setAutoresizingMask:CPViewHeightSizable | CPViewWidthSizable]; + [_fontBrowser setAutoresizingMask:CPViewHeightSizable | CPViewWidthSizable]; [self _setupBrowser:_fontBrowser]; [self _setupBrowser:_traitBrowser]; @@ -189,7 +213,6 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], - (void)textViewDidChangeSelection:(CPNotification)notification { [self _refreshWithTextView:[notification object]]; - } - (void)_refreshWithTextView:(CPTextView)textView @@ -219,6 +242,9 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], [self setCurrentFont:font]; [self setCurrentTrait:trait]; [self setCurrentSize:[font size] + ""]; //cast to string + + // Update Preview + [_previewView setPreviewFont:font]; if (!color) return; @@ -372,6 +398,8 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], if ([self currentTrait] != typefaceIndex) [self setCurrentTrait:typefaceIndex ]; + + [_previewView setPreviewFont:font]; _fontChanges = kNothingChanged; } @@ -390,18 +418,24 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], 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 @@ -443,4 +477,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:@"Aa"]; + [_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]]; From e413f3a9fd5274f6e0fdde59f9aba910ef21891b Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 31 Jan 2026 21:12:35 +0100 Subject: [PATCH 082/103] fixed: column sizing --- AppKit/CPTextView/CPFontPanel.j | 221 +++++++++++++++++++------------- 1 file changed, 132 insertions(+), 89 deletions(-) diff --git a/AppKit/CPTextView/CPFontPanel.j b/AppKit/CPTextView/CPFontPanel.j index a92045947..701a0251e 100644 --- a/AppKit/CPTextView/CPFontPanel.j +++ b/AppKit/CPTextView/CPFontPanel.j @@ -1,28 +1,29 @@ /* - * 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 - */ + 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" @@ -31,7 +32,6 @@ @import "CPText.j" @import "CPFontManager.j" - @class CPTextStorage @class CPLayoutManager @class CPTextContainer @@ -40,28 +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, - kPreviewHeight = 70, - 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", @"64", @"72", @"96", @"144", @"288"]; - +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 @@ -72,10 +74,10 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], id _fontBrowser; id _traitBrowser; id _sizeBrowser; - + // Preview _CPFontPanelPreviewView _previewView; - + CPArray _availableFonts; id _textColorWell; CPColor _textColor; @@ -84,7 +86,6 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], int _fontChanges; } - #pragma mark - #pragma mark Class methods @@ -118,7 +119,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], /*! @ignore */ - (id)init { - if (self = [super initWithContentRect:CGRectMake(100, 90, 450, 420) 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"]; @@ -155,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) @@ -166,6 +206,9 @@ 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], @@ -179,40 +222,37 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], [_previewView setAutoresizingMask:CPViewWidthSizable]; [contentView addSubview:_previewView]; - // Browser Layout Calculations - var browserY = previewY + kPreviewHeight + 10, - browserHeight = CGRectGetHeight(contentBounds) - browserY - 10, - availableWidth = CGRectGetWidth(contentBounds) - 20, // 10px padding L/R - - // Define Column Widths - sizeWidth = 50, - spacing = 5, - remainingWidth = availableWidth - sizeWidth - (spacing * 2), - // Split remaining roughly 60% font name, 40% trait - fontWidth = Math.floor(remainingWidth * 0.60), - traitWidth = remainingWidth - fontWidth; + // Initialize Browsers with zero rect, _layoutBrowsers will size them + _fontBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()]; + _traitBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()]; + _sizeBrowser = [[CPBrowser alloc] initWithFrame:CGRectMakeZero()]; - _fontBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(10, browserY, fontWidth, browserHeight)]; - _traitBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(10 + fontWidth + spacing, browserY, traitWidth, browserHeight)]; - _sizeBrowser = [[CPBrowser alloc] initWithFrame:CGRectMake(10 + fontWidth + traitWidth + (spacing * 2), browserY, sizeWidth, browserHeight)]; - - [_sizeBrowser setAutoresizingMask:CPViewHeightSizable | CPViewMinXMargin]; - [_traitBrowser setAutoresizingMask:CPViewHeightSizable | CPViewWidthSizable]; - [_fontBrowser setAutoresizingMask:CPViewHeightSizable | CPViewWidthSizable]; + // 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 @@ -242,7 +282,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], [self setCurrentFont:font]; [self setCurrentTrait:trait]; [self setCurrentSize:[font size] + ""]; //cast to string - + // Update Preview [_previewView setPreviewFont:font]; @@ -282,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: @@ -301,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; @@ -314,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 @@ -324,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 @@ -349,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 @@ -398,7 +440,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], if ([self currentTrait] != typefaceIndex) [self setCurrentTrait:typefaceIndex ]; - + [_previewView setPreviewFont:font]; _fontChanges = kNothingChanged; @@ -413,6 +455,7 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], //////////////////////////////////////////////////////////////////// // TODO: ask CPFontManager for traits // + - (void)browserClicked:(id)aBrowser { if (aBrowser === _fontBrowser) @@ -427,15 +470,15 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], { _fontChanges = kSizeChanged; } - + // 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]; + [_previewView setPreviewFont:updatedFont]; } - (void)dblClicked:(id)sender @@ -451,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 @@ -491,20 +534,21 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], - (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:@"Aa"]; + [_sampleText setStringValue:@"AaYy-0123"]; [_sampleText setAlignment:CPCenterTextAlignment]; [_sampleText setVerticalAlignment:CPCenterVerticalTextAlignment]; [_sampleText setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; [_sampleText setTextColor:[CPColor blackColor]]; - + [self addSubview:_sampleText]; } return self; @@ -549,29 +593,29 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], // 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; + 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); @@ -588,5 +632,4 @@ var _availableTraits= [@"Normal", @"Italic", @"Bold", @"Bold Italic"], @end - [CPFontManager setFontPanelFactory:[CPFontPanel class]]; From 7ee4003840c02b423399004860a2c583d327cc22 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 1 Feb 2026 19:29:59 +0100 Subject: [PATCH 083/103] fixed: prevent timezone shift from reverting month navigation in _CPDatePickerCalendar --- AppKit/CPDatePicker/_CPDatePickerCalendar.j | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/AppKit/CPDatePicker/_CPDatePickerCalendar.j b/AppKit/CPDatePicker/_CPDatePickerCalendar.j index af43569b3..101185add 100644 --- a/AppKit/CPDatePicker/_CPDatePickerCalendar.j +++ b/AppKit/CPDatePicker/_CPDatePickerCalendar.j @@ -251,7 +251,15 @@ - (void)_displayNextMonth { - [self setDateValue:[_monthView nextMonth]]; + // Copy the date so we don't modify the view's state directly + var nextDate = [[_monthView nextMonth] copy]; + + // Set to the middle of the month (15th). + // This prevents [setDateValue:]'s timezone adjustment from + // shifting the date back into the previous month (e.g., Nov 1 -> Oct 31). + nextDate.setDate(15); + + [self setDateValue:nextDate]; } - (void)_displayPreviousMonth From d58340319550bf8f26bc0014b86ddaf838fc9c6a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 1 Feb 2026 19:43:17 +0100 Subject: [PATCH 084/103] fixed: crash when changing typing attributes without selection --- AppKit/CPTextView/CPTextView.j | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 6c186de21..ce6396e52 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -1867,6 +1867,10 @@ Sets the selection to a range of characters in response to user action. } else { + attributes = [_textStorage attributesAtIndex:_selectionRange.location + longestEffectiveRange:_selectionRange + inRange:_selectionRange]; + oldFont = [attributes objectForKey:CPFontAttributeName] || [self font]; [_typingAttributes setObject:[sender convertFont:oldFont] forKey:CPFontAttributeName]; } } From 57d0750f3cde47caa19d69b51e12b08e3718c2a2 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 1 Feb 2026 21:02:42 +0100 Subject: [PATCH 085/103] Revise README for updated application demos and tutorials Updated links and descriptions in the README to reflect current resources. --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 2544da7ed..859570108 100644 --- a/README.markdown +++ b/README.markdown @@ -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://ansb.uniklinik-freiburg.de/ThemeKitchenSinkA3/). +* **💻 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. From 0e86264837608da7e273e7da65521ba2485d062d Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 6 Feb 2026 16:16:08 +0100 Subject: [PATCH 086/103] fixed: faulty event factory covermethod --- AppKit/CPEvent.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPEvent.j b/AppKit/CPEvent.j index 1d486521a..61f7038a8 100644 --- a/AppKit/CPEvent.j +++ b/AppKit/CPEvent.j @@ -128,7 +128,7 @@ var _CPEventPeriodicEventPeriod = 0, // 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 + 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 From ea47767bb89ee88deddc4f0e33a1e71a69134446 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 21 Feb 2026 11:51:25 +0100 Subject: [PATCH 087/103] fixed: menu key support conflicting with browser native menu keys --- AppKit/CPMenu/CPMenu.j | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AppKit/CPMenu/CPMenu.j b/AppKit/CPMenu/CPMenu.j index 962307a66..4ef91ea7e 100644 --- a/AppKit/CPMenu/CPMenu.j +++ b/AppKit/CPMenu/CPMenu.j @@ -1077,6 +1077,9 @@ var _CPMenuBarVisible = NO, anEvent._isKeyEquivalent = YES; // prevent the menu keystroke from beeing inserted into textview [self performActionForItemAtIndex:index]; + + // we are done with this event in cappuccino space. do not let the browser do something weird additionally (e.g. command-o). + _CPDOMEventStop(anEvent._DOMEvent); } else { From 980e2f2c916e5446ac3808d6f77cb8b0b0173b32 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 21 Feb 2026 12:08:47 +0100 Subject: [PATCH 088/103] fixed: dom protection was missing --- AppKit/CPMenu/CPMenu.j | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AppKit/CPMenu/CPMenu.j b/AppKit/CPMenu/CPMenu.j index 4ef91ea7e..5ee13d4fc 100644 --- a/AppKit/CPMenu/CPMenu.j +++ b/AppKit/CPMenu/CPMenu.j @@ -1078,8 +1078,10 @@ var _CPMenuBarVisible = NO, 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). _CPDOMEventStop(anEvent._DOMEvent); +#endif } else { From df594ae48e6eb97f9189d3fc9062f9e93e744fd7 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 21 Feb 2026 18:48:19 +0100 Subject: [PATCH 089/103] fixed: copy/paste issue --- AppKit/CPMenu/CPMenu.j | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AppKit/CPMenu/CPMenu.j b/AppKit/CPMenu/CPMenu.j index 5ee13d4fc..d8569ed43 100644 --- a/AppKit/CPMenu/CPMenu.j +++ b/AppKit/CPMenu/CPMenu.j @@ -1080,7 +1080,11 @@ var _CPMenuBarVisible = NO, #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). - _CPDOMEventStop(anEvent._DOMEvent); + // 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 From 802c644d94160925817713dbc6b0a8959a07844c Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 22 Feb 2026 14:54:46 +0100 Subject: [PATCH 090/103] Fixed: CPWindow setContentView: overlapping the toolbar --- AppKit/CPWindow/CPWindow.j | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AppKit/CPWindow/CPWindow.j b/AppKit/CPWindow/CPWindow.j index 023193871..1b233d067 100644 --- a/AppKit/CPWindow/CPWindow.j +++ b/AppKit/CPWindow/CPWindow.j @@ -1266,6 +1266,10 @@ CPTexturedBackgroundWindowMask [_contentView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; [_windowView addSubview:_contentView]; + // The window view manages the exact layout of the content view (e.g. offsetting for the toolbar). + if ([_windowView respondsToSelector:@selector(tile)]) + [_windowView tile]; + /* If the initial first responder has been set to something other than the window, set it to the window because it will no longer be valid. From 19cceb4dd9417dfbcf3eae3836f72e9790de9c92 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 23 Feb 2026 14:24:09 +0100 Subject: [PATCH 091/103] fixed: key duplication in textview on ALT --- AppKit/CPEvent.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPEvent.j b/AppKit/CPEvent.j index 61f7038a8..5cc439e08 100644 --- a/AppKit/CPEvent.j +++ b/AppKit/CPEvent.j @@ -590,7 +590,7 @@ var _CPEventPeriodicEventPeriod = 0, // of the event, including the _isActionKey flag that was set at creation time. return ( // Is it a command shortcut? - (_modifierFlags & (CPCommandKeyMask | CPControlKeyMask | CPAlternateKeyMask)) || + (_modifierFlags & (CPCommandKeyMask | CPControlKeyMask)) || // Is it a key that doesn't produce a character? ([_characters length] === 0) || From fca30325e4540531fbfba7a553cef08e8689ae2e Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 3 Mar 2026 18:18:32 +0100 Subject: [PATCH 092/103] Use setTimeout to prevent flickering on copy/paste --- AppKit/CPTextView/CPTextView.j | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index ce6396e52..43b0a7a8b 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2847,7 +2847,12 @@ var _CPCopyPlaceholder = '-'; } var nativeString = nativeClipboard.getData('text/plain'); - [currentFirstResponder _pasteString:nativeString || [pasteboard stringForType:CPStringPboardType] || '']; + + // Use setTimeout to prevent flickering + setTimeout(function() + { + [currentFirstResponder _pasteString:nativeString || [pasteboard stringForType:CPStringPboardType] || '']; + }, 20); }; // COPY handler @@ -2890,7 +2895,11 @@ var _CPCopyPlaceholder = '-'; nativeClipboard.setData('text/rtf', rtfForPasting); // Then, perform the delete part of the cut operation in the text view - [currentFirstResponder deleteBackward:self]; + // Use setTimeout to prevent flickering + setTimeout(function() + { + [currentFirstResponder deleteBackward:self]; + }, 20); }; #endif } From 622d738b44a7824a7cc50dd504c5c49e8f156e80 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 19:25:32 +0100 Subject: [PATCH 093/103] new: CPTreeController --- AppKit/CPTreeController.j | 674 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100644 AppKit/CPTreeController.j diff --git a/AppKit/CPTreeController.j b/AppKit/CPTreeController.j new file mode 100644 index 000000000..f1bee03fc --- /dev/null +++ b/AppKit/CPTreeController.j @@ -0,0 +1,674 @@ +/* + * CPTreeController.j + * AppKit + * + * Adapted for Cappuccino + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +@import +@import +@import "CPObjectController.j" +@import "CPKeyValueBinding.j" +@import "CPTreeNode.j" + +/*! + @class CPTreeController + + CPTreeController is a bindings-compatible class that manages a tree of objects. + It provides selection and sort management for hierarchical data. + */ +@implementation CPTreeController : CPObjectController +{ + BOOL _avoidsEmptySelection; + BOOL _preservesSelection; + BOOL _selectsInsertedObjects; + BOOL _alwaysUsesMultipleValuesMarker; + + CPString _childrenKeyPath; + CPString _countKeyPath; + CPString _leafKeyPath; + + CPArray _sortDescriptors; + id _arrangedObjects; // The proxy root node containing the tree + + CPArray _selectionIndexPaths; // Array of CPIndexPath objects + + 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 +{ + self = [super init]; + + if (self) + { + _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:[[self newObject]]]; +} + +// --- Properties --- + +- (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]; +} + +// --- Key Paths --- + +- (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; +} + +// --- Node Key Path Overrides --- + +- (CPString)childrenKeyPathForNode:(CPTreeNode)node +{ + return [self childrenKeyPath]; +} + +- (CPString)countKeyPathForNode:(CPTreeNode)node +{ + return [self countKeyPath]; +} + +- (CPString)leafKeyPathForNode:(CPTreeNode)node +{ + return [self leafKeyPath]; +} + +// --- Content and Arranged Objects --- + +- (void)setContent:(id)value +{ + if (_disableSetContent) + return; + + if (value == nil) + value = []; + + if (![value isKindOfClass:[CPArray class]]) + value = [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[self content]; +} + +- (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]; + + // Rebuild the proxy tree. In a full implementation, this observes children using _childrenKeyPath + // and applies _sortDescriptors recursively.[self __rebuildArrangedObjectsTree]; + + if ([self preservesSelection]) + [self __setSelectedObjects:oldSelectedObjects]; + else + [self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection]; +} + +- (void)__rebuildArrangedObjectsTree +{ + // A simplified rebuilding logic: we set the content as the represented object's children + // of the root proxy node. + var rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil]; + var contentArray = [self contentArray]; + + // Sort top level if needed + var sortedContent = contentArray; + if ([_sortDescriptors count] > 0) + sortedContent = [contentArray sortedArrayUsingDescriptors:_sortDescriptors]; + + var count = [sortedContent count]; + var children = [CPMutableArray arrayWithCapacity:count]; + + for (var i = 0; i < count; i++) + { + var node = [[CPTreeNode alloc] initWithRepresentedObject:sortedContent[i]]; + // Note: Recursive population and sorting of children based on _childrenKeyPath + // would occur here in a complete tree parser.[children addObject:node]; + } + + [[rootNode mutableChildNodes] addObjectsFromArray:children]; + _arrangedObjects = rootNode; +} + +// --- Selection Management --- + +- (CPIndexPath)selectionIndexPath +{ + return [_selectionIndexPaths count] > 0 ? [_selectionIndexPaths objectAtIndex:0] : nil; +} + +- (BOOL)setSelectionIndexPath:(CPIndexPath)indexPath +{ + var paths = indexPath ? [indexPath] : []; + 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 = []; + + if (![newPaths count] && avoidEmpty) + { + if ([[[self arrangedObjects] childNodes] count] > 0) + newPaths = [[CPIndexPath indexPathWithIndex:0]]; + } + + if ([_selectionIndexPaths isEqualToArray:newPaths]) + return NO; + + _selectionIndexPaths = [newPaths copy]; + + var binderClass = [[self class] _binderClassForBinding:@"selectionIndexPaths"]; + [[binderClass getBinding:@"selectionIndexPaths" forObject:self] reverseSetValueFor:@"selectionIndexPaths"]; + + return YES; +} + +- (BOOL)addSelectionIndexPaths:(CPArray)indexPaths +{ + var newPaths = [_selectionIndexPaths mutableCopy]; + [newPaths addObjectsFromArray:indexPaths]; + // Remove duplicates and maintain sorted order + // ... + return[self setSelectionIndexPaths:newPaths]; +} + +- (BOOL)removeSelectionIndexPaths:(CPArray)indexPaths +{ + var newPaths = [_selectionIndexPaths mutableCopy]; + [newPaths removeObjectsInArray:indexPaths]; + return [self setSelectionIndexPaths:newPaths]; +} + +- (CPArray)selectedNodes +{ + var nodes = [], + count = [_selectionIndexPaths count]; + + for (var i = 0; i < count; i++) + { + var node = [[self arrangedObjects] descendantNodeAtIndexPath:_selectionIndexPaths[i]]; + if (node)[nodes addObject:node]; + } + return nodes; +} + +- (CPArray)selectedObjects +{ + var objects = [], + nodes = [self selectedNodes], + count = [nodes count]; + + for (var i = 0; i < count; i++) + [objects addObject:[nodes[i] representedObject]]; + + return objects; +} + +- (BOOL)__setSelectedObjects:(CPArray)objects +{ + // Search the tree for index paths matching the passed objects and update selection + // (Omitted recursive search for brevity) + return YES; +} + +// --- Adding, Inserting, Removing --- + +- (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]; + + var selectionPath = [self selectionIndexPath] || [CPIndexPath indexPathWithIndex:[[[self arrangedObjects] childNodes] count]]; + + // Increment the last index by 1 to add *after* the current selection + var length = [selectionPath length], + lastIndex = [selectionPath indexAtPosition:length - 1]; + + var 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]; + + var 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]; + var 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:[anObject] atArrangedObjectIndexPaths:[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[i], + path = indexPaths[i], + length = [path length]; + + if (length === 1) + { + // Insert at root level + [_contentObject insertObject:object atIndex:[path indexAtPosition:0]]; + } + else + { + // Insert into a parent node's children + var parentPath = [path indexPathByRemovingLastIndex], + parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath], + parentObj = [parentNode representedObject], + childIndex = [path indexAtPosition:length - 1]; + + var children = [parentObj valueForKeyPath:_childrenKeyPath]; + if (!children) + { + children = [CPMutableArray array]; + [parentObj setValue:children forKeyPath:_childrenKeyPath]; + } + [children insertObject:object atIndex:childIndex]; + } + } + + [[CPBinder 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:[indexPath]]; +} + +- (void)removeObjectsAtArrangedObjectIndexPaths:(CPArray)indexPaths +{ + [self willChangeValueForKey:@"content"]; + _disableSetContent = YES; + + // Remove in reverse order to prevent shifting indices from invalidating remaining paths + var sortedPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)], + count = [sortedPaths count]; + + for (var i = count - 1; i >= 0; i--) + { + var path = sortedPaths[i], + length = [path length]; + + if (length === 1) + { + [_contentObject removeObjectAtIndex:[path indexAtPosition:0]]; + } + else + { + var parentPath = [path indexPathByRemovingLastIndex], + parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath], + parentObj = [parentNode representedObject], + childIndex = [path indexAtPosition:length - 1]; + + var children = [parentObj valueForKeyPath:_childrenKeyPath]; + if (children) + [children removeObjectAtIndex:childIndex]; + } + } + + [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange]; + _disableSetContent = NO; + + [self _rearrangeObjects]; + [self didChangeValueForKey:@"content"]; +} + +- (void)moveNode:(CPTreeNode)node toIndexPath:(CPIndexPath)indexPath +{ + [self moveNodes:[node] toIndexPath:indexPath]; +} + +- (void)moveNodes:(CPArray)nodes toIndexPath:(CPIndexPath)startingIndexPath +{ + // A proper implementation handles repositioning inside the tree while maintaining object state[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:[]]; + } + + 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 + From 12960f8160d64b38b37ebea55eaa8f5a008ac1c9 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 19:27:01 +0100 Subject: [PATCH 094/103] new: testcase --- Tests/AppKit/CPTreeControllerTest.j | 250 ++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 Tests/AppKit/CPTreeControllerTest.j diff --git a/Tests/AppKit/CPTreeControllerTest.j b/Tests/AppKit/CPTreeControllerTest.j new file mode 100644 index 000000000..4d2f3fd3f --- /dev/null +++ b/Tests/AppKit/CPTreeControllerTest.j @@ -0,0 +1,250 @@ +/* + * CPTreeControllerTest.j + * + * Test suite for CPTreeController, adapted for Cappuccino + */ + +@import +@import +@import +@import +@import + +@class OrgNode + +@implementation CPTreeControllerTest : OJTestCase +{ + CPTreeController _treeController @accessors(property=treeController); + CPArray _contentArray @accessors(property=contentArray); + + CPArray observations; + int aCount @accessors; +} + +- (CPArray)makeTestTree +{ + // Root Level + var engineering = [OrgNode nodeWithName:@"Engineering"], + marketing =[OrgNode nodeWithName:@"Marketing"]; + + // Children of Engineering + var webTeam = [OrgNode nodeWithName:@"Web Team"], + backendTeam = [OrgNode nodeWithName:@"Backend Team"]; + + [engineering setChildren:[CPMutableArray arrayWithObjects:webTeam, backendTeam]]; + + // Children of Web Team + var dev1 =[OrgNode nodeWithName:@"Francisco"], + dev2 = [OrgNode nodeWithName:@"Ross"]; + + [webTeam setChildren:[CPMutableArray arrayWithObjects:dev1, dev2]]; + + return [CPMutableArray arrayWithObjects:engineering, marketing]; +} + +- (void)setUp +{ + // Init global CPApp used internally in AppKit + [[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:[] 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]; + + // Select Engineering -> Web Team (Index Path: [0, 0]) + 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[0] representedObject] name]]; +} + +- (void)testAddChild +{ + var controller = [self treeController]; + [controller setObjectClass:[OrgNode class]]; + + // Select Engineering -> Backend Team (Index Path:[0, 1]) + var parentPath = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:1]; + [controller setSelectionIndexPath:parentPath]; + + // Insert a child into Backend Team + var newDev = [OrgNode nodeWithName:@"Tom"]; + var insertPath =[parentPath indexPathByAddingIndex:0]; + + [controller insertObject:newDev atArrangedObjectIndexPath:insertPath]; + + // Validate it was added to the content + 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]; + + // Insert at root level, index 1 (between Engineering and Marketing) + 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]]; + + // Insert nested (Engineering -> HR) + 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]; + + // Remove Engineering -> Web Team -> Francisco (Index Path: [0, 0, 0]) + 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]; + + // Remove both Engineering (0) and Marketing (1) + var paths = [[CPIndexPath indexPathWithIndex:0], + [CPIndexPath indexPathWithIndex:1] + ]; + + [controller removeObjectsAtArrangedObjectIndexPaths:paths]; + + [self assert:0 equals:[[controller contentArray] count] message:@"All root nodes should be removed"]; +} + +- (void)testAvoidsEmptySelection +{ + var controller = [self treeController];[controller setAvoidsEmptySelection:YES]; + + // Set empty selection manually + [controller setSelectionIndexPaths:[]]; + + [self assertTrue:([[controller selectionIndexPaths] count] == 1) message:@"Selection should fallback to the first item when avoidsEmptySelection is YES"]; + [self assert:[CPIndexPath indexPathWithIndex:0] equals:[controller selectionIndexPath]]; + + [controller setAvoidsEmptySelection:NO];[controller setSelectionIndexPaths:[]]; + + [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]; + // Use a custom key path + [controller setChildrenKeyPath:@"subItems"]; + + var data =[OrgNode nodeWithName:@"Root"]; + [data setValue:[CPMutableArray arrayWithObject:[OrgNode nodeWithName:@"Sub"]] forKey:@"subItems"]; + + [controller setContent:[CPMutableArray arrayWithObject:data]]; + + // Insert at [0, 0] + 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]]; + + // Verify proxy tree rebuilt + [self assert:2 equals:[[[controller arrangedObjects] childNodes] count]]; +} + +- (void)testSelectedObjects +{ + var controller =[self treeController]; + + // Select Marketing [1] + var path =[CPIndexPath indexPathWithIndex:1]; + [controller setSelectionIndexPath:path]; + + var selectedObjects =[controller selectedObjects]; + [self assert:1 equals:[selectedObjects count]];[self assert:@"Marketing" equals:[selectedObjects[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); // For testing custom key paths +} + ++ (id)nodeWithName:(CPString)aName +{ + return [[self alloc] initWithName:aName]; +} + +- (id)initWithName:(CPString)aName +{ + if (self = [super init]) + { + _name = aName; + _children = [CPMutableArray array]; + _subItems =[CPMutableArray array]; + } + + return self; +} + +- (CPString)description +{ + return [CPString stringWithFormat:@"", [self name]]; +} + +@end \ No newline at end of file From 10d18e35ede6d0ccbbf49e5a00f07ace3f09f4b1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 19:28:04 +0100 Subject: [PATCH 095/103] formatting --- AppKit/CPTreeController.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPTreeController.j b/AppKit/CPTreeController.j index f1bee03fc..3f5246494 100644 --- a/AppKit/CPTreeController.j +++ b/AppKit/CPTreeController.j @@ -2,7 +2,7 @@ * CPTreeController.j * AppKit * - * Adapted for Cappuccino + * 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 From 4970a2742d90e1bdc61ea2a5d88444a04f71d539 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 19:47:31 +0100 Subject: [PATCH 096/103] fixed: recursive tree building --- AppKit/CPTreeController.j | 97 +++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 40 deletions(-) diff --git a/AppKit/CPTreeController.j b/AppKit/CPTreeController.j index 3f5246494..3274d2a08 100644 --- a/AppKit/CPTreeController.j +++ b/AppKit/CPTreeController.j @@ -264,7 +264,8 @@ [self _rearrangeObjects]; - if ([self preservesSelection])[self __setSelectedObjects:oldSelectedObjects]; + if ([self preservesSelection]) + [self __setSelectedObjects:oldSelectedObjects]; else[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection]; } @@ -275,7 +276,7 @@ - (id)contentArray { - return[self content]; + return [self content]; } - (id)arrangedObjects @@ -300,8 +301,7 @@ else oldSelectionIndexPaths = [self selectionIndexPaths]; - // Rebuild the proxy tree. In a full implementation, this observes children using _childrenKeyPath - // and applies _sortDescriptors recursively.[self __rebuildArrangedObjectsTree]; + // Rebuild the proxy tree from the content using _childrenKeyPath and _sortDescriptors[self __rebuildArrangedObjectsTree]; if ([self preservesSelection]) [self __setSelectedObjects:oldSelectedObjects]; @@ -311,28 +311,50 @@ - (void)__rebuildArrangedObjectsTree { - // A simplified rebuilding logic: we set the content as the represented object's children - // of the root proxy node. var rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil]; var contentArray = [self contentArray]; - // Sort top level if needed - var sortedContent = contentArray; - if ([_sortDescriptors count] > 0) - sortedContent = [contentArray sortedArrayUsingDescriptors:_sortDescriptors]; + if (contentArray && [contentArray count] > 0) + { + var children = [self _buildTreeNodesForObjects:contentArray]; + [[rootNode mutableChildNodes] addObjectsFromArray:children]; + } - var count = [sortedContent count]; - var children = [CPMutableArray arrayWithCapacity:count]; + _arrangedObjects = rootNode; +} + +// Recursively builds the node tree mapping model objects to CPTreeNodes +- (CPArray)_buildTreeNodesForObjects:(CPArray)objects +{ + var count = [objects count]; + if (count === 0) + return[]; + + var sortedObjects = objects; + if ([_sortDescriptors count] > 0) + sortedObjects = [objects sortedArrayUsingDescriptors:_sortDescriptors]; + + var nodes = [CPMutableArray arrayWithCapacity:count]; for (var i = 0; i < count; i++) { - var node = [[CPTreeNode alloc] initWithRepresentedObject:sortedContent[i]]; - // Note: Recursive population and sorting of children based on _childrenKeyPath - // would occur here in a complete tree parser.[children addObject:node]; + var obj = sortedObjects[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]; } - [[rootNode mutableChildNodes] addObjectsFromArray:children]; - _arrangedObjects = rootNode; + return nodes; } // --- Selection Management --- @@ -389,16 +411,15 @@ { var newPaths = [_selectionIndexPaths mutableCopy]; [newPaths addObjectsFromArray:indexPaths]; - // Remove duplicates and maintain sorted order - // ... - return[self setSelectionIndexPaths:newPaths]; + // Remove duplicates and maintain sorted order (omitted for brevity) + return [self setSelectionIndexPaths:newPaths]; } - (BOOL)removeSelectionIndexPaths:(CPArray)indexPaths { var newPaths = [_selectionIndexPaths mutableCopy]; [newPaths removeObjectsInArray:indexPaths]; - return [self setSelectionIndexPaths:newPaths]; + return[self setSelectionIndexPaths:newPaths]; } - (CPArray)selectedNodes @@ -409,7 +430,8 @@ for (var i = 0; i < count; i++) { var node = [[self arrangedObjects] descendantNodeAtIndexPath:_selectionIndexPaths[i]]; - if (node)[nodes addObject:node]; + if (node) + [nodes addObject:node]; } return nodes; } @@ -437,7 +459,7 @@ - (BOOL)canInsert { - return [self isEditable]; + return[self isEditable]; } - (BOOL)canInsertChild @@ -473,11 +495,12 @@ if (![self canAddChild]) return; - var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject], + var newObject = [self automaticallyPreparesContent] ?[self newObject] : [self _defaultNewObject], parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:[self selectionIndexPath]], childCount = [[parentNode childNodes] count]; var insertPath = [[self selectionIndexPath] indexPathByAddingIndex:childCount]; + [self insertObject:newObject atArrangedObjectIndexPath:insertPath]; } @@ -486,7 +509,7 @@ if (![self canInsert]) return; - var newObject = [self automaticallyPreparesContent] ? [self newObject] :[self _defaultNewObject]; + var newObject = [self automaticallyPreparesContent] ?[self newObject] : [self _defaultNewObject]; var indexPath = [self selectionIndexPath] ||[CPIndexPath indexPathWithIndex:0]; [self insertObject:newObject atArrangedObjectIndexPath:indexPath]; @@ -497,19 +520,18 @@ if (![self canInsertChild]) return; - var newObject = [self automaticallyPreparesContent] ? [self newObject] :[self _defaultNewObject], + 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:[anObject] atArrangedObjectIndexPaths:[indexPath]]; +{[self insertObjects:[anObject] atArrangedObjectIndexPaths:[indexPath]]; } - (void)insertObjects:(CPArray)objects atArrangedObjectIndexPaths:(CPArray)indexPaths -{ - [self willChangeValueForKey:@"content"]; +{[self willChangeValueForKey:@"content"]; _disableSetContent = YES; var count = [objects count]; @@ -521,8 +543,7 @@ if (length === 1) { - // Insert at root level - [_contentObject insertObject:object atIndex:[path indexAtPosition:0]]; + // Insert at root level[_contentObject insertObject:object atIndex:[path indexAtPosition:0]]; } else { @@ -547,15 +568,13 @@ [self _rearrangeObjects]; - if ([self selectsInsertedObjects]) - [self setSelectionIndexPaths:indexPaths]; + if ([self selectsInsertedObjects])[self setSelectionIndexPaths:indexPaths]; [self didChangeValueForKey:@"content"]; } - (void)remove:(id)sender -{ - [self removeObjectsAtArrangedObjectIndexPaths:_selectionIndexPaths]; +{[self removeObjectsAtArrangedObjectIndexPaths:_selectionIndexPaths]; } - (void)removeObjectAtArrangedObjectIndexPath:(CPIndexPath)indexPath @@ -608,7 +627,7 @@ - (void)moveNodes:(CPArray)nodes toIndexPath:(CPIndexPath)startingIndexPath { - // A proper implementation handles repositioning inside the tree while maintaining object state[CPException raise:CPUnsupportedMethodException reason:@"moveNodes:toIndexPath: is not yet implemented in CPTreeController."]; + [CPException raise:CPUnsupportedMethodException reason:@"moveNodes:toIndexPath: is not yet implemented in CPTreeController."]; } @end @@ -644,8 +663,7 @@ CPTreeControllerLeafKeyPath = @"CPTreeControllerLeafKeyPath if (![self content] && [self automaticallyPreparesContent]) [self prepareContent]; - else if (![self content]) - [self _setContentArray:[]]; + else if (![self content])[self _setContentArray:[]]; } return self; @@ -671,4 +689,3 @@ CPTreeControllerLeafKeyPath = @"CPTreeControllerLeafKeyPath } @end - From 98c01b88e29e0dbae1297cc4aef3c0ea2014e053 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 20:00:09 +0100 Subject: [PATCH 097/103] fixed various binding issues --- AppKit/CPTreeController.j | 377 +++++++++++----------------- Tests/AppKit/CPTreeControllerTest.j | 159 ++++++------ 2 files changed, 224 insertions(+), 312 deletions(-) diff --git a/AppKit/CPTreeController.j b/AppKit/CPTreeController.j index 3274d2a08..aefaa0660 100644 --- a/AppKit/CPTreeController.j +++ b/AppKit/CPTreeController.j @@ -25,12 +25,6 @@ @import "CPKeyValueBinding.j" @import "CPTreeNode.j" -/*! - @class CPTreeController - - CPTreeController is a bindings-compatible class that manages a tree of objects. - It provides selection and sort management for hierarchical data. - */ @implementation CPTreeController : CPObjectController { BOOL _avoidsEmptySelection; @@ -43,10 +37,9 @@ CPString _leafKeyPath; CPArray _sortDescriptors; - id _arrangedObjects; // The proxy root node containing the tree - - CPArray _selectionIndexPaths; // Array of CPIndexPath objects + id _arrangedObjects; + CPArray _selectionIndexPaths; BOOL _disableSetContent; } @@ -91,7 +84,7 @@ + (CPSet)keyPathsForValuesAffectingCanInsert { - return[CPSet setWithObjects:@"selectionIndexPaths"]; + return [CPSet setWithObjects:@"selectionIndexPaths"]; } + (CPSet)keyPathsForValuesAffectingCanInsertChild @@ -101,20 +94,16 @@ - (id)init { - self = [super init]; - - if (self) + if (self = [super init]) { _preservesSelection = YES; _selectsInsertedObjects = YES; _avoidsEmptySelection = YES; _alwaysUsesMultipleValuesMarker = NO; - _childrenKeyPath = @"children"; [self _init]; } - return self; } @@ -126,57 +115,22 @@ } - (void)prepareContent -{ - [self _setContentArray:[[self newObject]]]; +{[self _setContentArray:[CPArray arrayWithObject:[self newObject]]]; } -// --- Properties --- +- (BOOL)preservesSelection { return _preservesSelection; } +- (void)setPreservesSelection:(BOOL)value { _preservesSelection = value; } -- (BOOL)preservesSelection -{ - return _preservesSelection; -} +- (BOOL)selectsInsertedObjects { return _selectsInsertedObjects; } +- (void)setSelectsInsertedObjects:(BOOL)value { _selectsInsertedObjects = value; } -- (void)setPreservesSelection:(BOOL)value -{ - _preservesSelection = value; -} +- (BOOL)avoidsEmptySelection { return _avoidsEmptySelection; } +- (void)setAvoidsEmptySelection:(BOOL)value { _avoidsEmptySelection = 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; -} +- (BOOL)alwaysUsesMultipleValuesMarker { return _alwaysUsesMultipleValuesMarker; } +- (void)setAlwaysUsesMultipleValuesMarker:(BOOL)aFlag { _alwaysUsesMultipleValuesMarker = aFlag; } +- (CPArray)sortDescriptors { return _sortDescriptors; } - (void)setSortDescriptors:(CPArray)value { if (_sortDescriptors === value) @@ -186,71 +140,31 @@ [self _rearrangeObjects]; } -// --- Key Paths --- - -- (CPString)childrenKeyPath -{ - return _childrenKeyPath; -} - +- (CPString)childrenKeyPath { return _childrenKeyPath; } - (void)setChildrenKeyPath:(CPString)aKeyPath { - if (_childrenKeyPath === aKeyPath) - return; - - _childrenKeyPath = aKeyPath; - [self rearrangeObjects]; + if (_childrenKeyPath === aKeyPath) return; + _childrenKeyPath = aKeyPath;[self rearrangeObjects]; } -- (CPString)countKeyPath -{ - return _countKeyPath; -} +- (CPString)countKeyPath { return _countKeyPath; } +- (void)setCountKeyPath:(CPString)aKeyPath { _countKeyPath = aKeyPath; } -- (void)setCountKeyPath:(CPString)aKeyPath -{ - _countKeyPath = aKeyPath; -} +- (CPString)leafKeyPath { return _leafKeyPath; } +- (void)setLeafKeyPath:(CPString)aKeyPath { _leafKeyPath = aKeyPath; } -- (CPString)leafKeyPath -{ - return _leafKeyPath; -} - -- (void)setLeafKeyPath:(CPString)aKeyPath -{ - _leafKeyPath = aKeyPath; -} - -// --- Node Key Path Overrides --- - -- (CPString)childrenKeyPathForNode:(CPTreeNode)node -{ - return [self childrenKeyPath]; -} - -- (CPString)countKeyPathForNode:(CPTreeNode)node -{ - return [self countKeyPath]; -} - -- (CPString)leafKeyPathForNode:(CPTreeNode)node -{ - return [self leafKeyPath]; -} - -// --- Content and Arranged Objects --- +- (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 == nil) - value = []; + if (_disableSetContent) return; + if (!value) + value = [CPArray array]; if (![value isKindOfClass:[CPArray class]]) - value = [value]; + value = [CPArray arrayWithObject:value]; var oldSelectedObjects = nil, oldSelectionIndexPaths = nil; @@ -264,25 +178,13 @@ [self _rearrangeObjects]; - if ([self preservesSelection]) - [self __setSelectedObjects:oldSelectedObjects]; + if ([self preservesSelection])[self __setSelectedObjects:oldSelectedObjects]; else[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection]; } -- (void)_setContentArray:(id)anArray -{ - [self setContent:anArray]; -} - -- (id)contentArray -{ - return [self content]; -} - -- (id)arrangedObjects -{ - return _arrangedObjects; -} +- (void)_setContentArray:(id)anArray {[self setContent:anArray]; } +- (id)contentArray { return [self content]; } +- (id)arrangedObjects { return _arrangedObjects; } - (void)rearrangeObjects { @@ -301,18 +203,16 @@ else oldSelectionIndexPaths = [self selectionIndexPaths]; - // Rebuild the proxy tree from the content using _childrenKeyPath and _sortDescriptors[self __rebuildArrangedObjectsTree]; + [self __rebuildArrangedObjectsTree]; - if ([self preservesSelection]) - [self __setSelectedObjects:oldSelectedObjects]; - else - [self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection]; + if ([self preservesSelection])[self __setSelectedObjects:oldSelectedObjects]; + else[self __setSelectionIndexPaths:oldSelectionIndexPaths avoidEmpty:_avoidsEmptySelection]; } - (void)__rebuildArrangedObjectsTree { - var rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil]; - var contentArray = [self contentArray]; + var rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil], + contentArray = [self contentArray]; if (contentArray && [contentArray count] > 0) { @@ -323,22 +223,20 @@ _arrangedObjects = rootNode; } -// Recursively builds the node tree mapping model objects to CPTreeNodes - (CPArray)_buildTreeNodesForObjects:(CPArray)objects { var count = [objects count]; - if (count === 0) - return[]; + if (count === 0) return [CPArray array]; var sortedObjects = objects; - if ([_sortDescriptors count] > 0) + if (_sortDescriptors && [_sortDescriptors count] > 0) sortedObjects = [objects sortedArrayUsingDescriptors:_sortDescriptors]; var nodes = [CPMutableArray arrayWithCapacity:count]; for (var i = 0; i < count; i++) { - var obj = sortedObjects[i], + var obj = [sortedObjects objectAtIndex:i], node = [[CPTreeNode alloc] initWithRepresentedObject:obj]; if (_childrenKeyPath) @@ -349,16 +247,12 @@ var childNodes = [self _buildTreeNodesForObjects:childObjects]; [[node mutableChildNodes] addObjectsFromArray:childNodes]; } - } - - [nodes addObject:node]; + }[nodes addObject:node]; } return nodes; } -// --- Selection Management --- - - (CPIndexPath)selectionIndexPath { return [_selectionIndexPaths count] > 0 ? [_selectionIndexPaths objectAtIndex:0] : nil; @@ -366,20 +260,18 @@ - (BOOL)setSelectionIndexPath:(CPIndexPath)indexPath { - var paths = indexPath ? [indexPath] : []; - return [self setSelectionIndexPaths:paths]; + var paths = indexPath ? [CPArray arrayWithObject:indexPath] : [CPArray array]; + return[self setSelectionIndexPaths:paths]; } -- (CPArray)selectionIndexPaths -{ - return _selectionIndexPaths; -} +- (CPArray)selectionIndexPaths { return _selectionIndexPaths; } - (BOOL)setSelectionIndexPaths:(CPArray)indexPaths { [self _selectionWillChange]; var result = [self __setSelectionIndexPaths:indexPaths avoidEmpty:NO]; [self _selectionDidChange]; + return result; } @@ -388,12 +280,12 @@ var newPaths = indexPaths; if (!newPaths) - newPaths = []; + newPaths = [CPArray array]; if (![newPaths count] && avoidEmpty) { if ([[[self arrangedObjects] childNodes] count] > 0) - newPaths = [[CPIndexPath indexPathWithIndex:0]]; + newPaths = [CPArray arrayWithObject:[CPIndexPath indexPathWithIndex:0]]; } if ([_selectionIndexPaths isEqualToArray:newPaths]) @@ -402,7 +294,11 @@ _selectionIndexPaths = [newPaths copy]; var binderClass = [[self class] _binderClassForBinding:@"selectionIndexPaths"]; - [[binderClass getBinding:@"selectionIndexPaths" forObject:self] reverseSetValueFor:@"selectionIndexPaths"]; + if (binderClass) + { + var binding = [binderClass getBinding:@"selectionIndexPaths" forObject:self]; + if (binding)[binding reverseSetValueFor:@"selectionIndexPaths"]; + } return YES; } @@ -410,8 +306,9 @@ - (BOOL)addSelectionIndexPaths:(CPArray)indexPaths { var newPaths = [_selectionIndexPaths mutableCopy]; + [newPaths addObjectsFromArray:indexPaths]; - // Remove duplicates and maintain sorted order (omitted for brevity) + return [self setSelectionIndexPaths:newPaths]; } @@ -424,12 +321,12 @@ - (CPArray)selectedNodes { - var nodes = [], + var nodes = [CPMutableArray array], count = [_selectionIndexPaths count]; for (var i = 0; i < count; i++) { - var node = [[self arrangedObjects] descendantNodeAtIndexPath:_selectionIndexPaths[i]]; + var node = [[self arrangedObjects] descendantNodeAtIndexPath:[_selectionIndexPaths objectAtIndex:i]]; if (node) [nodes addObject:node]; } @@ -438,55 +335,70 @@ - (CPArray)selectedObjects { - var objects = [], + var objects = [CPMutableArray array], nodes = [self selectedNodes], count = [nodes count]; for (var i = 0; i < count; i++) - [objects addObject:[nodes[i] representedObject]]; + [objects addObject:[[nodes objectAtIndex:i] representedObject]]; return objects; } - (BOOL)__setSelectedObjects:(CPArray)objects { - // Search the tree for index paths matching the passed objects and update selection - // (Omitted recursive search for brevity) - return YES; + 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]; } -// --- Adding, Inserting, Removing --- - -- (BOOL)canInsert +- (CPIndexPath)_indexPathForObject:(id)anObject inNode:(CPTreeNode)node { - return[self isEditable]; + 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)canInsertChild -{ - return [self isEditable] && [_selectionIndexPaths count] > 0; -} - -- (BOOL)canAddChild -{ - return [self canInsertChild]; -} +- (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; + if (![self canInsert]) return; - var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject]; + var newObject = [self automaticallyPreparesContent] ? [self newObject] :[self _defaultNewObject], + selectionPath = [self selectionIndexPath]; - var selectionPath = [self selectionIndexPath] || [CPIndexPath indexPathWithIndex:[[[self arrangedObjects] childNodes] count]]; + if (!selectionPath) + selectionPath = [CPIndexPath indexPathWithIndex:[[[self arrangedObjects] childNodes] count]]; - // Increment the last index by 1 to add *after* the current selection var length = [selectionPath length], - lastIndex = [selectionPath indexAtPosition:length - 1]; + lastIndex = [selectionPath indexAtPosition:length - 1], + insertPath = [selectionPath indexPathByRemovingLastIndex]; - var insertPath = [selectionPath indexPathByRemovingLastIndex]; insertPath = [insertPath indexPathByAddingIndex:lastIndex + 1]; + [self insertObject:newObject atArrangedObjectIndexPath:insertPath]; } @@ -497,28 +409,25 @@ var newObject = [self automaticallyPreparesContent] ?[self newObject] : [self _defaultNewObject], parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:[self selectionIndexPath]], - childCount = [[parentNode childNodes] count]; - - var insertPath = [[self selectionIndexPath] indexPathByAddingIndex:childCount]; + childCount = [[parentNode childNodes] count], + insertPath = [[self selectionIndexPath] indexPathByAddingIndex:childCount]; [self insertObject:newObject atArrangedObjectIndexPath:insertPath]; } - (void)insert:(id)sender { - if (![self canInsert]) - return; + if (![self canInsert]) return; - var newObject = [self automaticallyPreparesContent] ?[self newObject] : [self _defaultNewObject]; - var indexPath = [self selectionIndexPath] ||[CPIndexPath indexPathWithIndex:0]; + var newObject = [self automaticallyPreparesContent] ? [self newObject] :[self _defaultNewObject], + indexPath = [self selectionIndexPath] || [CPIndexPath indexPathWithIndex:0]; [self insertObject:newObject atArrangedObjectIndexPath:indexPath]; } - (void)insertChild:(id)sender { - if (![self canInsertChild]) - return; + if (![self canInsertChild]) return; var newObject = [self automaticallyPreparesContent] ? [self newObject] : [self _defaultNewObject], insertPath = [[self selectionIndexPath] indexPathByAddingIndex:0]; @@ -527,7 +436,8 @@ } - (void)insertObject:(id)anObject atArrangedObjectIndexPath:(CPIndexPath)indexPath -{[self insertObjects:[anObject] atArrangedObjectIndexPaths:[indexPath]]; +{ + [self insertObjects:[CPArray arrayWithObject:anObject] atArrangedObjectIndexPaths:[CPArray arrayWithObject:indexPath]]; } - (void)insertObjects:(CPArray)objects atArrangedObjectIndexPaths:(CPArray)indexPaths @@ -537,35 +447,42 @@ var count = [objects count]; for (var i = 0; i < count; i++) { - var object = objects[i], - path = indexPaths[i], + var object = [objects objectAtIndex:i], + path = [indexPaths objectAtIndex:i], length = [path length]; if (length === 1) - { - // Insert at root level[_contentObject insertObject:object atIndex:[path indexAtPosition:0]]; + {[_contentObject insertObject:object atIndex:[path indexAtPosition:0]]; } else { - // Insert into a parent node's children var parentPath = [path indexPathByRemovingLastIndex], - parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath], - parentObj = [parentNode representedObject], - childIndex = [path indexAtPosition:length - 1]; + parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath]; - var children = [parentObj valueForKeyPath:_childrenKeyPath]; - if (!children) + if (parentNode) { - children = [CPMutableArray array]; - [parentObj setValue:children forKeyPath:_childrenKeyPath]; + 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]; } - [children insertObject:object atIndex:childIndex]; } } - [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange]; - _disableSetContent = NO; + var binding = [[self class] _binderClassForBinding:@"contentArray"]; + if (binding) + [[binding getBinding:@"contentArray" forObject:self] _contentArrayDidChange]; + _disableSetContent = NO; [self _rearrangeObjects]; if ([self selectsInsertedObjects])[self setSelectionIndexPaths:indexPaths]; @@ -574,12 +491,12 @@ } - (void)remove:(id)sender -{[self removeObjectsAtArrangedObjectIndexPaths:_selectionIndexPaths]; +{ + [self removeObjectsAtArrangedObjectIndexPaths:_selectionIndexPaths]; } - (void)removeObjectAtArrangedObjectIndexPath:(CPIndexPath)indexPath -{ - [self removeObjectsAtArrangedObjectIndexPaths:[indexPath]]; +{[self removeObjectsAtArrangedObjectIndexPaths:[CPArray arrayWithObject:indexPath]]; } - (void)removeObjectsAtArrangedObjectIndexPaths:(CPArray)indexPaths @@ -587,47 +504,50 @@ [self willChangeValueForKey:@"content"]; _disableSetContent = YES; - // Remove in reverse order to prevent shifting indices from invalidating remaining paths var sortedPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)], count = [sortedPaths count]; for (var i = count - 1; i >= 0; i--) { - var path = sortedPaths[i], + var path = [sortedPaths objectAtIndex:i], length = [path length]; if (length === 1) - { - [_contentObject removeObjectAtIndex:[path indexAtPosition:0]]; + {[_contentObject removeObjectAtIndex:[path indexAtPosition:0]]; } else { var parentPath = [path indexPathByRemovingLastIndex], - parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath], - parentObj = [parentNode representedObject], - childIndex = [path indexAtPosition:length - 1]; + parentNode = [[self arrangedObjects] descendantNodeAtIndexPath:parentPath]; - var children = [parentObj valueForKeyPath:_childrenKeyPath]; - if (children) - [children removeObjectAtIndex:childIndex]; + if (parentNode) + { + var parentObj = [parentNode representedObject], + childIndex = [path indexAtPosition:length - 1], + mutableChildren = [parentObj mutableArrayValueForKeyPath:_childrenKeyPath]; + + if (mutableChildren && childIndex <[mutableChildren count]) + [mutableChildren removeObjectAtIndex:childIndex]; + } } } - [[CPBinder getBinding:@"contentArray" forObject:self] _contentArrayDidChange]; - _disableSetContent = NO; + 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:[node] toIndexPath: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."]; +{[CPException raise:CPUnsupportedMethodException reason:@"moveNodes:toIndexPath: is not yet implemented in CPTreeController."]; } @end @@ -663,7 +583,8 @@ CPTreeControllerLeafKeyPath = @"CPTreeControllerLeafKeyPath if (![self content] && [self automaticallyPreparesContent]) [self prepareContent]; - else if (![self content])[self _setContentArray:[]]; + else if (![self content]) + [self _setContentArray:[CPArray array]]; } return self; diff --git a/Tests/AppKit/CPTreeControllerTest.j b/Tests/AppKit/CPTreeControllerTest.j index 4d2f3fd3f..0538de760 100644 --- a/Tests/AppKit/CPTreeControllerTest.j +++ b/Tests/AppKit/CPTreeControllerTest.j @@ -1,7 +1,7 @@ /* * CPTreeControllerTest.j * - * Test suite for CPTreeController, adapted for Cappuccino + * Test suite for CPTreeController */ @import @@ -23,20 +23,17 @@ - (CPArray)makeTestTree { - // Root Level var engineering = [OrgNode nodeWithName:@"Engineering"], - marketing =[OrgNode nodeWithName:@"Marketing"]; + marketing = [OrgNode nodeWithName:@"Marketing"]; - // Children of Engineering var webTeam = [OrgNode nodeWithName:@"Web Team"], - backendTeam = [OrgNode nodeWithName:@"Backend Team"]; - + backendTeam = [OrgNode nodeWithName:@"Backend Team"]; + [engineering setChildren:[CPMutableArray arrayWithObjects:webTeam, backendTeam]]; - // Children of Web Team - var dev1 =[OrgNode nodeWithName:@"Francisco"], - dev2 = [OrgNode nodeWithName:@"Ross"]; - + var dev1 = [OrgNode nodeWithName:@"Francisco"], + dev2 = [OrgNode nodeWithName:@"Ross"]; + [webTeam setChildren:[CPMutableArray arrayWithObjects:dev1, dev2]]; return [CPMutableArray arrayWithObjects:engineering, marketing]; @@ -44,23 +41,25 @@ - (void)setUp { - // Init global CPApp used internally in AppKit [[CPApplication alloc] init]; _contentArray = [self makeTestTree]; _treeController = [[CPTreeController alloc] init]; - [_treeController setChildrenKeyPath:@"children"];[_treeController setContent:[_contentArray copy]]; + [_treeController setChildrenKeyPath:@"children"]; + [_treeController setContent:[_contentArray copy]]; } - (void)testInitWithContent -{[self assert:[_contentArray count] equals:[[_treeController contentArray] count]]; +{ + [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:[] equals:[emptyController contentArray]];[self assert:0 equals:[[[emptyController arrangedObjects] childNodes] count]]; + [self assert:[CPArray array] equals:[emptyController contentArray]]; + [self assert:0 equals:[[[emptyController arrangedObjects] childNodes] count]]; } - (void)testSetContent @@ -75,54 +74,50 @@ - (void)testSelectionPaths { var controller = [self treeController]; - - // Select Engineering -> Web Team (Index Path: [0, 0]) - var path = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0];[controller setSelectionIndexPath:path]; - - var selectedPath = [controller selectionIndexPath];[self assert:path equals:selectedPath]; - + + 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[0] representedObject] name]]; + [self assert:@"Web Team" equals:[[[selectedNodes objectAtIndex:0] representedObject] name]]; } - (void)testAddChild { var controller = [self treeController]; [controller setObjectClass:[OrgNode class]]; - - // Select Engineering -> Backend Team (Index Path:[0, 1]) + var parentPath = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:1]; [controller setSelectionIndexPath:parentPath]; - - // Insert a child into Backend Team + var newDev = [OrgNode nodeWithName:@"Tom"]; - var insertPath =[parentPath indexPathByAddingIndex:0]; - + var insertPath = [parentPath indexPathByAddingIndex:0]; [controller insertObject:newDev atArrangedObjectIndexPath:insertPath]; - - // Validate it was added to the content + 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]]; + 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]; - - // Insert at root level, index 1 (between Engineering and Marketing) - var path =[CPIndexPath indexPathWithIndex:1]; - var hrDept = [OrgNode nodeWithName:@"Human Resources"];[controller insertObject:hrDept atArrangedObjectIndexPath:path]; - + + 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]]; - - // Insert nested (Engineering -> HR) + var nestedPath = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0]; - var subDept =[OrgNode nodeWithName:@"Sub Dept"]; - + 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"]; @@ -131,61 +126,56 @@ - (void)testRemoveObjectAtArrangedObjectIndexPath { var controller = [self treeController]; - - // Remove Engineering -> Web Team -> Francisco (Index Path: [0, 0, 0]) + 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"]; + 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]; - - // Remove both Engineering (0) and Marketing (1) - var paths = [[CPIndexPath indexPathWithIndex:0], - [CPIndexPath indexPathWithIndex:1] - ]; - + + 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)testAvoidsEmptySelection { - var controller = [self treeController];[controller setAvoidsEmptySelection:YES]; - - // Set empty selection manually - [controller setSelectionIndexPaths:[]]; - + var controller = [self treeController]; + [controller setAvoidsEmptySelection:YES]; + [controller setSelectionIndexPaths:[CPArray array]]; + [self assertTrue:([[controller selectionIndexPaths] count] == 1) message:@"Selection should fallback to the first item when avoidsEmptySelection is YES"]; [self assert:[CPIndexPath indexPathWithIndex:0] equals:[controller selectionIndexPath]]; - - [controller setAvoidsEmptySelection:NO];[controller setSelectionIndexPaths:[]]; - + [controller setAvoidsEmptySelection:NO]; + [controller setSelectionIndexPaths:[CPArray array]]; [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]; - // Use a custom key path [controller setChildrenKeyPath:@"subItems"]; - - var data =[OrgNode nodeWithName:@"Root"]; + + var data = [OrgNode nodeWithName:@"Root"]; [data setValue:[CPMutableArray arrayWithObject:[OrgNode nodeWithName:@"Sub"]] forKey:@"subItems"]; - + [controller setContent:[CPMutableArray arrayWithObject:data]]; - - // Insert at [0, 0] + var path = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0]; - var newItem = [OrgNode nodeWithName:@"New Sub"];[controller insertObject:newItem atArrangedObjectIndexPath:path]; - + 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"]; @@ -193,36 +183,36 @@ - (void)testContentBinding { - var controller = [[CPTreeController alloc] init];[controller bind:@"contentArray" toObject:self withKeyPath:@"contentArray" options:nil]; + var controller = [[CPTreeController alloc] init]; + + [controller bind:@"contentArray" toObject:self withKeyPath:@"contentArray" options:nil]; [self assert:[self contentArray] equals:[controller contentArray]]; - - // Verify proxy tree rebuilt [self assert:2 equals:[[[controller arrangedObjects] childNodes] count]]; } - (void)testSelectedObjects { - var controller =[self treeController]; - - // Select Marketing [1] - var path =[CPIndexPath indexPathWithIndex:1]; + 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[0] name]]; + + 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); // For testing custom key paths + CPMutableArray _subItems @accessors(property=subItems); } + (id)nodeWithName:(CPString)aName @@ -236,7 +226,7 @@ { _name = aName; _children = [CPMutableArray array]; - _subItems =[CPMutableArray array]; + _subItems = [CPMutableArray array]; } return self; @@ -244,7 +234,8 @@ - (CPString)description { - return [CPString stringWithFormat:@"", [self name]]; + return[CPString stringWithFormat:@"", [self name]]; } -@end \ No newline at end of file +@end + From cd4c3cffe087029ce8b29bf5c78a9772ba62a173 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 20:05:50 +0100 Subject: [PATCH 098/103] fixed: test cases --- Tests/AppKit/CPTreeControllerTest.j | 41 +++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/Tests/AppKit/CPTreeControllerTest.j b/Tests/AppKit/CPTreeControllerTest.j index 0538de760..bf94102c2 100644 --- a/Tests/AppKit/CPTreeControllerTest.j +++ b/Tests/AppKit/CPTreeControllerTest.j @@ -100,7 +100,6 @@ 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]]; } @@ -111,6 +110,7 @@ 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]]; @@ -142,23 +142,49 @@ { var controller = [self treeController]; - var paths = [CPArray arrayWithObjects:[CPIndexPath indexPathWithIndex:0],[CPIndexPath indexPathWithIndex:1]]; + 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)testAvoidsEmptySelection +- (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]; - [controller setSelectionIndexPaths:[CPArray array]]; + + 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]; - [controller setSelectionIndexPaths:[CPArray array]]; + + // 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"]; } @@ -168,8 +194,8 @@ [controller setChildrenKeyPath:@"subItems"]; var data = [OrgNode nodeWithName:@"Root"]; - [data setValue:[CPMutableArray arrayWithObject:[OrgNode nodeWithName:@"Sub"]] forKey:@"subItems"]; + [data setValue:[CPMutableArray arrayWithObject:[OrgNode nodeWithName:@"Sub"]] forKey:@"subItems"]; [controller setContent:[CPMutableArray arrayWithObject:data]]; var path = [[CPIndexPath indexPathWithIndex:0] indexPathByAddingIndex:0]; @@ -186,7 +212,6 @@ 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]]; } @@ -199,6 +224,7 @@ [controller setSelectionIndexPath:path]; var selectedObjects = [controller selectedObjects]; + [self assert:1 equals:[selectedObjects count]]; [self assert:@"Marketing" equals:[[selectedObjects objectAtIndex:0] name]]; } @@ -238,4 +264,3 @@ } @end - From b1f2fb44a82be68930c8f373f6d05686bedd2ae1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 21:43:41 +0100 Subject: [PATCH 099/103] new: CPOutlineView bindings support --- AppKit/CPOutlineView+CPBindings.j | 229 ++++++++++++++++++++++++++++ AppKit/CPOutlineView.j | 2 + Tests/AppKit/CPTreeControllerTest.j | 2 +- 3 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 AppKit/CPOutlineView+CPBindings.j diff --git a/AppKit/CPOutlineView+CPBindings.j b/AppKit/CPOutlineView+CPBindings.j new file mode 100644 index 000000000..658ff777e --- /dev/null +++ b/AppKit/CPOutlineView+CPBindings.j @@ -0,0 +1,229 @@ +/* + * CPOutlineView+CPBindings.j + * AppKit + * + * Adds Cocoa Bindings support to CPOutlineView. + */ + +@import +@import +@import +@import +@import "CPKeyValueBinding.j" +@import "CPTreeNode.j" + +@class CPOutlineView; + +@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) + ++ (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)bind +{ + [super bind]; + [_source setDataSource:self]; +} + +- (void)unbind +{ + if ([_source dataSource] === self) + [_source setDataSource:nil]; + + [super unbind]; +} + +- (void)updateSource +{ + var value = [self valueForBinding:CPObservedKeyPathKey]; + + if (!value || ![value isKindOfClass:[CPTreeNode class]]) + _rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil]; + else + _rootNode = value; + + [_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 +{ + // Normally column values are resolved via the table column's own bindings, + // but we return the represented object here as a standard fallback for cell-based tables. + 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 + +- (void)bind +{ + [super bind]; + + // Observe selection changes originating from the user clicking the outline view + [[CPNotificationCenter defaultCenter] + addObserver:self + selector:@selector(outlineViewSelectionDidChange:) + name:CPOutlineViewSelectionDidChangeNotification + object:_source]; +} + +- (void)unbind +{ + [[CPNotificationCenter defaultCenter] + removeObserver:self + name:CPOutlineViewSelectionDidChangeNotification + object:_source]; + + [super unbind]; +} + +- (void)updateSource +{ + var indexPaths = [self valueForBinding:CPObservedKeyPathKey] || [], + indexes = [CPMutableIndexSet indexSet], + contentBinder = [CPBinder getBinding:@"content" forObject:_source]; + + var rootNode = [contentBinder respondsToSelector:@selector(rootNode)] ? [contentBinder rootNode] : nil; + + if (rootNode) + { + for (var i = 0, count = [indexPaths count]; i < count; i++) + { + var item = [rootNode descendantNodeAtIndexPath:[indexPaths objectAtIndex:i]]; + if (item) + { + // Auto-expand all parents so the selection becomes visible + var parentsToExpand = [CPMutableArray array], + parent = [item parentNode]; + + while (parent && parent !== rootNode) + { + [parentsToExpand insertObject:parent atIndex:0]; // Top-down + 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]; + } + } + } + + // Suppress KVO while we programmatically adjust the CPOutlineView selection[self suppressSpecificNotificationFromObject:_source keyPath:@"selectionIndexPaths"]; + [_source selectRowIndexes:indexes byExtendingSelection:NO]; + [self unsuppressSpecificNotificationFromObject:_source keyPath:@"selectionIndexPaths"]; +} + +- (void)outlineViewSelectionDidChange:(CPNotification)note +{ + // We only want to push the change back if we aren't currently syncing down from the model + if ([self isSpecificNotificationSuppressedFromObject:_source keyPath:@"selectionIndexPaths"]) + return; + + var paths = [_source selectionIndexPaths]; + + // Reverse-set the value to push it up to the CPTreeController's selectionIndexPaths[self reverseSetValueFor:CPObservedKeyPathKey value:paths]; +} + +@end diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index 2f0cd73c0..ee76438b2 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -2344,3 +2344,5 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) ? [CPColor colorWithCalibratedWhite:0.4 alpha: 1.0] : [CPColor colorWithCalibratedWhite:0.5 alpha: 1.0]); }; + +@import "CPOutlineView+CPBindings.j" diff --git a/Tests/AppKit/CPTreeControllerTest.j b/Tests/AppKit/CPTreeControllerTest.j index bf94102c2..9487e4e46 100644 --- a/Tests/AppKit/CPTreeControllerTest.j +++ b/Tests/AppKit/CPTreeControllerTest.j @@ -260,7 +260,7 @@ - (CPString)description { - return[CPString stringWithFormat:@"", [self name]]; + return [CPString stringWithFormat:@"", [self name]]; } @end From 8ac2b944001bdd5acd4c2af8013c887fb59eefbf Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 21:48:10 +0100 Subject: [PATCH 100/103] refac --- AppKit/CPOutlineView+CPBindings.j | 229 ------------------------------ AppKit/CPOutlineView.j | 214 +++++++++++++++++++++++++++- 2 files changed, 213 insertions(+), 230 deletions(-) delete mode 100644 AppKit/CPOutlineView+CPBindings.j diff --git a/AppKit/CPOutlineView+CPBindings.j b/AppKit/CPOutlineView+CPBindings.j deleted file mode 100644 index 658ff777e..000000000 --- a/AppKit/CPOutlineView+CPBindings.j +++ /dev/null @@ -1,229 +0,0 @@ -/* - * CPOutlineView+CPBindings.j - * AppKit - * - * Adds Cocoa Bindings support to CPOutlineView. - */ - -@import -@import -@import -@import -@import "CPKeyValueBinding.j" -@import "CPTreeNode.j" - -@class CPOutlineView; - -@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) - -+ (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)bind -{ - [super bind]; - [_source setDataSource:self]; -} - -- (void)unbind -{ - if ([_source dataSource] === self) - [_source setDataSource:nil]; - - [super unbind]; -} - -- (void)updateSource -{ - var value = [self valueForBinding:CPObservedKeyPathKey]; - - if (!value || ![value isKindOfClass:[CPTreeNode class]]) - _rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil]; - else - _rootNode = value; - - [_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 -{ - // Normally column values are resolved via the table column's own bindings, - // but we return the represented object here as a standard fallback for cell-based tables. - 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 - -- (void)bind -{ - [super bind]; - - // Observe selection changes originating from the user clicking the outline view - [[CPNotificationCenter defaultCenter] - addObserver:self - selector:@selector(outlineViewSelectionDidChange:) - name:CPOutlineViewSelectionDidChangeNotification - object:_source]; -} - -- (void)unbind -{ - [[CPNotificationCenter defaultCenter] - removeObserver:self - name:CPOutlineViewSelectionDidChangeNotification - object:_source]; - - [super unbind]; -} - -- (void)updateSource -{ - var indexPaths = [self valueForBinding:CPObservedKeyPathKey] || [], - indexes = [CPMutableIndexSet indexSet], - contentBinder = [CPBinder getBinding:@"content" forObject:_source]; - - var rootNode = [contentBinder respondsToSelector:@selector(rootNode)] ? [contentBinder rootNode] : nil; - - if (rootNode) - { - for (var i = 0, count = [indexPaths count]; i < count; i++) - { - var item = [rootNode descendantNodeAtIndexPath:[indexPaths objectAtIndex:i]]; - if (item) - { - // Auto-expand all parents so the selection becomes visible - var parentsToExpand = [CPMutableArray array], - parent = [item parentNode]; - - while (parent && parent !== rootNode) - { - [parentsToExpand insertObject:parent atIndex:0]; // Top-down - 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]; - } - } - } - - // Suppress KVO while we programmatically adjust the CPOutlineView selection[self suppressSpecificNotificationFromObject:_source keyPath:@"selectionIndexPaths"]; - [_source selectRowIndexes:indexes byExtendingSelection:NO]; - [self unsuppressSpecificNotificationFromObject:_source keyPath:@"selectionIndexPaths"]; -} - -- (void)outlineViewSelectionDidChange:(CPNotification)note -{ - // We only want to push the change back if we aren't currently syncing down from the model - if ([self isSpecificNotificationSuppressedFromObject:_source keyPath:@"selectionIndexPaths"]) - return; - - var paths = [_source selectionIndexPaths]; - - // Reverse-set the value to push it up to the CPTreeController's selectionIndexPaths[self reverseSetValueFor:CPObservedKeyPathKey value:paths]; -} - -@end diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index ee76438b2..fba31f06e 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -2345,4 +2345,216 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) : [CPColor colorWithCalibratedWhite:0.5 alpha: 1.0]); }; -@import "CPOutlineView+CPBindings.j" +@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) + ++ (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)bind +{ + [super bind]; + [_source setDataSource:self]; +} + +- (void)unbind +{ + if ([_source dataSource] === self) + [_source setDataSource:nil]; + + [super unbind]; +} + +- (void)updateSource +{ + var value = [self valueForBinding:CPObservedKeyPathKey]; + + if (!value || ![value isKindOfClass:[CPTreeNode class]]) + _rootNode = [[CPTreeNode alloc] initWithRepresentedObject:nil]; + else + _rootNode = value; + + [_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 +{ + // Normally column values are resolved via the table column's own bindings, + // but we return the represented object here as a standard fallback for cell-based tables. + 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 + +- (void)bind +{ + [super bind]; + + // Observe selection changes originating from the user clicking the outline view + [[CPNotificationCenter defaultCenter] + addObserver:self + selector:@selector(outlineViewSelectionDidChange:) + name:CPOutlineViewSelectionDidChangeNotification + object:_source]; +} + +- (void)unbind +{ + [[CPNotificationCenter defaultCenter] + removeObserver:self + name:CPOutlineViewSelectionDidChangeNotification + object:_source]; + + [super unbind]; +} + +- (void)updateSource +{ + var indexPaths = [self valueForBinding:CPObservedKeyPathKey] || [], + indexes = [CPMutableIndexSet indexSet], + contentBinder = [CPBinder getBinding:@"content" forObject:_source]; + + var rootNode = [contentBinder respondsToSelector:@selector(rootNode)] ? [contentBinder rootNode] : nil; + + if (rootNode) + { + for (var i = 0, count = [indexPaths count]; i < count; i++) + { + var item = [rootNode descendantNodeAtIndexPath:[indexPaths objectAtIndex:i]]; + if (item) + { + // Auto-expand all parents so the selection becomes visible + var parentsToExpand = [CPMutableArray array], + parent = [item parentNode]; + + while (parent && parent !== rootNode) + { + [parentsToExpand insertObject:parent atIndex:0]; // Top-down + 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]; + } + } + } + + // Suppress KVO while we programmatically adjust the CPOutlineView selection[self suppressSpecificNotificationFromObject:_source keyPath:@"selectionIndexPaths"]; + [_source selectRowIndexes:indexes byExtendingSelection:NO]; + [self unsuppressSpecificNotificationFromObject:_source keyPath:@"selectionIndexPaths"]; +} + +- (void)outlineViewSelectionDidChange:(CPNotification)note +{ + // We only want to push the change back if we aren't currently syncing down from the model + if ([self isSpecificNotificationSuppressedFromObject:_source keyPath:@"selectionIndexPaths"]) + return; + + var paths = [_source selectionIndexPaths]; + + // Reverse-set the value to push it up to the CPTreeController's selectionIndexPaths[self reverseSetValueFor:CPObservedKeyPathKey value:paths]; +} + +@end From c70079ae71c3bf74e1df5c7e2e8bba02c0ee6e5f Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 8 Mar 2026 18:03:52 +0100 Subject: [PATCH 101/103] improved: bindings to CPOutlineView --- AppKit/AppKit.j | 1 + AppKit/CPOutlineView.j | 153 ++++++++++--- AppKit/CPTreeController.j | 31 ++- AppKit/CPTreeNode.j | 122 +++++++---- .../CPTreeControllerTest/AppController.j | 153 +++++++++++++ Tests/Manual/CPTreeControllerTest/Info.plist | 12 ++ Tests/Manual/CPTreeControllerTest/Jakefile | 93 ++++++++ .../CPTreeControllerTest/index-debug.html | 204 ++++++++++++++++++ Tests/Manual/CPTreeControllerTest/index.html | 166 ++++++++++++++ Tests/Manual/CPTreeControllerTest/main.j | 18 ++ 10 files changed, 873 insertions(+), 80 deletions(-) create mode 100644 Tests/Manual/CPTreeControllerTest/AppController.j create mode 100644 Tests/Manual/CPTreeControllerTest/Info.plist create mode 100644 Tests/Manual/CPTreeControllerTest/Jakefile create mode 100644 Tests/Manual/CPTreeControllerTest/index-debug.html create mode 100644 Tests/Manual/CPTreeControllerTest/index.html create mode 100644 Tests/Manual/CPTreeControllerTest/main.j diff --git a/AppKit/AppKit.j b/AppKit/AppKit.j index a893e0204..edf1dd794 100644 --- a/AppKit/AppKit.j +++ b/AppKit/AppKit.j @@ -116,3 +116,4 @@ @import "CPWindowController.j" @import "CPWorkspace.j" @import "CPFontPanel.j" +@import "CPTreeController.j" diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index fba31f06e..593da156f 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -773,6 +773,11 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, [self reloadItem:anItem reloadChildren:NO]; } +- (int)_numberOfRows +{ + return _itemsForRows ? _itemsForRows.length : 0; +} + /*! Reloads the data for a given item and optionally the children. @@ -784,6 +789,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, _pendingItemToClean = []; _itemAddedDuringLastLoading = []; + var previousRowCount = _itemsForRows.length; + if (!!shouldReloadChildren || !anItem) [self _loadItemInfoForItem:anItem intermediate:NO]; else @@ -791,6 +798,11 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, [self _cleanPendingItem]; + // Safely update the table size and force a synchronous layout recalculation + // BEFORE the views are reloaded, avoiding the clipping issue. + if (_itemsForRows.length !== previousRowCount) + [self noteNumberOfRowsChanged]; + [super _reloadDataViews]; } @@ -837,9 +849,20 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, for (var i = [previousItems count] - 1; i >= 0; i--) { - var item = previousItems[i]; + var item = previousItems[i], + found = NO; - if (![children containsObject:item]) + // Use strict identity (===) instead of containsObject: (which triggers isEqual:) + for (var j = 0, count = children.length; j < count; j++) + { + if (children[j] === item) + { + found = YES; + break; + } + } + + if (!found) [self _addPendingItem:item]; } } @@ -853,7 +876,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, var children = itemInfo.children; - for (var i = [children count]; i >= 0; i--) + // Fixed out-of-bounds index (was previously [children count]) + for (var i = children.length - 1; i >= 0; i--) { var child = children[i]; [self _addPendingItem:child]; @@ -864,7 +888,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, - (void)_cleanPendingItem { - for (var i = [_pendingItemToClean count]; i >= 0; i--) + for (var i = [_pendingItemToClean count] - 1; i >= 0; i--) { var item = _pendingItemToClean[i]; @@ -908,7 +932,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, var weight = itemInfo.weight, descendants = anItem ? [anItem] : []; - [_itemAddedDuringLastLoading addObject:anItem]; + if (anItem) + [_itemAddedDuringLastLoading addObject:anItem]; if (itemInfo.isExpanded && [self _sendDataSourceShouldDeferDisplayingChildrenOfItem:anItem]) { @@ -1085,7 +1110,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, var parent = itemInfo.parent; // Check if the parent is the root item because we never return the actual root item - if (itemInfo[[parent UID]] === _rootItemInfo) + if (parent && itemInfo[[parent UID]] === _rootItemInfo) parent = nil; return parent; @@ -2179,7 +2204,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_menuForTableColumn_item_)) { var item = [_outlineView itemAtRow:aRow]; - return [_outlineView._outlineViewDelegate outlineView:_outlineView menuForTableColumn:aTableColumn item:item] + return [_outlineView._outlineViewDelegate outlineView:_outlineView menuForTableColumn:aTableColumn item:item]; } // We reimplement CPView menuForEvent: because we can't call it directly. CPTableView implements menuForEvent: @@ -2386,6 +2411,10 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) @implementation CPOutlineView (CPBinder) +- (id)content { return nil; } +- (void)setContent:(id)aContent { } +- (void)setSelectionIndexPaths:(CPArray)paths { } + + (Class)_binderClassForBinding:(CPString)aBinding { if (aBinding === @"content") @@ -2413,8 +2442,12 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) - (void)bind { - [super bind]; + // 1. Set the data source FIRST so that it is ready when + //[super bind] triggers the initial synchronous setValueFor: [_source setDataSource:self]; + + // 2. Establish KVO (which immediately triggers setValueFor:) + [super bind]; } - (void)unbind @@ -2425,16 +2458,30 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) [super unbind]; } -- (void)updateSource +- (void)setValueFor:(CPString)aBinding { - var value = [self valueForBinding:CPObservedKeyPathKey]; - + 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; - [_source reloadData]; + // 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 @@ -2464,8 +2511,6 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) - (id)outlineView:(CPOutlineView)outlineView objectValueForTableColumn:(CPTableColumn)tableColumn byItem:(id)item { - // Normally column values are resolved via the table column's own bindings, - // but we return the represented object here as a standard fallback for cell-based tables. if ([item respondsToSelector:@selector(representedObject)]) return [item representedObject]; @@ -2474,7 +2519,6 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) @end - // --- Selection Index Paths Binder --- /*! @@ -2483,12 +2527,14 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) It also intercepts changes from the CPTreeController and auto-expands the tree to highlight them. */ @implementation _CPOutlineViewSelectionIndexPathsBinder : CPBinder +{ + BOOL _isSyncingFromModel; +} - (void)bind { [super bind]; - // Observe selection changes originating from the user clicking the outline view [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(outlineViewSelectionDidChange:) @@ -2506,28 +2552,33 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) [super unbind]; } -- (void)updateSource +- (void)setValueFor:(CPString)aBinding { - var indexPaths = [self valueForBinding:CPObservedKeyPathKey] || [], - indexes = [CPMutableIndexSet indexSet], - contentBinder = [CPBinder getBinding:@"content" forObject:_source]; - - var rootNode = [contentBinder respondsToSelector:@selector(rootNode)] ? [contentBinder rootNode] : nil; - + // 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) { - // Auto-expand all parents so the selection becomes visible var parentsToExpand = [CPMutableArray array], parent = [item parentNode]; while (parent && parent !== rootNode) { - [parentsToExpand insertObject:parent atIndex:0]; // Top-down + [parentsToExpand insertObject:parent atIndex:0]; parent = [parent parentNode]; } @@ -2535,26 +2586,64 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) [_source expandItem:parentsToExpand[j]]; var row = [_source rowForItem:item]; + if (row !== CPNotFound && row >= 0) [indexes addIndex:row]; } } } - // Suppress KVO while we programmatically adjust the CPOutlineView selection[self suppressSpecificNotificationFromObject:_source keyPath:@"selectionIndexPaths"]; + // Adjust the CPOutlineView selection [_source selectRowIndexes:indexes byExtendingSelection:NO]; - [self unsuppressSpecificNotificationFromObject:_source keyPath:@"selectionIndexPaths"]; + + // 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 ([self isSpecificNotificationSuppressedFromObject:_source keyPath:@"selectionIndexPaths"]) + if (_isSyncingFromModel) return; - - var paths = [_source selectionIndexPaths]; - - // Reverse-set the value to push it up to the CPTreeController's selectionIndexPaths[self reverseSetValueFor:CPObservedKeyPathKey value:paths]; + + // In CPBinder, reverseSetValueFor: takes the name of the property on _source + // it should fetch the updated value from. Since CPOutlineView has the selectionIndexPaths method: + [self reverseSetValueFor:@"selectionIndexPaths"]; +} + +@end + +@implementation _CPOutlineViewContentBinder (DynamicColumns) + +- (id)outlineView:(CPOutlineView)outlineView objectValueForTableColumn:(CPTableColumn)tableColumn byItem:(id)item +{ + var rep = [item respondsToSelector:@selector(representedObject)] ? [item representedObject] : item; + + // Dynamically fetch the value using the column's identifier (e.g., "name") + if (rep && [tableColumn identifier]) + return [rep valueForKey:[tableColumn identifier]]; + + return rep; +} + +// Add this to support inline bidirectional editing in the outline view +- (void)outlineView:(CPOutlineView)outlineView setObjectValue:(id)value forTableColumn:(CPTableColumn)tableColumn byItem:(id)item +{ + var rep = [item respondsToSelector:@selector(representedObject)] ?[item representedObject] : item; + + // Push the inline edit back to the model using the column's identifier + if (rep && [tableColumn identifier]) + [rep setValue:value forKey:[tableColumn identifier]]; +} + +- (id)content +{ + // CPTableView internals probe the binder for its flat content to draw rows. + // For an outline view, the flat content is exactly the internally mapped items for rows. + if (_source && _source._itemsForRows) + return _source._itemsForRows; + + return []; } @end diff --git a/AppKit/CPTreeController.j b/AppKit/CPTreeController.j index aefaa0660..8bee3ef95 100644 --- a/AppKit/CPTreeController.j +++ b/AppKit/CPTreeController.j @@ -183,7 +183,7 @@ } - (void)_setContentArray:(id)anArray {[self setContent:anArray]; } -- (id)contentArray { return [self content]; } +- (id)contentArray { return _contentObject; } - (id)arrangedObjects { return _arrangedObjects; } - (void)rearrangeObjects @@ -226,9 +226,12 @@ - (CPArray)_buildTreeNodesForObjects:(CPArray)objects { var count = [objects count]; - if (count === 0) return [CPArray array]; + + if (count === 0) + return []; var sortedObjects = objects; + if (_sortDescriptors && [_sortDescriptors count] > 0) sortedObjects = [objects sortedArrayUsingDescriptors:_sortDescriptors]; @@ -242,12 +245,15 @@ if (_childrenKeyPath) { var childObjects = [obj valueForKeyPath:_childrenKeyPath]; + if (childObjects && [childObjects count] > 0) { var childNodes = [self _buildTreeNodesForObjects:childObjects]; [[node mutableChildNodes] addObjectsFromArray:childNodes]; } - }[nodes addObject:node]; + } + + [nodes addObject:node]; } return nodes; @@ -291,15 +297,23 @@ 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"]; + + if (binding) + [binding reverseSetValueFor:@"selectionIndexPaths"]; } + [self didChangeValueForKey:@"selectionIndexPaths"]; + return YES; } @@ -441,7 +455,8 @@ } - (void)insertObjects:(CPArray)objects atArrangedObjectIndexPaths:(CPArray)indexPaths -{[self willChangeValueForKey:@"content"]; +{ + [self willChangeValueForKey:@"content"]; _disableSetContent = YES; var count = [objects count]; @@ -496,7 +511,8 @@ } - (void)removeObjectAtArrangedObjectIndexPath:(CPIndexPath)indexPath -{[self removeObjectsAtArrangedObjectIndexPaths:[CPArray arrayWithObject:indexPath]]; +{ + [self removeObjectsAtArrangedObjectIndexPaths:[CPArray arrayWithObject:indexPath]]; } - (void)removeObjectsAtArrangedObjectIndexPaths:(CPArray)indexPaths @@ -513,7 +529,8 @@ length = [path length]; if (length === 1) - {[_contentObject removeObjectAtIndex:[path indexAtPosition:0]]; + { + [_contentObject removeObjectAtIndex:[path indexAtPosition:0]]; } else { diff --git a/AppKit/CPTreeNode.j b/AppKit/CPTreeNode.j index 0f5f88a33..a095ca13c 100644 --- a/AppKit/CPTreeNode.j +++ b/AppKit/CPTreeNode.j @@ -22,13 +22,12 @@ @import @import - +@import @implementation CPTreeNode : CPObject { - id _representedObject @accessors(readonly, property=representedObject); - - CPTreeNode _parentNode @accessors(readonly, property=parentNode); + id _representedObject @accessors(property=representedObject); + CPTreeNode _parentNode @accessors(property=parentNode); CPMutableArray _childNodes; } @@ -52,36 +51,34 @@ - (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; + var index = [_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 +87,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 +118,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 +154,36 @@ 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]]; - - return node; + var index = [indexPath indexAtPosition:0], + count = [_childNodes count]; + + if (index >= count) + return nil; + + var child = [_childNodes objectAtIndex:index]; + + if ([indexPath length] == 1) + return child; + + return [child descendantNodeAtIndexPath:[indexPath indexPathByRemovingFirstIndex]]; } @end +// Coding implementation remains correct var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey", CPTreeNodeParentNodeKey = @"CPTreeNodeParentNodeKey", CPTreeNodeChildNodesKey = @"CPTreeNodeChildNodesKey"; @@ -163,6 +199,10 @@ var CPTreeNodeRepresentedObjectKey = @"CPTreeNodeRepresentedObjectKey", _representedObject = [aCoder decodeObjectForKey:CPTreeNodeRepresentedObjectKey]; _parentNode = [aCoder decodeObjectForKey:CPTreeNodeParentNodeKey]; _childNodes = [aCoder decodeObjectForKey:CPTreeNodeChildNodesKey]; + + // Safety check to ensure decoding gave us a CPArray + if (!_childNodes) + _childNodes = [[CPMutableArray alloc] init]; } return self; diff --git a/Tests/Manual/CPTreeControllerTest/AppController.j b/Tests/Manual/CPTreeControllerTest/AppController.j new file mode 100644 index 000000000..15e24c0e4 --- /dev/null +++ b/Tests/Manual/CPTreeControllerTest/AppController.j @@ -0,0 +1,153 @@ +/* + * AppController.j + * TreeControllerBindingsTest + * + * Created for testing CPOutlineView and CPTreeController bindings. + */ + +@import +@import + +@implementation AppController : CPObject +{ + CPTreeController treeController; + CPTextField logField; +} + +- (void)applicationDidFinishLaunching:(CPNotification)aNotification +{ + var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask], + contentView = [theWindow contentView]; + + // 1. Create the Data Model + var root1 = [[Node alloc] initWithName:@"Root 1" children:[]], + child1 = [[Node alloc] initWithName:@"Child 1.1" children:[]], + child2 = [[Node alloc] initWithName:@"Child 1.2" children:[]], + root2 = [[Node alloc] initWithName:@"Root 2" children:[]], + child3 = [[Node alloc] initWithName:@"Child 2.1" children:[]]; + + [root1 setChildren:[child1, child2]]; + [root2 setChildren:[child3]]; + var contentArray = [root1, root2]; + + // 2. Setup the Tree Controller + treeController = [[CPTreeController alloc] init]; + [treeController setChildrenKeyPath:@"children"]; + [treeController setContent:contentArray]; + + // 3. Setup the Outline View + var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(20, 20, 250, 300)]; + [scrollView setAutohidesScrollers:YES]; + + var outlineView = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 250, 300)]; + var column = [[CPTableColumn alloc] initWithIdentifier:@"name"]; + [[column headerView] setStringValue:@"Node Name"]; + [column setWidth:240]; + [column setEditable:YES]; // Editable to test bidirectional bindings in the tree + + [outlineView addTableColumn:column]; + [outlineView setOutlineTableColumn:column]; + [outlineView setAllowsMultipleSelection:YES]; + [scrollView setDocumentView:outlineView]; + [contentView addSubview:scrollView]; + + // 4. Establish Bindings for the Outline View + [outlineView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil]; + [outlineView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil]; + + var scrollView2 = [[CPScrollView alloc] initWithFrame:CGRectMake(300, 20, 250, 300)]; + [scrollView2 setAutohidesScrollers:YES]; + var outlineView2 = [[CPOutlineView alloc] initWithFrame:CGRectMake(0, 0, 250, 300)]; + var column2 = [[CPTableColumn alloc] initWithIdentifier:@"name"]; + [[column2 headerView] setStringValue:@"Node Name"]; + [column2 setWidth:240]; + [column2 setEditable:YES]; // Editable to test bidirectional bindings in the tree + + [outlineView2 addTableColumn:column2]; + [outlineView2 setOutlineTableColumn:column2]; + [outlineView2 setAllowsMultipleSelection:YES]; + [scrollView2 setDocumentView:outlineView2]; + [contentView addSubview:scrollView2]; + + // 4. Establish Bindings for the Outline View + [outlineView2 bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil]; + [outlineView2 bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil]; + + + + [theWindow orderFront:self]; +} + +- (void)selectSpecificNode:(id)sender +{ + // Programmatically select index path [0, 1] which is "Child 1.2" + // This tests the `_CPOutlineViewSelectionIndexPathsBinder` auto-expand logic. + var path =[CPIndexPath indexPathWithIndexes:[0, 1]]; + [treeController setSelectionIndexPath:path]; +} + +- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context +{ + if (keyPath === @"selectionIndexPaths") + { + var selectedObjects = [treeController selectedObjects]; + if ([selectedObjects count] > 0) + { + var names = [CPMutableArray array]; + for (var i = 0; i <[selectedObjects count]; i++) + [names addObject:[selectedObjects[i] name]];[logField setStringValue:[names componentsJoinedByString:@", "]]; + } + else + { + [logField setStringValue:@"Nothing selected"]; + } + } +} + +@end + + +// --- Custom Data Model --- + +@implementation Node : CPObject +{ + CPString name; + CPArray children; +} + +- (id)initWithName:(CPString)aName children:(CPArray)someChildren +{ + self = [super init]; + if (self) + { + name = aName; + children = someChildren; + } + return self; +} + +// Explicit accessors to ensure Key-Value Observing (KVO) works flawlessly. +- (void)setName:(CPString)aName +{ + [self willChangeValueForKey:@"name"]; + name = aName;[self didChangeValueForKey:@"name"]; +} + +- (CPString)name +{ + return name; +} + +- (void)setChildren:(CPArray)someChildren +{ + [self willChangeValueForKey:@"children"]; + children = someChildren; + [self didChangeValueForKey:@"children"]; +} + +- (CPArray)children +{ + return children; +} + +@end diff --git a/Tests/Manual/CPTreeControllerTest/Info.plist b/Tests/Manual/CPTreeControllerTest/Info.plist new file mode 100644 index 000000000..3af283991 --- /dev/null +++ b/Tests/Manual/CPTreeControllerTest/Info.plist @@ -0,0 +1,12 @@ + + + + + CPApplicationDelegateClass + AppController + CPBundleName + treecontroller + CPPrincipalClass + CPApplication + + diff --git a/Tests/Manual/CPTreeControllerTest/Jakefile b/Tests/Manual/CPTreeControllerTest/Jakefile new file mode 100644 index 000000000..8f62ca5cd --- /dev/null +++ b/Tests/Manual/CPTreeControllerTest/Jakefile @@ -0,0 +1,93 @@ +/* + * Jakefile + * tooltips + * + * Created by You on April 26, 2011. + * Copyright 2011, Your Company All rights reserved. + */ + +var ENV = require("system").env, + FILE = require("file"), + JAKE = require("jake"), + task = JAKE.task, + FileList = JAKE.FileList, + app = require("cappuccino/jake").app, + configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug", + OS = require("os"); + +app ("tooltips", function(task) +{ + task.setBuildIntermediatesPath(FILE.join("Build", "tooltips.build", configuration)); + task.setBuildPath(FILE.join("Build", configuration)); + + task.setProductName("tooltips"); + task.setIdentifier("com.yourcompany.tooltips"); + task.setVersion("1.0"); + task.setAuthor("Your Company"); + task.setEmail("feedback @nospam@ yourcompany.com"); + task.setSummary("tooltips"); + task.setSources((new FileList("**/*.j")).exclude(FILE.join("Build", "**"))); + task.setResources(new FileList("Resources/**")); + task.setIndexFilePath("index.html"); + task.setInfoPlistPath("Info.plist"); + + if (configuration === "Debug") + task.setCompilerFlags("-DDEBUG -g"); + else + task.setCompilerFlags("-O"); +}); + +task ("default", ["tooltips"], function() +{ + printResults(configuration); +}); + +task ("build", ["default"]); + +task ("debug", function() +{ + ENV["CONFIGURATION"] = "Debug"; + JAKE.subjake(["."], "build", ENV); +}); + +task ("release", function() +{ + ENV["CONFIGURATION"] = "Release"; + JAKE.subjake(["."], "build", ENV); +}); + +task ("run", ["debug"], function() +{ + OS.system(["open", FILE.join("Build", "Debug", "tooltips", "index.html")]); +}); + +task ("run-release", ["release"], function() +{ + OS.system(["open", FILE.join("Build", "Release", "tooltips", "index.html")]); +}); + +task ("deploy", ["release"], function() +{ + FILE.mkdirs(FILE.join("Build", "Deployment", "tooltips")); + OS.system(["press", "-f", FILE.join("Build", "Release", "tooltips"), FILE.join("Build", "Deployment", "tooltips")]); + printResults("Deployment") +}); + +task ("desktop", ["release"], function() +{ + FILE.mkdirs(FILE.join("Build", "Desktop", "tooltips")); + require("cappuccino/nativehost").buildNativeHost(FILE.join("Build", "Release", "tooltips"), FILE.join("Build", "Desktop", "tooltips", "tooltips.app")); + printResults("Desktop") +}); + +task ("run-desktop", ["desktop"], function() +{ + OS.system([FILE.join("Build", "Desktop", "tooltips", "tooltips.app", "Contents", "MacOS", "NativeHost"), "-i"]); +}); + +function printResults(configuration) +{ + print("----------------------------"); + print(configuration+" app built at path: "+FILE.join("Build", configuration, "tooltips")); + print("----------------------------"); +} diff --git a/Tests/Manual/CPTreeControllerTest/index-debug.html b/Tests/Manual/CPTreeControllerTest/index-debug.html new file mode 100644 index 000000000..a36b1d3b9 --- /dev/null +++ b/Tests/Manual/CPTreeControllerTest/index-debug.html @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + __project.name__ + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/CPTreeControllerTest/index.html b/Tests/Manual/CPTreeControllerTest/index.html new file mode 100644 index 000000000..ac42c98a7 --- /dev/null +++ b/Tests/Manual/CPTreeControllerTest/index.html @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + __project.name__ + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/CPTreeControllerTest/main.j b/Tests/Manual/CPTreeControllerTest/main.j new file mode 100644 index 000000000..9e6a15286 --- /dev/null +++ b/Tests/Manual/CPTreeControllerTest/main.j @@ -0,0 +1,18 @@ +/* + * AppController.j + * tooltips + * + * Created by You on April 26, 2011. + * Copyright 2011, Your Company All rights reserved. + */ + +@import +@import + +@import "AppController.j" + + +function main(args, namedArgs) +{ + CPApplicationMain(args, namedArgs); +} From 031e5e7ca3fcbdbf4c48c23dc60d1db1d91c3689 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 8 Mar 2026 19:52:58 +0100 Subject: [PATCH 102/103] fixed: selection issues --- AppKit/CPOutlineView.j | 29 +++++++++++++++-------------- AppKit/CPTreeController.j | 1 - AppKit/CPTreeNode.j | 32 ++++++++++++++++++-------------- Tests/AppKit/CPTreeNodeTest.j | 2 +- 4 files changed, 34 insertions(+), 30 deletions(-) diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index 593da156f..3e8cb34d8 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -1110,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 (parent && itemInfo[[parent UID]] === _rootItemInfo) + if (parent && _itemInfosForItems[[parent UID]] === _rootItemInfo) parent = nil; return parent; @@ -2531,25 +2531,26 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) BOOL _isSyncingFromModel; } -- (void)bind +- (id)initWithBinding:(CPString)aBinding name:(CPString)aName to:(id)aDestination keyPath:(CPString)aKeyPath options:(CPDictionary)options from:(id)aSource { - [super bind]; - - [[CPNotificationCenter defaultCenter] + self = [super initWithBinding:aBinding name:aName to:aDestination keyPath:aKeyPath options:options from:aSource]; + + [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(outlineViewSelectionDidChange:) name:CPOutlineViewSelectionDidChangeNotification - object:_source]; + object:aSource]; } -- (void)unbind ++ (void)unbind:(CPString)aBinding forObject:(id)anObject { - [[CPNotificationCenter defaultCenter] - removeObserver:self - name:CPOutlineViewSelectionDidChangeNotification - object:_source]; - - [super unbind]; + if (aBinding === "selectionIndexPaths") + [[CPNotificationCenter defaultCenter] + removeObserver:self + name:CPOutlineViewSelectionDidChangeNotification + object:anObject]; + + [super unbind:aBinding forObject:anObject]; } - (void)setValueFor:(CPString)aBinding @@ -2606,7 +2607,7 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) if (_isSyncingFromModel) return; - // In CPBinder, reverseSetValueFor: takes the name of the property on _source + // 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"]; } diff --git a/AppKit/CPTreeController.j b/AppKit/CPTreeController.j index 8bee3ef95..2d0e46802 100644 --- a/AppKit/CPTreeController.j +++ b/AppKit/CPTreeController.j @@ -300,7 +300,6 @@ [self willChangeValueForKey:@"selectionIndexPaths"]; _selectionIndexPaths = [newPaths copy]; - var binderClass = [[self class] _binderClassForBinding:@"selectionIndexPaths"]; diff --git a/AppKit/CPTreeNode.j b/AppKit/CPTreeNode.j index a095ca13c..f25c83649 100644 --- a/AppKit/CPTreeNode.j +++ b/AppKit/CPTreeNode.j @@ -54,7 +54,8 @@ // If we have a parent, calculate path based on parent's path + our index if (_parentNode) { - var index = [_childNodes indexOfObjectIdenticalTo:self]; + // 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. @@ -68,7 +69,7 @@ // 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; + return nil; } - (BOOL)isLeaf @@ -167,18 +168,21 @@ if (!indexPath || [indexPath length] == 0) return self; - var index = [indexPath indexAtPosition:0], - count = [_childNodes count]; - - if (index >= count) - return nil; - - var child = [_childNodes objectAtIndex:index]; - - if ([indexPath length] == 1) - return child; - - return [child descendantNodeAtIndexPath:[indexPath indexPathByRemovingFirstIndex]]; + 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 diff --git a/Tests/AppKit/CPTreeNodeTest.j b/Tests/AppKit/CPTreeNodeTest.j index 4e40b5f98..a9c8c9c19 100644 --- a/Tests/AppKit/CPTreeNodeTest.j +++ b/Tests/AppKit/CPTreeNodeTest.j @@ -25,7 +25,7 @@ indexPath = [CPIndexPath indexPathWithIndex:1]; - [self assert:undefined equals:[treeNode descendantNodeAtIndexPath:indexPath]]; + [self assert:nil equals:[treeNode descendantNodeAtIndexPath:indexPath]]; } @end From 9f4db09bec1928ee84b78e4475e58dcb332b7e08 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 8 Mar 2026 21:07:08 +0100 Subject: [PATCH 103/103] removed: dead code --- AppKit/CPOutlineView.j | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index 3e8cb34d8..0c334fc1d 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -2440,24 +2440,6 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) CPTreeNode _rootNode; } -- (void)bind -{ - // 1. Set the data source FIRST so that it is ready when - //[super bind] triggers the initial synchronous setValueFor: - [_source setDataSource:self]; - - // 2. Establish KVO (which immediately triggers setValueFor:) - [super bind]; -} - -- (void)unbind -{ - if ([_source dataSource] === self) - [_source setDataSource:nil]; - - [super unbind]; -} - - (void)setValueFor:(CPString)aBinding { var destination = [_info objectForKey:CPObservedObjectKey],