diff --git a/AppKit/CPTableView.j b/AppKit/CPTableView.j index f0501fabb..e9376612e 100644 --- a/AppKit/CPTableView.j +++ b/AppKit/CPTableView.j @@ -395,6 +395,8 @@ CPTableViewFirstColumnOnlyAutoresizingStyle = 5; CPInteger _editingColumn; SEL _doubleAction; + id _doubleClickTarget @accessors(property=doubleClickTarget); + id _doubleClickArgument @accessors(property=doubleClickArgument); CPInteger _clickedRow; CPInteger _clickedColumn; unsigned _columnAutoResizingStyle; @@ -3709,13 +3711,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; @@ -4733,7 +4795,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]; + } } /* @@ -6148,6 +6215,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]; } diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index f64d8d6a4..926e2b993 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) diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 49169206f..ef793cb86 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -365,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: diff --git a/README.markdown b/README.markdown index 859570108..d82987db6 100644 --- a/README.markdown +++ b/README.markdown @@ -3,18 +3,12 @@ # 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. ---- - ## 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. 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