From 480917eeab650b7e542eb15e8870f0a691f936a2 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 6 Apr 2026 13:15:55 +0200 Subject: [PATCH 1/7] 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 2/7] 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 872af9ce2a8d8f68f2cde84f03a85048978aa240 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 6 May 2026 19:54:43 +0200 Subject: [PATCH 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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: