Files
cappuccino/AppKit/CPTextView/_CPTableTextAttachment.j
T
2026-06-13 22:38:36 +02:00

333 lines
11 KiB
Plaintext

/* _CPTableTextAttachment.j
* A self-contained, renderable text attachment representation of a table.
*
* Copyright (C) 2026 Daniel Boehringer
*
* 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 "CPView.j"
@import "CPTextView.j"
@import "CPTextField.j"
@import <Foundation/CPAttributedString.j>
@implementation _CPTableTextAttachment : CPView
{
CPArray _headers;
CPArray _rows;
}
- (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows
{
return [self initWithHeaders:headers rows:rows width:500.0];
}
- (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows width:(float)totalWidth
{
self = [super initWithFrame:CGRectMake(0, 0, totalWidth, 20)];
if (self)
{
_headers = headers;
_rows = rows;
var numCols = [headers count];
if (numCols == 0 && [rows count] > 0)
numCols = [[rows objectAtIndex:0] count];
// Apply borders on the outer left and top container edges
if (self._DOMElement) {
self._DOMElement.style.borderTop = "1px solid #e0e0e0";
self._DOMElement.style.borderLeft = "1px solid #e0e0e0";
}
// Initialize header cells
if ([headers count] > 0) {
for (var c = 0; c < numCols; c++) {
var headerText = [headers objectAtIndex:c];
var cellView = [self createCellWithText:headerText frame:CGRectMakeZero() isHeader:YES];
[self addSubview:cellView];
}
}
// Initialize body rows
for (var r = 0; r < [rows count]; r++) {
var rowData = [rows objectAtIndex:r];
for (var c = 0; c < numCols; c++) {
var cellText = @"";
if (c < [rowData count])
cellText = [rowData objectAtIndex:c];
var cellView = [self createCellWithText:cellText frame:CGRectMakeZero() isHeader:NO];
[self addSubview:cellView];
}
}
[self resizeToWidth:totalWidth];
}
return self;
}
- (CPArray)headers
{
return _headers;
}
- (CPArray)rows
{
return _rows;
}
- (CPView)viewForWidth:(float)width
{
[self resizeToWidth:width];
return self;
}
- (CPView)createCellWithText:(CPString)text frame:(CGRect)frame isHeader:(BOOL)isHeader
{
var initialWidth = (frame.size.width > 0) ? frame.size.width : 120.0;
var initialHeight = (frame.size.height > 0) ? frame.size.height : 28.0;
var cellContainer = [[CPView alloc] initWithFrame:CGRectMake(frame.origin.x, frame.origin.y, initialWidth, initialHeight)];
[cellContainer setBackgroundColor:isHeader ? [CPColor colorWithWhite:0.92 alpha:1.0] : [CPColor whiteColor]];
// Bottom and right edge borders to construct the grid
var borderView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, initialWidth, initialHeight)];
[borderView setBackgroundColor:[CPColor clearColor]];
[borderView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
if (borderView._DOMElement) {
borderView._DOMElement.style.borderBottom = "1px solid #e0e0e0";
borderView._DOMElement.style.borderRight = "1px solid #e0e0e0";
borderView._DOMElement.style.boxSizing = "border-box";
}
[cellContainer addSubview:borderView];
var textContainer = [[CPTextContainer alloc] initWithContainerSize:CGSizeMake(initialWidth - 8, 1e7)];
var textView = [[CPTextView alloc] initWithFrame:CGRectMake(4, 2, initialWidth - 8, initialHeight - 4) textContainer:textContainer];
[textView setEditable:NO];
[textView setSelectable:YES];
[textView setBackgroundColor:[CPColor clearColor]];
[textView setVerticallyResizable:YES];
[textView setHorizontallyResizable:NO];
[[textView textContainer] setWidthTracksTextView:YES];
// Construct cell text using standard fonts
var cellFont = isHeader ? [CPFont boldSystemFontOfSize:11.0] : [CPFont systemFontOfSize:11.0];
var parsedText = [[CPAttributedString alloc] initWithString:text attributes:@{
CPFontAttributeName: cellFont,
CPForegroundColorAttributeName: [CPColor blackColor]
}];
var storage = [textView textStorage];
if (storage && [storage respondsToSelector:@selector(setAttributedString:)]) {
[storage setAttributedString:parsedText];
} else {
[textView setEditable:YES];
[textView setString:@""];
[textView insertText:parsedText];
[textView setEditable:NO];
}
[cellContainer addSubview:textView];
return cellContainer;
}
- (CPTextView)getTextViewFromCell:(CPView)cellView
{
var subviews = [cellView subviews];
for (var i = 0; i < [subviews count]; i++) {
var sub = [subviews objectAtIndex:i];
if ([sub isKindOfClass:[CPTextView class]]) {
return sub;
}
}
return nil;
}
- (void)resizeToWidth:(float)newWidth
{
var numCols = [_headers count];
if (numCols == 0 && [_rows count] > 0) {
numCols = [[_rows objectAtIndex:0] count];
}
if (numCols == 0) return;
var subviews = [self subviews];
var colNaturalWidths = [];
var colMinWidths = [];
for (var c = 0; c < numCols; c++) {
colNaturalWidths[c] = 80.0;
colMinWidths[c] = 60.0;
}
var measureTextField = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 10000.0, 24.0)];
[measureTextField setFont:[CPFont systemFontOfSize:13.0]];
var measureCell = function(cellText, isHeader, colIndex) {
var cellFont = isHeader ? [CPFont boldSystemFontOfSize:11.0] : [CPFont systemFontOfSize:11.0];
[measureTextField setFont:cellFont];
[measureTextField setStringValue:cellText];
[measureTextField sizeToFit];
var naturalW = CGRectGetWidth([measureTextField frame]) + 24.0;
if (naturalW > colNaturalWidths[colIndex]) {
colNaturalWidths[colIndex] = naturalW;
}
var words = cellText.split(/[\s\-]/);
var maxWordW = 50.0;
for (var w = 0; w < words.length; w++) {
var word = words[w].trim();
if (word.length === 0) continue;
[measureTextField setStringValue:word];
[measureTextField sizeToFit];
var wordW = CGRectGetWidth([measureTextField frame]) + 30.0;
if (wordW > maxWordW) {
maxWordW = wordW;
}
}
if (maxWordW > colMinWidths[colIndex]) {
colMinWidths[colIndex] = maxWordW;
}
};
for (var c = 0; c < [_headers count]; c++) {
measureCell([_headers objectAtIndex:c], YES, c);
}
for (var r = 0; r < [_rows count]; r++) {
var rowData = [_rows objectAtIndex:r];
for (var c = 0; c < numCols; c++) {
var cellText = @"";
if (c < [rowData count]) {
cellText = [rowData objectAtIndex:c];
}
measureCell(cellText, NO, c);
}
}
var totalMinWidth = 0.0;
for (var c = 0; c < numCols; c++) {
totalMinWidth += colMinWidths[c];
}
var colWidths = [];
if (newWidth <= totalMinWidth) {
var remainingWidth = newWidth;
for (var c = 0; c < numCols; c++) {
var w = Math.floor((colMinWidths[c] / totalMinWidth) * newWidth);
colWidths[c] = w;
remainingWidth -= w;
}
if (numCols > 0) colWidths[numCols - 1] += remainingWidth;
} else {
for (var c = 0; c < numCols; c++) {
colWidths[c] = colMinWidths[c];
}
var totalGrowthCapacity = 0.0;
var growthCapacities = [];
for (var c = 0; c < numCols; c++) {
var capacity = Math.max(0.0, colNaturalWidths[c] - colMinWidths[c]);
growthCapacities[c] = capacity;
totalGrowthCapacity += capacity;
}
var extraWidth = newWidth - totalMinWidth;
var remainingExtra = extraWidth;
for (var c = 0; c < numCols; c++) {
if (totalGrowthCapacity > 0) {
var w = Math.floor((growthCapacities[c] / totalGrowthCapacity) * extraWidth);
colWidths[c] += w;
remainingExtra -= w;
}
}
if (numCols > 0) {
colWidths[numCols - 1] += remainingExtra;
}
}
var cellIndex = 0;
var currentY = 0;
var layoutRow = function(startIndex) {
var maxCellHeight = 28.0;
for (var c = 0; c < numCols; c++) {
var idx = startIndex + c;
if (idx < [subviews count]) {
var cellView = [subviews objectAtIndex:idx];
var textView = [self getTextViewFromCell:cellView];
if (textView) {
var targetWidth = Math.max(10.0, colWidths[c] - 8);
[[textView textContainer] setContainerSize:CGSizeMake(targetWidth, 1e7)];
var usedRect = [[textView layoutManager] usedRectForTextContainer:[textView textContainer]];
var wrappedHeight = CGRectGetHeight(usedRect) + 12.0;
if (wrappedHeight > maxCellHeight) {
maxCellHeight = wrappedHeight;
}
}
}
}
var currentX = 0;
for (var c = 0; c < numCols; c++) {
var idx = startIndex + c;
if (idx < [subviews count]) {
var cellView = [subviews objectAtIndex:idx];
[cellView setFrame:CGRectMake(currentX, currentY, colWidths[c], maxCellHeight)];
var textView = [self getTextViewFromCell:cellView];
if (textView) {
var targetWidth = Math.max(10.0, colWidths[c] - 8);
var textY = 4.0;
var finalTextViewHeight = maxCellHeight - 8.0;
[textView setFrame:CGRectMake(4, textY, targetWidth, finalTextViewHeight)];
}
var cellSubviews = [cellView subviews];
if ([cellSubviews count] > 0) {
[[cellSubviews objectAtIndex:0] setFrame:CGRectMake(0, 0, colWidths[c], maxCellHeight)];
}
}
currentX += colWidths[c];
}
return maxCellHeight;
};
if ([_headers count] > 0) {
var headerHeight = layoutRow(cellIndex);
cellIndex += numCols;
currentY += headerHeight;
}
for (var r = 0; r < [_rows count]; r++) {
var rowHeight = layoutRow(cellIndex);
cellIndex += numCols;
currentY += rowHeight;
}
[self setFrameSize:CGSizeMake(newWidth, currentY)];
}
@end