From 6ce85bae7e260a8ac9207070bebc2b7f715ba650 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 23 Jun 2025 07:21:08 +0200 Subject: [PATCH 001/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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 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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 0eb87f85b4523ccd1057bfd6d6fcd95104316151 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 5 Jul 2025 09:01:12 +0200 Subject: [PATCH 018/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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 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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 134688ef5c5cab102206efc3bba3c0d3c2ce86e8 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 18 Jul 2025 21:48:09 +0200 Subject: [PATCH 030/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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/136] 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 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 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/136] 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/136] 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/136] 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/136] 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/136] 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 547be633b67f40081818df63bcacd990dc9605b6 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 7 Dec 2025 09:18:36 +0100 Subject: [PATCH 045/136] fixed: CPOutlineView should be its own tableView delegate and datasource --- AppKit/CPOutlineView.j | 150 +++++++++++++++++------------------------ 1 file changed, 60 insertions(+), 90 deletions(-) diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index 2f0cd73c0..fde775128 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -252,7 +252,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, Theme states for custom data views are documented in CPTableView. */ -@implementation CPOutlineView : CPTableView +@implementation CPOutlineView : CPTableView { id _outlineViewDataSource; id _outlineViewDelegate; @@ -311,8 +311,8 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, [self setIndentationPerLevel:16.0]; [self setIndentationMarkerFollowsDataView:YES]; - [super setDataSource:[[_CPOutlineViewTableViewDataSource alloc] initWithOutlineView:self]]; - [super setDelegate:[[_CPOutlineViewTableViewDelegate alloc] initWithOutlineView:self]]; + [super setDataSource:self]; + [super setDelegate:self]; [self setDisclosureControlPrototype:[[CPDisclosureButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 10.0, 10.0)]]; } @@ -1975,43 +1975,28 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, return _implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldSelectItem_; } -@end - -@implementation _CPOutlineViewTableViewDataSource : CPObject -{ - CPObject _outlineView; -} - -- (id)initWithOutlineView:(CPOutlineView)anOutlineView -{ - self = [super init]; - - if (self) - _outlineView = anOutlineView; - - return self; -} +/*** CPTableViewDataSource methods ***/ - (CPInteger)numberOfRowsInTableView:(CPTableView)anOutlineView { - return _outlineView._itemsForRows.length; + return _itemsForRows.length; } - (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow { - return [_outlineView._outlineViewDataSource outlineView:_outlineView objectValueForTableColumn:aTableColumn byItem:_outlineView._itemsForRows[aRow]]; + return [_outlineViewDataSource outlineView:self objectValueForTableColumn:aTableColumn byItem:_itemsForRows[aRow]]; } - (void)tableView:(CPTableView)aTableView setObjectValue:(id)aValue forTableColumn:(CPTableColumn)aColumn row:(CPInteger)aRow { - if (!(_outlineView._implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_setObjectValue_forTableColumn_byItem_)) + if (!(_implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_setObjectValue_forTableColumn_byItem_)) return; - [_outlineView._outlineViewDataSource outlineView:_outlineView setObjectValue:aValue forTableColumn:aColumn byItem:_outlineView._itemsForRows[aRow]]; + [_outlineViewDataSource outlineView:self setObjectValue:aValue forTableColumn:aColumn byItem:_itemsForRows[aRow]]; } - (BOOL)tableView:(CPTableView)aTableColumn writeRowsWithIndexes:(CPIndexSet)theIndexes toPasteboard:(CPPasteboard)thePasteboard { - if (!(_outlineView._implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_writeItems_toPasteboard_)) + if (!(_implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_writeItems_toPasteboard_)) return NO; var items = [], @@ -2019,27 +2004,27 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, while (index !== CPNotFound) { - [items addObject:[_outlineView itemAtRow:index]]; + [items addObject:[self itemAtRow:index]]; index = [theIndexes indexGreaterThanIndex:index]; } - return [_outlineView._outlineViewDataSource outlineView:_outlineView writeItems:items toPasteboard:thePasteboard]; + return [_outlineViewDataSource outlineView:self writeItems:items toPasteboard:thePasteboard]; } - (int)_childIndexForDropOperation:(CPTableViewDropOperation)theDropOperation row:(CPInteger)theRow offset:(CGPoint)theOffset { - if (_outlineView._shouldRetargetChildIndex) - return _outlineView._retargedChildIndex; + if (_shouldRetargetChildIndex) + return _retargedChildIndex; var childIndex = CPNotFound; if (theDropOperation === CPTableViewDropAbove) { - var parentItem = [_outlineView _parentItemForUpperRow:theRow - 1 andLowerRow:theRow atMouseOffset:theOffset], - itemInfo = (parentItem != nil) ? _outlineView._itemInfosForItems[[parentItem UID]] : _outlineView._rootItemInfo, + var parentItem = [self _parentItemForUpperRow:theRow - 1 andLowerRow:theRow atMouseOffset:theOffset], + itemInfo = (parentItem != nil) ? _itemInfosForItems[[parentItem UID]] : _rootItemInfo, children = itemInfo.children; - childIndex = [children indexOfObject:[_outlineView itemAtRow:theRow]]; + childIndex = [children indexOfObject:[self itemAtRow:theRow]]; if (childIndex === CPNotFound) childIndex = children.length; @@ -2050,155 +2035,140 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, return childIndex; } -- (void)_parentItemForDropOperation:(CPTableViewDropOperation)theDropOperation row:(CPInteger)theRow offset:(CGPoint)theOffset +- (id)_parentItemForDropOperation:(CPTableViewDropOperation)theDropOperation row:(CPInteger)theRow offset:(CGPoint)theOffset { if (theDropOperation === CPTableViewDropAbove) - return [_outlineView _parentItemForUpperRow:theRow - 1 andLowerRow:theRow atMouseOffset:theOffset]; + return [self _parentItemForUpperRow:theRow - 1 andLowerRow:theRow atMouseOffset:theOffset]; - return [_outlineView itemAtRow:theRow]; + return [self itemAtRow:theRow]; } - (CPDragOperation)tableView:(CPTableView)aTableView validateDrop:(id /*< CPDraggingInfo >*/)theInfo proposedRow:(CPInteger)theRow proposedDropOperation:(CPTableViewDropOperation)theOperation { - if (!(_outlineView._implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_validateDrop_proposedItem_proposedChildIndex_)) + if (!(_implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_validateDrop_proposedItem_proposedChildIndex_)) return CPDragOperationNone; // Make sure the retargeted item and index are reset - _outlineView._retargetedItem = nil; - _outlineView._shouldRetargetItem = NO; + _retargetedItem = nil; + _shouldRetargetItem = NO; - _outlineView._retargedChildIndex = nil; - _outlineView._shouldRetargetChildIndex = NO; + _retargedChildIndex = nil; + _shouldRetargetChildIndex = NO; - var location = [_outlineView convertPoint:[theInfo draggingLocation] fromView:nil], + var location = [self convertPoint:[theInfo draggingLocation] fromView:nil], parentItem = [self _parentItemForDropOperation:theOperation row:theRow offset:location], childIndex = [self _childIndexForDropOperation:theOperation row:theRow offset:location]; - return [_outlineView._outlineViewDataSource outlineView:_outlineView validateDrop:theInfo proposedItem:parentItem proposedChildIndex:childIndex]; + return [_outlineViewDataSource outlineView:self validateDrop:theInfo proposedItem:parentItem proposedChildIndex:childIndex]; } - (BOOL)tableView:(CPTableView)aTableView acceptDrop:(id /**/)theInfo row:(CPInteger)theRow dropOperation:(CPTableViewDropOperation)theOperation { - if (!(_outlineView._implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_acceptDrop_item_childIndex_)) + if (!(_implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_acceptDrop_item_childIndex_)) return NO; - var location = [_outlineView convertPoint:[theInfo draggingLocation] fromView:nil], + var location = [self convertPoint:[theInfo draggingLocation] fromView:nil], parentItem = [self _parentItemForDropOperation:theOperation row:theRow offset:location], childIndex = [self _childIndexForDropOperation:theOperation row:theRow offset:location]; - _outlineView._retargetedItem = nil; - _outlineView._shouldRetargetItem = NO; + _retargetedItem = nil; + _shouldRetargetItem = NO; - _outlineView._retargedChildIndex = nil; - _outlineView._shouldRetargetChildIndex = NO; + _retargedChildIndex = nil; + _shouldRetargetChildIndex = NO; - return [_outlineView._outlineViewDataSource outlineView:_outlineView acceptDrop:theInfo item:parentItem childIndex:childIndex]; + return [_outlineViewDataSource outlineView:self acceptDrop:theInfo item:parentItem childIndex:childIndex]; } - (void)tableView:(CPTableView)aTableView sortDescriptorsDidChange:(CPArray)oldSortDescriptors { - if ((_outlineView._implementedOutlineViewDataSourceMethods & + if ((_implementedOutlineViewDataSourceMethods & CPOutlineViewDataSource_outlineView_sortDescriptorsDidChange_)) { - [[_outlineView dataSource] outlineView:_outlineView sortDescriptorsDidChange:oldSortDescriptors]; + [[self dataSource] outlineView:self sortDescriptorsDidChange:oldSortDescriptors]; } } -@end - -@implementation _CPOutlineViewTableViewDelegate : CPObject -{ - CPOutlineView _outlineView; -} - -- (id)initWithOutlineView:(CPOutlineView)anOutlineView -{ - self = [super init]; - - if (self) - _outlineView = anOutlineView; - - return self; -} +/*** CPTableViewDelegate methods ***/ - (BOOL)tableView:(CPTableView)theTableView shouldSelectRow:(CPInteger)theRow { - return SHOULD_SELECT_ITEM(_outlineView, [_outlineView itemAtRow:theRow]); + return SHOULD_SELECT_ITEM(self, [self itemAtRow:theRow]); } - (BOOL)selectionShouldChangeInTableView:(CPTableView)theTableView { - return SELECTION_SHOULD_CHANGE(_outlineView); + return SELECTION_SHOULD_CHANGE(self); } - (BOOL)tableView:(CPTableView)aTableView shouldEditTableColumn:(CPTableColumn)aColumn row:(CPInteger)aRow { - if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldEditTableColumn_item_)) - return [_outlineView._outlineViewDelegate outlineView:_outlineView shouldEditTableColumn:aColumn item:[_outlineView itemAtRow:aRow]]; + if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldEditTableColumn_item_)) + return [_outlineViewDelegate outlineView:self shouldEditTableColumn:aColumn item:[self itemAtRow:aRow]]; return NO; } - (float)tableView:(CPTableView)theTableView heightOfRow:(CPInteger)theRow { - if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_heightOfRowByItem_)) - return [_outlineView._outlineViewDelegate outlineView:_outlineView heightOfRowByItem:[_outlineView itemAtRow:theRow]]; + if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_heightOfRowByItem_)) + return [_outlineViewDelegate outlineView:self heightOfRowByItem:[self itemAtRow:theRow]]; return [theTableView rowHeight]; } - (void)tableView:(CPTableView)aTableView willDisplayView:(id)aView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex { - if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_willDisplayView_forTableColumn_item_)) + if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_willDisplayView_forTableColumn_item_)) { - var item = [_outlineView itemAtRow:aRowIndex]; - [_outlineView._outlineViewDelegate outlineView:_outlineView willDisplayView:aView forTableColumn:aTableColumn item:item]; + var item = [self itemAtRow:aRowIndex]; + [_outlineViewDelegate outlineView:self willDisplayView:aView forTableColumn:aTableColumn item:item]; } } - (void)tableView:(CPTableView)aTableView willRemoveView:(id)aView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex { - if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_willRemoveView_forTableColumn_item_)) + if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_willRemoveView_forTableColumn_item_)) { - var item = [_outlineView itemAtRow:aRowIndex]; - [_outlineView._outlineViewDelegate outlineView:_outlineView willRemoveView:aView forTableColumn:aTableColumn item:item]; + var item = [self itemAtRow:aRowIndex]; + [_outlineViewDelegate outlineView:self willRemoveView:aView forTableColumn:aTableColumn item:item]; } } - (BOOL)tableView:(CPTableView)aTableView isGroupRow:(CPInteger)aRow { - if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_isGroupItem_)) - return [_outlineView._outlineViewDelegate outlineView:_outlineView isGroupItem:[_outlineView itemAtRow:aRow]]; + if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_isGroupItem_)) + return [_outlineViewDelegate outlineView:self isGroupItem:[self itemAtRow:aRow]]; return NO; } - (CPMenu)tableView:(CPTableView)aTableView menuForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow { - if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_menuForTableColumn_item_)) + if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_menuForTableColumn_item_)) { - var item = [_outlineView itemAtRow:aRow]; - return [_outlineView._outlineViewDelegate outlineView:_outlineView menuForTableColumn:aTableColumn item:item] + var item = [self itemAtRow:aRow]; + return [_outlineViewDelegate outlineView:self menuForTableColumn:aTableColumn item:item] } // We reimplement CPView menuForEvent: because we can't call it directly. CPTableView implements menuForEvent: // to call this delegate method. - return [_outlineView menu] || [[_outlineView class] defaultMenu]; + return [self menu] || [[self class] defaultMenu]; } - (CPIndexSet)tableView:(CPTableView)aTableView selectionIndexesForProposedSelection:(CPIndexSet)anIndexSet { - if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_selectionIndexesForProposedSelection_)) - return [_outlineView._outlineViewDelegate outlineView:_outlineView selectionIndexesForProposedSelection:anIndexSet]; + if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_selectionIndexesForProposedSelection_)) + return [_outlineViewDelegate outlineView:self selectionIndexesForProposedSelection:anIndexSet]; return anIndexSet; } - (BOOL)tableView:(CPTableView)aTableView shouldSelectTableColumn:(CPTableColumn)aTableColumn { - if ((_outlineView._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldSelectTableColumn_)) - return [_outlineView._outlineViewDelegate outlineView:_outlineView shouldSelectTableColumn:aTableColumn]; + if ((_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldSelectTableColumn_)) + return [_outlineViewDelegate outlineView:self shouldSelectTableColumn:aTableColumn]; return YES; } @@ -2304,8 +2274,8 @@ var CPOutlineViewIndentationPerLevelKey = @"CPOutlineViewIndentationPerLevelKey" _outlineViewDataSource = [aCoder decodeObjectForKey:CPOutlineViewDataSourceKey]; _outlineViewDelegate = [aCoder decodeObjectForKey:CPOutlineViewDelegateKey]; - [super setDataSource:[[_CPOutlineViewTableViewDataSource alloc] initWithOutlineView:self]]; - [super setDelegate:[[_CPOutlineViewTableViewDelegate alloc] initWithOutlineView:self]]; + [super setDataSource:self]; + [super setDelegate:self]; [self _updateIsViewBased]; } From 261557717339e30322f68053ba04d2a368bc05b1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 19 Jan 2026 07:50:18 +0100 Subject: [PATCH 046/136] 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 047/136] 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 048/136] 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 049/136] 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 050/136] 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 051/136] 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 052/136] 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 053/136] 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 054/136] 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 055/136] 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 056/136] 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 057/136] 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 058/136] 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 059/136] 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 060/136] 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 061/136] 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 062/136] 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 063/136] 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 064/136] 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 2ff5eb39eb21e26120c84b095c06fbde5b66032b Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 15 Feb 2026 12:32:13 +0100 Subject: [PATCH 065/136] Fix CPColorWell drag-and-drop and responder chain integration --- AppKit/CPColorPanel.j | 34 +++---- AppKit/CPColorWell.j | 220 ++++++++++++++++++++---------------------- 2 files changed, 115 insertions(+), 139 deletions(-) diff --git a/AppKit/CPColorPanel.j b/AppKit/CPColorPanel.j index ba1521b5f..36df15bba 100644 --- a/AppKit/CPColorPanel.j +++ b/AppKit/CPColorPanel.j @@ -51,6 +51,7 @@ CPColorPickerViewWidth = 265; CPColorPickerViewHeight = 370; CPColorPanelColorDidChangeNotification = @"CPColorPanelColorDidChangeNotification"; +CPColorDragType = "CPColorDragType"; var PREVIEW_HEIGHT = 20.0, TOOLBAR_HEIGHT = 32.0, @@ -129,8 +130,6 @@ var SharedColorPanel = nil, if (self) { - //[[self contentView] setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]]; - [self setTitle:@"Color Panel"]; [self setLevel:CPFloatingWindowLevel]; @@ -152,6 +151,7 @@ var SharedColorPanel = nil, _color = aColor; [_previewView setBackgroundColor:_color]; + // Push color via Responder Chain [CPApp sendAction:@selector(changeColor:) to:nil from:self]; if (_target && _action) @@ -161,8 +161,11 @@ var SharedColorPanel = nil, postNotificationName:CPColorPanelColorDidChangeNotification object:self]; - [_activePicker setColor:_color]; - [_opacitySlider setFloatValue:[_color alphaComponent]]; + if (_activePicker) + [_activePicker setColor:_color]; + + if (_opacitySlider) + [_opacitySlider setFloatValue:[_color alphaComponent]]; } /*! @@ -174,7 +177,7 @@ var SharedColorPanel = nil, { [self setColor:aColor]; - if (bool) + if (bool && _activePicker) [_activePicker setColor:_color]; } @@ -332,7 +335,7 @@ var SharedColorPanel = nil, buttonForLater = button; } - // FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox + // Preview var previewBox = [[CPView alloc] initWithFrame:CGRectMake(76, TOOLBAR_HEIGHT + 10, CGRectGetWidth(bounds) - 86, PREVIEW_HEIGHT)]; _previewView = [[_CPColorPanelPreview alloc] initWithFrame:CGRectInset([previewBox bounds], 2.0, 2.0)]; @@ -350,7 +353,7 @@ var SharedColorPanel = nil, [_previewLabel setTextColor:[CPColor blackColor]]; [_previewLabel setAlignment:CPRightTextAlignment]; - // FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox + // Swatches var swatchBox = [[CPView alloc] initWithFrame:CGRectMake(76, TOOLBAR_HEIGHT + 10 + PREVIEW_HEIGHT + 5, CGRectGetWidth(bounds) - 86, SWATCH_HEIGHT + 2.0)]; [swatchBox setBackgroundColor:[CPColor colorWithWhite:0.8 alpha:1.0]]; @@ -412,8 +415,6 @@ var SharedColorPanel = nil, @end -CPColorDragType = "CPColorDragType"; - var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; /* @ignore */ @@ -442,7 +443,6 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; for (var i = 0; i < 50; i++) { - // FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox var view = [[CPView alloc] initWithFrame:CGRectMake(13 * i + 1, 1, 12, 12)], fillView = [[CPView alloc] initWithFrame:CGRectInset([view bounds], 1.0, 1.0)]; @@ -494,7 +494,6 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; - (CPArray)saveColorList { var result = []; - // FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox for (var i = 0; i < _swatches.length; i++) result.push([[[_swatches[i] subviews][0] backgroundColor] hexString]); @@ -521,7 +520,6 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; - (void)setColor:(CPColor)aColor atIndex:(int)index { - // FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox [[_swatches[index] subviews][0] setBackgroundColor:aColor]; [self saveColorList]; } @@ -544,16 +542,11 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; if (point.x > [self bounds].size.width - 1 || point.x < 1) return NO; - [[CPPasteboard pasteboardWithName:CPDragPboard] declareTypes:[CPArray arrayWithObject:CPColorDragType] owner:self]; - var swatch = _swatches[FLOOR(point.x / 13)]; - - // FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox _dragColor = [[swatch subviews][0] backgroundColor]; var bounds = CGRectMakeCopy([swatch bounds]); - // FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox var dragView = [[CPView alloc] initWithFrame:bounds], dragFillView = [[CPView alloc] initWithFrame:CGRectInset(bounds, 1.0, 1.0)]; @@ -566,7 +559,7 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; at:CGPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0) offset:CGPointMake(0.0, 0.0) event:anEvent - pasteboard:nil + pasteboard:[CPPasteboard pasteboardWithName:CPDragPboard] source:self slideBack:YES]; } @@ -636,11 +629,8 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; { var point = [self convertPoint:[anEvent locationInWindow] fromView:nil]; - [[CPPasteboard pasteboardWithName:CPDragPboard] declareTypes:[CPColorDragType] owner:self]; - var bounds = CGRectMake(0, 0, 15, 15); - // FIXME: http://280north.lighthouseapp.com/projects/13294-cappuccino/tickets/25-implement-cpbox var dragView = [[CPView alloc] initWithFrame:bounds], dragFillView = [[CPView alloc] initWithFrame:CGRectInset(bounds, 1.0, 1.0)]; @@ -653,7 +643,7 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; at:CGPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0) offset:CGPointMake(0.0, 0.0) event:anEvent - pasteboard:nil + pasteboard:[CPPasteboard pasteboardWithName:CPDragPboard] source:self slideBack:YES]; } diff --git a/AppKit/CPColorWell.j b/AppKit/CPColorWell.j index fb1b0f0dc..7c2ece7d7 100644 --- a/AppKit/CPColorWell.j +++ b/AppKit/CPColorWell.j @@ -26,9 +26,6 @@ @import "CPColor.j" @import "CPColorPanel.j" - -var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiveNotification"; - /*! @ingroup appkit @class CPColorWell @@ -39,9 +36,7 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv */ @implementation CPColorWell : CPControl { - BOOL _active; BOOL _bordered; - CPColor _color; } @@ -83,46 +78,18 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv if (self) { - _active = NO; _color = [CPColor whiteColor]; [self setBordered:YES]; + + // Register for Drag & Drop + [self registerForDraggedTypes:[CPArray arrayWithObject:CPColorDragType]]; } return self; } -- (void)_registerNotifications -{ - var defaultCenter = [CPNotificationCenter defaultCenter]; - - [defaultCenter - addObserver:self - selector:@selector(colorWellDidBecomeExclusive:) - name:_CPColorWellDidBecomeExclusiveNotification - object:nil]; - - [defaultCenter - addObserver:self - selector:@selector(colorPanelWillClose:) - name:CPWindowWillCloseNotification - object:[CPColorPanel sharedColorPanel]]; -} - -- (void)_removeNotifications -{ - var defaultCenter = [CPNotificationCenter defaultCenter]; - - [defaultCenter - removeObserver:self - name:_CPColorWellDidBecomeExclusiveNotification - object:nil]; - - [defaultCenter - removeObserver:self - name:CPWindowWillCloseNotification - object:[CPColorPanel sharedColorPanel]]; - -} +#pragma mark - +#pragma mark Draw /*! Sets whether the color well is bordered. @@ -143,7 +110,8 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv return [self hasThemeState:CPThemeStateBordered]; } -// Managing Color From Color Wells +#pragma mark - +#pragma mark Managing Color /*! Returns the color well's current color. @@ -164,6 +132,10 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv _color = aColor; [self setNeedsLayout]; + + // If we are active, update the panel to match us + if ([self isFirstResponder]) + [[CPColorPanel sharedColorPanel] setColor:_color]; } /*! @@ -175,7 +147,21 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv [self setColor:[aSender color]]; } -// Activating and Deactivating Color Wells +/*! + Standard action method sent by CPColorPanel up the responder chain. +*/ +- (void)changeColor:(id)aSender +{ + if ([aSender isKindOfClass:[CPColorPanel class]]) + { + [self setColor:[aSender color]]; + [self sendAction:[self action] to:[self target]]; + } +} + +#pragma mark - +#pragma mark Activating and Deactivating + /*! Activates the color well, displays the color panel, and makes the panel's current color the same as its own. If exclusive is \c YES, deactivates any other CPColorWells. \c NO, keeps them active. @@ -183,23 +169,8 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv */ - (void)activate:(BOOL)shouldBeExclusive { - if (shouldBeExclusive) - // FIXME: make this queue! - [[CPNotificationCenter defaultCenter] - postNotificationName:_CPColorWellDidBecomeExclusiveNotification - object:self]; - - - if ([self isActive]) - return; - - _active = YES; - - [[CPNotificationCenter defaultCenter] - addObserver:self - selector:@selector(colorPanelDidChangeColor:) - name:CPColorPanelColorDidChangeNotification - object:[CPColorPanel sharedColorPanel]]; + [[self window] makeFirstResponder:self]; + [[CPColorPanel sharedColorPanel] orderFront:self]; } /*! @@ -207,78 +178,117 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv */ - (void)deactivate { - if (![self isActive]) - return; - - _active = NO; - - [[CPNotificationCenter defaultCenter] - removeObserver:self - name:CPColorPanelColorDidChangeNotification - object:[CPColorPanel sharedColorPanel]]; + if ([self isFirstResponder]) + [[self window] makeFirstResponder:nil]; } /*! - Returns \c YES if the color well is active. + Returns \c YES if the color well is active (is First Responder). */ - (BOOL)isActive { - return _active; + return [self isFirstResponder]; } -- (void)colorPanelDidChangeColor:(CPNotification)aNotification +- (BOOL)acceptsFirstResponder { - [self takeColorFrom:[aNotification object]]; - - [self sendAction:[self action] to:[self target]]; + return [self isEnabled]; } -- (void)colorWellDidBecomeExclusive:(CPNotification)aNotification +- (BOOL)becomeFirstResponder { - if (self != [aNotification object]) - [self deactivate]; + [self setThemeState:CPThemeStateFirstResponder]; + + var panel = [CPColorPanel sharedColorPanel]; + [panel setColor:_color]; + + // Ensure panel knows who to talk to if it's already open + [panel setTarget:nil]; + [panel setAction:@selector(changeColor:)]; + + [[CPNotificationCenter defaultCenter] postNotificationName:_CPColorWellDidBecomeExclusiveNotification object:self]; + + return YES; } -- (void)colorPanelWillClose:(CPNotification)aNotification +- (BOOL)resignFirstResponder { - [self deactivate]; + [self unsetThemeState:CPThemeStateFirstResponder]; + return YES; } -- (void)stopTracking:(CGPoint)lastPoint at:(CGPoint)aPoint mouseIsUp:(BOOL)mouseIsUp -{ - [self highlight:NO]; +#pragma mark - +#pragma mark Event Handling - if (!mouseIsUp || !CGRectContainsPoint([self bounds], aPoint) || ![self isEnabled]) +- (void)mouseDown:(CPEvent)anEvent +{ + if (![self isEnabled]) return; - + [self activate:YES]; - - var colorPanel = [CPColorPanel sharedColorPanel]; - - [colorPanel setPlatformWindow:[[self window] platformWindow]]; - - [colorPanel setColor:_color]; - [colorPanel orderFront:self]; } +#pragma mark - +#pragma mark Drag and Drop + +- (void)draggingEntered:(id)sender +{ + var pasteboard = [sender draggingPasteboard]; + + if ([[pasteboard types] containsObject:CPColorDragType]) + { + [self setThemeState:CPThemeStateHighlighted]; + return CPDragOperationCopy; + } + + return CPDragOperationNone; +} + +- (void)draggingExited:(id)sender +{ + [self unsetThemeState:CPThemeStateHighlighted]; +} + +- (BOOL)performDragOperation:(id)sender +{ + var pasteboard = [sender draggingPasteboard]; + + if ([[pasteboard types] containsObject:CPColorDragType]) + { + var data = [pasteboard dataForType:CPColorDragType], + newColor = [CPKeyedUnarchiver unarchiveObjectWithData:data]; + + if (newColor) + { + [self setColor:newColor]; + [self sendAction:[self action] to:[self target]]; + [self activate:YES]; // Become active after drop + [self unsetThemeState:CPThemeStateHighlighted]; + return YES; + } + } + + return NO; +} + +#pragma mark - +#pragma mark Layout + - (CGRect)contentRectForBounds:(CGRect)bounds { var contentInset = [self currentValueForThemeAttribute:@"content-inset"]; - return CGRectInsetByInset(bounds, contentInset); } - (CGRect)bezelRectForBounds:(CGRect)bounds { var bezelInset = [self currentValueForThemeAttribute:@"bezel-inset"]; - return CGRectInsetByInset(bounds, bezelInset); } - (CGRect)contentBorderRectForBounds:(CGRect)bounds { var contentBorderInset = [self currentValueForThemeAttribute:@"content-border-inset"]; - return CGRectInsetByInset(bounds, contentBorderInset); } @@ -300,9 +310,7 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv - (CPView)createEphemeralSubviewNamed:(CPString)aName { var view = [[CPView alloc] initWithFrame:CGRectMakeZero()]; - [view setHitTests:NO]; - return view; } @@ -328,28 +336,6 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv [contentBorderView setBackgroundColor:[self currentValueForThemeAttribute:@"content-border-color"]]; } - -#pragma mark - -#pragma mark Observers method - -- (void)_addObservers -{ - if (_isObserving) - return; - - [super _addObservers]; - [self _registerNotifications]; -} - -- (void)_removeObservers -{ - if (!_isObserving) - return; - - [super _removeObservers]; - [self _removeNotifications]; -} - @end @implementation CPColorWellValueBinder : CPBinder @@ -398,9 +384,9 @@ var CPColorWellColorKey = "CPColorWellColorKey", if (self) { - _active = NO; _color = [aCoder decodeObjectForKey:CPColorWellColorKey]; [self setBordered:[aCoder decodeBoolForKey:CPColorWellBorderedKey]]; + [self registerForDraggedTypes:[CPArray arrayWithObject:CPColorDragType]]; } return self; From c82e5750f0eee86b7ec56c1780240e6a8bdf71d5 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 15 Feb 2026 18:28:20 +0100 Subject: [PATCH 066/136] fixed: the first version was not functional --- AppKit/CPColorPanel.j | 35 +++++++++----- AppKit/CPColorWell.j | 107 +++++++++++++++++++++++++----------------- 2 files changed, 86 insertions(+), 56 deletions(-) diff --git a/AppKit/CPColorPanel.j b/AppKit/CPColorPanel.j index 36df15bba..06c404595 100644 --- a/AppKit/CPColorPanel.j +++ b/AppKit/CPColorPanel.j @@ -148,10 +148,13 @@ var SharedColorPanel = nil, */ - (void)setColor:(CPColor)aColor { + if ([_color isEqual:aColor]) + return; + _color = aColor; [_previewView setBackgroundColor:_color]; - // Push color via Responder Chain + // Push color via Responder Chain (targets First Responder, i.e., the active CPColorWell) [CPApp sendAction:@selector(changeColor:) to:nil from:self]; if (_target && _action) @@ -163,7 +166,7 @@ var SharedColorPanel = nil, if (_activePicker) [_activePicker setColor:_color]; - + if (_opacitySlider) [_opacitySlider setFloatValue:[_color alphaComponent]]; } @@ -432,7 +435,7 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; [self setBackgroundColor:[CPColor grayColor]]; - [self registerForDraggedTypes:[CPArray arrayWithObjects:CPColorDragType]]; + [self registerForDraggedTypes:[CPArray arrayWithObjects:@"CPColorDragType"]]; var whiteColor = [CPColor whiteColor]; @@ -555,18 +558,22 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; [dragView addSubview:dragFillView]; + var pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard]; + [pasteboard declareTypes:[CPArray arrayWithObject:@"CPColorDragType"] owner:self]; + [pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_dragColor] forType:@"CPColorDragType"]; + [self dragView:dragView at:CGPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0) offset:CGPointMake(0.0, 0.0) event:anEvent - pasteboard:[CPPasteboard pasteboardWithName:CPDragPboard] + pasteboard:pasteboard source:self slideBack:YES]; } - (void)pasteboard:(CPPasteboard)aPasteboard provideDataForType:(CPString)aType { - if (aType == CPColorDragType) + if (aType == @"CPColorDragType") [aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_dragColor] forType:aType]; } @@ -576,10 +583,10 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; pasteboard = [aSender draggingPasteboard], swatch = nil; - if (![pasteboard availableTypeFromArray:[CPColorDragType]] || location.x > [self bounds].size.width - 1 || location.x < 1) + if (![pasteboard availableTypeFromArray:[@"CPColorDragType"]] || location.x > [self bounds].size.width - 1 || location.x < 1) return NO; - [self setColor:[CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:CPColorDragType]] atIndex:FLOOR(location.x / 13)]; + [self setColor:[CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:@"CPColorDragType"]] atIndex:FLOOR(location.x / 13)]; } @end @@ -594,7 +601,7 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; { self = [super initWithFrame:aFrame]; - [self registerForDraggedTypes:[CPArray arrayWithObjects:CPColorDragType]]; + [self registerForDraggedTypes:[CPArray arrayWithObjects:@"CPColorDragType"]]; return self; } @@ -613,10 +620,10 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; { var pasteboard = [aSender draggingPasteboard]; - if (![pasteboard availableTypeFromArray:[CPColorDragType]]) + if (![pasteboard availableTypeFromArray:[@"CPColorDragType"]]) return NO; - var color = [CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:CPColorDragType]]; + var color = [CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:@"CPColorDragType"]]; [_colorPanel setColor:color updatePicker:YES]; } @@ -639,18 +646,22 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; [dragView addSubview:dragFillView]; + var pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard]; + [pasteboard declareTypes:[CPArray arrayWithObject:@"CPColorDragType"] owner:self]; + [pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[self backgroundColor]] forType:@"CPColorDragType"]; + [self dragView:dragView at:CGPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0) offset:CGPointMake(0.0, 0.0) event:anEvent - pasteboard:[CPPasteboard pasteboardWithName:CPDragPboard] + pasteboard:pasteboard source:self slideBack:YES]; } - (void)pasteboard:(CPPasteboard)aPasteboard provideDataForType:(CPString)aType { - if (aType == CPColorDragType) + if (aType == @"CPColorDragType") [aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[self backgroundColor]] forType:aType]; } diff --git a/AppKit/CPColorWell.j b/AppKit/CPColorWell.j index 7c2ece7d7..73ed88b8a 100644 --- a/AppKit/CPColorWell.j +++ b/AppKit/CPColorWell.j @@ -26,6 +26,8 @@ @import "CPColor.j" @import "CPColorPanel.j" +var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiveNotification"; + /*! @ingroup appkit @class CPColorWell @@ -38,6 +40,7 @@ { BOOL _bordered; CPColor _color; + BOOL _isChangingColorFromPanel; // Guard flag to prevent recursion } + (Class)_binderClassForBinding:(CPString)aBinding @@ -72,6 +75,31 @@ [theBinding reverseSetValueFor:@"color"]; } +- (BOOL)isFirstResponder +{ + return [[self window] firstResponder] === self; +} + +- (BOOL)acceptsFirstResponder +{ + return YES; +} + +- (void)activate:(BOOL)shouldBeExclusive +{ + [[self window] makeFirstResponder:self]; + [[CPColorPanel sharedColorPanel] orderFront:self]; +} + +/*! + Deactivates the color well. +*/ +- (void)deactivate +{ + if ([self isFirstResponder]) + [[self window] makeFirstResponder:nil]; +} + - (id)initWithFrame:(CGRect)aFrame { self = [super initWithFrame:aFrame]; @@ -81,8 +109,8 @@ _color = [CPColor whiteColor]; [self setBordered:YES]; - // Register for Drag & Drop - [self registerForDraggedTypes:[CPArray arrayWithObject:CPColorDragType]]; + // Register using string literal to avoid dependency issues + [self registerForDraggedTypes:[CPArray arrayWithObject:@"CPColorDragType"]]; } return self; @@ -126,15 +154,16 @@ */ - (void)setColor:(CPColor)aColor { - if (_color == aColor) + if ([_color isEqual:aColor]) return; _color = aColor; [self setNeedsLayout]; - // If we are active, update the panel to match us - if ([self isFirstResponder]) + // Only push back to the panel if we initiated the change (not if the panel pushed it to us) + // AND if we are the current focus. + if (!_isChangingColorFromPanel && [self isFirstResponder]) [[CPColorPanel sharedColorPanel] setColor:_color]; } @@ -148,13 +177,17 @@ } /*! - Standard action method sent by CPColorPanel up the responder chain. + Standard action method sent by CPColorPanel via the Responder Chain. */ - (void)changeColor:(id)aSender { if ([aSender isKindOfClass:[CPColorPanel class]]) { + _isChangingColorFromPanel = YES; [self setColor:[aSender color]]; + _isChangingColorFromPanel = NO; + + // Forward the action to our target (e.g. controller) [self sendAction:[self action] to:[self target]]; } } @@ -167,43 +200,19 @@ If exclusive is \c YES, deactivates any other CPColorWells. \c NO, keeps them active. @param shouldBeExclusive whether other color wells should be deactivated. */ -- (void)activate:(BOOL)shouldBeExclusive -{ - [[self window] makeFirstResponder:self]; - [[CPColorPanel sharedColorPanel] orderFront:self]; -} - -/*! - Deactivates the color well. -*/ -- (void)deactivate -{ - if ([self isFirstResponder]) - [[self window] makeFirstResponder:nil]; -} - -/*! - Returns \c YES if the color well is active (is First Responder). -*/ -- (BOOL)isActive -{ - return [self isFirstResponder]; -} - -- (BOOL)acceptsFirstResponder -{ - return [self isEnabled]; -} - - (BOOL)becomeFirstResponder { [self setThemeState:CPThemeStateFirstResponder]; var panel = [CPColorPanel sharedColorPanel]; - [panel setColor:_color]; - // Ensure panel knows who to talk to if it's already open - [panel setTarget:nil]; + // Sync panel to our current color + [panel setColor:_color]; + + // EXPLICITLY set ourselves as the target. + // This ensures that when the user clicks the panel (making Panel key), + // the panel still knows to send messages back to us. + [panel setTarget:self]; [panel setAction:@selector(changeColor:)]; [[CPNotificationCenter defaultCenter] postNotificationName:_CPColorWellDidBecomeExclusiveNotification object:self]; @@ -214,6 +223,13 @@ - (BOOL)resignFirstResponder { [self unsetThemeState:CPThemeStateFirstResponder]; + + var panel = [CPColorPanel sharedColorPanel]; + + // Clean up if we were the target + if ([panel target] == self) + [panel setTarget:nil]; + return YES; } @@ -224,7 +240,7 @@ { if (![self isEnabled]) return; - + [self activate:YES]; } @@ -235,7 +251,7 @@ { var pasteboard = [sender draggingPasteboard]; - if ([[pasteboard types] containsObject:CPColorDragType]) + if ([[pasteboard types] containsObject:@"CPColorDragType"]) { [self setThemeState:CPThemeStateHighlighted]; return CPDragOperationCopy; @@ -253,17 +269,20 @@ { var pasteboard = [sender draggingPasteboard]; - if ([[pasteboard types] containsObject:CPColorDragType]) + if ([[pasteboard types] containsObject:@"CPColorDragType"]) { - var data = [pasteboard dataForType:CPColorDragType], + var data = [pasteboard dataForType:@"CPColorDragType"], newColor = [CPKeyedUnarchiver unarchiveObjectWithData:data]; - if (newColor) + if (newColor && [newColor isKindOfClass:[CPColor class]]) { [self setColor:newColor]; [self sendAction:[self action] to:[self target]]; - [self activate:YES]; // Become active after drop + + // Activate nicely after drop + [self activate:YES]; [self unsetThemeState:CPThemeStateHighlighted]; + return YES; } } @@ -386,7 +405,7 @@ var CPColorWellColorKey = "CPColorWellColorKey", { _color = [aCoder decodeObjectForKey:CPColorWellColorKey]; [self setBordered:[aCoder decodeBoolForKey:CPColorWellBorderedKey]]; - [self registerForDraggedTypes:[CPArray arrayWithObject:CPColorDragType]]; + [self registerForDraggedTypes:[CPArray arrayWithObject:@"CPColorDragType"]]; } return self; From 0f9cadf8d3a9daec75672326bac4b36805cbe2f5 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 15 Feb 2026 19:53:00 +0100 Subject: [PATCH 067/136] fixed: life update --- AppKit/CPColorWell.j | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/AppKit/CPColorWell.j b/AppKit/CPColorWell.j index 73ed88b8a..ec2309122 100644 --- a/AppKit/CPColorWell.j +++ b/AppKit/CPColorWell.j @@ -205,16 +205,17 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv [self setThemeState:CPThemeStateFirstResponder]; var panel = [CPColorPanel sharedColorPanel]; - - // Sync panel to our current color - [panel setColor:_color]; // EXPLICITLY set ourselves as the target. // This ensures that when the user clicks the panel (making Panel key), // the panel still knows to send messages back to us. - [panel setTarget:self]; + [panel setTarget:self]; [panel setAction:@selector(changeColor:)]; - + + // Sync panel to our current color + [panel setColor:_color]; + + [[CPNotificationCenter defaultCenter] postNotificationName:_CPColorWellDidBecomeExclusiveNotification object:self]; return YES; @@ -225,7 +226,7 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv [self unsetThemeState:CPThemeStateFirstResponder]; var panel = [CPColorPanel sharedColorPanel]; - + // Clean up if we were the target if ([panel target] == self) [panel setTarget:nil]; From 0b83c20648a6862805e941c7b57535379ae686b3 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 15 Feb 2026 19:53:17 +0100 Subject: [PATCH 068/136] fixed: life update --- AppKit/CPColorPanel.j | 2 -- 1 file changed, 2 deletions(-) diff --git a/AppKit/CPColorPanel.j b/AppKit/CPColorPanel.j index 06c404595..e94da6970 100644 --- a/AppKit/CPColorPanel.j +++ b/AppKit/CPColorPanel.j @@ -397,8 +397,6 @@ var SharedColorPanel = nil, [contentView addSubview:opacityLabel]; [contentView addSubview:_opacitySlider]; - _target = nil; - _action = nil; _activePicker = nil; [_previewView setBackgroundColor:_color]; From ea47767bb89ee88deddc4f0e33a1e71a69134446 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 21 Feb 2026 11:51:25 +0100 Subject: [PATCH 069/136] 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 070/136] 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 071/136] 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 072/136] 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 073/136] 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 bce326de9d61b37350007a4f9d3f7e386a78ec93 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 28 Feb 2026 15:57:19 +0100 Subject: [PATCH 074/136] fixed: Colorwell did not reflect enabled state visually --- AppKit/CPColorWell.j | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AppKit/CPColorWell.j b/AppKit/CPColorWell.j index ec2309122..d31e88112 100644 --- a/AppKit/CPColorWell.j +++ b/AppKit/CPColorWell.j @@ -346,8 +346,10 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv positioned:CPWindowAbove relativeToEphemeralSubviewNamed:@"bezel-view"]; - [contentView setBackgroundColor:_color]; + + // Dim the color presentation if the control is disabled + [contentView setAlphaValue:[self isEnabled] ? 1.0 : 0.5]; var contentBorderView = [self layoutEphemeralSubviewNamed:@"content-border-view" positioned:CPWindowAbove From 8db24fcaf68a9e4e83152556dd42aa897fa54f52 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 28 Feb 2026 16:46:34 +0100 Subject: [PATCH 075/136] new: testcases from enquora --- Tests/AppKit/CPColorPanelTest.j | 203 ++++++++++++++++++++++++++++++++ Tests/AppKit/CPColorWellTest.j | 85 +++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 Tests/AppKit/CPColorPanelTest.j diff --git a/Tests/AppKit/CPColorPanelTest.j b/Tests/AppKit/CPColorPanelTest.j new file mode 100644 index 000000000..031f00d3b --- /dev/null +++ b/Tests/AppKit/CPColorPanelTest.j @@ -0,0 +1,203 @@ +/* + * CPColorPanelTest.j + * AppKit + * + * Created to test CPColorPanel architectural refactoring. + */ + +@import + +/* + * Mock color picker for testing + */ +@implementation _MockColorPicker : CPObject +{ + CPArray _receivedColors; + CPView _view; +} + +- (id)init +{ + if (self = [super init]) + { + _receivedColors = []; + _view = [[CPView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + } + return self; +} + +- (void)setColor:(CPColor)aColor +{ + [_receivedColors addObject:aColor]; +} + +- (CPArray)receivedColors +{ + return _receivedColors; +} + +- (CPView)provideNewView:(BOOL)initial +{ + return _view; +} + +@end + +@implementation CPColorPanelTest : OJTestCase +{ + CPColorPanel _panel; +} + +- (void)setUp +{ + // Get shared panel and ensure it's initialized + _panel = [CPColorPanel sharedColorPanel]; + [_panel _loadContentsIfNecessary]; +} + +- (void)tearDown +{ + // Reset panel state between tests + [_panel setColor:[CPColor whiteColor]]; +} + +/* + * Test that setting a color updates the opacity slider to match the color's alpha component. + * This ensures the slider always reflects the current color's opacity. + */ +- (void)testSetColorUpdatesOpacitySlider +{ + var initialColor = [CPColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0]; + [_panel setColor:initialColor]; + + // Track if setColor gets called + var setColorCalled = NO; + var originalSetColor = _panel.setColor; + _panel.setColor = function(aColor) + { + setColorCalled = YES; + originalSetColor.call(this, aColor); + }; + + // Simulate user changing opacity slider + [_panel._opacitySlider setFloatValue:0.5]; + [_panel _opacityChanged:_panel._opacitySlider]; + + _panel.setColor = originalSetColor; // Restore + + [self assertFalse:setColorCalled + message:"_opacityChanged should update state directly without calling setColor"]; +} + +/* + * Test that _opacityChanged: creates a new color with the correct alpha value. + */ +- (void)testOpacityChangeUpdatesColorAlpha +{ + var initialColor = [CPColor colorWithRed:0.5 green:0.5 blue:1.0 alpha:1.0]; + [_panel setColor:initialColor]; + + [_panel._opacitySlider setFloatValue:0.3]; + [_panel _opacityChanged:_panel._opacitySlider]; + + var newColor = [_panel color]; + [self assert:0.3 equals:[newColor alphaComponent] + message:"Color's alpha should match slider value"]; + + // RGB components should remain unchanged + var components = [newColor components]; + [self assert:0.5 equals:components[0] message:"Red component should be unchanged"]; + [self assert:0.5 equals:components[1] message:"Green component should be unchanged"]; + [self assert:1.0 equals:components[2] message:"Blue component should be unchanged"]; +} + +/* + * Test that the active picker receives setColor: when a color changes. + * Regression test: active picker was not notified of color changes. + */ +- (void)testActivePickerNotifiedOfColorChanges +{ + var mockPicker = [[_MockColorPicker alloc] init]; + _panel._activePicker = mockPicker; + + var color1 = [CPColor redColor]; + var color2 = [CPColor blueColor]; + + [_panel setColor:color1]; + [_panel setColor:color2]; + + var receivedColors = [mockPicker receivedColors]; + [self assert:2 equals:[receivedColors count] + message:"Active picker should receive setColor for each color change"]; + [self assert:color1 same:receivedColors[0]]; + [self assert:color2 same:receivedColors[1]]; +} + +/* + * Test that a picker receives setColor: when it becomes active. + * Regression test: picker was not notified of current color on activation. + */ +- (void)testPickerNotifiedOnActivation +{ + var testColor = [CPColor colorWithRed:0.5 green:0.5 blue:1.0 alpha:1.0]; + [_panel setColor:testColor]; + + var mockPicker = [[_MockColorPicker alloc] init]; + _panel._colorPickers = [mockPicker]; + + var button = [[CPButton alloc] initWithFrame:CGRectMake(0, 0, 32, 32)]; + [button setTag:0]; + + [_panel _setPicker:button]; + + var receivedColors = [mockPicker receivedColors]; + [self assert:1 equals:[receivedColors count] + message:"Picker should receive setColor when activated"]; + [self assert:testColor same:receivedColors[0]]; +} + +/* + * Test that opacity() method returns the current color's alpha component. + */ +- (void)testOpacityMethodReturnsAlpha +{ + var testColor = [CPColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.42]; + [_panel setColor:testColor]; + + [self assert:0.42 equals:[_panel opacity] + message:"opacity method should return color's alpha component"]; +} + +/* + * Test that setColor: with equal color does not trigger updates. + * This is an optimization to prevent unnecessary work. + */ +- (void)testSetColorWithEqualColorDoesNothing +{ + var testColor = [CPColor colorWithRed:1.0 green:0.5 blue:0.0 alpha:0.7]; + [_panel setColor:testColor]; + + var mockPicker = [[_MockColorPicker alloc] init]; + _panel._activePicker = mockPicker; + + // Set same color again + [_panel setColor:testColor]; + + var receivedColors = [mockPicker receivedColors]; + [self assert:0 equals:[receivedColors count] + message:"Setting equal color should not trigger picker update"]; +} + +/* + * Test color() method returns the current color. + */ +- (void)testColorMethodReturnsCurrentColor +{ + var testColor = [CPColor colorWithRed:0.2 green:0.4 blue:0.6 alpha:0.8]; + [_panel setColor:testColor]; + + [self assert:testColor same:[_panel color] + message:"color method should return current color"]; +} + +@end \ No newline at end of file diff --git a/Tests/AppKit/CPColorWellTest.j b/Tests/AppKit/CPColorWellTest.j index ba8c83c03..ea5c2b9cb 100644 --- a/Tests/AppKit/CPColorWellTest.j +++ b/Tests/AppKit/CPColorWellTest.j @@ -31,4 +31,89 @@ [self assert:[CPColor greenColor] equals:[colorWell color] message:"color well archived color"]; } +/** + * Tests that color well objects maintain visual state consistency. + * + * Validates the object's responsibility to synchronize public properties + * with internal theme state management. + */ +- (void)testColorWellMaintainsVisualStateConsistency +{ + [colorWell setBordered:NO]; + [self assertFalse:[colorWell isBordered] message:"object correctly reports bordered state"]; + + [colorWell setBordered:YES]; + [self assertTrue:[colorWell isBordered] message:"object correctly reports changed bordered state"]; +} + +/** + * Tests that color well objects properly handle activation responsibilities. + * + * Validates the object's role in responder chain participation and + * exclusive activation behavior. + */ +- (void)testColorWellHandlesActivationResponsibilities +{ + // Note: We test activation through observable effects, not internal state + [colorWell activate:YES]; + [self assertTrue:[colorWell acceptsFirstResponder] message:"activated object accepts responder chain participation"]; +} + +/** + * Tests that disabled color well objects behave appropriately. + * + * Validates the object's responsibility to modify its behavior + * when in a disabled state. + */ +- (void)testDisabledColorWellModifiesBehaviorAppropriately +{ + [colorWell setEnabled:NO]; + [self assertFalse:[colorWell acceptsFirstResponder] message:"disabled object modifies responder behavior"]; + + [colorWell setEnabled:YES]; + [self assertTrue:[colorWell acceptsFirstResponder] message:"re-enabled object restores normal behavior"]; +} + +/** + * Tests that color well objects interact correctly with other wells. + * + * Validates the object's responsibility to handle exclusive activation + * scenarios with other color well instances. + */ +- (void)testColorWellInteractsCorrectlyWithOtherWells +{ + var well1 = colorWell, + well2 = [[CPColorWell alloc] initWithFrame:CGRectMake(0, 0, 30, 30)]; + + [well1 activate:YES]; // Exclusive activation + [well2 activate:NO]; // Non-exclusive should still work + + [self assertTrue:[well2 acceptsFirstResponder] message:"objects maintain independence where appropriate"]; +} + +/** + * Tests basic color assignment functionality. + * + * Validates the core contract: setting a color should change what's + * returned by the color getter. + */ +- (void)testBasicColorAssignment +{ + var testColor = [CPColor colorWithCalibratedRed:0.2 green:0.4 blue:0.6 alpha:0.8]; + [colorWell setColor:testColor]; + [self assert:[colorWell color] equals:testColor message:"object correctly handles color assignment"]; +} + +/** + * Tests default initialization properties. + * + * Validates that newly created objects have expected default state. + */ +- (void)testDefaultInitializationProperties +{ + [self assertNotNull:colorWell message:"object can be initialized"]; + [self assert:[colorWell color] equals:[CPColor whiteColor] message:"object has correct default color"]; + [self assertTrue:[colorWell isBordered] message:"object has correct default bordered state"]; + [self assertFalse:[colorWell isActive] message:"object starts inactive"]; +} @end From ec9c6585651d02b5803299df455ac43a37a81821 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 28 Feb 2026 17:07:02 +0100 Subject: [PATCH 076/136] fixed: acceptsFirstResponder was uncoditionally true --- AppKit/CPColorWell.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPColorWell.j b/AppKit/CPColorWell.j index d31e88112..85696f5ff 100644 --- a/AppKit/CPColorWell.j +++ b/AppKit/CPColorWell.j @@ -82,7 +82,7 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv - (BOOL)acceptsFirstResponder { - return YES; + return [self isEnabled]; } - (void)activate:(BOOL)shouldBeExclusive From 1bbf87e2cbf268989df1a05f0556df1bf3185de0 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 28 Feb 2026 18:26:47 +0100 Subject: [PATCH 077/136] fixed: testcases --- Tests/AppKit/CPColorPanelTest.j | 37 ++++++++------------------------- Tests/AppKit/CPColorWellTest.j | 12 ----------- 2 files changed, 9 insertions(+), 40 deletions(-) diff --git a/Tests/AppKit/CPColorPanelTest.j b/Tests/AppKit/CPColorPanelTest.j index 031f00d3b..59ac3dfda 100644 --- a/Tests/AppKit/CPColorPanelTest.j +++ b/Tests/AppKit/CPColorPanelTest.j @@ -28,7 +28,7 @@ - (void)setColor:(CPColor)aColor { - [_receivedColors addObject:aColor]; + [_receivedColors addObject:aColor]; } - (CPArray)receivedColors @@ -67,38 +67,20 @@ */ - (void)testSetColorUpdatesOpacitySlider { - var initialColor = [CPColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0]; - [_panel setColor:initialColor]; + var initialColor = [CPColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.5];[_panel setColor:initialColor]; - // Track if setColor gets called - var setColorCalled = NO; - var originalSetColor = _panel.setColor; - _panel.setColor = function(aColor) - { - setColorCalled = YES; - originalSetColor.call(this, aColor); - }; - - // Simulate user changing opacity slider - [_panel._opacitySlider setFloatValue:0.5]; - [_panel _opacityChanged:_panel._opacitySlider]; - - _panel.setColor = originalSetColor; // Restore - - [self assertFalse:setColorCalled - message:"_opacityChanged should update state directly without calling setColor"]; + [self assert:0.5 equals:[_panel._opacitySlider floatValue] + message:"Opacity slider should match the color's alpha component"]; } /* - * Test that _opacityChanged: creates a new color with the correct alpha value. + * Test that setOpacity: creates a new color with the correct alpha value. */ - (void)testOpacityChangeUpdatesColorAlpha { var initialColor = [CPColor colorWithRed:0.5 green:0.5 blue:1.0 alpha:1.0]; - [_panel setColor:initialColor]; - - [_panel._opacitySlider setFloatValue:0.3]; - [_panel _opacityChanged:_panel._opacitySlider]; + [_panel setColor:initialColor];[_panel._opacitySlider setFloatValue:0.3]; + [_panel setOpacity:_panel._opacitySlider]; var newColor = [_panel color]; [self assert:0.3 equals:[newColor alphaComponent] @@ -193,11 +175,10 @@ */ - (void)testColorMethodReturnsCurrentColor { - var testColor = [CPColor colorWithRed:0.2 green:0.4 blue:0.6 alpha:0.8]; - [_panel setColor:testColor]; + var testColor = [CPColor colorWithRed:0.2 green:0.4 blue:0.6 alpha:0.8];[_panel setColor:testColor]; [self assert:testColor same:[_panel color] message:"color method should return current color"]; } -@end \ No newline at end of file +@end diff --git a/Tests/AppKit/CPColorWellTest.j b/Tests/AppKit/CPColorWellTest.j index ea5c2b9cb..72f052f53 100644 --- a/Tests/AppKit/CPColorWellTest.j +++ b/Tests/AppKit/CPColorWellTest.j @@ -104,16 +104,4 @@ [self assert:[colorWell color] equals:testColor message:"object correctly handles color assignment"]; } -/** - * Tests default initialization properties. - * - * Validates that newly created objects have expected default state. - */ -- (void)testDefaultInitializationProperties -{ - [self assertNotNull:colorWell message:"object can be initialized"]; - [self assert:[colorWell color] equals:[CPColor whiteColor] message:"object has correct default color"]; - [self assertTrue:[colorWell isBordered] message:"object has correct default bordered state"]; - [self assertFalse:[colorWell isActive] message:"object starts inactive"]; -} @end From 89d345dfdcf6b3da91c3a332028b3ce1c93a9f68 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 28 Feb 2026 19:35:51 +0100 Subject: [PATCH 078/136] fixed: added missing isActive --- AppKit/CPColorWell.j | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AppKit/CPColorWell.j b/AppKit/CPColorWell.j index 85696f5ff..61ebe1a30 100644 --- a/AppKit/CPColorWell.j +++ b/AppKit/CPColorWell.j @@ -91,6 +91,11 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv [[CPColorPanel sharedColorPanel] orderFront:self]; } +- (BOOL)isActive +{ + return [self isFirstResponder] && [self isEnabled]; +} + /*! Deactivates the color well. */ From 8f48d01c33c1334a65a50a35636eca098d29ce34 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 28 Feb 2026 19:37:59 +0100 Subject: [PATCH 079/136] removed: enabled/disabled drawing emulation via alpha --- AppKit/CPColorWell.j | 3 --- 1 file changed, 3 deletions(-) diff --git a/AppKit/CPColorWell.j b/AppKit/CPColorWell.j index 61ebe1a30..60b789440 100644 --- a/AppKit/CPColorWell.j +++ b/AppKit/CPColorWell.j @@ -353,9 +353,6 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv [contentView setBackgroundColor:_color]; - // Dim the color presentation if the control is disabled - [contentView setAlphaValue:[self isEnabled] ? 1.0 : 0.5]; - var contentBorderView = [self layoutEphemeralSubviewNamed:@"content-border-view" positioned:CPWindowAbove relativeToEphemeralSubviewNamed:@"content-view"]; From fca30325e4540531fbfba7a553cef08e8689ae2e Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 3 Mar 2026 18:18:32 +0100 Subject: [PATCH 080/136] 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 f7a27cf905e0a46639aa021584299699c0ba2e17 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 14:26:51 +0100 Subject: [PATCH 081/136] fixed: delegate method was called more than once by textfield --- AppKit/CPTextField.j | 55 +++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/AppKit/CPTextField.j b/AppKit/CPTextField.j index 03416af31..1bf0a751e 100644 --- a/AppKit/CPTextField.j +++ b/AppKit/CPTextField.j @@ -68,7 +68,11 @@ var CPTextFieldDOMCurrentElement = nil, CPTextFieldCachedDragFunction = nil, CPTextFieldBlurHandler = nil, CPTextFieldInputFunction = nil, - CPTexFieldCurrentCSSSelectableField = nil; + CPTexFieldCurrentCSSSelectableField = nil + CPTextFieldLastValidationFailureEvent = nil, + CPTextFieldLastValidationFailureString = nil, + CPTextFieldLastValidationFailureField = nil, + CPTextFieldLastValidationFailureResult = NO; var CPSecureTextFieldCharacter = "\u2022"; @@ -767,33 +771,27 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); verticalAlign = [self currentValueForThemeAttribute:"vertical-alignment"], left = CGRectGetMinX(contentRect); - // If the browser has a built in left padding, compensate for it. We need the input text to be exactly on top of the original text. - if (CPFeatureIsCompatible(CPInput1PxLeftPadding)) - left -= 1; - switch (verticalAlign) { case CPTopVerticalTextAlignment: - var topPoint = CGRectGetMinY(contentRect) + "px"; + var topPoint = CEIL(CGRectGetMinY(contentRect)) + "px"; break; case CPCenterVerticalTextAlignment: - var topPoint = (CGRectGetMidY(contentRect) - (lineHeight / 2)) + "px"; + var topPoint = CEIL((CGRectGetMidY(contentRect) - (lineHeight / 2))) + "px"; break; case CPBottomVerticalTextAlignment: - var topPoint = (CGRectGetMaxY(contentRect) - lineHeight) + "px"; + var topPoint = CEIL((CGRectGetMaxY(contentRect) - lineHeight)) + "px"; break; default: - var topPoint = CGRectGetMinY(contentRect) + "px"; + var topPoint = CEIL(CGRectGetMinY(contentRect)) + "px"; break; } - if ([self hasThemeState:CPTextFieldStatePlaceholder]) - element.style.color = [[self valueForThemeAttribute:@"text-color" inState:CPTextFieldStatePlaceholder] cssString]; - else - element.style.color = [[self valueForThemeAttribute:@"text-color" inState:CPThemeStateEditing] cssString]; + // Use currentValueForThemeAttribute to respect all current states (HUD, Placeholder, Editing, etc.) + element.style.color = [[self currentValueForThemeAttribute:@"text-color"] cssString]; switch ([self alignment]) { @@ -997,7 +995,26 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); var acceptInvalidValue = NO; if (_implementedDelegateMethods & CPTextFieldDelegate_control_didFailToFormatString_errorDescription_) - acceptInvalidValue = [_delegate control:self didFailToFormatString:aValue errorDescription:error]; + { + var currentEvent = [CPApp currentEvent]; + + if (currentEvent && + CPTextFieldLastValidationFailureField === self && + CPTextFieldLastValidationFailureString === aValue && + CPTextFieldLastValidationFailureEvent === currentEvent) + { + acceptInvalidValue = CPTextFieldLastValidationFailureResult; + } + else + { + acceptInvalidValue = [_delegate control:self didFailToFormatString:aValue errorDescription:error]; + + CPTextFieldLastValidationFailureField = self; + CPTextFieldLastValidationFailureString = aValue; + CPTextFieldLastValidationFailureEvent = currentEvent; + CPTextFieldLastValidationFailureResult = acceptInvalidValue; + } + } if (acceptInvalidValue === NO) return NO; @@ -1272,6 +1289,16 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); [super textDidChange:note]; } +- (void)validateEditing +{ +#if PLATFORM(DOM) + var element = [self _inputElement]; + + if (element) + [self _setStringValue:element.value isNewValue:YES errorDescription:nil]; +#endif +} + - (void)textDidBeginEditing:(CPNotification)note { //this looks to prevent false propagation of notifications for other objects From 837df80b64db702ef36d0f78dc898822b92e78ee Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 14:33:31 +0100 Subject: [PATCH 082/136] fixed: accidental code spill over --- AppKit/CPTextField.j | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/AppKit/CPTextField.j b/AppKit/CPTextField.j index 1bf0a751e..3c2daa367 100644 --- a/AppKit/CPTextField.j +++ b/AppKit/CPTextField.j @@ -68,7 +68,7 @@ var CPTextFieldDOMCurrentElement = nil, CPTextFieldCachedDragFunction = nil, CPTextFieldBlurHandler = nil, CPTextFieldInputFunction = nil, - CPTexFieldCurrentCSSSelectableField = nil + CPTexFieldCurrentCSSSelectableField = nil, CPTextFieldLastValidationFailureEvent = nil, CPTextFieldLastValidationFailureString = nil, CPTextFieldLastValidationFailureField = nil, @@ -336,8 +336,7 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); characters:nil charactersIgnoringModifiers:nil isARepeat:NO - keyCode:nil - isActionKey:NO]; + keyCode:nil]; [CPTextFieldInputOwner keyUp:cappEvent]; @@ -771,27 +770,33 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); verticalAlign = [self currentValueForThemeAttribute:"vertical-alignment"], left = CGRectGetMinX(contentRect); + // If the browser has a built in left padding, compensate for it. We need the input text to be exactly on top of the original text. + if (CPFeatureIsCompatible(CPInput1PxLeftPadding)) + left -= 1; + switch (verticalAlign) { case CPTopVerticalTextAlignment: - var topPoint = CEIL(CGRectGetMinY(contentRect)) + "px"; + var topPoint = CGRectGetMinY(contentRect) + "px"; break; case CPCenterVerticalTextAlignment: - var topPoint = CEIL((CGRectGetMidY(contentRect) - (lineHeight / 2))) + "px"; + var topPoint = (CGRectGetMidY(contentRect) - (lineHeight / 2)) + "px"; break; case CPBottomVerticalTextAlignment: - var topPoint = CEIL((CGRectGetMaxY(contentRect) - lineHeight)) + "px"; + var topPoint = (CGRectGetMaxY(contentRect) - lineHeight) + "px"; break; default: - var topPoint = CEIL(CGRectGetMinY(contentRect)) + "px"; + var topPoint = CGRectGetMinY(contentRect) + "px"; break; } - // Use currentValueForThemeAttribute to respect all current states (HUD, Placeholder, Editing, etc.) - element.style.color = [[self currentValueForThemeAttribute:@"text-color"] cssString]; + if ([self hasThemeState:CPTextFieldStatePlaceholder]) + element.style.color = [[self valueForThemeAttribute:@"text-color" inState:CPTextFieldStatePlaceholder] cssString]; + else + element.style.color = [[self valueForThemeAttribute:@"text-color" inState:CPThemeStateEditing] cssString]; switch ([self alignment]) { @@ -1289,16 +1294,6 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); [super textDidChange:note]; } -- (void)validateEditing -{ -#if PLATFORM(DOM) - var element = [self _inputElement]; - - if (element) - [self _setStringValue:element.value isNewValue:YES errorDescription:nil]; -#endif -} - - (void)textDidBeginEditing:(CPNotification)note { //this looks to prevent false propagation of notifications for other objects From 9aaa5656576e1d0370d3885e874eb2e3ad4c10a0 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 14:40:34 +0100 Subject: [PATCH 083/136] fixed: accidental code spillover --- AppKit/CPTextField.j | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AppKit/CPTextField.j b/AppKit/CPTextField.j index 3c2daa367..7b2f03590 100644 --- a/AppKit/CPTextField.j +++ b/AppKit/CPTextField.j @@ -336,7 +336,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); characters:nil charactersIgnoringModifiers:nil isARepeat:NO - keyCode:nil]; + keyCode:nil + isActionKey:NO]; [CPTextFieldInputOwner keyUp:cappEvent]; From c519784ecc2ea669d083ae79cad1e53a2b1e5bc8 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 16:17:36 +0100 Subject: [PATCH 084/136] New: support for right-aligned buttons in CPButtonBar (#745) --- AppKit/CPButtonBar.j | 88 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 16 deletions(-) diff --git a/AppKit/CPButtonBar.j b/AppKit/CPButtonBar.j index 28b17538d..44aba3384 100644 --- a/AppKit/CPButtonBar.j +++ b/AppKit/CPButtonBar.j @@ -36,6 +36,7 @@ BOOL _hasResizeControl; BOOL _resizeControlIsLeftAligned; CPArray _buttons; + CPArray _rightButtons; } + (id)plusButton @@ -104,6 +105,7 @@ if (self) { _buttons = []; + _rightButtons = []; [self setNeedsLayout]; } @@ -157,6 +159,30 @@ return [CPArray arrayWithArray:_buttons]; } +- (void)setRightButtons:(CPArray)buttons +{ + for (var i = [_rightButtons count] - 1; i >= 0; i--) + { + [_rightButtons[i] removeFromSuperview]; + [_rightButtons[i] removeObserver:self forKeyPath:@"hidden"]; + } + + _rightButtons = [CPArray arrayWithArray:buttons]; + + for (var i = [_rightButtons count] - 1; i >= 0; i--) + { + [_rightButtons[i] addObserver:self forKeyPath:@"hidden" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:nil]; + [_rightButtons[i] setBordered:YES]; + } + + [self setNeedsLayout]; +} + +- (CPArray)rightButtons +{ + return [CPArray arrayWithArray:_rightButtons]; +} + - (void)setHasResizeControl:(BOOL)shouldHaveResizeControl { if (_hasResizeControl === shouldHaveResizeControl) @@ -243,46 +269,73 @@ } } - var currentButtonOffset = _resizeControlIsLeftAligned ? CGRectGetMaxX([self bounds]) + 1 : -1, - bounds = [self bounds], + var rightButtonsNotHidden = [CPArray arrayWithArray:_rightButtons], + rightCount = [rightButtonsNotHidden count]; + + while (rightCount--) + { + var button = rightButtonsNotHidden[rightCount]; + + if ([button isHidden]) + { + [button removeFromSuperview]; + [rightButtonsNotHidden removeObject:button]; + } + } + + var bounds = [self bounds], height = CGRectGetHeight(bounds) - 1, frameWidth = CGRectGetWidth(bounds), resizeRect = _hasResizeControl ? [self rectForEphemeralSubviewNamed:"resize-control-view"] : CGRectMakeZero(), resizeWidth = CGRectGetWidth(resizeRect), availableWidth = frameWidth - resizeWidth - 1; - for (var i = 0, count = [buttonsNotHidden count]; i < count; i++) + var currentLeftOffset = _resizeControlIsLeftAligned ? resizeWidth - 1 : -1, + currentRightOffset = _resizeControlIsLeftAligned ? CGRectGetMaxX(bounds) + 1 : CGRectGetMaxX(bounds) - resizeWidth + 1; + + var setupButton = function(button, isRightAligned) { - var button = buttonsNotHidden[i], - width = CGRectGetWidth([button frame]); + var width = CGRectGetWidth([button frame]); if (availableWidth > width) availableWidth -= width; else - break; + return NO; - if (_resizeControlIsLeftAligned) + if (isRightAligned) { - [button setFrame:CGRectMake(currentButtonOffset - width, 1, width, height)]; - currentButtonOffset -= width - 1; + [button setFrame:CGRectMake(currentRightOffset - width, 1, width, height)]; + currentRightOffset -= width - 1; } else - { - [button setFrame:CGRectMake(currentButtonOffset, 1, width, height)]; - currentButtonOffset += width - 1; + {[button setFrame:CGRectMake(currentLeftOffset, 1, width, height)]; + currentLeftOffset += width - 1; } [button setValue:normalColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateNormal, CPThemeStateBordered]]; - [button setValue:highlightedColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateHighlighted, CPThemeStateBordered, ]]; + [button setValue:highlightedColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateHighlighted, CPThemeStateBordered]]; [button setValue:disabledColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateDisabled, CPThemeStateBordered]]; [button setValue:textColor forThemeAttribute:@"text-color" inState:CPThemeStateBordered]; - // FIXME shouldn't need this - [button setValue:normalColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateNormal, CPThemeStateBordered, CPPopUpButtonStatePullsDown]]; + // FIXME shouldn't need this[button setValue:normalColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateNormal, CPThemeStateBordered, CPPopUpButtonStatePullsDown]]; [button setValue:highlightedColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateHighlighted, CPThemeStateBordered, CPPopUpButtonStatePullsDown]]; [button setValue:disabledColor forThemeAttribute:@"bezel-color" inStates:[CPThemeStateDisabled, CPThemeStateBordered, CPPopUpButtonStatePullsDown]]; [self addSubview:button]; + + return YES; + }; + + for (var i = 0, count = [buttonsNotHidden count]; i < count; i++) + { + if (!setupButton(buttonsNotHidden[i], _resizeControlIsLeftAligned)) + break; + } + + for (var i = 0, count = [rightButtonsNotHidden count]; i < count; i++) + { + if (!setupButton(rightButtonsNotHidden[i], YES)) + break; } if (_hasResizeControl) @@ -314,7 +367,8 @@ var CPButtonBarHasResizeControlKey = @"CPButtonBarHasResizeControlKey", CPButtonBarResizeControlIsLeftAlignedKey = @"CPButtonBarResizeControlIsLeftAlignedKey", - CPButtonBarButtonsKey = @"CPButtonBarButtonsKey"; + CPButtonBarButtonsKey = @"CPButtonBarButtonsKey", + CPButtonBarRightButtonsKey = @"CPButtonBarRightButtonsKey"; @implementation CPButtonBar (CPCoding) @@ -325,6 +379,7 @@ var CPButtonBarHasResizeControlKey = @"CPButtonBarHasResizeControlKey", [aCoder encodeBool:_hasResizeControl forKey:CPButtonBarHasResizeControlKey]; [aCoder encodeBool:_resizeControlIsLeftAligned forKey:CPButtonBarResizeControlIsLeftAlignedKey]; [aCoder encodeObject:_buttons forKey:CPButtonBarButtonsKey]; + [aCoder encodeObject:_rightButtons forKey:CPButtonBarRightButtonsKey]; } - (id)initWithCoder:(CPCoder)aCoder @@ -332,6 +387,7 @@ var CPButtonBarHasResizeControlKey = @"CPButtonBarHasResizeControlKey", if (self = [super initWithCoder:aCoder]) { _buttons = [aCoder decodeObjectForKey:CPButtonBarButtonsKey] || []; + _rightButtons = [aCoder decodeObjectForKey:CPButtonBarRightButtonsKey] || []; _hasResizeControl = [aCoder decodeBoolForKey:CPButtonBarHasResizeControlKey]; _resizeControlIsLeftAligned = [aCoder decodeBoolForKey:CPButtonBarResizeControlIsLeftAlignedKey]; } From 96d3ad2c47d11ea730f89053134b178e8b2d9986 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 17:11:25 +0100 Subject: [PATCH 085/136] fixed: CPTokenField autocomplete when placed in a CPToolbar --- AppKit/CPTokenField.j | 2 +- AppKit/CPToolbar.j | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 61ed7d3dd..c69d481ca 100644 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -551,7 +551,7 @@ CPTokenFieldDeleteButtonType = 1; CPTokenFieldCachedDragFunction = nil; document.body.ondrag = CPTokenFieldCachedDragFunction; - document.body.onselectstart = CPTokenFieldCachedSelectStartFunction + document.body.onselectstart = CPTokenFieldCachedSelectStartFunction; } #endif diff --git a/AppKit/CPToolbar.j b/AppKit/CPToolbar.j index c60276b64..0be382d0a 100644 --- a/AppKit/CPToolbar.j +++ b/AppKit/CPToolbar.j @@ -1021,6 +1021,22 @@ var LABEL_MARGIN = 2.0; BOOL _FIXME_isHUD; } +- (BOOL)acceptsFirstResponder +{ + if (_view && [_view acceptsFirstResponder]) + return YES; + + return NO; +} + +- (BOOL)becomeFirstResponder +{ + if (_view && [_view acceptsFirstResponder]) + return [[self window] makeFirstResponder:_view]; + + return [super becomeFirstResponder]; +} + - (id)initWithToolbarItem:(CPToolbarItem)aToolbarItem toolbar:(CPToolbar)aToolbar { self = [super init]; From 4e5c0aed67531d68d3fe1aa44b2b9957e35f10bd Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 17:29:49 +0100 Subject: [PATCH 086/136] formatting --- AppKit/CPColorPanel.j | 2 +- AppKit/CPColorWell.j | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/AppKit/CPColorPanel.j b/AppKit/CPColorPanel.j index e94da6970..86224946b 100644 --- a/AppKit/CPColorPanel.j +++ b/AppKit/CPColorPanel.j @@ -51,7 +51,7 @@ CPColorPickerViewWidth = 265; CPColorPickerViewHeight = 370; CPColorPanelColorDidChangeNotification = @"CPColorPanelColorDidChangeNotification"; -CPColorDragType = "CPColorDragType"; +CPColorDragType = @"CPColorDragType"; var PREVIEW_HEIGHT = 20.0, TOOLBAR_HEIGHT = 32.0, diff --git a/AppKit/CPColorWell.j b/AppKit/CPColorWell.j index 60b789440..765cb47d4 100644 --- a/AppKit/CPColorWell.j +++ b/AppKit/CPColorWell.j @@ -308,12 +308,14 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv - (CGRect)bezelRectForBounds:(CGRect)bounds { var bezelInset = [self currentValueForThemeAttribute:@"bezel-inset"]; + return CGRectInsetByInset(bounds, bezelInset); } - (CGRect)contentBorderRectForBounds:(CGRect)bounds { var contentBorderInset = [self currentValueForThemeAttribute:@"content-border-inset"]; + return CGRectInsetByInset(bounds, contentBorderInset); } @@ -335,7 +337,9 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv - (CPView)createEphemeralSubviewNamed:(CPString)aName { var view = [[CPView alloc] initWithFrame:CGRectMakeZero()]; + [view setHitTests:NO]; + return view; } @@ -352,7 +356,7 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv relativeToEphemeralSubviewNamed:@"bezel-view"]; [contentView setBackgroundColor:_color]; - + var contentBorderView = [self layoutEphemeralSubviewNamed:@"content-border-view" positioned:CPWindowAbove relativeToEphemeralSubviewNamed:@"content-view"]; From 622d738b44a7824a7cc50dd504c5c49e8f156e80 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 7 Mar 2026 19:25:32 +0100 Subject: [PATCH 087/136] 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 088/136] 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 089/136] 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 090/136] 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 091/136] 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 092/136] 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 093/136] 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 094/136] 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 095/136] 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 096/136] 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 097/136] 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], From fdba5f55c5db3ec16b23437ce026c568547b31b5 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 8 Mar 2026 21:16:54 +0100 Subject: [PATCH 098/136] fixed> syntax error --- AppKit/CPOutlineView.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index f9b74d3fb..d13bdd1fe 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -252,7 +252,7 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, Theme states for custom data views are documented in CPTableView. */ -@implementation CPOutlineView : CPTableView +@implementation CPOutlineView : CPTableView { id _outlineViewDataSource; id _outlineViewDelegate; From f223644640e79e757ead6cf3be0e45ebcfae4c75 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 11 Mar 2026 06:26:10 +0100 Subject: [PATCH 099/136] fixed: canvas text sizing is not functional anymore --- AppKit/CPStringDrawing.j | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/AppKit/CPStringDrawing.j b/AppKit/CPStringDrawing.j index c94f9752b..81cdca94d 100644 --- a/AppKit/CPStringDrawing.j +++ b/AppKit/CPStringDrawing.j @@ -69,13 +69,9 @@ CPCanvasStringSizingIsFunctional = NO; CPStringSizeMeasuringContext = CGBitmapGraphicsContextCreate(); // This is to make sure that Canvas based string sizing is functional before we use it. - // Currently, Chrome has issues with certain strings, FF had issues in the past. - // Unfortunately, this test does fit in CPCompatibility.j where things are not sufficiently initialized. - - var testingFont = [CPFont systemFontOfSize:12]; - var testingText = 'A A A A A A A A'; - CPStringSizeMeasuringContext.font = [testingFont cssString]; - CPCanvasStringSizingIsFunctional = ROUND(CPStringSizeMeasuringContext.measureText(testingText).width) == ROUND([CPPlatformString sizeOfString:testingText withFont:testingFont forWidth:NULL].width); + // Unfortunately, as of today canvas sizing is not funtional any more. Neither in Chrome nor in FF + + CPCanvasStringSizingIsFunctional = NO; } #endif } From 5b820b270c0004049dd4d948020cf007d05b625e Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 13 Mar 2026 16:03:30 +0100 Subject: [PATCH 100/136] fixed: smartpaste crash --- AppKit/CPTextView/CPTextView.j | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 43b0a7a8b..1ea9dbef6 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -475,6 +475,9 @@ var kDelegateRespondsTo_textShouldBeginEditing if (!stringForPasting) return; + if (![stringForPasting isKindOfClass:[CPAttributedString class]]) + stringForPasting = [[CPAttributedString alloc] initWithString:stringForPasting attributes:nil]; + var shouldUseSmartPasting = [self _shouldUseSmartPasting]; if (shouldUseSmartPasting) From b39c3e8d4b5f4fb1b7d0d32b5469dd77945cf3ff Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 14 Mar 2026 13:01:58 +0100 Subject: [PATCH 101/136] fixed: caret is invisible at the end of the line --- AppKit/CPTextView/CPTextView.j | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 1ea9dbef6..599b587cb 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2298,6 +2298,9 @@ Sets the selection to a range of characters in response to user action. caretRect.origin.x += _textContainerOrigin.x; caretRect.origin.y += _textContainerOrigin.y; + caretRect.size.width = MAX(1.0, caretRect.size.width); + caretRect.size.height = MAX(1.0, caretRect.size.height); + return caretRect; } - (void)updateInsertionPointStateAndRestartTimer:(BOOL)flag From 6671d371e95c46935f94e0d0a85f7929ca172dab Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 15 Mar 2026 08:51:03 +0100 Subject: [PATCH 102/136] fixed: treeview binding issues --- AppKit/CPOutlineView.j | 72 +++++++++++++++--------------------------- AppKit/CPTableView.j | 23 ++++++++++---- 2 files changed, 42 insertions(+), 53 deletions(-) diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index d13bdd1fe..0380ad91d 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -2413,7 +2413,7 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) - (void)setValueFor:(CPString)aBinding { var destination = [_info objectForKey:CPObservedObjectKey], - keyPath =[_info objectForKey:CPObservedKeyPathKey], + keyPath = [_info objectForKey:CPObservedKeyPathKey], value = [destination valueForKeyPath:keyPath]; if (!value || ![value isKindOfClass:[CPTreeNode class]]) @@ -2421,19 +2421,10 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) else _rootNode = value; - // Because CPBinder triggers setValueFor: synchronously during its initialization - // (before -bind is ever called), we must lazily assign the data source here. if ([_source dataSource] !== self) - { - // Assigning the data source automatically triggers [_source reloadData] - // inside CPOutlineView, so we don't need to call it manually here. [_source setDataSource:self]; - } else - { - // If it was already set, we just manually trigger the reload. [_source reloadData]; - } } - (CPTreeNode)rootNode @@ -2463,10 +2454,31 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) - (id)outlineView:(CPOutlineView)outlineView objectValueForTableColumn:(CPTableColumn)tableColumn byItem:(id)item { - if ([item respondsToSelector:@selector(representedObject)]) - return [item representedObject]; + 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] &&[tableColumn identifier] !== @"") + return [rep valueForKey:[tableColumn identifier]]; - return item; + return rep; +} + +- (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] && [tableColumn identifier] !== @"") + [rep setValue:value forKey:[tableColumn identifier]]; +} + +- (id)content +{ + // CPTableView internals probe the binder for its flat content to draw rows. + if (_source && _source._itemsForRows) + return _source._itemsForRows; + + return []; } @end @@ -2566,37 +2578,3 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) @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/CPTableView.j b/AppKit/CPTableView.j index fcef50846..c37356ef7 100644 --- a/AppKit/CPTableView.j +++ b/AppKit/CPTableView.j @@ -3627,17 +3627,28 @@ Your delegate can implement this method to avoid subclassing the tableview to ad - (void)_setObjectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow forView:(CPView)aDataView useCache:(BOOL)useCache { + var providedByDataSource = NO; + if (_implementedDataSourceMethods & CPTableViewDataSource_tableView_objectValueForTableColumn_row_) - [aDataView setObjectValue:[self _objectValueForTableColumn:aTableColumn row:aRow useCache:useCache]]; + { + var objectValue =[self _objectValueForTableColumn:aTableColumn row:aRow useCache:useCache]; + [aDataView setObjectValue:objectValue]; + providedByDataSource = YES; + } // This gives the table column an opportunity to apply its bindings. - // It will override the value set above if there is a binding. + // It will override the value set above if there is an explicit column binding. + var columnHasBindings = [[[CPBinder allBindingsForObject:aTableColumn] allKeys] count] > 0; - if (_contentBindingExplicitlySet) - [self _prepareContentBindedDataView:aDataView forRow:aRow]; - else - // For both cell-based and view-based + if (columnHasBindings) + { [aTableColumn _prepareDataView:aDataView forRow:aRow]; + } + // Only forcefully bind the raw content object if the data source didn't already provide a formatted value + else if (_contentBindingExplicitlySet && !providedByDataSource) + { + [self _prepareContentBindedDataView:aDataView forRow:aRow]; + } } - (void)_prepareContentBindedDataView:(CPView)dataView forRow:(CPInteger)aRow From 43936f640ce38bcbc7a26da50a53bfdb6ad71978 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 15 Mar 2026 08:53:04 +0100 Subject: [PATCH 103/136] formatting --- AppKit/CPOutlineView.j | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index 0380ad91d..8b00173d3 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -2454,12 +2454,12 @@ var colorForDisclosureTriangle = function(isSelected, isHighlighted) - (id)outlineView:(CPOutlineView)outlineView objectValueForTableColumn:(CPTableColumn)tableColumn byItem:(id)item { - var rep =[item respondsToSelector:@selector(representedObject)] ? [item representedObject] : 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] &&[tableColumn identifier] !== @"") + if (rep && [tableColumn identifier] && [tableColumn identifier] !== @"") return [rep valueForKey:[tableColumn identifier]]; - + return rep; } From b77a374f485f6b2dd0a386a7c770a1e9a97e93dd Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 15 Mar 2026 08:57:06 +0100 Subject: [PATCH 104/136] formatting --- AppKit/CPTableView.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPTableView.j b/AppKit/CPTableView.j index c37356ef7..5512e974a 100644 --- a/AppKit/CPTableView.j +++ b/AppKit/CPTableView.j @@ -3631,7 +3631,7 @@ Your delegate can implement this method to avoid subclassing the tableview to ad if (_implementedDataSourceMethods & CPTableViewDataSource_tableView_objectValueForTableColumn_row_) { - var objectValue =[self _objectValueForTableColumn:aTableColumn row:aRow useCache:useCache]; + var objectValue = [self _objectValueForTableColumn:aTableColumn row:aRow useCache:useCache]; [aDataView setObjectValue:objectValue]; providedByDataSource = YES; } From ea4adf1039609ea5193e7fae9930cc11e7aa84d6 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 15 Mar 2026 17:08:21 +0100 Subject: [PATCH 105/136] fixed: racecondition in row expansion animation --- AppKit/CPOutlineView.j | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index 8b00173d3..25d28913f 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -786,6 +786,13 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, */ - (void)reloadItem:(id)anItem reloadChildren:(BOOL)shouldReloadChildren { + + if (_rowsAnimation && [_rowsAnimation isAnimating]) + { + [_rowsAnimation stopAnimation]; + [self animationDidEnd:_rowsAnimation]; + } + _pendingItemToClean = []; _itemAddedDuringLastLoading = []; From 5e297dd29301f21cd97efd42ac0db22c793a0d97 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 15 Mar 2026 17:39:33 +0100 Subject: [PATCH 106/136] removed: erroneous code spillover --- AppKit/CPOutlineView.j | 6 ------ 1 file changed, 6 deletions(-) diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index 25d28913f..0ffc88505 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -787,12 +787,6 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, - (void)reloadItem:(id)anItem reloadChildren:(BOOL)shouldReloadChildren { - if (_rowsAnimation && [_rowsAnimation isAnimating]) - { - [_rowsAnimation stopAnimation]; - [self animationDidEnd:_rowsAnimation]; - } - _pendingItemToClean = []; _itemAddedDuringLastLoading = []; From d39a583127227e0e804653fea3996122ef0e1ca0 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 24 Mar 2026 21:18:43 +0100 Subject: [PATCH 107/136] fixed: locale inference --- Foundation/CPLocale.j | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Foundation/CPLocale.j b/Foundation/CPLocale.j index c506ce299..493c62894 100644 --- a/Foundation/CPLocale.j +++ b/Foundation/CPLocale.j @@ -99,13 +99,21 @@ var sharedSystemLocale = nil, if (language) { - // Browsers use locale strings such as "en-US", but CPLocale uses "en_US". - language = language.replace("-", "_").substring(0, 5); - // Some browsers have "en_us" at this point, while we want "en_US". - language = language.substring(0, 3).toLowerCase() + language.substring(3, 5).toUpperCase(); + // Split the BCP 47 tag into parts (e.g.,["zh", "Hans", "CN"] or ["en", "US"] or ["fr"]) + var parts = language.split("-"); - if ([availableLocaleIdentifiers indexOfObject:language] !== CPNotFound) - localeIdentifier = language; + if (parts.length > 1) + { + // Grab the language and the LAST part (the region code) + var langCode = parts[0].toLowerCase(); + var regionCode = parts[parts.length - 1].toUpperCase(); + language = langCode + "_" + regionCode; + } + else + { + // It's just a 2-letter code, keep it as is (or map it if your app strictly requires xx_XX) + language = parts[0].toLowerCase(); + } } } From 63e3f6c3fc98fd0c40b9aa3f05a5ccfbc873ea21 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 24 Mar 2026 21:33:24 +0100 Subject: [PATCH 108/136] fixed single region of FF --- Foundation/CPLocale.j | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/Foundation/CPLocale.j b/Foundation/CPLocale.j index 493c62894..d27e20652 100644 --- a/Foundation/CPLocale.j +++ b/Foundation/CPLocale.j @@ -90,7 +90,7 @@ var sharedSystemLocale = nil, if (!sharedCurrentLocale) { var localeIdentifier = @"en_US", - language; + language; if (typeof navigator !== "undefined") { @@ -104,15 +104,32 @@ var sharedSystemLocale = nil, if (parts.length > 1) { - // Grab the language and the LAST part (the region code) + // Chrome behavior: e.g., "de-DE" or "en-US" var langCode = parts[0].toLowerCase(); var regionCode = parts[parts.length - 1].toUpperCase(); - language = langCode + "_" + regionCode; + + // Update localeIdentifier! + localeIdentifier = langCode + "_" + regionCode; } else { - // It's just a 2-letter code, keep it as is (or map it if your app strictly requires xx_XX) - language = parts[0].toLowerCase(); + // Firefox behavior: e.g., "de", "fr", "en" + var langCode = parts[0].toLowerCase(); + + // Create a mapping of 2-letter language codes to default region codes + var defaultRegions = { + "de": "DE", + "en": "US", + "es": "ES", + "fr": "FR", + "sv": "SE" + }; + + // Look up the default region, or fallback to the uppercased language code (e.g. "it" -> "IT") + var regionCode = defaultRegions[langCode] || langCode.toUpperCase(); + + // Update localeIdentifier! + localeIdentifier = langCode + "_" + regionCode; } } } From c7e89d301861a4174245e4d9be950f8d8410bea1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 24 Mar 2026 21:34:39 +0100 Subject: [PATCH 109/136] formatting --- Foundation/CPLocale.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Foundation/CPLocale.j b/Foundation/CPLocale.j index d27e20652..a51bb04b0 100644 --- a/Foundation/CPLocale.j +++ b/Foundation/CPLocale.j @@ -90,7 +90,7 @@ var sharedSystemLocale = nil, if (!sharedCurrentLocale) { var localeIdentifier = @"en_US", - language; + language; if (typeof navigator !== "undefined") { From 043029cf41b2fd4505dd16b8ba5c015f9a12b880 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 3 Apr 2026 13:30:13 +0200 Subject: [PATCH 110/136] new: _CPKVOCollectionOperatorObserver --- Foundation/CPKeyValueObserving.j | 176 ++++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 2 deletions(-) diff --git a/Foundation/CPKeyValueObserving.j b/Foundation/CPKeyValueObserving.j index f789fcbf9..3d3aa4315 100644 --- a/Foundation/CPKeyValueObserving.j +++ b/Foundation/CPKeyValueObserving.j @@ -763,9 +763,12 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti if (!anObserver) return; - var forwarder = nil; + var forwarder = nil, + collectionOperatorMatch = aPath.match(/^(.*)\.(@\w+)(?:\.(.*))?$/); - if (aPath.indexOf('.') !== CPNotFound && aPath.charAt(0) !== '@') + if (collectionOperatorMatch) + forwarder = [[_CPKVOCollectionOperatorObserver alloc] initWithTarget:_targetObject observer:anObserver keyPath:aPath options:options context:aContext]; + else if (aPath.indexOf('.') !== CPNotFound && aPath.charAt(0) !== '@') forwarder = [[_CPKVOForwardingObserver alloc] initWithKeyPath:aPath object:_targetObject observer:anObserver options:options context:aContext]; else [self _replaceModifiersForKey:aPath]; @@ -1265,6 +1268,175 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti @end + +@implementation _CPKVOCollectionOperatorObserver : CPObject +{ + id _target; + id _originalObserver; + CPString _fullKeyPath; + unsigned _options; + id _context; + + CPString _collectionKeyPath; + CPString _operator; + CPString _valueKeyPath; + + CPArray _observedItems; +} + +- (id)initWithTarget:(id)aTarget observer:(id)anObserver keyPath:(CPString)aKeyPath options:(unsigned)options context:(id)aContext +{ + self = [super init]; + + if (self) + { + _target = aTarget; + _originalObserver = anObserver; + _fullKeyPath = aKeyPath; + _options = options; + _context = aContext; + _observedItems = [CPArray array]; + + // Parse key path like "collectionKeyPath.@sum.valueKeyPath" + // Captures: 1=collectionKeyPath, 2=@sum, 3=valueKeyPath + var match = aKeyPath.match(/^(.*)\.(@\w+)(?:\.(.*))?$/); + if (match) + { + _collectionKeyPath = match[1]; + _operator = match[2]; + _valueKeyPath = match[3]; + } + + // Observe the collection itself on the target to catch array resets/mutations + [_target addObserver:self forKeyPath:_collectionKeyPath options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:nil]; + + [self _setupItemObservers]; + } + + return self; +} + +- (void)_setupItemObservers +{ + var collection = [_target valueForKeyPath:_collectionKeyPath]; + + if (collection && [collection respondsToSelector:@selector(objectEnumerator)]) + { + var enumerator = [collection objectEnumerator], + item; + + while ((item = [enumerator nextObject]) !== nil) + { + if (_valueKeyPath && [item respondsToSelector:@selector(addObserver:forKeyPath:options:context:)]) + [item addObserver:self forKeyPath:_valueKeyPath options:_options context:nil]; + + [_observedItems addObject:item]; + } + } +} + +- (void)_tearDownItemObservers +{ + if (_valueKeyPath) + { + var count = [_observedItems count]; + while (count--) + { + var item = [_observedItems objectAtIndex:count]; + if ([item respondsToSelector:@selector(removeObserver:forKeyPath:context:)]) + [item removeObserver:self forKeyPath:_valueKeyPath context:nil]; + } + } + + [_observedItems removeAllObjects]; +} + +- (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context +{ + // Branch 1: The collection on the target was modified + if (object === _target && [keyPath isEqualToString:_collectionKeyPath]) + { + var kind = [change objectForKey:CPKeyValueChangeKindKey]; + + if (kind === CPKeyValueChangeSetting) + { + [self _tearDownItemObservers]; + [self _setupItemObservers]; + } + else if (kind === CPKeyValueChangeInsertion || kind === CPKeyValueChangeReplacement) + { + if (kind === CPKeyValueChangeReplacement) + { + var oldItems = [change objectForKey:CPKeyValueChangeOldKey]; + if (oldItems) + { + for (var i = 0, len = oldItems.length; i < len; i++) + { + var item = oldItems[i]; + if (_valueKeyPath && [item respondsToSelector:@selector(removeObserver:forKeyPath:context:)]) + [item removeObserver:self forKeyPath:_valueKeyPath context:nil]; + [_observedItems removeObject:item]; + } + } + } + + var newItems = [change objectForKey:CPKeyValueChangeNewKey]; + if (newItems) + { + for (var i = 0, len = newItems.length; i < len; i++) + { + var item = newItems[i]; + if (_valueKeyPath && [item respondsToSelector:@selector(addObserver:forKeyPath:options:context:)]) + [item addObserver:self forKeyPath:_valueKeyPath options:_options context:nil]; + [_observedItems addObject:item]; + } + } + } + else if (kind === CPKeyValueChangeRemoval) + { + var oldItems = [change objectForKey:CPKeyValueChangeOldKey]; + if (oldItems) + { + for (var i = 0, len = oldItems.length; i < len; i++) + { + var item = oldItems[i]; + if (_valueKeyPath && [item respondsToSelector:@selector(removeObserver:forKeyPath:context:)]) + [item removeObserver:self forKeyPath:_valueKeyPath context:nil]; + [_observedItems removeObject:item]; + } + } + } + + [self _notifyOriginalObserver]; + } + // Branch 2: One of the children items has updated + else if (_valueKeyPath && [keyPath isEqualToString:_valueKeyPath] && [_observedItems containsObject:object]) + { + [self _notifyOriginalObserver]; + } +} + +- (void)_notifyOriginalObserver +{ + // Ask standard KVC to evaluate the entire aggregate string (e.g., @sum.value) + var newValue = [_target valueForKeyPath:_fullKeyPath], + change = @{ + CPKeyValueChangeKindKey: CPKeyValueChangeSetting, + CPKeyValueChangeNewKey: (newValue !== nil ? newValue : [CPNull null]) + }; + + [_originalObserver observeValueForKeyPath:_fullKeyPath ofObject:_target change:change context:_context]; +} + +- (void)finalize +{ + [_target removeObserver:self forKeyPath:_collectionKeyPath]; + [self _tearDownItemObservers]; +} + +@end + + @implementation _CPKVOForwardingObserver : CPObject { id _object; From 4b81001330e83c51d62ad16dec023008decba8f4 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 3 Apr 2026 13:33:06 +0200 Subject: [PATCH 111/136] new: testCollectionOperatorKeyPath --- Tests/Foundation/CPKVOTest.j | 63 ++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/Tests/Foundation/CPKVOTest.j b/Tests/Foundation/CPKVOTest.j index 631e2ac80..b3215ec4e 100644 --- a/Tests/Foundation/CPKVOTest.j +++ b/Tests/Foundation/CPKVOTest.j @@ -3,6 +3,8 @@ @class CarTester @class ToManyTester +@class AccountTester +@class TransactionTester @implementation CPKVOTest : OJTestCase { @@ -436,6 +438,35 @@ [self assertTrue:_sawObservation message:@"Never recieved an observation"]; } +- (void)testCollectionOperatorKeyPath +{ + var account = [[AccountTester alloc] init], + t1 = [[TransactionTester alloc] initWithAmount:10.0], + t2 = [[TransactionTester alloc] initWithAmount:20.0]; + + [account setTransactions:[t1, t2]]; + + // 1. Test updating a child property triggers an aggregate update + [account addObserver:self forKeyPath:@"transactions.@sum.amount" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testCollectionOperatorKeyPath_ItemChange"]; + + _sawObservation = NO; + [t1 setAmount:15.0]; // KVC sum goes from 30.0 -> 35.0 + + [self assertTrue:_sawObservation message:@"Never received an observation when child item's property changed"]; + [account removeObserver:self forKeyPath:@"transactions.@sum.amount"]; + + // 2. Test replacing the whole array triggers an aggregate update + [account addObserver:self forKeyPath:@"transactions.@sum.amount" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testCollectionOperatorKeyPath_ArrayReplace"]; + + var t3 = [[TransactionTester alloc] initWithAmount:5.0]; + + _sawObservation = NO; + [account setTransactions:[t1, t2, t3]]; // KVC sum goes from 35.0 -> 40.0 + + [self assertTrue:_sawObservation message:@"Never received an observation when collection was replaced"]; + [account removeObserver:self forKeyPath:@"transactions.@sum.amount"]; +} + - (void)testPerformance { bob = [PersonTester new]; @@ -777,6 +808,16 @@ testNestedNotificationsBobCount += 1; break; + case "testCollectionOperatorKeyPath_ItemChange": + [self assert:newValue equals:35.0]; + [self assert:oldValue equals:30.0]; + break; + + case "testCollectionOperatorKeyPath_ArrayReplace": + [self assert:newValue equals:40.0]; + [self assert:oldValue equals:35.0]; + break; + default: [self assertFalse:YES message:@"unhandled observation, must be an error"]; return; @@ -955,3 +996,25 @@ } @end + +@implementation TransactionTester : CPObject +{ + float amount @accessors; +} + +- (id)initWithAmount:(float)anAmount +{ + self = [super init]; + if (self) { + amount = anAmount; + } + return self; +} + +@end + +@implementation AccountTester : CPObject +{ + CPArray transactions @accessors; +} +@end From 7fbea7913ad294172b83ba0156f74321aa640815 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 3 Apr 2026 13:39:42 +0200 Subject: [PATCH 112/136] new: manual test --- Tests/Manual/CPButtonBarTest/AppController.j | 104 +++++++++ Tests/Manual/CPButtonBarTest/Info.plist | 12 ++ Tests/Manual/CPButtonBarTest/Jakefile | 81 +++++++ .../CPButtonBarTest/Resources/spinner.gif | Bin 0 -> 1434 bytes Tests/Manual/CPButtonBarTest/index-debug.html | 204 ++++++++++++++++++ Tests/Manual/CPButtonBarTest/index.html | 166 ++++++++++++++ Tests/Manual/CPButtonBarTest/main.j | 18 ++ 7 files changed, 585 insertions(+) create mode 100644 Tests/Manual/CPButtonBarTest/AppController.j create mode 100644 Tests/Manual/CPButtonBarTest/Info.plist create mode 100644 Tests/Manual/CPButtonBarTest/Jakefile create mode 100644 Tests/Manual/CPButtonBarTest/Resources/spinner.gif create mode 100644 Tests/Manual/CPButtonBarTest/index-debug.html create mode 100644 Tests/Manual/CPButtonBarTest/index.html create mode 100644 Tests/Manual/CPButtonBarTest/main.j diff --git a/Tests/Manual/CPButtonBarTest/AppController.j b/Tests/Manual/CPButtonBarTest/AppController.j new file mode 100644 index 000000000..20e1af7ae --- /dev/null +++ b/Tests/Manual/CPButtonBarTest/AppController.j @@ -0,0 +1,104 @@ +/* + * AppController.j + * CPButtonBar Right Buttons Manual Test + */ + +@import +@import +@import +@import + +@implementation AppController : CPObject +{ + CPButtonBar buttonBar; +} + +- (void)applicationDidFinishLaunching:(CPNotification)aNotification +{ + var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask], + contentView = [theWindow contentView]; + + var bounds = [contentView bounds]; + + // 1. Create the ButtonBar + buttonBar = [[CPButtonBar alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(bounds) - 28, CGRectGetWidth(bounds), 28)]; + [buttonBar setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin]; + + // Add resize control to ensure bounds calculations are correct + [buttonBar setHasResizeControl:YES]; + + // 2. Setup standard Left Buttons + var plusButton = [CPButtonBar plusButton], + minusButton = [CPButtonBar minusButton]; + + [buttonBar setButtons:[plusButton, minusButton]]; + + // 3. Setup the new Right Buttons (PR #745) + var actionButton = [CPButtonBar actionPopupButton], + customRightBtn = [[CPButton alloc] initWithFrame:CGRectMake(0, 0, 70, 24)]; + + [customRightBtn setTitle:@"Settings"]; + [buttonBar setRightButtons:[customRightBtn, actionButton]]; + + [contentView addSubview:buttonBar]; + + // --- Testing Controls --- + + // Button to test resizing alignment layout + var toggleAlignmentBtn = [CPButton buttonWithTitle:@"Toggle Resize Control Alignment"]; + [toggleAlignmentBtn setFrameOrigin:CGPointMake(20, 20)]; + [toggleAlignmentBtn sizeToFit]; + [toggleAlignmentBtn setTarget:self]; + [toggleAlignmentBtn setAction:@selector(toggleResizeAlignment:)]; + [contentView addSubview:toggleAlignmentBtn]; + + // Button to test the KVO "hidden" observer on rightButtons + var toggleVisibilityBtn = [CPButton buttonWithTitle:@"Toggle Right Button Visibility"]; + [toggleVisibilityBtn setFrameOrigin:CGPointMake(20, 60)]; + [toggleVisibilityBtn sizeToFit]; + [toggleVisibilityBtn setTarget:self]; + [toggleVisibilityBtn setAction:@selector(toggleRightVisibility:)]; + [contentView addSubview:toggleVisibilityBtn]; + + // Button to test CPCoding archiving/unarchiving of rightButtons + var testArchivingBtn = [CPButton buttonWithTitle:@"Test Archiving (CPCoding)"]; + [testArchivingBtn setFrameOrigin:CGPointMake(20, 100)]; + [testArchivingBtn sizeToFit]; + [testArchivingBtn setTarget:self]; + [testArchivingBtn setAction:@selector(testArchiving:)]; + [contentView addSubview:testArchivingBtn]; + + [theWindow orderFront:self]; +} + +- (void)toggleResizeAlignment:(id)sender +{ + // Flipping the resize control to the left shouldn't break the right-aligned buttons + [buttonBar setResizeControlIsLeftAligned:![buttonBar resizeControlIsLeftAligned]]; +} + +- (void)toggleRightVisibility:(id)sender +{ + var rightBtns = [buttonBar rightButtons]; + if ([rightBtns count] > 0) + { + var btn = rightBtns[0]; + [btn setHidden:![btn isHidden]]; + } +} + +- (void)testArchiving:(id)sender +{ + // Test the new CPButtonBarRightButtonsKey in CPCoding + var data = [CPKeyedArchiver archivedDataWithRootObject:buttonBar]; + var unarchivedBar = [CPKeyedUnarchiver unarchiveObjectWithData:data]; + + var bounds = [[buttonBar superview] bounds]; + + // Place the cloned bar directly above the original one + [unarchivedBar setFrame:CGRectMake(0, CGRectGetHeight(bounds) - 56, CGRectGetWidth(bounds), 28)]; + + [[buttonBar superview] addSubview:unarchivedBar]; +} + +@end diff --git a/Tests/Manual/CPButtonBarTest/Info.plist b/Tests/Manual/CPButtonBarTest/Info.plist new file mode 100644 index 000000000..e9a691d12 --- /dev/null +++ b/Tests/Manual/CPButtonBarTest/Info.plist @@ -0,0 +1,12 @@ + + + + + CPApplicationDelegateClass + AppController + CPBundleName + CPButtonBarTest + CPPrincipalClass + CPApplication + + diff --git a/Tests/Manual/CPButtonBarTest/Jakefile b/Tests/Manual/CPButtonBarTest/Jakefile new file mode 100644 index 000000000..542de14fd --- /dev/null +++ b/Tests/Manual/CPButtonBarTest/Jakefile @@ -0,0 +1,81 @@ +/* + * Jakefile + * CPBrowserTest + * + * Created by Ross Boucher on March 23, 2010. + * Copyright 2010, 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 ("CPBrowserTest", function(task) +{ + task.setBuildIntermediatesPath(FILE.join("Build", "CPBrowserTest.build", configuration)); + task.setBuildPath(FILE.join("Build", configuration)); + + task.setProductName("CPBrowserTest"); + task.setIdentifier("com.yourcompany.CPBrowserTest"); + task.setVersion("1.0"); + task.setAuthor("Your Company"); + task.setEmail("feedback @nospam@ yourcompany.com"); + task.setSummary("CPBrowserTest"); + 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"); +}); + +function printResults(configuration) +{ + print("----------------------------"); + print(configuration+" app built at path: "+FILE.join("Build", configuration, "CPBrowserTest")); + print("----------------------------"); +} + +task ("default", ["CPBrowserTest"], 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", "CPBrowserTest", "index.html")]); +}); + +task ("run-release", ["release"], function() +{ + OS.system(["open", FILE.join("Build", "Release", "CPBrowserTest", "index.html")]); +}); + +task ("deploy", ["release"], function() +{ + FILE.mkdirs(FILE.join("Build", "Deployment", "CPBrowserTest")); + OS.system(["press", "-f", FILE.join("Build", "Release", "CPBrowserTest"), FILE.join("Build", "Deployment", "CPBrowserTest")]); + printResults("Deployment") +}); diff --git a/Tests/Manual/CPButtonBarTest/Resources/spinner.gif b/Tests/Manual/CPButtonBarTest/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/CPButtonBarTest/index-debug.html b/Tests/Manual/CPButtonBarTest/index-debug.html new file mode 100644 index 000000000..a36b1d3b9 --- /dev/null +++ b/Tests/Manual/CPButtonBarTest/index-debug.html @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + __project.name__ + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/CPButtonBarTest/index.html b/Tests/Manual/CPButtonBarTest/index.html new file mode 100644 index 000000000..ac42c98a7 --- /dev/null +++ b/Tests/Manual/CPButtonBarTest/index.html @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + __project.name__ + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/CPButtonBarTest/main.j b/Tests/Manual/CPButtonBarTest/main.j new file mode 100644 index 000000000..b953537a0 --- /dev/null +++ b/Tests/Manual/CPButtonBarTest/main.j @@ -0,0 +1,18 @@ +/* + * AppController.j + * CPButtonBarTest + * + * Created by Ross Boucher on March 23, 2010. + * Copyright 2010, Your Company All rights reserved. + */ + +@import +@import + +@import "AppController.j" + + +function main(args, namedArgs) +{ + CPApplicationMain(args, namedArgs); +} From 0e5f3b1697b0b99be46e04591381aa1e1303ee6d Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 3 Apr 2026 13:49:33 +0200 Subject: [PATCH 113/136] fixed caching --- Foundation/CPKeyValueObserving.j | 122 ++++++++++++++++++------------- 1 file changed, 70 insertions(+), 52 deletions(-) diff --git a/Foundation/CPKeyValueObserving.j b/Foundation/CPKeyValueObserving.j index 3d3aa4315..855342d6a 100644 --- a/Foundation/CPKeyValueObserving.j +++ b/Foundation/CPKeyValueObserving.j @@ -1282,6 +1282,7 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti CPString _valueKeyPath; CPArray _observedItems; + id _cachedValue; } - (id)initWithTarget:(id)aTarget observer:(id)anObserver keyPath:(CPString)aKeyPath options:(unsigned)options context:(id)aContext @@ -1297,8 +1298,6 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti _context = aContext; _observedItems = [CPArray array]; - // Parse key path like "collectionKeyPath.@sum.valueKeyPath" - // Captures: 1=collectionKeyPath, 2=@sum, 3=valueKeyPath var match = aKeyPath.match(/^(.*)\.(@\w+)(?:\.(.*))?$/); if (match) { @@ -1307,8 +1306,9 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti _valueKeyPath = match[3]; } - // Observe the collection itself on the target to catch array resets/mutations - [_target addObserver:self forKeyPath:_collectionKeyPath options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:nil]; + _cachedValue = [_target valueForKeyPath:_fullKeyPath]; + + [_target addObserver:self forKeyPath:_collectionKeyPath options:_options context:nil]; [self _setupItemObservers]; } @@ -1353,19 +1353,50 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti - (void)observeValueForKeyPath:(CPString)keyPath ofObject:(id)object change:(CPDictionary)change context:(id)context { - // Branch 1: The collection on the target was modified + var isPrior = [[change objectForKey:CPKeyValueChangeNotificationIsPriorKey] boolValue]; + if (object === _target && [keyPath isEqualToString:_collectionKeyPath]) { - var kind = [change objectForKey:CPKeyValueChangeKindKey]; + // Only tear down/rebuild observers when the actual change happens, not during the "Prior" notification + if (!isPrior) + { + var kind = [change objectForKey:CPKeyValueChangeKindKey]; - if (kind === CPKeyValueChangeSetting) - { - [self _tearDownItemObservers]; - [self _setupItemObservers]; - } - else if (kind === CPKeyValueChangeInsertion || kind === CPKeyValueChangeReplacement) - { - if (kind === CPKeyValueChangeReplacement) + if (kind === CPKeyValueChangeSetting) + { + [self _tearDownItemObservers]; + [self _setupItemObservers]; + } + else if (kind === CPKeyValueChangeInsertion || kind === CPKeyValueChangeReplacement) + { + if (kind === CPKeyValueChangeReplacement) + { + var oldItems = [change objectForKey:CPKeyValueChangeOldKey]; + if (oldItems) + { + for (var i = 0, len = oldItems.length; i < len; i++) + { + var item = oldItems[i]; + if (_valueKeyPath && [item respondsToSelector:@selector(removeObserver:forKeyPath:context:)]) + [item removeObserver:self forKeyPath:_valueKeyPath context:nil]; + [_observedItems removeObject:item]; + } + } + } + + var newItems = [change objectForKey:CPKeyValueChangeNewKey]; + if (newItems) + { + for (var i = 0, len = newItems.length; i < len; i++) + { + var item = newItems[i]; + if (_valueKeyPath && [item respondsToSelector:@selector(addObserver:forKeyPath:options:context:)]) + [item addObserver:self forKeyPath:_valueKeyPath options:_options context:nil]; + [_observedItems addObject:item]; + } + } + } + else if (kind === CPKeyValueChangeRemoval) { var oldItems = [change objectForKey:CPKeyValueChangeOldKey]; if (oldItems) @@ -1379,53 +1410,41 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti } } } - - var newItems = [change objectForKey:CPKeyValueChangeNewKey]; - if (newItems) - { - for (var i = 0, len = newItems.length; i < len; i++) - { - var item = newItems[i]; - if (_valueKeyPath && [item respondsToSelector:@selector(addObserver:forKeyPath:options:context:)]) - [item addObserver:self forKeyPath:_valueKeyPath options:_options context:nil]; - [_observedItems addObject:item]; - } - } - } - else if (kind === CPKeyValueChangeRemoval) - { - var oldItems = [change objectForKey:CPKeyValueChangeOldKey]; - if (oldItems) - { - for (var i = 0, len = oldItems.length; i < len; i++) - { - var item = oldItems[i]; - if (_valueKeyPath && [item respondsToSelector:@selector(removeObserver:forKeyPath:context:)]) - [item removeObserver:self forKeyPath:_valueKeyPath context:nil]; - [_observedItems removeObject:item]; - } - } } - [self _notifyOriginalObserver]; + [self _notifyOriginalObserverIsPrior:isPrior]; } - // Branch 2: One of the children items has updated else if (_valueKeyPath && [keyPath isEqualToString:_valueKeyPath] && [_observedItems containsObject:object]) { - [self _notifyOriginalObserver]; + [self _notifyOriginalObserverIsPrior:isPrior]; } } -- (void)_notifyOriginalObserver +- (void)_notifyOriginalObserverIsPrior:(BOOL)isPrior { - // Ask standard KVC to evaluate the entire aggregate string (e.g., @sum.value) - var newValue = [_target valueForKeyPath:_fullKeyPath], - change = @{ - CPKeyValueChangeKindKey: CPKeyValueChangeSetting, - CPKeyValueChangeNewKey: (newValue !== nil ? newValue : [CPNull null]) - }; + var change = [CPMutableDictionary dictionaryWithObject:CPKeyValueChangeSetting forKey:CPKeyValueChangeKindKey]; - [_originalObserver observeValueForKeyPath:_fullKeyPath ofObject:_target change:change context:_context]; + if (isPrior) + [change setObject:YES forKey:CPKeyValueChangeNotificationIsPriorKey]; + + if (_options & CPKeyValueObservingOptionOld) + [change setObject:(_cachedValue !== nil ? _cachedValue : [CPNull null]) forKey:CPKeyValueChangeOldKey]; + + if (!isPrior) + { + var newValue = [_target valueForKeyPath:_fullKeyPath]; + if (_options & CPKeyValueObservingOptionNew) + [change setObject:(newValue !== nil ? newValue : [CPNull null]) forKey:CPKeyValueChangeNewKey]; + + [_originalObserver observeValueForKeyPath:_fullKeyPath ofObject:_target change:change context:_context]; + + // Update the cache for the next notification + _cachedValue = newValue; + } + else + { + [_originalObserver observeValueForKeyPath:_fullKeyPath ofObject:_target change:change context:_context]; + } } - (void)finalize @@ -1436,7 +1455,6 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti @end - @implementation _CPKVOForwardingObserver : CPObject { id _object; From 0bd83b6635dab461afd62eb2c69d03e754c2ffaf Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 3 Apr 2026 15:20:59 +0200 Subject: [PATCH 114/136] new: clear CPPredicateEditor rows when objectValue is set to nil --- AppKit/CPRuleEditor/CPPredicateEditor.j | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/AppKit/CPRuleEditor/CPPredicateEditor.j b/AppKit/CPRuleEditor/CPPredicateEditor.j index 16cf1f685..5076b7ddc 100644 --- a/AppKit/CPRuleEditor/CPPredicateEditor.j +++ b/AppKit/CPRuleEditor/CPPredicateEditor.j @@ -269,6 +269,8 @@ _currentAnimation = nil; _sendAction = NO; + var rows = []; + if (predicate != nil) { if ((_nestingMode == CPRuleEditorNestingModeSimple || _nestingMode == CPRuleEditorNestingModeCompound) @@ -276,10 +278,13 @@ predicate = [[CPCompoundPredicate alloc] initWithType:[self _compoundPredicateTypeForRootRows] subpredicates:[CPArray arrayWithObject:predicate]]; var row = [self _rowObjectFromPredicate:predicate]; + if (row != nil) - [_boundArrayOwner setValue:[CPArray arrayWithObject:row] forKey:_boundArrayKeyPath]; + [rows addObject:row]; } + [_boundArrayOwner setValue:rows forKey:_boundArrayKeyPath]; + [self setAnimation:animation]; } From 65cb048b1bf355e1501213784245511ac9a3f8bc Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 3 Apr 2026 17:40:45 +0200 Subject: [PATCH 115/136] Fixed: incorrect CPWebView sizing after animation --- AppKit/CPWebView.j | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AppKit/CPWebView.j b/AppKit/CPWebView.j index f39d9ee40..35a65860e 100644 --- a/AppKit/CPWebView.j +++ b/AppKit/CPWebView.j @@ -278,7 +278,11 @@ CPWebViewAppKitScrollMaxPollCount = 3; if (_effectiveScrollMode === CPWebViewScrollAppKit) { - var visibleRect = [_frameView visibleRect]; + // Use `[_scrollView documentVisibleRect]` instead of `[_frameView visibleRect]`. + // `visibleRect` accounts for window clipping, which collapses to 0x0 if the view is + // animated off-screen, resulting in an incorrectly shrunken web layout. + var visibleRect = [_scrollView documentVisibleRect]; + [_frameView setFrameSize:CGSizeMake(CGRectGetMaxX(visibleRect), CGRectGetMaxY(visibleRect))]; // try to get the document size so we can correctly set the frame From 8aee2a511f70de88ed9eff2765704672c6bdc3d5 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 3 Apr 2026 19:00:09 +0200 Subject: [PATCH 116/136] fixed: colorpanel issue --- AppKit/CPColorPanel.j | 95 ++++++++++++++++++++++++++++++++----------- AppKit/CPColorWell.j | 62 +++++++++++++++++++++++++--- 2 files changed, 128 insertions(+), 29 deletions(-) diff --git a/AppKit/CPColorPanel.j b/AppKit/CPColorPanel.j index 86224946b..169a12fba 100644 --- a/AppKit/CPColorPanel.j +++ b/AppKit/CPColorPanel.j @@ -51,7 +51,7 @@ CPColorPickerViewWidth = 265; CPColorPickerViewHeight = 370; CPColorPanelColorDidChangeNotification = @"CPColorPanelColorDidChangeNotification"; -CPColorDragType = @"CPColorDragType"; +CPColorDragType = CPColorPboardType; var PREVIEW_HEIGHT = 20.0, TOOLBAR_HEIGHT = 32.0, @@ -154,11 +154,21 @@ var SharedColorPanel = nil, _color = aColor; [_previewView setBackgroundColor:_color]; - // Push color via Responder Chain (targets First Responder, i.e., the active CPColorWell) - [CPApp sendAction:@selector(changeColor:) to:nil from:self]; + // Check if the color change originated from user interaction inside the panel itself. + // We only broadcast `changeColor:` if the user picked a color via the panel's UI. + // If an external CPColorWell called `setColor:` programmatically, broadcasting it + // back down the responder chain would incorrectly change the previous First Responder. + var currentEvent = [CPApp currentEvent], + isFromPanel = currentEvent && ([currentEvent window] === self); - if (_target && _action) - [CPApp sendAction:_action to:_target from:self]; + if (isFromPanel) + { + // Push color via Responder Chain (targets First Responder, i.e., the active CPColorWell) + [CPApp sendAction:@selector(changeColor:) to:nil from:self]; + + if (_target && _action) + [CPApp sendAction:_action to:_target from:self]; + } [[CPNotificationCenter defaultCenter] postNotificationName:CPColorPanelColorDidChangeNotification @@ -425,15 +435,17 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; CPColor _dragColor; CPColorPanel _colorPanel; CPCookie _swatchCookie; + CGPoint _mouseDownPoint; } - (id)initWithFrame:(CGRect)aFrame { self = [super initWithFrame:aFrame]; + _mouseDownPoint = CGPointMake(0, 0); [self setBackgroundColor:[CPColor grayColor]]; - [self registerForDraggedTypes:[CPArray arrayWithObjects:@"CPColorDragType"]]; + [self registerForDraggedTypes:[CPArray arrayWithObjects:CPColorDragType]]; var whiteColor = [CPColor whiteColor]; @@ -525,23 +537,34 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; [self saveColorList]; } +- (void)mouseDown:(CPEvent)anEvent +{ + _mouseDownPoint = [anEvent locationInWindow]; +} + - (void)mouseUp:(CPEvent)anEvent { var point = [self convertPoint:[anEvent locationInWindow] fromView:nil], bounds = [self bounds]; if (!CGRectContainsPoint(bounds, point) || point.x > [self bounds].size.width - 1 || point.x < 1) - return NO; + return; [_colorPanel setColor:[self colorAtIndex:FLOOR(point.x / 13)] updatePicker:YES]; } - (void)mouseDragged:(CPEvent)anEvent { - var point = [self convertPoint:[anEvent locationInWindow] fromView:nil]; + var windowPoint = [anEvent locationInWindow]; + + // Prevent accidental drags from rapid clicking causing small micro-movements + if (ABS(windowPoint.x - _mouseDownPoint.x) < 3 && ABS(windowPoint.y - _mouseDownPoint.y) < 3) + return; + + var point = [self convertPoint:windowPoint fromView:nil]; if (point.x > [self bounds].size.width - 1 || point.x < 1) - return NO; + return; var swatch = _swatches[FLOOR(point.x / 13)]; _dragColor = [[swatch subviews][0] backgroundColor]; @@ -557,8 +580,8 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; [dragView addSubview:dragFillView]; var pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard]; - [pasteboard declareTypes:[CPArray arrayWithObject:@"CPColorDragType"] owner:self]; - [pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_dragColor] forType:@"CPColorDragType"]; + [pasteboard declareTypes:[CPArray arrayWithObject:CPColorDragType] owner:self]; + [pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_dragColor] forType:CPColorDragType]; [self dragView:dragView at:CGPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0) @@ -571,20 +594,27 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; - (void)pasteboard:(CPPasteboard)aPasteboard provideDataForType:(CPString)aType { - if (aType == @"CPColorDragType") + if (aType == CPColorDragType) [aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_dragColor] forType:aType]; } -- (void)performDragOperation:(id /**/)aSender +- (BOOL)performDragOperation:(id /**/)aSender { var location = [self convertPoint:[aSender draggingLocation] fromView:nil], pasteboard = [aSender draggingPasteboard], swatch = nil; - if (![pasteboard availableTypeFromArray:[@"CPColorDragType"]] || location.x > [self bounds].size.width - 1 || location.x < 1) + if (![pasteboard availableTypeFromArray:[CPColorDragType]] || location.x > [self bounds].size.width - 1 || location.x < 1) return NO; - [self setColor:[CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:@"CPColorDragType"]] atIndex:FLOOR(location.x / 13)]; + [self setColor:[CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:CPColorDragType]] atIndex:FLOOR(location.x / 13)]; + + return YES; +} + +- (unsigned)draggingSourceOperationMaskForLocal:(BOOL)isLocal +{ + return CPDragOperationCopy; } @end @@ -593,13 +623,15 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; @implementation _CPColorPanelPreview : CPView { CPColorPanel _colorPanel; + CGPoint _mouseDownPoint; } - (id)initWithFrame:(CGRect)aFrame { self = [super initWithFrame:aFrame]; + _mouseDownPoint = CGPointMake(0, 0); - [self registerForDraggedTypes:[CPArray arrayWithObjects:@"CPColorDragType"]]; + [self registerForDraggedTypes:[CPArray arrayWithObjects:CPColorDragType]]; return self; } @@ -614,15 +646,17 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; return _colorPanel; } -- (void)performDragOperation:(id /**/)aSender +- (BOOL)performDragOperation:(id /**/)aSender { var pasteboard = [aSender draggingPasteboard]; - if (![pasteboard availableTypeFromArray:[@"CPColorDragType"]]) + if (![pasteboard availableTypeFromArray:[CPColorDragType]]) return NO; - var color = [CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:@"CPColorDragType"]]; + var color = [CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:CPColorDragType]]; [_colorPanel setColor:color updatePicker:YES]; + + return YES; } - (BOOL)isOpaque @@ -630,9 +664,19 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; return YES; } +- (void)mouseDown:(CPEvent)anEvent +{ + _mouseDownPoint = [anEvent locationInWindow]; +} + - (void)mouseDragged:(CPEvent)anEvent { - var point = [self convertPoint:[anEvent locationInWindow] fromView:nil]; + var windowPoint = [anEvent locationInWindow]; + + if (ABS(windowPoint.x - _mouseDownPoint.x) < 3 && ABS(windowPoint.y - _mouseDownPoint.y) < 3) + return; + + var point = [self convertPoint:windowPoint fromView:nil]; var bounds = CGRectMake(0, 0, 15, 15); @@ -645,8 +689,8 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; [dragView addSubview:dragFillView]; var pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard]; - [pasteboard declareTypes:[CPArray arrayWithObject:@"CPColorDragType"] owner:self]; - [pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[self backgroundColor]] forType:@"CPColorDragType"]; + [pasteboard declareTypes:[CPArray arrayWithObject:CPColorDragType] owner:self]; + [pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[self backgroundColor]] forType:CPColorDragType]; [self dragView:dragView at:CGPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0) @@ -659,10 +703,15 @@ var CPColorPanelSwatchesCookie = "CPColorPanelSwatchesCookie"; - (void)pasteboard:(CPPasteboard)aPasteboard provideDataForType:(CPString)aType { - if (aType == @"CPColorDragType") + if (aType == CPColorDragType) [aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[self backgroundColor]] forType:aType]; } +- (unsigned)draggingSourceOperationMaskForLocal:(BOOL)isLocal +{ + return CPDragOperationCopy; +} + @end @import "CPColorPicker.j" diff --git a/AppKit/CPColorWell.j b/AppKit/CPColorWell.j index 765cb47d4..9f3131f5c 100644 --- a/AppKit/CPColorWell.j +++ b/AppKit/CPColorWell.j @@ -25,6 +25,7 @@ @import "CPView.j" @import "CPColor.j" @import "CPColorPanel.j" +@import "CPPasteboard.j" var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiveNotification"; @@ -41,6 +42,7 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv BOOL _bordered; CPColor _color; BOOL _isChangingColorFromPanel; // Guard flag to prevent recursion + CGPoint _mouseDownPoint; } + (Class)_binderClassForBinding:(CPString)aBinding @@ -112,10 +114,10 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv if (self) { _color = [CPColor whiteColor]; + _mouseDownPoint = CGPointMakeZero(); [self setBordered:YES]; - // Register using string literal to avoid dependency issues - [self registerForDraggedTypes:[CPArray arrayWithObject:@"CPColorDragType"]]; + [self registerForDraggedTypes:[CPArray arrayWithObject:CPColorPboardType]]; } return self; @@ -247,9 +249,46 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv if (![self isEnabled]) return; + _mouseDownPoint = [anEvent locationInWindow]; [self activate:YES]; } +- (void)mouseDragged:(CPEvent)anEvent +{ + if (![self isEnabled]) + return; + + var windowPoint = [anEvent locationInWindow]; + + // Prevent accidental drags from rapid clicking causing small micro-movements + if (ABS(windowPoint.x - _mouseDownPoint.x) < 3 && ABS(windowPoint.y - _mouseDownPoint.y) < 3) + return; + + var bounds = CGRectMake(0, 0, 15, 15); + var dragView = [[CPView alloc] initWithFrame:bounds], + dragFillView = [[CPView alloc] initWithFrame:CGRectInset(bounds, 1.0, 1.0)]; + + [dragView setBackgroundColor:[CPColor blackColor]]; + [dragFillView setBackgroundColor:_color]; + + [dragView addSubview:dragFillView]; + + var pasteboard = [CPPasteboard pasteboardWithName:CPDragPboard]; + [pasteboard declareTypes:[CPArray arrayWithObject:CPColorPboardType] owner:self]; + [pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_color] forType:CPColorPboardType]; + + var point = [self convertPoint:windowPoint fromView:nil]; + + [self dragView:dragView + at:CGPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0) + offset:CGPointMake(0.0, 0.0) + event:anEvent + pasteboard:pasteboard + source:self + slideBack:YES]; +} + + #pragma mark - #pragma mark Drag and Drop @@ -257,7 +296,7 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv { var pasteboard = [sender draggingPasteboard]; - if ([[pasteboard types] containsObject:@"CPColorDragType"]) + if ([[pasteboard types] containsObject:CPColorPboardType]) { [self setThemeState:CPThemeStateHighlighted]; return CPDragOperationCopy; @@ -275,9 +314,9 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv { var pasteboard = [sender draggingPasteboard]; - if ([[pasteboard types] containsObject:@"CPColorDragType"]) + if ([[pasteboard types] containsObject:CPColorPboardType]) { - var data = [pasteboard dataForType:@"CPColorDragType"], + var data = [pasteboard dataForType:CPColorPboardType], newColor = [CPKeyedUnarchiver unarchiveObjectWithData:data]; if (newColor && [newColor isKindOfClass:[CPColor class]]) @@ -296,6 +335,17 @@ var _CPColorWellDidBecomeExclusiveNotification = @"_CPColorWellDidBecomeExclusiv return NO; } +- (void)pasteboard:(CPPasteboard)aPasteboard provideDataForType:(CPString)aType +{ + if (aType == CPColorPboardType) + [aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:_color] forType:aType]; +} + +- (unsigned)draggingSourceOperationMaskForLocal:(BOOL)isLocal +{ + return CPDragOperationCopy; +} + #pragma mark - #pragma mark Layout @@ -414,7 +464,7 @@ var CPColorWellColorKey = "CPColorWellColorKey", { _color = [aCoder decodeObjectForKey:CPColorWellColorKey]; [self setBordered:[aCoder decodeBoolForKey:CPColorWellBorderedKey]]; - [self registerForDraggedTypes:[CPArray arrayWithObject:@"CPColorDragType"]]; + [self registerForDraggedTypes:[CPArray arrayWithObject:CPColorPboardType]]; } return self; From f34cbbdf298e7c123706b9bf1275666895a9a340 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 4 Apr 2026 15:00:53 +0200 Subject: [PATCH 117/136] New: toolTips for CPTableView and CPOutlineView --- AppKit/CPOutlineView.j | 26 ++++++++++++++++++++++++++ AppKit/CPTableView.j | 12 +++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j index 0ffc88505..bf2716fee 100644 --- a/AppKit/CPOutlineView.j +++ b/AppKit/CPOutlineView.j @@ -1512,6 +1512,21 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, return [super frameOfDataViewAtColumn:aColumn row:aRow]; } +- (void)_applyToolTipToDataView:(CPView)aDataView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow +{ + if (_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_toolTipForView_rect_tableColumn_item_mouseLocation_) + { + var item = [self itemAtRow:aRow], + tooltip = [self _sendDelegateToolTipForView:aDataView rect:[aDataView frame] tableColumn:aTableColumn item:item mouseLocation:CGPointMakeZero()]; + + [aDataView setToolTip:tooltip]; + } + else + { + [super _applyToolTipToDataView:aDataView forTableColumn:aTableColumn row:aRow]; + } +} + /*! Retargets the drop item for the outlineview. @@ -2118,6 +2133,17 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff = 0, /*** CPTableViewDelegate methods ***/ +/*! + @ignore +*/ +- (CPString)_sendDelegateToolTipForView:(id)aView rect:(CGRect)aRect tableColumn:(CPTableColumn)aTableColumn item:(id)anItem mouseLocation:(CGPoint)aPoint +{ + if (!(_implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_toolTipForView_rect_tableColumn_item_mouseLocation_)) + return nil; + + return [_outlineViewDelegate outlineView:self toolTipForView:aView rect:aRect tableColumn:aTableColumn item:anItem mouseLocation:aPoint]; +} + - (BOOL)tableView:(CPTableView)theTableView shouldSelectRow:(CPInteger)theRow { return SHOULD_SELECT_ITEM(self, [self itemAtRow:theRow]); diff --git a/AppKit/CPTableView.j b/AppKit/CPTableView.j index 5512e974a..7a1ea6129 100644 --- a/AppKit/CPTableView.j +++ b/AppKit/CPTableView.j @@ -3616,10 +3616,21 @@ Your delegate can implement this method to avoid subclassing the tableview to ad [self _setEditingState:NO forView:dataView]; [self _sendDelegateWillDisplayView:dataView forTableColumn:tableColumn row:row]; + [self _applyToolTipToDataView:dataView forTableColumn:tableColumn row:row]; return dataView; } +- (void)_applyToolTipToDataView:(CPView)aDataView forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow +{ + var tooltip = nil; + + if (_implementedDelegateMethods & CPTableViewDelegate_tableView_toolTipForView_rect_tableColumn_row_mouseLocation_) + tooltip = [self _sendDelegateToolTipForView:aDataView rect:[aDataView frame] tableColumn:aTableColumn row:aRow mouseLocation:CGPointMakeZero()]; + + [aDataView setToolTip:tooltip]; +} + - (void)_setObjectValueForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow forView:(CPView)aDataView { [self _setObjectValueForTableColumn:aTableColumn row:aRow forView:aDataView useCache:!_invalidateObjectValuesCache]; @@ -5969,7 +5980,6 @@ Your delegate can implement this method to avoid subclassing the tableview to ad /*! @ignore - Not yet implemented */ - (CPString)_sendDelegateToolTipForView:(id)aView rect:(CGRect)aRect tableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex mouseLocation:(CGPoint)aPoint { From 7fad67484c742ec0563f93f9b5a02db98c20c2ef Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 6 Apr 2026 11:56:23 +0200 Subject: [PATCH 118/136] new: manual tests --- .../Manual/CPOutlineViewTest/AppController.j | 13 ++++++++++ .../Manual/TableTest/DataView/AppController.j | 25 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/Tests/Manual/CPOutlineViewTest/AppController.j b/Tests/Manual/CPOutlineViewTest/AppController.j index cbab1aed5..c27ff5976 100644 --- a/Tests/Manual/CPOutlineViewTest/AppController.j +++ b/Tests/Manual/CPOutlineViewTest/AppController.j @@ -308,4 +308,17 @@ var rowHeights = [ ]; return anItem.customHeight; } +- (CPString)outlineView:(CPOutlineView)anOutlineView toolTipForView:(CPView)aView rect:(CGRect)aRect tableColumn:(CPTableColumn)aTableColumn item:(id)anItem mouseLocation:(CGPoint)aPoint +{ + if (anItem === nil) + return nil; + + var title = [anItem title], + childCount = [[anItem children] count], + childString = childCount === 1 ? @"1 child" : childCount + @" children"; + + // Dynamischer Tooltip basierend auf dem 'Menu' Objekt + return [CPString stringWithFormat:@"Outline Node: %@\nContains: %@", title, childString]; +} + @end diff --git a/Tests/Manual/TableTest/DataView/AppController.j b/Tests/Manual/TableTest/DataView/AppController.j index 9ab88c35b..4552e9aff 100644 --- a/Tests/Manual/TableTest/DataView/AppController.j +++ b/Tests/Manual/TableTest/DataView/AppController.j @@ -64,6 +64,9 @@ var AppControllerInstance = nil; - (void)awakeFromCib { [[tableView tableColumns][0] setDataView:dataView]; + + // Set the delegate so we can test the new toolTip feature + [tableView setDelegate:self]; rows = [ [CPDictionary dictionaryWithJSObject:{ id:1, filename: "Jack.png", size:1327, uploading:NO, progress:0 }], @@ -88,6 +91,28 @@ var AppControllerInstance = nil; return !uploading; } +// -------------------------------------------------------------------------------------- +// Tool tip test +// -------------------------------------------------------------------------------------- +- (CPString)tableView:(CPTableView)aTableView toolTipForView:(CPView)aView rect:(CGRect)aRect tableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex mouseLocation:(CGPoint)aPoint +{ + // Make sure the row exists + if (aRowIndex < 0 || aRowIndex >= [rows count]) + return nil; + + var info =[rows objectAtIndex:aRowIndex], + filename = [info valueForKey:@"filename"], + isUploading =[info valueForKey:@"uploading"], + progress = [info valueForKey:@"progress"]; + + // Provide a dynamic tooltip based on the state of the data in the row + if (isUploading) + return[CPString stringWithFormat:@"Uploading '%@' (%d%% complete)", filename, progress]; + + return[CPString stringWithFormat:@"File: %@\nStatus: Idle", filename]; +} +// -------------------------------------------------------------------------------------- + - (@action)start:(id)sender { // Note we use setUploading:YES instead of uploading = YES. From 2ea11eb5abd822ebd2692f648033fc4be94902a6 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 6 Apr 2026 11:58:00 +0200 Subject: [PATCH 119/136] formatting --- Tests/Manual/TableTest/DataView/AppController.j | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Tests/Manual/TableTest/DataView/AppController.j b/Tests/Manual/TableTest/DataView/AppController.j index 4552e9aff..ec5c38dd1 100644 --- a/Tests/Manual/TableTest/DataView/AppController.j +++ b/Tests/Manual/TableTest/DataView/AppController.j @@ -100,18 +100,17 @@ var AppControllerInstance = nil; if (aRowIndex < 0 || aRowIndex >= [rows count]) return nil; - var info =[rows objectAtIndex:aRowIndex], + var info = [rows objectAtIndex:aRowIndex], filename = [info valueForKey:@"filename"], - isUploading =[info valueForKey:@"uploading"], + isUploading = [info valueForKey:@"uploading"], progress = [info valueForKey:@"progress"]; // Provide a dynamic tooltip based on the state of the data in the row if (isUploading) - return[CPString stringWithFormat:@"Uploading '%@' (%d%% complete)", filename, progress]; + return [CPString stringWithFormat:@"Uploading '%@' (%d%% complete)", filename, progress]; - return[CPString stringWithFormat:@"File: %@\nStatus: Idle", filename]; + return [CPString stringWithFormat:@"File: %@\nStatus: Idle", filename]; } -// -------------------------------------------------------------------------------------- - (@action)start:(id)sender { From 480917eeab650b7e542eb15e8870f0a691f936a2 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 6 Apr 2026 13:15:55 +0200 Subject: [PATCH 120/136] new: doubleClickTarget and doubleClickArgument for CPTableView --- AppKit/CPTableView.j | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/AppKit/CPTableView.j b/AppKit/CPTableView.j index 7a1ea6129..4a1949984 100644 --- a/AppKit/CPTableView.j +++ b/AppKit/CPTableView.j @@ -296,6 +296,8 @@ CPTableViewFirstColumnOnlyAutoresizingStyle = 5; _CPTableDrawView _tableDrawView; SEL _doubleAction; + id _doubleClickTarget @accessors(property=doubleClickTarget); + id _doubleClickArgument @accessors(property=doubleClickArgument); CPInteger _clickedRow; CPInteger _clickedColumn; unsigned _columnAutoResizingStyle; @@ -4734,7 +4736,12 @@ Your delegate can implement this method to avoid subclassing the tableview to ad //double click actions if ([[CPApp currentEvent] clickCount] === 2 && _doubleAction) - [self sendAction:_doubleAction to:_target]; + { + var target = _doubleClickTarget || _target, + argument = [self infoForBinding:@"doubleClickArgument"] ? _doubleClickArgument : self; + + [CPApp sendAction:_doubleAction to:target from:argument]; + } } /* @@ -6098,6 +6105,11 @@ Your delegate can implement this method to avoid subclassing the tableview to ad { if (aBinding == @"content") _contentBindingExplicitlySet = YES; + else if (aBinding == @"doubleClickTarget") + { + if ([options objectForKey:CPSelectorNameBindingOption]) + [self setDoubleAction:CPSelectorFromString([options objectForKey:CPSelectorNameBindingOption])]; + } [super bind:aBinding toObject:anObject withKeyPath:aKeyPath options:options]; } From 57987b6f6760eb57610db0e6b5698670bef51514 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 6 Apr 2026 17:26:38 +0200 Subject: [PATCH 121/136] Fixed: CPTableView bug where removing a column breaks internal state --- AppKit/CPTableView.j | 70 +++++++++++++++++-- .../Manual/TableTest/OldTest/AppController.j | 18 +++-- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/AppKit/CPTableView.j b/AppKit/CPTableView.j index 7a1ea6129..bef6e7d4d 100644 --- a/AppKit/CPTableView.j +++ b/AppKit/CPTableView.j @@ -3486,13 +3486,73 @@ Your delegate can implement this method to avoid subclassing the tableview to ad [removeIndexes addIndex:columnIdx]; } - var rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])]; - [self _unloadDataViewsInRows:rowIndexes columns:removeIndexes]; + if ([removeIndexes count] > 0) + { + var rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])]; + [self _unloadDataViewsInRows:rowIndexes columns:removeIndexes]; - [_tableColumns removeObjectsAtIndexes:removeIndexes]; + [_tableColumns removeObjectsAtIndexes:removeIndexes]; - _dirtyTableColumnRangeIndex = 0; - [self _recalculateTableColumnRanges]; + _dirtyTableColumnRangeIndex = 0; + [self _recalculateTableColumnRanges]; + + // Shift cached index sets downwards to account for the removed columns + var shiftIndexSet = function(indexSet) + { + var newSet = [CPIndexSet indexSet]; + [indexSet enumerateIndexesUsingBlock:function(idx, stop) + { + if (![removeIndexes containsIndex:idx]) + { + var shift = 0, + remIdx = [removeIndexes firstIndex]; + + while (remIdx !== CPNotFound && remIdx < idx) + { + shift++; + remIdx = [removeIndexes indexGreaterThanIndex:remIdx]; + } + + [newSet addIndex:idx - shift]; + } + }]; + return newSet; + }; + + _exposedColumns = shiftIndexSet(_exposedColumns); + _selectedColumnIndexes = shiftIndexSet(_selectedColumnIndexes); + + // Shift individual index variables + var shiftIndex = function(idx) + { + if (idx === CPNotFound || idx === -1) + return idx; + + if ([removeIndexes containsIndex:idx]) + return CPNotFound; + + var shift = 0, + remIdx = [removeIndexes firstIndex]; + + while (remIdx !== CPNotFound && remIdx < idx) + { + shift++; + remIdx = [removeIndexes indexGreaterThanIndex:remIdx]; + } + + return idx - shift; + }; + + _editingColumn = shiftIndex(_editingColumn); + + _draggedColumnIndex = shiftIndex(_draggedColumnIndex); + if (_draggedColumnIndex === CPNotFound) + _draggedColumnIndex = -1; + + _clickedColumn = shiftIndex(_clickedColumn); + if (_clickedColumn === CPNotFound) + _clickedColumn = -1; + } [_differedColumnDataToRemove removeAllObjects]; _needsDifferedTableColumnRemove = NO; diff --git a/Tests/Manual/TableTest/OldTest/AppController.j b/Tests/Manual/TableTest/OldTest/AppController.j index d28b73b64..8c8d4741f 100644 --- a/Tests/Manual/TableTest/OldTest/AppController.j +++ b/Tests/Manual/TableTest/OldTest/AppController.j @@ -164,10 +164,20 @@ tableTestDragType = @"CPTableViewTestDragType"; - (void)removeColumn:(id)sender { - // if ([[tableView tableColumns] containsObject:randomColumn]) - [tableView removeTableColumn:randomColumn]; - //else - // [tableView addTableColumn:randomColumn]; + var columns = [tableView tableColumns]; + + // Check if we still have columns left to remove + if (columns && [columns count] > 0) + { + var columnToRemove = [columns lastObject]; + [tableView removeTableColumn:columnToRemove]; + + CPLog.debug(@"Removed column with identifier: " + [columnToRemove identifier]); + } + else + { + CPLog.debug(@"No more columns to remove!"); + } } - (void)addColumn:(id)sender From a784435c2bd30afa5735731e423ba7bf145bcb29 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 8 Apr 2026 15:17:46 +0200 Subject: [PATCH 122/136] formatting --- AppKit/CPTreeController.j | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/AppKit/CPTreeController.j b/AppKit/CPTreeController.j index 2d0e46802..d80e46ade 100644 --- a/AppKit/CPTreeController.j +++ b/AppKit/CPTreeController.j @@ -54,7 +54,7 @@ + (CPSet)keyPathsForValuesAffectingContentArray { - return[CPSet setWithObjects:@"content"]; + return [CPSet setWithObjects:@"content"]; } + (CPSet)keyPathsForValuesAffectingArrangedObjects @@ -64,7 +64,7 @@ + (CPSet)keyPathsForValuesAffectingSelectionIndexPath { - return[CPSet setWithObjects:@"selectionIndexPaths"]; + return [CPSet setWithObjects:@"selectionIndexPaths"]; } + (CPSet)keyPathsForValuesAffectingSelectedObjects @@ -79,7 +79,7 @@ + (CPSet)keyPathsForValuesAffectingCanAddChild { - return[CPSet setWithObjects:@"selectionIndexPaths"]; + return [CPSet setWithObjects:@"selectionIndexPaths"]; } + (CPSet)keyPathsForValuesAffectingCanInsert @@ -267,7 +267,7 @@ - (BOOL)setSelectionIndexPath:(CPIndexPath)indexPath { var paths = indexPath ? [CPArray arrayWithObject:indexPath] : [CPArray array]; - return[self setSelectionIndexPaths:paths]; + return [self setSelectionIndexPaths:paths]; } - (CPArray)selectionIndexPaths { return _selectionIndexPaths; } @@ -329,7 +329,7 @@ { var newPaths = [_selectionIndexPaths mutableCopy]; [newPaths removeObjectsInArray:indexPaths]; - return[self setSelectionIndexPaths:newPaths]; + return [self setSelectionIndexPaths:newPaths]; } - (CPArray)selectedNodes @@ -361,7 +361,7 @@ - (BOOL)__setSelectedObjects:(CPArray)objects { if (!objects || [objects count] === 0) - return[self __setSelectionIndexPaths:[CPArray array] avoidEmpty:_avoidsEmptySelection]; + return [self __setSelectionIndexPaths:[CPArray array] avoidEmpty:_avoidsEmptySelection]; var newPaths = [CPMutableArray array]; for (var i = 0, count = [objects count]; i < count; i++) @@ -371,7 +371,7 @@ [newPaths addObject:path]; } - return[self __setSelectionIndexPaths:newPaths avoidEmpty:_avoidsEmptySelection]; + return [self __setSelectionIndexPaths:newPaths avoidEmpty:_avoidsEmptySelection]; } - (CPIndexPath)_indexPathForObject:(id)anObject inNode:(CPTreeNode)node @@ -392,7 +392,7 @@ return nil; } -- (BOOL)canInsert { return[self isEditable]; } +- (BOOL)canInsert { return [self isEditable]; } - (BOOL)canInsertChild { return [self isEditable] &&[_selectionIndexPaths count] > 0; } - (BOOL)canAddChild { return [self canInsertChild]; } From e0e16eeca539a1f9a3c01d48b36ca1fdc5d4e9fc Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 8 Apr 2026 15:18:46 +0200 Subject: [PATCH 123/136] formatting --- AppKit/CPTreeController.j | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/AppKit/CPTreeController.j b/AppKit/CPTreeController.j index d80e46ade..0ed14b749 100644 --- a/AppKit/CPTreeController.j +++ b/AppKit/CPTreeController.j @@ -178,8 +178,10 @@ [self _rearrangeObjects]; - 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)_setContentArray:(id)anArray {[self setContent:anArray]; } @@ -205,8 +207,10 @@ [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 From 79a9ae31d3747efaa87de501ea9e4c6145863300 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 8 Apr 2026 15:23:45 +0200 Subject: [PATCH 124/136] Fixed: CPTreeController selection bug for asynchronously loaded nodes --- AppKit/CPTreeController.j | 65 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/AppKit/CPTreeController.j b/AppKit/CPTreeController.j index 0ed14b749..550d1cca7 100644 --- a/AppKit/CPTreeController.j +++ b/AppKit/CPTreeController.j @@ -285,6 +285,68 @@ return result; } +- (void)_ensureTreeNodesExistForIndexPaths:(CPArray)indexPaths +{ + if (!_childrenKeyPath) + return; + + var count = [indexPaths count]; + + for (var i = 0; i < count; i++) + { + var path = [indexPaths objectAtIndex:i]; + var length = [path length]; + var currentNode = [self arrangedObjects]; + + for (var j = 0; j < length; j++) + { + var index = [path indexAtPosition:j]; + + var obj = [currentNode representedObject]; + var expectedChildObjects = nil; + + if (!obj && currentNode === [self arrangedObjects]) + expectedChildObjects = [self contentArray]; + else if (obj) + expectedChildObjects = [obj valueForKeyPath:_childrenKeyPath]; + + if (expectedChildObjects) + { + var childNodes = [currentNode mutableChildNodes]; + var needsRebuild = NO; + + if (!childNodes || [childNodes count] !== [expectedChildObjects count]) + { + needsRebuild = YES; + } + else + { + for (var k = 0, kCount = [childNodes count]; k < kCount; k++) + { + if ([[childNodes objectAtIndex:k] representedObject] !== [expectedChildObjects objectAtIndex:k]) + { + needsRebuild = YES; + break; + } + } + } + + if (needsRebuild) + { + [childNodes removeAllObjects]; + var newNodes = [self _buildTreeNodesForObjects:expectedChildObjects];[childNodes addObjectsFromArray:newNodes]; + } + } + + var children = [currentNode childNodes]; + if (children && index < [children count]) + currentNode = [children objectAtIndex:index]; + else + break; + } + } +} + - (BOOL)__setSelectionIndexPaths:(CPArray)indexPaths avoidEmpty:(BOOL)avoidEmpty { var newPaths = indexPaths; @@ -292,6 +354,9 @@ if (!newPaths) newPaths = [CPArray array]; + if ([newPaths count] > 0) + [self _ensureTreeNodesExistForIndexPaths:newPaths]; + if (![newPaths count] && avoidEmpty) { if ([[[self arrangedObjects] childNodes] count] > 0) From 872af9ce2a8d8f68f2cde84f03a85048978aa240 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 6 May 2026 19:54:43 +0200 Subject: [PATCH 125/136] Revise Cappuccino introduction and project status Reorganized and condensed introductory information about Cappuccino, emphasizing its purpose and benefits for building desktop-class applications. --- README.markdown | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.markdown b/README.markdown index 859570108..de4a02829 100644 --- a/README.markdown +++ b/README.markdown @@ -3,10 +3,6 @@ # Cappuccino: Build Desktop-Class Web Applications -Cappuccino is an open-source framework that supports building powerful, desktop-class applications running in any modern web browser. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. - -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 2026. It is stable, fast, and ready for new projects. @@ -14,7 +10,7 @@ Cappuccino faithfully implements the proven design patterns of NeXTSTEP/Apple's ## Why Use Cappuccino? -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. +Cappuccino is an open-source framework that supports building powerful, desktop-class applications running in any modern web browser. 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. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: * **💻 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. From 9d24c855e27632e616612169d0d4b734242fa396 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 6 May 2026 19:55:15 +0200 Subject: [PATCH 126/136] Update project status in README Removed outdated information and clarified project status. --- README.markdown | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.markdown b/README.markdown index de4a02829..d82987db6 100644 --- a/README.markdown +++ b/README.markdown @@ -6,8 +6,6 @@ > **✨ 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 2026. It is stable, fast, and ready for new projects. ---- - ## Why Use Cappuccino? Cappuccino is an open-source framework that supports building powerful, desktop-class applications running in any modern web browser. 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. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: From 78e1ea43c81dd84f59b976dd86f13ac951ea920c Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 17 May 2026 15:58:16 +0200 Subject: [PATCH 127/136] new: layouting speedup in CPTextView --- AppKit/CPTextView/CPLayoutManager.j | 66 ++++++++++++++++++++++++++++- AppKit/CPTextView/CPTypesetter.j | 6 ++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index f64d8d6a4..103e19921 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -286,6 +286,8 @@ _oncontextmenuhandler = function () { return false; }; if (removeRange.length) _removeInvalidLineFragmentsRange = CPMakeRangeCopy(removeRange); + else + _removeInvalidLineFragmentsRange = nil; // We erased all lines if (!startIndex) @@ -716,7 +718,7 @@ _oncontextmenuhandler = function () { return false; }; var index = location - lineFragment._range.location; - return lineFragment._glyphsFrames[index]._descent; + return [lineFragment glyphFrames][index]._descent; } - (void)setLineFragmentRect:(CGRect)fragmentRect forGlyphRange:(CPRange)glyphRange usedRect:(CGRect)usedRect @@ -1074,6 +1076,7 @@ var _objectsInRange = function(aList, aRange) BOOL _isInvalid; BOOL _isLast; + BOOL _exactFramesCalculated; CGRect _fragmentRect; CGRect _usedRect; CGPoint _location; @@ -1161,6 +1164,7 @@ var _objectsInRange = function(aList, aRange) _range = CPMakeRangeCopy(aRange); _textContainer = aContainer; _isInvalid = NO; + _exactFramesCalculated = NO; _runs = []; _glyphsFrames = []; _glyphsOffsets = []; @@ -1207,6 +1211,8 @@ var _objectsInRange = function(aList, aRange) - (void)setAdvancements:(CPArray)someAdvancements { + _exactFramesCalculated = NO; + var count = someAdvancements.length, origin = CGPointMake(_fragmentRect.origin.x + _location.x, _fragmentRect.origin.y), height = _usedRect.size.height; @@ -1223,6 +1229,62 @@ var _objectsInRange = function(aList, aRange) } } +- (CPArray)glyphFrames +{ + if (!_exactFramesCalculated && _glyphsFrames && _runs && _runs.length > 0) + { + _exactFramesCalculated = YES; + var originX = _fragmentRect.origin.x + _location.x, + currentX = originX, + frameIndex = 0, + l = _runs.length; + + // Wir gehen alle Runs (Wörter/Textblöcke in dieser Zeile) durch + for (var r = 0; r < l; r++) + { + var run = _runs[r], + runStr = run.string; + + if (!runStr) + { + // Attachments / Unsichtbares überspringen + if (frameIndex < _glyphsFrames.length) + { + var frame = _glyphsFrames[frameIndex]; + frame.origin.x = currentX; + currentX += frame.size.width; + frameIndex++; + } + continue; + } + + var runLen = runStr.length, + runFont = run.font; + + // Nun messen wir für diesen sichtbaren Run den exakten Substring inkl. Kerning/Ligaturen + for (var i = 0; i < runLen; i++) + { + if (frameIndex >= _glyphsFrames.length) break; + + var frame = _glyphsFrames[frameIndex], + prefix = runStr.substr(0, i), + prefixWidth = prefix.length > 0 ? [prefix sizeWithFont:runFont inWidth:NULL].width : 0.0, + prefixWithChar = runStr.substr(0, i + 1), + prefixWithCharWidth = [prefixWithChar sizeWithFont:runFont inWidth:NULL].width; + + frame.origin.x = currentX + prefixWidth; + frame.size.width = prefixWithCharWidth - prefixWidth; + + frameIndex++; + } + + currentX += [runStr sizeWithFont:runFont inWidth:NULL].width; + } + } + + return _glyphsFrames; +} + - (void)_adjustForHeight:(double)height { var count = _glyphsFrames.length; @@ -1281,6 +1343,8 @@ var _objectsInRange = function(aList, aRange) - (void)drawInContext:(CGContext)context atPoint:(CGPoint)aPoint forRange:(CPRange)aRange { + [self glyphFrames]; // Erzwingt die exakte Berechnung, bevor gezeichnet wird! + var runs = _objectsInRange(_runs, aRange), c = runs.length, orig = CGPointMake(_fragmentRect.origin.x, _fragmentRect.origin.y); diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 49169206f..e8946fcfc 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -316,8 +316,10 @@ var CPSystemTypesetterFactory, lineRange.length++; measuringRange.length++; - var currentCharCode = theString.charCodeAt(glyphIndex), // use pure javascript methods for performance reasons - rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor; + var currentCharCode = theString.charCodeAt(glyphIndex), + charStr = theString.charAt(glyphIndex), + charWidth = [charStr sizeWithFont:currentFont inWidth:NULL].width, + rangeWidth = prevRangeWidth + charWidth; switch (currentCharCode) // faster than sending actionForControlCharacterAtIndex: called for each char. { From d6c5d8b4f2bdc25bb670b0aaf32b33a0204d1bd1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 17 May 2026 16:47:12 +0200 Subject: [PATCH 128/136] formatting --- AppKit/CPTextView/CPLayoutManager.j | 4 ++++ AppKit/CPTextView/CPTypesetter.j | 3 +++ 2 files changed, 7 insertions(+) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index 103e19921..6179243b9 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -1229,6 +1229,10 @@ var _objectsInRange = function(aList, aRange) } } +// The spacings with kerning/ligatures are recalculated here for the visible part +// See comment in layoutGlyphsInLayoutManager: of CPSimpleTypesetter +// This gives an overall speed imrovement of ~20% in my testing (depending on text size / view size of course) + - (CPArray)glyphFrames { if (!_exactFramesCalculated && _glyphsFrames && _runs && _runs.length > 0) diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index e8946fcfc..418bfbb4e 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -318,6 +318,9 @@ var CPSystemTypesetterFactory, var currentCharCode = theString.charCodeAt(glyphIndex), charStr = theString.charAt(glyphIndex), + // this is ignoring kerning and ligatures but 100x faster than calculating the full string + // we cut corners here and do the exact calculation in - glyphFrames of _CPLineFragment (CPLayoutManager.j) + // this gives an overall speed imrovement of ~20% in my testing (depending on text size / view size of course) charWidth = [charStr sizeWithFont:currentFont inWidth:NULL].width, rangeWidth = prevRangeWidth + charWidth; From 7f25de0fad1a3dea879ad6d74dde4683960d6bb9 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 17 May 2026 21:30:11 +0200 Subject: [PATCH 129/136] improved: kerning safe optimization --- AppKit/CPTextView/CPLayoutManager.j | 68 +---------------------------- AppKit/CPTextView/CPTypesetter.j | 20 ++++++--- 2 files changed, 14 insertions(+), 74 deletions(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index 6179243b9..926e2b993 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -718,7 +718,7 @@ _oncontextmenuhandler = function () { return false; }; var index = location - lineFragment._range.location; - return [lineFragment glyphFrames][index]._descent; + return lineFragment._glyphsFrames[index]._descent; } - (void)setLineFragmentRect:(CGRect)fragmentRect forGlyphRange:(CPRange)glyphRange usedRect:(CGRect)usedRect @@ -1076,7 +1076,6 @@ var _objectsInRange = function(aList, aRange) BOOL _isInvalid; BOOL _isLast; - BOOL _exactFramesCalculated; CGRect _fragmentRect; CGRect _usedRect; CGPoint _location; @@ -1164,7 +1163,6 @@ var _objectsInRange = function(aList, aRange) _range = CPMakeRangeCopy(aRange); _textContainer = aContainer; _isInvalid = NO; - _exactFramesCalculated = NO; _runs = []; _glyphsFrames = []; _glyphsOffsets = []; @@ -1211,8 +1209,6 @@ var _objectsInRange = function(aList, aRange) - (void)setAdvancements:(CPArray)someAdvancements { - _exactFramesCalculated = NO; - var count = someAdvancements.length, origin = CGPointMake(_fragmentRect.origin.x + _location.x, _fragmentRect.origin.y), height = _usedRect.size.height; @@ -1229,66 +1225,6 @@ var _objectsInRange = function(aList, aRange) } } -// The spacings with kerning/ligatures are recalculated here for the visible part -// See comment in layoutGlyphsInLayoutManager: of CPSimpleTypesetter -// This gives an overall speed imrovement of ~20% in my testing (depending on text size / view size of course) - -- (CPArray)glyphFrames -{ - if (!_exactFramesCalculated && _glyphsFrames && _runs && _runs.length > 0) - { - _exactFramesCalculated = YES; - var originX = _fragmentRect.origin.x + _location.x, - currentX = originX, - frameIndex = 0, - l = _runs.length; - - // Wir gehen alle Runs (Wörter/Textblöcke in dieser Zeile) durch - for (var r = 0; r < l; r++) - { - var run = _runs[r], - runStr = run.string; - - if (!runStr) - { - // Attachments / Unsichtbares überspringen - if (frameIndex < _glyphsFrames.length) - { - var frame = _glyphsFrames[frameIndex]; - frame.origin.x = currentX; - currentX += frame.size.width; - frameIndex++; - } - continue; - } - - var runLen = runStr.length, - runFont = run.font; - - // Nun messen wir für diesen sichtbaren Run den exakten Substring inkl. Kerning/Ligaturen - for (var i = 0; i < runLen; i++) - { - if (frameIndex >= _glyphsFrames.length) break; - - var frame = _glyphsFrames[frameIndex], - prefix = runStr.substr(0, i), - prefixWidth = prefix.length > 0 ? [prefix sizeWithFont:runFont inWidth:NULL].width : 0.0, - prefixWithChar = runStr.substr(0, i + 1), - prefixWithCharWidth = [prefixWithChar sizeWithFont:runFont inWidth:NULL].width; - - frame.origin.x = currentX + prefixWidth; - frame.size.width = prefixWithCharWidth - prefixWidth; - - frameIndex++; - } - - currentX += [runStr sizeWithFont:runFont inWidth:NULL].width; - } - } - - return _glyphsFrames; -} - - (void)_adjustForHeight:(double)height { var count = _glyphsFrames.length; @@ -1347,8 +1283,6 @@ var _objectsInRange = function(aList, aRange) - (void)drawInContext:(CGContext)context atPoint:(CGPoint)aPoint forRange:(CPRange)aRange { - [self glyphFrames]; // Erzwingt die exakte Berechnung, bevor gezeichnet wird! - var runs = _objectsInRange(_runs, aRange), c = runs.length, orig = CGPointMake(_fragmentRect.origin.x, _fragmentRect.origin.y); diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 418bfbb4e..ef793cb86 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -316,13 +316,8 @@ var CPSystemTypesetterFactory, lineRange.length++; measuringRange.length++; - var currentCharCode = theString.charCodeAt(glyphIndex), - charStr = theString.charAt(glyphIndex), - // this is ignoring kerning and ligatures but 100x faster than calculating the full string - // we cut corners here and do the exact calculation in - glyphFrames of _CPLineFragment (CPLayoutManager.j) - // this gives an overall speed imrovement of ~20% in my testing (depending on text size / view size of course) - charWidth = [charStr sizeWithFont:currentFont inWidth:NULL].width, - rangeWidth = prevRangeWidth + charWidth; + var currentCharCode = theString.charCodeAt(glyphIndex), // use pure javascript methods for performance reasons + rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor; switch (currentCharCode) // faster than sending actionForControlCharacterAtIndex: called for each char. { @@ -370,6 +365,17 @@ var CPSystemTypesetterFactory, wrapWidth = rangeWidth; wrapRange._height = _lineHeight; wrapRange._base = _lineBase; + + // Optimization: Start measuring from the next character to avoid O(n^2) + // string width calculation within a line since spaces do not carry ligatures or kerning. + // Only reset the measuring range if the next character is NOT another space. + // This prevents compounded subpixel rounding errors with contiguous spaces. + if (theString.charCodeAt(glyphIndex + 1) !== 32) + { + currentAnchor = rangeWidth; + measuringRange = CPMakeRange(glyphIndex + 1, 0); + } + break; case 10: From abe159ecebaa151a2ffa6e9b2bf6ba04b80d875b Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 21 May 2026 09:44:54 +0200 Subject: [PATCH 130/136] CPLayoutManager: Fix O(N^2) layout bottleneck when loading large text documents --- AppKit/CPTextView/CPLayoutManager.j | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index 926e2b993..fd0accf43 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -305,13 +305,17 @@ _oncontextmenuhandler = function () { return false; }; - (BOOL)_rescuingInvalidFragmentsWasPossibleForGlyphRange:(CPRange)aRange { - var l = _lineFragments.length, - location = aRange.location, - found = NO, - targetLine = 0; + // 1. EARLY EXIT: If there are no fragments to rescue (e.g. setting new text), do nothing. + if (!_lineFragmentsForRescue || _lineFragmentsForRescue.length === 0) + return NO; - // try to find the first linefragment of the desired range - for (; targetLine < l; targetLine++) + var l = _lineFragments.length, + location = aRange.location, + found = NO, + targetLine = l - 1; // Start from the END of the array + + // 2. REVERSE SEARCH: The fragment we want is almost always at the end. + for (; targetLine >= 0; targetLine--) { if (CPLocationInRange(location, _lineFragments[targetLine]._range)) { @@ -334,9 +338,6 @@ _oncontextmenuhandler = function () { return false; }; newLength = [[_textStorage string].length], removalSkip = 1; - // if (ABS(newLength - oldLength) > 1) - // return NO; - if (![oldLineFragment isVisuallyIdenticalToFragment:newLineFragment]) { isIdentical = NO; From c27b48d27a2abf56396aeab3dab88376b6519478 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 23 May 2026 08:46:29 +0200 Subject: [PATCH 131/136] Update documentation links in README --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index d82987db6..47c3c0b17 100644 --- a/README.markdown +++ b/README.markdown @@ -78,7 +78,7 @@ Pure JavaScript and Objective-J can be mixed and matched, even in the same file. ## Find Out More * **Official Website:** [cappuccino.dev](http://cappuccino.dev) -* **Documentation & Tutorials:** [cappuccino.dev/learn/](http://cappuccino.dev/learn/), [cappuccino cookbook](https://cappuccino-cookbook.5apps.com) +* **Documentation & Tutorials:** [cappuccino.dev/learn/](http://cappuccino.dev/learn/), [Browser online documentation](https://daboe01.github.io/CappDoc/), [cappuccino cookbook](https://cappuccino-cookbook.5apps.com) * **Gitter Community Chat:** [gitter.im/cappuccino/cappuccino](https://gitter.im/cappuccino/cappuccino) * **GitHub Wiki:** [github.com/cappuccino/cappuccino/wiki](https://github.com/cappuccino/cappuccino/wiki) * **FAQ:** [cappuccino.dev/support/faq.html](http://cappuccino.dev/support/faq.html) From 9312c9615495a480cd4d80a0714d062086d51832 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 23 May 2026 09:05:07 +0200 Subject: [PATCH 132/136] Clarify Cappuccino's purpose in README --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 47c3c0b17..f528fee34 100644 --- a/README.markdown +++ b/README.markdown @@ -8,7 +8,7 @@ ## Why Use Cappuccino? -Cappuccino is an open-source framework that supports building powerful, desktop-class applications running in any modern web browser. 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. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: +Cappuccino is not intended for building websites. It is for building **applications**—especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: * **💻 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. From 8258c8ef0be3e2291ecb22c90760b7c2f953b45f Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 23 May 2026 13:51:25 +0200 Subject: [PATCH 133/136] Revise Cappuccino usage description in README --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index f528fee34..d261d22f6 100644 --- a/README.markdown +++ b/README.markdown @@ -8,7 +8,7 @@ ## Why Use Cappuccino? -Cappuccino is not intended for building websites. It is for building **applications**—especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: +Cappuccino is for building **applications** in the browser —especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: * **💻 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. From dcb06fbc4f7cd515c5ef7555cf672f25a9d43387 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 23 May 2026 13:51:50 +0200 Subject: [PATCH 134/136] Fix formatting issues in README.markdown --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index d261d22f6..300f255b6 100644 --- a/README.markdown +++ b/README.markdown @@ -8,7 +8,7 @@ ## Why Use Cappuccino? -Cappuccino is for building **applications** in the browser —especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: +Cappuccino is for building **applications** in the browser — especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits: * **💻 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. From 6099311cbdb1c1e94f8e6e3ef6912726573024bb Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 25 May 2026 06:48:27 +0200 Subject: [PATCH 135/136] Update _CPRTFParser.j --- AppKit/CPTextView/_CPRTFParser.j | 65 ++++++++++++++------------------ 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 2fdb03985..1adac0311 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -40,6 +40,15 @@ FIXME: this class should be redone using a 'real' parser var hexTable = []; +var cp1252Map = { + 0x80: 0x20AC, 0x82: 0x201A, 0x83: 0x0192, 0x84: 0x201E, 0x85: 0x2026, + 0x86: 0x2020, 0x87: 0x2021, 0x88: 0x02C6, 0x89: 0x2030, 0x8A: 0x0160, + 0x8B: 0x2039, 0x8C: 0x0152, 0x8E: 0x017D, 0x91: 0x2018, 0x92: 0x2019, + 0x93: 0x201C, 0x94: 0x201D, 0x95: 0x2022, 0x96: 0x2013, 0x97: 0x2014, + 0x98: 0x02DC, 0x99: 0x2122, 0x9A: 0x0161, 0x9B: 0x203A, 0x9C: 0x0153, + 0x9E: 0x017E, 0x9F: 0x0178 +}; + // Hold the attributes of the current run @implementation _RTFAttribute : CPObject { @@ -346,25 +355,23 @@ var kRgsymRtf = { return ''; case "ipfnHex": - ch = _rtf.charAt(++_currentParseIndex); - var hex = ''; - - while (new RegExp("[a-fA-F0-9\\']").test(ch)) + // Konsumiere exakt 2 Zeichen nach dem \' + for (var i = 0; i < 2; i++) { - if (ch == "'") + var nextCh = _rtf.charAt(++_currentParseIndex); + if (/[a-fA-F0-9]/.test(nextCh)) { - _currentParseIndex++; - continue; + hex += nextCh; + } + else + { + _currentParseIndex--; + break; } - - hex += (ch + ''); - ch = _rtf.charAt(++_currentParseIndex); } - //ch = parseInt(ch, 16); - //console.log("hex : " + hex); + _hexreturn = YES; - _currentParseIndex--; if (_curState !== 0) return ''; @@ -734,32 +741,16 @@ var kRgsymRtf = { { if (ch.length > 0) { - if (parseInt(ch, 16) & 0x80) - { - hex += ch.toUpperCase(); - } - else - { - [self _appendPlainString: String.fromCharCode(parseInt((hex + ch), 16))]; - hex = ''; - } - - if (hex.length == 4) - { - var temp = parseInt(hex, 16); - - if (hexTable && hexTable[hex.toUpperCase()] !== undefined) - temp = parseInt(hexTable[hex.toUpperCase()], 16); - - [self _appendPlainString: String.fromCharCode(temp)]; - hex = ''; + var byteVal = parseInt(ch, 16); + var unicodeVal = byteVal; + + // Windows-1252 Mapping für den Bereich 0x80 - 0x9F anwenden + if (byteVal >= 0x80 && byteVal <= 0x9F) { + unicodeVal = cp1252Map[byteVal] || byteVal; } + + [self _appendPlainString: String.fromCharCode(unicodeVal)]; } - else - { - CPLogConsole("hex skipped"); - } - _hexreturn = NO; } else if (ch !== undefined && _curState === 0) From 141eb7b98ec27887bc215da650906ccad728192d Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 25 May 2026 06:56:32 +0200 Subject: [PATCH 136/136] fixed: wrong characters in output --- AppKit/CPTextView/_CPRTFParser.j | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 1adac0311..0530d5403 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -330,17 +330,16 @@ var kRgsymRtf = { - (BOOL)pushState { - _states.push["group"]; + _states.push(_curState); return YES; } - (BOOL)popState { - _states.pop(); - - if (_curState > 0) - _curState--; - + if (_states.length > 0) + { + _curState = _states.pop(); + } return YES; } @@ -603,9 +602,6 @@ var kRgsymRtf = { } - if (_states.length > 0) - _curState = 1; - return ''; } }