improved: table generation

This commit is contained in:
daboe01
2026-06-16 10:30:00 +02:00
parent 77433a3a1c
commit da061aac1d
2 changed files with 200 additions and 65 deletions
+42 -43
View File
@@ -89,12 +89,7 @@ function _points2twips(a) { return (a) * 20.0; }
{
if (self = [super init])
{
// maintain a dictionary for the used colours
// (for rtf-header generation)
colorDict = [CPMutableDictionary new];
//maintain a dictionary for the used fonts
//(for rtf-header generation)
fontDict = [CPMutableDictionary new];
fgColor = [CPColor blackColor];
@@ -104,7 +99,6 @@ function _points2twips(a) { return (a) * 20.0; }
return self;
}
// private stuff follows
- (CPString)fontTable
{
if (![fontDict count])
@@ -319,7 +313,6 @@ function _points2twips(a) { return (a) * 20.0; }
break;
}
// write first line indent and left indent
var twips = _points2twips([paraStyle firstLineHeadIndent]);
if (twips != 0.0)
@@ -392,7 +385,6 @@ function _points2twips(a) { return (a) * 20.0; }
var unwrap = function(obj) {
if (!obj) return null;
// If the object contains the table structures, bypass unwrapping
if ((typeof obj.respondsToSelector === "function" && ([obj respondsToSelector:@selector(headers)] || [obj respondsToSelector:@selector(rows)])) ||
obj.headers || obj._headers || obj.rows || obj._rows) {
return obj;
@@ -434,7 +426,6 @@ function _points2twips(a) { return (a) * 20.0; }
tableAttachment = unwrap(tableAttachment);
// Ultimate fallback scanner for layout character placeholder sequences
if (!tableAttachment && (substring === "\uFFFC" || substring === ""))
{
var keys = [attributes allKeys],
@@ -462,20 +453,48 @@ function _points2twips(a) { return (a) * 20.0; }
var headers = null,
rows = null;
if (typeof tableAttachment.respondsToSelector === "function") {
if ([tableAttachment respondsToSelector:@selector(headers)]) {
headers = [tableAttachment headers];
// Try to fetch from the active live view of the attachment first to capture user edits
var activeView = null;
if (typeof tableAttachment.respondsToSelector === "function" && [tableAttachment respondsToSelector:@selector(view)]) {
activeView = [tableAttachment view];
}
if (!activeView) {
activeView = tableAttachment._view || tableAttachment.view;
}
if (activeView) {
if (typeof activeView.respondsToSelector === "function") {
if ([activeView respondsToSelector:@selector(headers)]) {
headers = [activeView headers];
}
if ([activeView respondsToSelector:@selector(rows)]) {
rows = [activeView rows];
}
}
if ([tableAttachment respondsToSelector:@selector(rows)]) {
rows = [tableAttachment rows];
if (!headers) {
headers = activeView._headers || activeView.headers;
}
if (!rows) {
rows = activeView._rows || activeView.rows;
}
}
if (!headers) {
headers = tableAttachment._headers || tableAttachment.headers;
}
if (!rows) {
rows = tableAttachment._rows || tableAttachment.rows;
// Fall back to the attachment's parsed properties if the view is nil or lacks the properties
if (!headers || !rows) {
if (typeof tableAttachment.respondsToSelector === "function") {
if ([tableAttachment respondsToSelector:@selector(headers)]) {
headers = [tableAttachment headers];
}
if ([tableAttachment respondsToSelector:@selector(rows)]) {
rows = [tableAttachment rows];
}
}
if (!headers) {
headers = tableAttachment._headers || tableAttachment.headers;
}
if (!rows) {
rows = tableAttachment._rows || tableAttachment.rows;
}
}
var getCount = function(arr) {
@@ -563,23 +582,12 @@ function _points2twips(a) { return (a) * 20.0; }
headerString += [self paragraphStyle:paraStyle];
}
/*
* analyze attributes of current run
*
* FIXME: All the character attributes should be output relative to the font
* attributes of the paragraph. So if the paragraph has underline on it should
* still be possible to switch it off for some characters, which currently is
* not possible.
*/
attribEnum = [attributes keyEnumerator];
while ((currAttrib = [attribEnum nextObject]) != nil)
{
if ([currAttrib isEqualToString:CPFontAttributeName])
{
/*
* handle fonts
*/
var font,
fontName,
traits;
@@ -588,15 +596,9 @@ function _points2twips(a) { return (a) * 20.0; }
fontName = [font familyName];
traits = [[CPFontManager sharedFontManager] traitsOfFont:font];
/*
* font name
*/
if (currentFont == nil || ![fontName isEqualToString:[currentFont familyName]])
headerString += [self fontToken:fontName];
/*
* font size
*/
if (currentFont == nil || [font size] != [currentFont size])
{
var points = [font size] * 2,
@@ -605,9 +607,7 @@ function _points2twips(a) { return (a) * 20.0; }
pString = [CPString stringWithFormat:@"\\fs%d", points];
headerString += pString;
}
/*
* font attributes
*/
if (traits & CPItalicFontMask)
{
headerString += @"\\i";
@@ -713,7 +713,7 @@ function _points2twips(a) { return (a) * 20.0; }
var nobraces;
if ([headerString length])
nobraces = [CPString stringWithFormat:@"%@ %@", headerString, substring];
nobraces = [CPString stringWithFormat:@"%@ %@}", headerString, substring];
else
nobraces = substring;
@@ -733,7 +733,7 @@ function _points2twips(a) { return (a) * 20.0; }
completeRange = CPMakeRange(0, length),
paragraphStart = YES;
while (CPMaxRange(currRange) < CPMaxRange(completeRange)) // save all "runs"
while (CPMaxRange(currRange) < CPMaxRange(completeRange))
{
var attributes,
substring,
@@ -749,7 +749,6 @@ function _points2twips(a) { return (a) * 20.0; }
result += runString;
// Dynamically compute paragraph boundaries based on standard line feeds
if (substring.length > 0 && substring.charAt(substring.length - 1) === '\n')
paragraphStart = YES;
else
+158 -22
View File
@@ -28,20 +28,29 @@
CPArray _headers;
CPArray _rows;
BOOL _isResizing;
BOOL _isEditable;
BOOL _acceptsRichText;
}
- (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows
{
return [self initWithHeaders:headers rows:rows width:500.0];
return [self initWithHeaders:headers rows:rows width:500.0 isEditable:YES acceptsRichText:YES];
}
- (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows width:(float)totalWidth
{
return [self initWithHeaders:headers rows:rows width:totalWidth isEditable:YES acceptsRichText:NO];
}
- (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows width:(float)totalWidth isEditable:(BOOL)isEditable acceptsRichText:(BOOL)acceptsRichText
{
self = [super initWithFrame:CGRectMake(0, 0, totalWidth, 20)];
if (self)
{
_headers = headers;
_rows = rows;
_isEditable = isEditable;
_acceptsRichText = acceptsRichText;
_isResizing = NO;
[self _rebuildTableWithWidth:totalWidth];
@@ -91,11 +100,13 @@
[self resizeToWidth:totalWidth];
// Trigger a parent layout manager re-layout once the table is fully reconstructed and sized.
// Trigger parent layout engine re-calculation
var textView = [self superview];
if (textView && [textView isKindOfClass:[CPTextView class]])
{
var layoutManager = [textView layoutManager];
if (layoutManager)
{
var charRange = [self _findCharacterRangeInLayoutManager:layoutManager];
@@ -138,22 +149,81 @@
- (CPArray)headers
{
var numCols = _headers ? [_headers count] : 0;
if (numCols == 0 && _rows && [_rows count] > 0)
numCols = [[_rows objectAtIndex:0] count];
if (numCols == 0)
return _headers;
var subviews = [self subviews];
if ([subviews count] < numCols)
return _headers; // Subviews are not yet rendered, return cached fallback
var currentHeaders = [CPMutableArray array];
for (var c = 0; c < numCols; c++)
{
var cellView = [subviews objectAtIndex:c];
var textView = [self getTextViewFromCell:cellView];
var text = @"";
if (textView)
{
text = _acceptsRichText ? [[textView textStorage] copy] : [textView string];
}
[currentHeaders addObject:text];
}
_headers = currentHeaders;
return _headers;
}
- (CPArray)rows
{
var numCols = _headers ? [_headers count] : 0;
if (numCols == 0 && _rows && [_rows count] > 0)
numCols = [[_rows objectAtIndex:0] count];
if (numCols == 0 || !_rows)
return _rows;
var subviews = [self subviews];
var headerOffset = (_headers && [_headers count] > 0) ? numCols : 0;
var expectedCount = headerOffset + ([_rows count] * numCols);
if ([subviews count] < expectedCount)
return _rows; // Subviews are not yet rendered, return cached fallback
var currentRows = [CPMutableArray array];
var cellIndex = headerOffset;
for (var r = 0; r < [_rows count]; r++)
{
var rowData = [CPMutableArray array];
for (var c = 0; c < numCols; c++)
{
var cellView = [subviews objectAtIndex:cellIndex++];
var textView = [self getTextViewFromCell:cellView];
var text = @"";
if (textView)
{
text = _acceptsRichText ? [[textView textStorage] copy] : [textView string];
}
[rowData addObject:text];
}
[currentRows addObject:rowData];
}
_rows = currentRows;
return _rows;
}
- (CPView)viewForWidth:(float)width
{
// Always call resize to ensure cell views are constructed, but do not guard here
[self resizeToWidth:width];
return self;
}
- (CPView)createCellWithText:(CPString)text frame:(CGRect)frame isHeader:(BOOL)isHeader
- (CPView)createCellWithText:(id)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;
@@ -176,23 +246,48 @@
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:YES];
[textView setEditable:_isEditable];
[textView setSelectable:YES];
[textView setBackgroundColor:[CPColor clearColor]];
[textView setVerticallyResizable:YES];
[textView setHorizontallyResizable:NO];
[[textView textContainer] setWidthTracksTextView:YES];
// Configure cell rich text mode
[textView setRichText:_acceptsRichText];
// Intercept changes within cell TextViews to notify the table
[textView setDelegate:self];
// Configure cell text style using standard CPTextView APIs
var cellFont = isHeader ? [CPFont boldSystemFontOfSize:11.0] : [CPFont systemFontOfSize:11.0];
[textView setFont:cellFont];
[textView setTextColor:[CPColor blackColor]];
[textView setString:text];
// Populate either rich text or plain text safely
if (text && [text isKindOfClass:[CPAttributedString class]])
{
[[textView textStorage] setAttributedString:text];
}
else if (text)
{
[textView setString:String(text)];
}
else
{
[textView setString:@""];
}
[cellContainer addSubview:textView];
return cellContainer;
}
- (void)textDidChange:(CPNotification)aNotification
{
// Live update cell layouts and heights when changes are typed
[self resizeToWidth:CGRectGetWidth([self frame])];
}
- (CPTextView)getTextViewFromCell:(CPView)cellView
{
var subviews = [cellView subviews];
@@ -212,9 +307,13 @@
_isResizing = YES;
var numCols = _headers ? [_headers count] : 0;
if (numCols == 0 && _rows && [_rows count] > 0) {
numCols = [[_rows objectAtIndex:0] count];
// Use dynamic getters to fetch live edited strings
var currentHeaders = [self headers];
var currentRows = [self rows];
var numCols = currentHeaders ? [currentHeaders count] : 0;
if (numCols == 0 && currentRows && [currentRows count] > 0) {
numCols = [[currentRows objectAtIndex:0] count];
}
if (numCols == 0) {
_isResizing = NO;
@@ -236,14 +335,26 @@
var cellFont = isHeader ? [CPFont boldSystemFontOfSize:11.0] : [CPFont systemFontOfSize:11.0];
[measureTextField setFont:cellFont];
[measureTextField setStringValue:cellText];
var plainText = (cellText && typeof cellText.string === "function") ? [cellText string] : (cellText || @"");
plainText = String(plainText);
if (cellText && typeof cellText.string === "function") {
if ([measureTextField respondsToSelector:@selector(setAttributedStringValue:)]) {
[measureTextField setAttributedStringValue:cellText];
} else {
[measureTextField setStringValue:plainText];
}
} else {
[measureTextField setStringValue:plainText];
}
[measureTextField sizeToFit];
var naturalW = CGRectGetWidth([measureTextField frame]) + 24.0;
if (naturalW > colNaturalWidths[colIndex]) {
colNaturalWidths[colIndex] = naturalW;
}
var words = cellText.split(/[\s\-]/);
var words = plainText.split(/[\s\-]/);
var maxWordW = 50.0;
for (var w = 0; w < words.length; w++) {
var word = words[w].trim();
@@ -260,15 +371,15 @@
}
};
if (_headers) {
for (var c = 0; c < [_headers count]; c++) {
measureCell([_headers objectAtIndex:c], YES, c);
if (currentHeaders) {
for (var c = 0; c < [currentHeaders count]; c++) {
measureCell([currentHeaders objectAtIndex:c], YES, c);
}
}
if (_rows) {
for (var r = 0; r < [_rows count]; r++) {
var rowData = [_rows objectAtIndex:r];
if (currentRows) {
for (var r = 0; r < [currentRows count]; r++) {
var rowData = [currentRows objectAtIndex:r];
for (var c = 0; c < numCols; c++) {
var cellText = @"";
if (c < [rowData count]) {
@@ -373,26 +484,51 @@
return maxCellHeight;
};
if (_headers && [_headers count] > 0) {
if (currentHeaders && [currentHeaders count] > 0) {
var headerHeight = layoutRow(cellIndex);
cellIndex += numCols;
currentY += headerHeight;
}
if (_rows) {
for (var r = 0; r < [_rows count]; r++) {
if (currentRows) {
for (var r = 0; r < [currentRows count]; r++) {
var rowHeight = layoutRow(cellIndex);
cellIndex += numCols;
currentY += rowHeight;
}
}
// GUARD FRAME SIZE MUTATIONS: Only execute setFrameSize if dimensions actually change.
// This stops infinite layout passes while ensuring subviews are laid out.
// GUARD FRAME SIZE MUTATIONS
var currentSize = [self frame].size;
if (ABS(currentSize.width - newWidth) > 0.1 || ABS(currentSize.height - currentY) > 0.1)
{
[self setFrameSize:CGSizeMake(newWidth, currentY)];
// we need to re-layout the textview here.
// but this is not easy as the layout engine is not re-entrant
// this does not work: (delay does not matter), layout is always off
// setTimeout(function() {
// var textView = [self superview];
//
// if (textView && [textView isKindOfClass:[CPTextView class]])
// {
// var layoutManager = [textView layoutManager];
//
// if (layoutManager)
// {
// var charRange = [self _findCharacterRangeInLayoutManager:layoutManager];
//
// if (charRange && charRange.location !== CPNotFound)
// {
// [layoutManager invalidateLayoutForCharacterRange:charRange isSoft:NO actualCharacterRange:nil];
// [layoutManager invalidateDisplayForGlyphRange:charRange];
// [layoutManager _validateLayoutAndGlyphs];
// [textView sizeToFit];
// }
// }
// }
// }, 0);
}
_isResizing = NO;