Merge remote-tracking branch 'upstream/main' into Aristo3

This commit is contained in:
daboe01 committed 2026-06-16 19:32:18 +02:00
commit b9e734a21b
7 files changed
+1657 -201

No files matched your search

+179 -68
View File
@@ -292,7 +292,6 @@ _oncontextmenuhandler = function () { return false; };
// We erased all lines
if (!startIndex)
[self setExtraLineFragmentRect:CGRectMake(0, 0) usedRect:CGRectMake(0, 0) textContainer:nil];
// document.title=startIndex;
[_typesetter layoutGlyphsInLayoutManager:self startingAtGlyphIndex:startIndex maxNumberOfLineFragments:-1 nextGlyphIndex:nil];
@@ -532,20 +531,26 @@ _oncontextmenuhandler = function () { return false; };
var frames = [fragment glyphFrames],
len = fragment._range.length;
for (var j = 0; j < len; j++)
if (frames)
{
if (CGRectContainsPoint(frames[j], point))
{
if (partialFraction)
partialFraction[0] = (point.x - frames[j].origin.x) / frames[j].size.width;
var maxLen = MIN(len, frames.length);
return fragment._range.location + j;
for (var j = 0; j < maxLen; j++)
{
var frame = frames[j];
if (frame && CGRectContainsPoint(frame, point))
{
if (partialFraction)
partialFraction[0] = (point.x - frame.origin.x) / frame.size.width;
return fragment._range.location + j;
}
}
}
}
}
// Not found, maybe a point left to the last character was clicked -> search again with broader constraints
if ([[_textStorage string] length])
{
for (var i = 0; i < c; i++)
@@ -554,30 +559,33 @@ _oncontextmenuhandler = function () { return false; };
if (fragment._textContainer === container)
{
// Within the horizontal territory of the current (not-empty) line?
if (fragment._range.length > 0 && point.y > fragment._fragmentRect.origin.y &&
point.y <= fragment._fragmentRect.origin.y + fragment._fragmentRect.size.height)
if (fragment._range.length > 0 && point.y > fragment._fragmentRect.origin.y &&
point.y <= fragment._fragmentRect.origin.y + fragment._fragmentRect.size.height)
{
if (i < c - 1 && _lineFragments[i + 1]._fragmentRect.origin.y === fragment._fragmentRect.origin.y)
continue;
var nlLoc = CPMaxRange(fragment._range),
frames = [fragment glyphFrames];
if (frames && frames.length > 0)
{
// Skip tabs and move on the last fragment in this line
if (i < c - 1 && _lineFragments[i + 1]._fragmentRect.origin.y === fragment._fragmentRect.origin.y)
continue;
var lastFrame = frames[frames.length - 1],
firstFrame = frames[0];
var nlLoc = CPMaxRange(fragment._range),
lastFrame = [fragment glyphFrames][fragment._range.length - 1],
firstFrame = [fragment glyphFrames][0];
if (lastFrame && firstFrame)
{
if (_isNewlineCharacter([[_textStorage string] characterAtIndex:nlLoc > 0 ? nlLoc - 1 : 0]))
nlLoc--;
// stay on the line the newline character belongs to
if (_isNewlineCharacter([[_textStorage string] characterAtIndex:nlLoc > 0 ? nlLoc - 1 : 0]))
nlLoc--;
// Clicked right to the last character
if (point.x > CGRectGetMaxX(lastFrame))
return nlLoc;
// Clicked left to the last character
else if (point.x <= CGRectGetMinX(firstFrame))
return fragment._range.location;
else
return nlLoc;
if (point.x > CGRectGetMaxX(lastFrame))
return nlLoc;
else if (point.x <= CGRectGetMinX(firstFrame))
return fragment._range.location;
else
return nlLoc;
}
}
}
}
}
@@ -707,7 +715,11 @@ _oncontextmenuhandler = function () { return false; };
var index = location - lineFragment._range.location;
return lineFragment._glyphsOffsets[index];
if (index < 0 || !lineFragment._glyphsOffsets || index >= lineFragment._glyphsOffsets.length)
return 0.0;
var offset = lineFragment._glyphsOffsets[index];
return (offset === undefined) ? 0.0 : offset;
}
- (double)_descentAtLocation:(unsigned)location
@@ -719,7 +731,11 @@ _oncontextmenuhandler = function () { return false; };
var index = location - lineFragment._range.location;
return lineFragment._glyphsFrames[index]._descent;
if (index < 0 || !lineFragment._glyphsFrames || index >= lineFragment._glyphsFrames.length)
return 0.0;
var frame = lineFragment._glyphsFrames[index];
return (frame && frame._descent !== undefined) ? frame._descent : 0.0;
}
- (void)setLineFragmentRect:(CGRect)fragmentRect forGlyphRange:(CPRange)glyphRange usedRect:(CGRect)usedRect
@@ -843,11 +859,16 @@ _oncontextmenuhandler = function () { return false; };
{
if (_lineFragments.length > 0 && index >= [self numberOfGlyphs] - 1)
{
var lineFragment= _lineFragments[_lineFragments.length - 1],
var lineFragment = _lineFragments[_lineFragments.length - 1],
glyphFrames = [lineFragment glyphFrames];
if (glyphFrames.length > 0)
return CGPointCreateCopy(glyphFrames[glyphFrames.length - 1].origin);
if (glyphFrames && glyphFrames.length > 0)
{
var frame = glyphFrames[glyphFrames.length - 1];
if (frame)
return CGPointCreateCopy(frame.origin);
}
}
var lineFragment = _objectWithLocationInRange(_lineFragments, index);
@@ -858,8 +879,17 @@ _oncontextmenuhandler = function () { return false; };
return CGPointCreateCopy(lineFragment._location);
var glyphFrames = [lineFragment glyphFrames];
var relativeIndex = index - lineFragment._range.location;
return CGPointCreateCopy(glyphFrames[index - lineFragment._range.location].origin);
if (glyphFrames && relativeIndex >= 0 && relativeIndex < glyphFrames.length)
{
var frame = glyphFrames[relativeIndex];
if (frame)
return CGPointCreateCopy(frame.origin);
}
return CGPointCreateCopy(lineFragment._location);
}
return CGPointMakeZero();
@@ -905,7 +935,6 @@ _oncontextmenuhandler = function () { return false; };
inTextContainer:(CPTextContainer)container
rectCount:(CGRectPointer)rectCount
{
var rectArray = [],
lineFragments = _objectsInRange(_lineFragments, selectedCharRange);
@@ -924,21 +953,29 @@ _oncontextmenuhandler = function () { return false; };
rect = nil,
len = fragment._range.length;
for (var j = 0; j < len; j++)
if (frames)
{
if (CPLocationInRange(fragment._range.location + j, selectedCharRange))
for (var j = 0; j < len; j++)
{
var correctedRect = CGRectCreateCopy(frames[j]);
correctedRect.size.height -= frames[j]._descent;
correctedRect.origin.y -= frames[j]._descent;
if (j < frames.length && CPLocationInRange(fragment._range.location + j, selectedCharRange))
{
var frame = frames[j];
if (!rect)
rect = CGRectCreateCopy(correctedRect);
else
rect = CGRectUnion(rect, correctedRect);
if (frame)
{
var correctedRect = CGRectCreateCopy(frame);
correctedRect.size.height -= frame._descent;
correctedRect.origin.y -= frame._descent;
if (_isNewlineCharacter([[_textStorage string] characterAtIndex:MAX(0, CPMaxRange(selectedCharRange) - 1)]))
rect.size.width = containerSize.width - rect.origin.x;
if (!rect)
rect = CGRectCreateCopy(correctedRect);
else
rect = CGRectUnion(rect, correctedRect);
if (_isNewlineCharacter([[_textStorage string] characterAtIndex:MAX(0, CPMaxRange(selectedCharRange) - 1)]))
rect.size.width = containerSize.width - rect.origin.x;
}
}
}
}
@@ -949,7 +986,7 @@ _oncontextmenuhandler = function () { return false; };
var len = rectArray.length;
for (var i = 0; i < len - 1; i++) // extend the width of all but the last one
for (var i = 0; i < len - 1; i++)
{
if (FLOOR(CGRectGetMaxY(rectArray[i])) == FLOOR(CGRectGetMaxY(rectArray[i + 1])))
continue;
@@ -1095,6 +1132,10 @@ var _objectsInRange = function(aList, aRange)
- (id)createDOMElementWithText:(CPString)aString andFont:(CPFont)aFont andColor:(CPColor)fgColor andBackgroundColor:(CPColor)bgColor andUnderline:(CPUnderlineStyle)aUnderline
{
if (!aString || aString.length === 0)
return nil;
#if PLATFORM(DOM)
var style,
span = document.createElement("span");
@@ -1175,18 +1216,16 @@ var _objectsInRange = function(aList, aRange)
effectiveRange = attributes ? CPIntersectionRange(aRange, effectiveRange) : aRange;
var string = [textStorage._string substringWithRange:effectiveRange],
underline = [attributes objectForKey:CPUnderlineStyleAttributeName] || CPUnderlineStyleNone;
underline = [attributes objectForKey:CPUnderlineStyleAttributeName] || CPUnderlineStyleNone,
paragraphStyle = [attributes objectForKey:CPParagraphStyleAttributeName] || [CPParagraphStyle defaultParagraphStyle];
// this is an attachment -> create a run for it
if (string === _CPAttachmentCharacterAsString)
{
if (![attributes objectForKey:_CPAttachmentInvisible])
{
var view = [attributes objectForKey:_CPAttachmentView],
viewCopy = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:view]],
elem = viewCopy._DOMElement,
run = {_range:CPMakeRangeCopy(effectiveRange), color:nil, font:nil, elem:elem, string:nil, view:viewCopy};
var view = [attributes objectForKey:_CPAttachmentView];
var run = {_range:CPMakeRangeCopy(effectiveRange), color:nil, font:nil, elem:nil, string:nil, view:view, paragraphStyle:paragraphStyle};
_runs.push(run);
}
}
@@ -1194,10 +1233,63 @@ var _objectsInRange = function(aList, aRange)
{
var color = [attributes objectForKey:CPForegroundColorAttributeName],
bgcolor = [attributes objectForKey:CPBackgroundColorAttributeName],
font = [attributes objectForKey:CPFontAttributeName] || [textStorage font] || [CPFont systemFontOfSize:12.0],
run = {_range:CPMakeRangeCopy(effectiveRange), color:color, font:font, elem:nil, string:string, bgcolor:bgcolor};
font = [attributes objectForKey:CPFontAttributeName] || [textStorage font] || [CPFont systemFontOfSize:12.0];
_runs.push(run);
var currentLoc = effectiveRange.location,
strLen = string.length,
startIdx = 0;
for (var i = 0; i < strLen; i++)
{
if (string.charCodeAt(i) === 9) // Tabulator-Zeichen '\t'
{
if (i > startIdx)
{
var subString = string.substring(startIdx, i),
subRange = CPMakeRange(currentLoc + startIdx, i - startIdx),
run = {
_range: subRange,
color: color,
font: font,
elem: nil,
string: subString,
bgcolor: bgcolor,
paragraphStyle: paragraphStyle
};
_runs.push(run);
}
var tabRange = CPMakeRange(currentLoc + i, 1),
tabRun = {
_range: tabRange,
color: nil,
font: nil,
elem: nil,
string: nil,
bgcolor: nil,
paragraphStyle: paragraphStyle
};
_runs.push(tabRun);
startIdx = i + 1;
}
}
if (startIdx < strLen)
{
var subString = string.substring(startIdx, strLen),
subRange = CPMakeRange(currentLoc + startIdx, strLen - startIdx),
run = {
_range: subRange,
color: color,
font: font,
elem: nil,
string: subString,
bgcolor: bgcolor,
paragraphStyle: paragraphStyle
};
_runs.push(run);
}
}
if (!CPMaxRange(effectiveRange))
@@ -1269,13 +1361,11 @@ var _objectsInRange = function(aList, aRange)
for (var i = 0; i < l; i++)
{
if (_runs[i].view && _runs[i].DOMactive)
[_runs[i].view removeFromSuperview];
if (_runs[i].elem && _runs[i].DOMactive)
{
if (_runs[i].view)
[_runs[i].view removeFromSuperview];
else
_textContainer._textView._DOMElement.removeChild(_runs[i].elem);
}
_textContainer._textView._DOMElement.removeChild(_runs[i].elem);
_runs[i].elem = nil;
_runs[i].DOMactive = NO;
@@ -1288,6 +1378,9 @@ var _objectsInRange = function(aList, aRange)
c = runs.length,
orig = CGPointMake(_fragmentRect.origin.x, _fragmentRect.origin.y);
if (_runs.length === 0)
return;
for (var i = 0; i < c; i++)
{
var run = runs[i];
@@ -1302,13 +1395,21 @@ var _objectsInRange = function(aList, aRange)
continue;
var loc = run._range.location - _runs[0]._range.location;
// Safety bounds check to protect against uninitialized/empty glyph frames or offsets
if (loc < 0 || loc >= _glyphsFrames.length || !_glyphsFrames[loc] || !_glyphsOffsets || loc >= _glyphsOffsets.length)
continue;
orig.x = _glyphsFrames[loc].origin.x + aPoint.x;
orig.y = _glyphsFrames[loc].origin.y + aPoint.y + _glyphsOffsets[loc];
if(run.elem)
if(run.elem || run.view)
{
run.elem.style.left = (orig.x) + "px";
run.elem.style.top = (orig.y) + "px";
if (run.elem)
{
run.elem.style.left = (orig.x) + "px";
run.elem.style.top = (orig.y) + "px";
}
if (run.view)
[run.view setFrameOrigin:orig];
@@ -1316,8 +1417,9 @@ var _objectsInRange = function(aList, aRange)
if (!run.DOMactive)
{
if (run.view)
[self._textContainer._textView addSubview:run.view];
else
[_textContainer._textView addSubview:run.view];
if (run.elem)
_textContainer._textView._DOMElement.appendChild(run.elem);
}
@@ -1358,6 +1460,12 @@ var _objectsInRange = function(aList, aRange)
if (newFragmentRuns[i].color !== oldFragmentRuns[i].color || newFragmentRuns[i].bgcolor !== oldFragmentRuns[i].bgcolor || newFragmentRuns[i].font !== oldFragmentRuns[i].font)
return NO;
var oldStyle = oldFragmentRuns[i].paragraphStyle || [CPParagraphStyle defaultParagraphStyle],
newStyle = newFragmentRuns[i].paragraphStyle || [CPParagraphStyle defaultParagraphStyle];
if (![oldStyle isEqual:newStyle])
return NO;
}
return YES;
@@ -1378,6 +1486,9 @@ var _objectsInRange = function(aList, aRange)
if (_runs[i].view)
_runs[i].view._frame.origin.y += verticalOffset;
if (_runs[i].elem)
_runs[i].elem.top = (_runs[i].elem.top + verticalOffset) + 'px';
_runs[i].DOMpatched = YES;
}
}
+133 -29
View File
@@ -46,6 +46,7 @@ CPRulerOrientationVertical = 1
float _imageValue @accessors(property=imageValue);
id _representedObject @accessors(property=representedObject);
CPTextField _label;
CPView _customHandleView;
}
- (id)initWithRulerView:(CPRulerView)aRulerView markerLocation:(float)aLocation imageValue:(float)anImageValue representedObject:(id)anObject
@@ -62,6 +63,9 @@ CPRulerOrientationVertical = 1
[_label setAlignment:CPCenterTextAlignment];
[self addSubview:_label];
_customHandleView = [[CPView alloc] initWithFrame:CGRectMakeZero()];
[self addSubview:_customHandleView];
[self updateMarkerIcon];
}
return self;
@@ -78,34 +82,76 @@ CPRulerOrientationVertical = 1
[self updateMarkerIcon];
}
// Dynamically sets the Unicode triangle direction based on the alignment or indent type
// Dynamically sets the Unicode arrow direction/type based on the alignment or indent type
- (void)setFrame:(CGRect)aFrame
{
[super setFrame:aFrame];
[self updateMarkerIcon];
}
// Dynamically sets the Unicode triangle direction based on the alignment or indent type,
// or draws custom split-height grab handles for indentation controls.
- (void)updateMarkerIcon
{
if ([_representedObject isKindOfClass:[CPTextTab class]])
var isIndentMarker = (_representedObject === @"CPFirstLineIndent" || _representedObject === @"CPHeadIndent");
if (isIndentMarker)
{
var align = [_representedObject alignment];
if (align === CPLeftTextAlignment)
[_label setStringValue:@"▶"]; // Left-aligned points Right
else if (align === CPCenterTextAlignment)
[_label setStringValue:@"▼"]; // Center-aligned points Down
else if (align === CPRightTextAlignment)
[_label setStringValue:@"◀"]; // Right-aligned points Left
}
else if ([_representedObject isKindOfClass:[CPString class]])
{
if (_representedObject === @"CPFirstLineIndent")
[_label setStringValue:@"⥔"]; // Dotted shaft arrow pointing down for first-line indent
else if (_representedObject === @"CPHeadIndent")
[_label setStringValue:@"⥜"]; // Solid shaft arrow pointing down for following-lines (head) indent
else if (_representedObject === @"CPTailIndent")
[_label setStringValue:@"⥘"]; // Solid downward triangle for tail indent
[_label setHidden:YES];
[_customHandleView setHidden:NO];
var frame = [self bounds];
[_customHandleView setFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
// Remove old internal rendering to update cleanly
[[_customHandleView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
var isFirstLine = (_representedObject === @"CPFirstLineIndent");
// Dark outline/border representation
[_customHandleView setBackgroundColor:[CPColor colorWithWhite:0.5 alpha:1.0]];
// Inner fill (top handle is lighter, bottom is slightly darker)
var innerView = [[CPView alloc] initWithFrame:CGRectMake(1.0, 1.0, frame.size.width - 2.0, frame.size.height - 2.0)];
if (isFirstLine)
[innerView setBackgroundColor:[CPColor colorWithWhite:0.92 alpha:1.0]];
else
[_label setStringValue:@"⇡"]; // Fallback standard up marker
[innerView setBackgroundColor:[CPColor colorWithWhite:0.80 alpha:1.0]];
[_customHandleView addSubview:innerView];
// Horizontal indicator line to visually guide drag interactions
var gripLine = [[CPView alloc] initWithFrame:CGRectMake(Math.floor(frame.size.width / 2.0) - 1.0, 2.0, 1.0, frame.size.height - 4.0)];
[gripLine setBackgroundColor:[CPColor colorWithWhite:0.6 alpha:1.0]];
[innerView addSubview:gripLine];
}
else
{
[_label setStringValue:@"⇡"]; // Fallback standard up marker
[_label setHidden:NO];
[_customHandleView setHidden:YES];
[_label setFrame:[self bounds]];
if ([_representedObject isKindOfClass:[CPTextTab class]])
{
var align = [_representedObject alignment];
if (align === CPLeftTextAlignment)
[_label setStringValue:@"▶"]; // Left-aligned points Right
else if (align === CPCenterTextAlignment)
[_label setStringValue:@"▼"]; // Center-aligned points Down
else if (align === CPRightTextAlignment)
[_label setStringValue:@"◀"]; // Right-aligned points Left
}
else if ([_representedObject isKindOfClass:[CPString class]])
{
if (_representedObject === @"CPTailIndent")
[_label setStringValue:@"⥘"]; // Solid downward triangle for tail indent
else
[_label setStringValue:@"⇡"]; // Fallback standard up marker
}
else
{
[_label setStringValue:@"⇡"]; // Fallback standard up marker
}
}
}
#pragma mark -
@@ -297,15 +343,31 @@ CPRulerOrientationVertical = 1
if (isHorizontal)
{
var x = markerLocation - scrollPoint.x - 6.0, // Center the 12px wide marker
y = rulerHeight - 11.0; // Sit perfectly above bottom border
y = rulerHeight - 11.0,
w = 12.0,
h = 12.0;
// Keep horizontal marker within the bounds of the ruler to prevent clipping
if (x < 0.0)
x = 0.0;
else if (x + 12.0 > rulerWidth)
x = rulerWidth - 12.0;
// Align the First Line Indent (upper half) and Head Indent (lower half) controls
if ([aMarker representedObject] === @"CPFirstLineIndent")
{
y = 0.0;
h = Math.floor(rulerHeight / 2.0);
}
else if ([aMarker representedObject] === @"CPHeadIndent")
{
y = Math.floor(rulerHeight / 2.0);
h = rulerHeight - y - 1.0; // Subtract 1px to stay cleanly above bottom border
}
else
{
// Keep normal horizontal markers within the bounds of the ruler to prevent clipping
if (x < 0.0)
x = 0.0;
else if (x + 12.0 > rulerWidth)
x = rulerWidth - 12.0;
}
[aMarker setFrame:CGRectMake(x, y, 12.0, 12.0)];
[aMarker setFrame:CGRectMake(x, y, w, h)];
}
else
{
@@ -379,7 +441,9 @@ CPRulerOrientationVertical = 1
if (newLocation < 0) newLocation = 0;
[_draggingMarker setImageValue:newLocation];
[self _positionMarker:_draggingMarker];
// Smoothly redraw ruler and margin bounds on every drag step
[self updateRuler];
// Check if dragged off the ruler (more than 15px off the boundary)
var draggedOff = isHorizontal ? (localPoint.y < -15 || localPoint.y > CGRectGetHeight([self bounds]) + 15)
@@ -432,6 +496,7 @@ CPRulerOrientationVertical = 1
}
_draggingMarker = nil;
[self updateRuler];
}
@@ -464,6 +529,45 @@ CPRulerOrientationVertical = 1
[bottomBorder setBackgroundColor:[CPColor colorWithWhite:0.75 alpha:1.0]];
[self addSubview:bottomBorder];
// Find indent markers to determine background highlight boundaries
var firstLineMarker = nil,
headMarker = nil;
for (var i = 0; i < [_markers count]; i++)
{
var m = [_markers objectAtIndex:i];
if ([m representedObject] === @"CPFirstLineIndent")
firstLineMarker = m;
else if ([m representedObject] === @"CPHeadIndent")
headMarker = m;
}
var halfHeight = Math.floor(rulerHeight / 2.0);
// Draw First Line Indent background - top half (lighter gray)
if (firstLineMarker)
{
var firstLineX = [firstLineMarker imageValue] - scrollPoint.x;
if (firstLineX > 0)
{
var firstLineBg = [[CPView alloc] initWithFrame:CGRectMake(0, 0, firstLineX, halfHeight)];
[firstLineBg setBackgroundColor:[CPColor colorWithWhite:0.93 alpha:1.0]];
[self addSubview:firstLineBg];
}
}
// Draw Head Indent background - bottom half (slightly darker gray)
if (headMarker)
{
var headX = [headMarker imageValue] - scrollPoint.x;
if (headX > 0)
{
var headBg = [[CPView alloc] initWithFrame:CGRectMake(0, halfHeight, headX, rulerHeight - halfHeight - 1.0)];
[headBg setBackgroundColor:[CPColor colorWithWhite:0.86 alpha:1.0]];
[self addSubview:headBg];
}
}
// Render ruler tick lines and labels on top of shaded areas
for (var val = start; val <= end; val += 10)
{
if (val < 0) continue;
+28 -3
View File
@@ -401,6 +401,7 @@ var kDelegateRespondsTo_textShouldBeginEditing
- (void)superviewFrameChanged:(CPNotification)aNotification
{
_exposedRect = nil;
[self sizeToFit];
}
- (void)viewWillMoveToSuperview:(CPView)aView
@@ -1402,7 +1403,10 @@ Sets the selection to a range of characters in response to user action.
- (void)moveLeftAndModifySelection:(id)sender
{
if ([self isSelectable])
{
[self _extendSelectionIntoDirection:-1 granularity:CPSelectByCharacter];
[self scrollRangeToVisible:_selectionRange];
}
}
- (void)moveBackward:(id)sender
@@ -1424,7 +1428,10 @@ Sets the selection to a range of characters in response to user action.
- (void)moveLeft:(id)sender
{
if ([self isSelectable])
{
[self _establishSelection:CPMakeRange(_selectionRange.location - (_selectionRange.length ? 0 : 1), 0) byExtending:NO];
[self scrollRangeToVisible:_selectionRange];
}
}
- (void)moveToEndOfParagraph:(id)sender
@@ -1658,7 +1665,10 @@ Sets the selection to a range of characters in response to user action.
- (void)moveRight:(id)sender
{
if ([self isSelectable])
{
[self _establishSelection:CPMakeRange(CPMaxRange(_selectionRange) + (_selectionRange.length ? 0 : 1), 0) byExtending:NO];
[self scrollRangeToVisible:_selectionRange];
}
}
- (void)_deleteForRange:(CPRange)changedRange
@@ -2139,9 +2149,15 @@ Sets the selection to a range of characters in response to user action.
if (CPEmptyRange(aRange))
{
if (aRange.location >= [_layoutManager numberOfCharacters])
rect = [_layoutManager extraLineFragmentRect];
{
rect = CGRectCreateCopy([_layoutManager extraLineFragmentRect]);
rect.size.width = 1.0;
}
else
rect = [_layoutManager lineFragmentRectForGlyphAtIndex:aRange.location effectiveRange:nil];
{
rect = CGRectCreateCopy([_layoutManager boundingRectForGlyphRange:CPMakeRange(aRange.location, 1) inTextContainer:_textContainer]);
rect.size.width = 1.0;
}
}
else
{
@@ -2637,6 +2653,10 @@ var compareTabStops = function(obj1, obj2, context) {
[_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName];
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
}
[_layoutManager _validateLayoutAndGlyphs];
[self sizeToFit];
[self setNeedsDisplay:YES];
}
- (void)rulerView:(CPRulerView)rulerView didRemoveMarker:(CPRulerMarker)marker
@@ -2856,11 +2876,16 @@ var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey",
_typingAttributes = [[_textStorage attributesAtIndex:0 effectiveRange:nil] copy];
if (!_typingAttributes)
_typingAttributes = [CPMutableDictionary dictionary];
if (![_typingAttributes valueForKey:CPForegroundColorAttributeName])
[_typingAttributes setObject:[CPColor blackColor] forKey:CPForegroundColorAttributeName];
_textColor = [_typingAttributes valueForKey:CPForegroundColorAttributeName];
[self setFont:[_typingAttributes valueForKey:CPFontAttributeName]];
var decodedFont = [_typingAttributes valueForKey:CPFontAttributeName] || [CPFont systemFontOfSize:12.0];
[self setFont:decodedFont];
[self setString:[_textStorage string]];
+448 -34
View File
@@ -21,8 +21,10 @@
@import <Foundation/CPAttributedString.j>
@import <Foundation/CPGeometry.j>
@import "CPTextStorage.j"
@import "CPFontManager.j"
@import "CPParagraphStyle.j"
@import "_CPTableTextAttachment.j"
@global CPLeftTextAlignment
@global CPRightTextAlignment
@@ -34,6 +36,8 @@
@global CPForegroundColorAttributeName
@global CPBackgroundColorAttributeName
@global CPParagraphStyleAttributeName
@global CPAttachmentAttributeName
@global CPUnderlineStyleAttributeName
@global CPLeftTabStopType
@global CPRightTabStopType
@@ -111,11 +115,6 @@ var cp1252Map = {
if (font)
return font;
//Before giving up and using a default font, we try if this is
//not the case of a font with a composite name, such as
//'Helvetica-Light'. In that case, even if we don't have
//exactly an 'Helvetica-Light' font family, we might have an
//'Helvetica' one.
var range = [fontName rangeOfString:@"-"];
if (range.location != CPNotFound)
@@ -125,7 +124,6 @@ var cp1252Map = {
font = [CPFont fontWithName:fontFamily size:fontSize];
}
/* Last resort, default font. :-( */
if (font == nil)
font = [CPFont systemFontOfSize:fontSize];
@@ -175,7 +173,15 @@ var cp1252Map = {
- (void)addTab:(float)location type:(CPTextTabType)type
{
var tab = [[CPTextTab alloc] initWithType:type
var alignment = CPLeftTextAlignment;
if (type === CPCenterTabStopType || type === CPCenterTextAlignment)
alignment = CPCenterTextAlignment;
else if (type === CPRightTabStopType || type === CPRightTextAlignment)
alignment = CPRightTextAlignment;
else if (type === CPDecimalTabStopType)
alignment = CPRightTextAlignment;
var tab = [[CPTextTab alloc] initWithType:alignment
location:location];
if (!_tabChanged)
@@ -203,25 +209,23 @@ var cp1252Map = {
if (bgColour)
[ret setObject:bgColour forKey:CPBackgroundColorAttributeName];
if (underline)
[ret setObject:[CPNumber numberWithInt:1] forKey:CPUnderlineStyleAttributeName];
return ret;
}
@end
// based on https://github.com/lazygyu/RTF-parser
var kRTFParserType_char = 0,
kRTFParserType_dest = 1,
kRTFParserType_prop = 2,
kRTFParserType_spec = 3;
// Keyword descriptions
var kRgsymRtf = {
// keyword dflt fPassDflt kwd idx
"b" : [ "b", 1, false, kRTFParserType_prop, "propBold"],
"ul" : [ "ul", 1, false, kRTFParserType_prop, "propUnderline"],
"i" : [ "i", 1, false, kRTFParserType_prop, "propItalic"],
// "li" : [ "li", 0, false, kRTFParserType_prop, "propPgnFormat"],
"pgnucltr" : [ "pgnucltr", "pgULtr", true, kRTFParserType_prop, "propPgnFormat"],
"pgnlcltr" : [ "pgnlcltr", "pgLLtr", true, kRTFParserType_prop, "propPgnFormat"],
"qc" : [ "qc", "justC", true, kRTFParserType_prop, "propJust"],
@@ -263,9 +267,7 @@ var kRgsymRtf = {
"ftnsep" : [ "ftnsep", 0, false, kRTFParserType_dest, "destSkip"],
"ftnsepc" : [ "ftnsepc", 0, false, kRTFParserType_dest, "destSkip"],
"fprq" : [ "fprq", 0, false, kRTFParserType_dest, "destSkip"],
// "fcharset" : [ "fcharset", 0, false, kRTFParserType_dest, "destSkip"],
"rquote" : [ "rquote", 0, false, kRTFParserType_char, "'"],
// "s" : [ "s", 0, false, kRTFParserType_dest, "destSkip"],
"header" : [ "header", 0, false, kRTFParserType_dest, "destSkip"],
"headerf" : [ "headerf", 0, false, kRTFParserType_dest, "destSkip"],
"headerl" : [ "headerl", 0, false, kRTFParserType_dest, "destSkip"],
@@ -289,7 +291,12 @@ var kRgsymRtf = {
"]" : [ "]", 0, false, kRTFParserType_char, ']'],
"{" : [ "{", 0, false, kRTFParserType_char, '{'],
"}" : [ "}", 0, false, kRTFParserType_char, '}'],
"\\" : [ "\\", 0, false, kRTFParserType_char, '\\']
"\\" : [ "\\", 0, false, kRTFParserType_char, '\\'],
"trowd" : [ "trowd", 0, false, kRTFParserType_spec, "ipfnTrowd"],
"cell" : [ "cell", 0, false, kRTFParserType_spec, "ipfnCell"],
"row" : [ "row", 0, false, kRTFParserType_spec, "ipfnRow"],
"cellx" : [ "cellx", 0, false, kRTFParserType_spec, "ipfnCellx"],
"intbl" : [ "intbl", 0, false, kRTFParserType_spec, "ipfnIntbl"]
};
@implementation _CPRTFParser : CPObject
@@ -307,6 +314,13 @@ var kRgsymRtf = {
CPArray _fontArray;
CPString _freename;
BOOL _parsingFontTable;
// Table parsing state
BOOL _inTableActive;
BOOL _waitingForNextRow;
CPMutableArray _tableRows;
CPMutableArray _currentRow;
CPString _currentCellText;
}
- (id)init
@@ -324,6 +338,15 @@ var kRgsymRtf = {
_fontArray = ['Arial']; // FIXME: should be name of system font
_freename = "";
_parsingFontTable = NO;
_inTableActive = NO;
_waitingForNextRow = NO;
_tableRows = nil;
_currentRow = nil;
_currentCellText = "";
// Safe Initialization
_currentRun = [_RTFAttribute new];
}
return self;
@@ -338,7 +361,6 @@ var kRgsymRtf = {
return sym[4];
case 1:
// CPLogConsole("skipped : " + sym[4]);
return '';
default:
@@ -349,7 +371,6 @@ var kRgsymRtf = {
- (BOOL)pushState
{
// Push stack as an object containing scoping context
_states.push({
curState: _curState,
run: [_currentRun copy]
@@ -366,7 +387,18 @@ var kRgsymRtf = {
[self _flushCurrentRun];
_currentRun = state.run;
if (!_currentRun)
{
_currentRun = [_RTFAttribute new];
}
_currentRun._range = CPMakeRange([_result length], 0);
if (_curState == 0)
{
_parsingFontTable = NO;
}
}
return YES;
}
@@ -383,7 +415,6 @@ var kRgsymRtf = {
case "ipfnHex":
var hex = '';
// Konsumiere exakt 2 Zeichen nach dem \'
for (var i = 0; i < 2; i++)
{
var nextCh = _rtf.charAt(++_currentParseIndex);
@@ -420,11 +451,88 @@ var kRgsymRtf = {
_codePage = code;
_currentParseIndex--;
break;
case "ipfnTrowd":
if (_waitingForNextRow)
{
_waitingForNextRow = NO;
}
if (!_inTableActive)
{
_inTableActive = YES;
_tableRows = [CPMutableArray array];
_currentRow = [CPMutableArray array];
_currentCellText = "";
}
else
{
_currentRow = [CPMutableArray array];
}
return '';
case "ipfnIntbl":
return '';
case "ipfnCell":
if (_inTableActive)
{
if (!_currentRow)
_currentRow = [CPMutableArray array];
[_currentRow addObject:_currentCellText];
_currentCellText = "";
}
return '';
case "ipfnRow":
if (_inTableActive)
{
if (!_currentRow)
_currentRow = [CPMutableArray array];
[_tableRows addObject:_currentRow];
_waitingForNextRow = YES;
}
return '';
case "ipfnCellx":
return '';
}
return '';
}
- (void)_flushTableIfAny
{
if (_tableRows && [_tableRows count] > 0)
{
[self _flushCurrentRun];
var headers = [_tableRows objectAtIndex:0];
var rows = [CPMutableArray array];
for (var idx = 1; idx < [_tableRows count]; idx++)
{
[rows addObject:[_tableRows objectAtIndex:idx]];
}
var attachment = [[_CPTableTextAttachment alloc] initWithHeaders:headers rows:rows width:500.0];
// Use standard Cocoa/Cappuccino NSAttachmentCharacter creation method
var tableAttrStr = [CPTextStorage attributedStringWithAttachment:attachment];
[_result appendAttributedString:tableAttrStr];
if (_currentRun)
{
_currentRun._range = CPMakeRange([_result length], 0);
}
_tableRows = nil;
_currentRow = nil;
_currentCellText = "";
_inTableActive = NO;
_waitingForNextRow = NO;
}
}
- (void)_flushCurrentRun
{
var newOffset = 0;
@@ -441,7 +549,6 @@ var kRgsymRtf = {
[_result setAttributes:dict range:_currentRun._range]; // flush previous run
// Deep copy the current run style for the next sequence of characters
_currentRun = [_currentRun copy];
}
else
@@ -454,8 +561,6 @@ var kRgsymRtf = {
- (CPString)_applyPropChange:sym parameter:param
{
//console.log("prop : " + sym[0] + " / param : " + param+ ' ');
switch (sym[0])
{
case "pard":
@@ -499,6 +604,23 @@ var kRgsymRtf = {
break;
case "ul": // underline
if (param === 0)
{
if (_currentRun && _currentRun.underline)
[self _flushCurrentRun];
_currentRun.underline = NO;
}
else
{
if (_currentRun && !_currentRun.underline)
[self _flushCurrentRun];
_currentRun.underline = YES;
}
break;
case "qc": // paragraph center
[_currentRun.paragraph setAlignment:CPCenterTextAlignment];
break;
@@ -543,7 +665,6 @@ var kRgsymRtf = {
if (sym[4] == "destSkip")
{
CPLogConsole("Dest skip start : [" + sym[0] + "]");
_curState++;
}
@@ -552,6 +673,14 @@ var kRgsymRtf = {
- (CPString)_translateKeyword:(CPString)keyword parameter:(CPString)param fParameter:(BOOL)fParam
{
if (_waitingForNextRow)
{
if (keyword !== "trowd" && keyword !== "cell" && keyword !== "row" && keyword !== "intbl" && keyword !== "cellx")
{
[self _flushTableIfAny];
}
}
if (kRgsymRtf[keyword] !== undefined)
{
var sym = kRgsymRtf[keyword];
@@ -687,8 +816,7 @@ var kRgsymRtf = {
break;
default:
CPLogConsole("skip : " + keyword + " param: " + param);
break;
}
return '';
@@ -744,9 +872,16 @@ var kRgsymRtf = {
- (void)_appendPlainString:(CPString) aString
{
[_result replaceCharactersInRange:CPMakeRange([_result length], 0) withString:aString];
if (_inTableActive)
{
_currentCellText += aString;
}
else
{
[_result replaceCharactersInRange:CPMakeRange([_result length], 0) withString:aString];
}
}
- (CPAttributedString)parseRTF:(CPString)rtf
{
rtf = rtf.replace(/\\\n/g, "\\par\n");
@@ -766,6 +901,11 @@ var kRgsymRtf = {
{
tmp = rtf.charAt(++_currentParseIndex);
if (_waitingForNextRow && tmp !== "\\" && tmp !== " " && tmp !== "\n" && tmp !== "\r" && tmp !== "\t")
{
[self _flushTableIfAny];
}
if (tmp !== "\\" && hex.length > 0)
{
[self _appendPlainString: String.fromCharCode(parseInt((hex), 16))];
@@ -788,19 +928,21 @@ var kRgsymRtf = {
break;
case "{":
if ([self pushState])
CPLogConsole("push");
if (_waitingForNextRow)
[self _flushTableIfAny];
if ([self pushState])
break;
case "}":
if (_waitingForNextRow)
[self _flushTableIfAny];
if ([self popState])
CPLogConsole("pop");
if (_freename)
{
CPLogConsole(_freename);
if (_parsingFontTable)
{
_fontArray.push(_freename);
@@ -829,7 +971,6 @@ var kRgsymRtf = {
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;
}
@@ -855,15 +996,288 @@ var kRgsymRtf = {
lastchar = 0;
if (_curState == 0)
{
[self _appendPlainString:tmp];
else if (tmp !== ';')
_freename += tmp;
}
else
{
if (tmp === ';')
{
if (_parsingFontTable && _freename)
{
var cleanFontName = _freename.trim();
var lastSpaceIdx = cleanFontName.lastIndexOf(' ');
if (lastSpaceIdx !== -1)
{
cleanFontName = cleanFontName.substring(lastSpaceIdx + 1);
}
_fontArray.push(cleanFontName);
_freename = "";
}
}
else
{
_freename += tmp;
}
}
break;
}
}
[self _flushTableIfAny];
return _result;
}
@end
/*
* CPMarkdownParser.j
*
* Parse a Markdown string into a CPAttributedString with inline style attributes
* and embedded _CPTableTextAttachment objects.
*
* Copyright (C) 2026 by Daniel Böhringer
*
* 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.
*/
@implementation CPMarkdownParser : CPObject
+ (CPAttributedString)attributedStringFromMarkdown:(CPString)markdown
{
if (!markdown) {
return [[CPAttributedString alloc] initWithString:@""];
}
var result = [[CPMutableAttributedString alloc] initWithString:@""];
var lines = markdown.split(/\r?\n/);
var i = 0;
while (i < lines.length) {
var line = lines[i];
if ([self isTableHeaderLine:line] && i + 1 < lines.length && [self isTableSeparatorLine:lines[i+1]]) {
var headers = [self parseTableCells:line];
var separatorLine = lines[i+1];
var rows = [CPMutableArray array];
i += 2;
while (i < lines.length && [self isTableRowLine:lines[i]]) {
[rows addObject:[self parseTableCells:lines[i]]];
i++;
}
var numCols = [headers count];
if (numCols == 0 && [rows count] > 0) {
numCols = [[rows objectAtIndex:0] count];
}
var totalNaturalW = 0.0;
var colNaturalWidths = [];
var measureTextField = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 10000.0, 24.0)];
[measureTextField setFont:[CPFont systemFontOfSize:11.0]];
for (var c = 0; c < numCols; c++) {
var cellW = 80.0;
if (c < headers.length) {
var parsedText = [self parseInlineMarkdown:headers[c] isHeader:YES headerLevel:3];
[measureTextField setStringValue:[parsedText string]];
[measureTextField sizeToFit];
cellW = Math.max(cellW, CGRectGetWidth([measureTextField frame]) + 24.0);
}
for (var r = 0; r < [rows count]; r++) {
var rowData = [rows objectAtIndex:r];
if (c < [rowData count]) {
var parsedText = [self parseInlineMarkdown:rowData[c] isHeader:NO headerLevel:3];
[measureTextField setStringValue:[parsedText string]];
[measureTextField sizeToFit];
cellW = Math.max(cellW, CGRectGetWidth([measureTextField frame]) + 24.0);
}
}
colNaturalWidths[c] = cellW;
totalNaturalW += cellW;
}
var matrixView = [[_CPTableTextAttachment alloc] initWithHeaders:headers rows:rows width:500.0];
// Render utilizing the correct atomic attachment character string
var tableAttrStr = [CPTextStorage attributedStringWithAttachment:matrixView];
[result appendAttributedString:tableAttrStr];
continue;
}
var isHeader = false;
var headerLevel = 0;
var headerMatch = line.match(/^(#{1,6})\s+(.*)$/);
if (headerMatch) {
headerLevel = headerMatch[1].length;
line = headerMatch[2];
isHeader = true;
}
var isListItem = false;
var listMatch = line.match(/^(\*|-)\s+(.*)$/);
if (listMatch) {
line = " • " + listMatch[2];
isListItem = true;
}
var parsedLine = [self parseInlineMarkdown:line isHeader:isHeader headerLevel:headerLevel];
[result appendAttributedString:parsedLine];
if (i < lines.length - 1) {
[result appendAttributedString:[[CPAttributedString alloc] initWithString:@"\n"]];
}
i++;
}
return result;
}
+ (BOOL)isTableHeaderLine:(CPString)line
{
var trimmed = line.trim();
return trimmed.indexOf('|') !== -1;
}
+ (BOOL)isTableSeparatorLine:(CPString)line
{
var trimmed = line.trim();
if (trimmed.indexOf('|') === -1) return NO;
var stripped = trimmed.replace(/[\s|:\-]/g, '');
return stripped.length === 0;
}
+ (BOOL)isTableRowLine:(CPString)line
{
var trimmed = line.trim();
return trimmed.indexOf('|') !== -1;
}
+ (CPArray)parseTableCells:(CPString)line
{
var parts = line.split('|');
var cells = [CPMutableArray array];
var startIdx = 0;
var endIdx = parts.length;
if (parts[0].trim() === "") startIdx = 1;
if (parts[parts.length - 1].trim() === "") endIdx = parts.length - 1;
for (var j = startIdx; j < endIdx; j++) {
[cells addObject:parts[j].trim()];
}
return cells;
}
+ (CPAttributedString)parseInlineMarkdown:(CPString)text isHeader:(BOOL)isHeader headerLevel:(int)level
{
var baseFontSize = 11.0;
var fontSize = baseFontSize;
var isBold = isHeader;
var isItalic = NO;
if (isHeader) {
if (level == 1) fontSize = 15.0;
else if (level == 2) fontSize = 13.0;
else fontSize = 12.0;
}
var result = [[CPMutableAttributedString alloc] initWithString:@""];
var currentSegment = "";
var i = 0;
var len = text.length;
var defaultFont = [CPFont systemFontOfSize:fontSize];
if (isBold) {
defaultFont = [CPFont boldSystemFontOfSize:fontSize];
}
while (i < len) {
if (i + 2 < len && text.substr(i, 3) === "***") {
if (currentSegment.length > 0) {
[result appendAttributedString:[self attributedStringWithText:currentSegment font:defaultFont bold:isBold italic:isItalic code:NO]];
currentSegment = "";
}
isBold = !isBold;
isItalic = !isItalic;
i += 3;
continue;
}
if (i + 1 < len && text.substr(i, 2) === "**") {
if (currentSegment.length > 0) {
[result appendAttributedString:[self attributedStringWithText:currentSegment font:defaultFont bold:isBold italic:isItalic code:NO]];
currentSegment = "";
}
isBold = !isBold;
i += 2;
continue;
}
if (text.charAt(i) === "*") {
if (currentSegment.length > 0) {
[result appendAttributedString:[self attributedStringWithText:currentSegment font:defaultFont bold:isBold italic:isItalic code:NO]];
currentSegment = "";
}
isItalic = !isItalic;
i++;
continue;
}
if (text.charAt(i) === "`") {
if (currentSegment.length > 0) {
[result appendAttributedString:[self attributedStringWithText:currentSegment font:defaultFont bold:isBold italic:isItalic code:NO]];
currentSegment = "";
}
var codeText = "";
i++;
while (i < len && text.charAt(i) !== "`") {
codeText += text.charAt(i);
i++;
}
[result appendAttributedString:[self attributedStringWithText:codeText font:defaultFont bold:NO italic:NO code:YES]];
i++;
continue;
}
currentSegment += text.charAt(i);
i++;
}
if (currentSegment.length > 0) {
[result appendAttributedString:[self attributedStringWithText:currentSegment font:defaultFont bold:isBold italic:isItalic code:NO]];
}
return result;
}
+ (CPAttributedString)attributedStringWithText:(CPString)text font:(CPFont)baseFont bold:(BOOL)b italic:(BOOL)it code:(BOOL)c
{
var fontName = [baseFont familyName];
var fontSize = [baseFont size];
var finalFont = baseFont;
if (c) {
finalFont = [CPFont fontWithName:@"Courier" size:fontSize];
} else {
finalFont = [CPFont _fontWithName:fontName size:fontSize bold:b italic:it];
}
if (!finalFont) {
finalFont = [CPFont systemFontOfSize:fontSize];
}
var dict = [CPDictionary dictionaryWithObjectsAndKeys:
finalFont, CPFontAttributeName,
[CPColor blackColor], CPForegroundColorAttributeName
];
return [[CPAttributedString alloc] initWithString:text attributes:dict];
}
@end
+230 -61
View File
@@ -1,12 +1,12 @@
/*
_CPRTFProducer.j
Serialize CPAttributedString to a RTF String
Copyright (C) 2014 Daniel Boehringer
This file is based on the RTFProducer from GNUStep
(which i co-authored with Fred Kiefer in 1999)
* _CPRTFProducer.j
*
* Serialize CPAttributedString to a RTF String
*
* Copyright (C) 2014 Daniel Boehringer
* This file is based on the RTFProducer from GNUStep
* (which I co-authored with Fred Kiefer in 1999)
*
* 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
@@ -27,6 +27,7 @@
@import "CPColor.j"
@import "CPGraphics.j"
@import "CPFontManager.j"
@import "_CPTableTextAttachment.j"
@global CPForegroundColorAttributeName
@global CPBackgroundColorAttributeName
@@ -43,6 +44,11 @@
@global CPJustifiedTextAlignment
@global CPNaturalTextAlignment
@global CPLeftTabStopType
@global CPRightTabStopType
@global CPCenterTabStopType
@global CPDecimalTabStopType
var PAPERSIZE = @"PaperSize",
LEFTMARGIN = @"LeftMargin",
RIGHTMARGIN = @"RightMargin",
@@ -83,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];
@@ -98,7 +99,6 @@ function _points2twips(a) { return (a) * 20.0; }
return self;
}
// private stuff follows
- (CPString)fontTable
{
if (![fontDict count])
@@ -313,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)
@@ -351,26 +350,29 @@ function _points2twips(a) { return (a) * 20.0; }
while ((tab = [enumerator nextObject]))
{
switch ([tab tabStopType])
var tabType = [tab respondsToSelector:@selector(tabStopType)] ? [tab tabStopType] : nil;
if (tabType === nil && [tab respondsToSelector:@selector(alignment)])
tabType = [tab alignment];
switch (tabType)
{
case CPLeftTabStopType:
// no tabkind emission needed
case CPLeftTextAlignment:
break;
/* case NSRightTabStopType:
case CPRightTabStopType:
case CPRightTextAlignment:
headerString += @"\\tqr";
break;
case NSCenterTabStopType:
headerString += @"\\tqc";
break;
case NSDecimalTabStopType:
break;
case CPCenterTabStopType:
case CPCenterTextAlignment:
headerString += @"\\tqc";
break;
case CPDecimalTabStopType:
headerString += @"\\tqdec";
break;
default:
NSLog(@"Unknown tab stop type.");
*/
}
break;
}
headerString += [CPString stringWithFormat:@"\\tx%d",_points2twips([tab location])];
headerString += [CPString stringWithFormat:@"\\tx%d",_points2twips([tab location])];
}
return headerString;
@@ -380,6 +382,194 @@ function _points2twips(a) { return (a) * 20.0; }
attributes:(CPDictionary) attributes
paragraphStart:(BOOL) first
{
var unwrap = function(obj) {
if (!obj) return null;
if ((typeof obj.respondsToSelector === "function" && ([obj respondsToSelector:@selector(headers)] || [obj respondsToSelector:@selector(rows)])) ||
obj.headers || obj._headers || obj.rows || obj._rows) {
return obj;
}
var unwrapped = null;
if (typeof obj.respondsToSelector === "function") {
if ([obj respondsToSelector:@selector(attachmentCell)]) {
unwrapped = [obj attachmentCell];
} else if ([obj respondsToSelector:@selector(content)]) {
unwrapped = [obj content];
} else if ([obj respondsToSelector:@selector(view)]) {
unwrapped = [obj view];
}
}
if (!unwrapped) {
unwrapped = obj._attachmentCell || obj._content || obj._view || obj.attachmentCell || obj.content || obj.view;
}
return unwrapped ? unwrapped : obj;
};
var tableAttachment = null;
if (typeof CPAttachmentAttributeName !== "undefined") {
tableAttachment = [attributes objectForKey:CPAttachmentAttributeName];
}
if (!tableAttachment) {
tableAttachment = [attributes objectForKey:@"CPAttachmentAttributeName"];
}
if (!tableAttachment) {
tableAttachment = [attributes objectForKey:@"TableAttachmentAttribute"];
}
if (!tableAttachment) {
tableAttachment = [attributes objectForKey:@"_CPAttachmentView"];
}
if (!tableAttachment && typeof _CPAttachmentView !== "undefined") {
tableAttachment = [attributes objectForKey:_CPAttachmentView];
}
tableAttachment = unwrap(tableAttachment);
if (!tableAttachment && (substring === "\uFFFC" || substring === ""))
{
var keys = [attributes allKeys],
count = [keys count];
for (var i = 0; i < count; i++)
{
var key = [keys objectAtIndex:i],
val = unwrap([attributes objectForKey:key]);
if (val && (
(typeof val.respondsToSelector === "function" && [val respondsToSelector:@selector(headers)]) ||
val._headers ||
val.headers ||
(typeof _CPTableTextAttachment !== "undefined" && [val isKindOfClass:[_CPTableTextAttachment class]])
)) {
tableAttachment = val;
break;
}
}
}
if (tableAttachment)
{
var headers = null,
rows = null;
// 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 (!headers) {
headers = activeView._headers || activeView.headers;
}
if (!rows) {
rows = activeView._rows || activeView.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) {
if (!arr) return 0;
if (typeof arr.count === "function") return [arr count];
return arr.length;
};
var getObjectAtIndex = function(arr, idx) {
if (!arr) return null;
if (typeof arr.objectAtIndex === "function") return [arr objectAtIndex:idx];
return arr[idx];
};
var numCols = getCount(headers);
if (numCols == 0 && getCount(rows) > 0) {
numCols = getCount(getObjectAtIndex(rows, 0));
}
if (numCols > 0)
{
var totalWidthTwips = 10000;
var colWidthTwips = Math.floor(totalWidthTwips / numCols);
var cellBoundaries = [];
var currentBoundary = 0;
for (var i = 0; i < numCols; i++)
{
currentBoundary += colWidthTwips;
cellBoundaries.push(currentBoundary);
}
var tableRTF = "";
var writeRow = function(rowData, isHeaderRow) {
var rowRTF = "\\trowd\\trgaph115\\trleft0";
for (var c = 0; c < numCols; c++) {
rowRTF += "\\clbrdrt\\brdrs\\brdrw10\\clbrdrb\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10";
rowRTF += "\\cellx" + cellBoundaries[c];
}
for (var c = 0; c < numCols; c++) {
var cellText = "";
if (c < getCount(rowData)) {
cellText = getObjectAtIndex(rowData, c);
}
if (cellText === null || cellText === undefined) {
cellText = "";
}
cellText = String(cellText);
cellText = cellText.replace(/\\/g, '\\\\');
cellText = cellText.replace(/{/g, '\\{');
cellText = cellText.replace(/}/g, '\\}');
cellText = cellText.replace(/\n/g, '\\line ');
if (isHeaderRow) {
rowRTF += "{\\intbl\\b " + cellText + "\\b0\\cell}";
} else {
rowRTF += "{\\intbl " + cellText + "\\cell}";
}
}
rowRTF += "\\row\n";
return rowRTF;
};
if (getCount(headers) > 0) {
tableRTF += writeRow(headers, YES);
}
var rowCount = getCount(rows);
for (var r = 0; r < rowCount; r++) {
tableRTF += writeRow(getObjectAtIndex(rows, r), NO);
}
return tableRTF;
}
}
var result = "",
headerString = "",
trailerString = "",
@@ -392,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;
@@ -417,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,
@@ -434,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";
@@ -522,11 +693,9 @@ function _points2twips(a) { return (a) * 20.0; }
substring = substring.replace(/\\/g, '\\\\');
substring = substring.replace(/\n/g, '\\par\n');
substring = substring.replace(/\t/g, '\\tab');
substring = substring.replace(/\t/g, '\\tab ');
substring = substring.replace(/{/g, '\\{');
substring = substring.replace(/}/g, '\\}');
// FIXME: All characters not in the standard encoding must be
// replaced by \'xx
if (!first)
{
@@ -544,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;
@@ -562,10 +731,9 @@ function _points2twips(a) { return (a) * 20.0; }
length = [string length],
currRange = CPMakeRange(loc, 0),
completeRange = CPMakeRange(0, length),
first = YES;
paragraphStart = YES;
// FIXME <!> split along newline characters and run as outer loop
while (CPMaxRange(currRange) < CPMaxRange(completeRange)) // save all "runs"
while (CPMaxRange(currRange) < CPMaxRange(completeRange))
{
var attributes,
substring,
@@ -577,10 +745,14 @@ function _points2twips(a) { return (a) * 20.0; }
substring = [string substringWithRange:currRange];
runString = [self runStringForString:substring
attributes:attributes
paragraphStart:YES];
paragraphStart:paragraphStart];
result += runString;
first = NO;
if (substring.length > 0 && substring.charAt(substring.length - 1) === '\n')
paragraphStart = YES;
else
paragraphStart = NO;
}
return result;
@@ -598,9 +770,6 @@ function _points2twips(a) { return (a) * 20.0; }
text = aText;
docDict = dict;
/*
* do not change order! (esp. body has to be generated first; builds context)
*/
bodyString = [self bodyString];
trailerString = [self trailerString];
headerString = [self headerString];
+537
View File
@@ -0,0 +1,537 @@
/* _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;
BOOL _isResizing;
BOOL _isEditable;
BOOL _acceptsRichText;
}
- (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows
{
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];
}
return self;
}
- (void)_rebuildTableWithWidth:(float)totalWidth
{
var numCols = _headers ? [_headers count] : 0;
if (numCols == 0 && _rows && [_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";
self._DOMElement.style.boxSizing = "border-box";
}
// Initialize header cells
if (_headers && [_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
if (_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];
// 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];
if (charRange && charRange.location !== CPNotFound)
{
[layoutManager invalidateLayoutForCharacterRange:charRange isSoft:NO actualCharacterRange:nil];
[layoutManager invalidateDisplayForGlyphRange:charRange];
[layoutManager _validateLayoutAndGlyphs];
[textView sizeToFit];
}
}
}
}
- (CPRange)_findCharacterRangeInLayoutManager:(CPLayoutManager)layoutManager
{
var lineFragments = layoutManager._lineFragments;
if (lineFragments)
{
var l = lineFragments.length;
for (var i = 0; i < l; i++)
{
var fragment = lineFragments[i];
var runs = fragment._runs;
if (runs)
{
var rc = runs.length;
for (var j = 0; j < rc; j++)
{
if (runs[j].view === self)
{
return runs[j]._range;
}
}
}
}
}
return CPMakeRange(CPNotFound, 0);
}
- (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
{
[self resizeToWidth:width];
return self;
}
- (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;
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:_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]];
// 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];
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
{
if (_isResizing)
return;
_isResizing = YES;
// 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;
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];
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 = plainText.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;
}
};
if (currentHeaders) {
for (var c = 0; c < [currentHeaders count]; c++) {
measureCell([currentHeaders objectAtIndex:c], YES, c);
}
}
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]) {
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 layoutManager = [textView layoutManager];
var usedRect = layoutManager ? [layoutManager usedRectForTextContainer:[textView textContainer]] : nil;
var wrappedHeight = (usedRect ? CGRectGetHeight(usedRect) : 0.0) + 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 (currentHeaders && [currentHeaders count] > 0) {
var headerHeight = layoutRow(cellIndex);
cellIndex += numCols;
currentY += headerHeight;
}
if (currentRows) {
for (var r = 0; r < [currentRows count]; r++) {
var rowHeight = layoutRow(cellIndex);
cellIndex += numCols;
currentY += rowHeight;
}
}
// 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;
}
@end
+102 -6
View File
@@ -2,7 +2,7 @@
* AppController.j
*
* Manual test application for the cappuccino text system
* Copyright (C) 2014 Daniel Boehringer
* Copyright (C) 2026 Daniel Boehringer
*/
@import <Foundation/Foundation.j>
@@ -14,6 +14,8 @@
@import <AppKit/CPTextField.j>
@import <AppKit/CPScrollView.j>
@import <AppKit/CPParagraphStyle.j>
@import <AppKit/CPTextStorage.j>
@import <AppKit/_CPTableTextAttachment.j>
@implementation AppController : CPObject
{
@@ -90,6 +92,25 @@
[_textView insertText:@" "];
}
- (void)insertTable:(id)sender
{
var headers = [@"Item Description", @"Quantity", @"Unit Price"];
var rows = [
[@"Cappuccino Web Framework Lic.", @"2", @"$199.00"],
[@"Objective-J Development Support", @"5", @"$150.00"],
[@"Cloud Compilation VM Server", @"1", @"$49.00"]
];
var tableAttachment = [[_CPTableTextAttachment alloc] initWithHeaders:headers rows:rows width:500.0];
// Insert single-character atomic text attachment
var tableAttrStr = [CPTextStorage attributedStringWithAttachment:tableAttachment];
[_textView insertText:@"\ntable (own line)\n"];
[_textView insertText:tableAttrStr];
[_textView insertText:@"\nend table"];
}
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
@@ -133,10 +154,18 @@
var rtfButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 150, 30)];
[rtfButton setTitle:@"RTF Round-trip ➔"];
[rtfButton setTarget:self];
[rtfButton setAction:@selector(makeRTF:)];
[rtfButton setAction:@selector(rtfRoundTrip:)];
[toolbarView addSubview:rtfButton];
currentX += 160;
// NEW: Markdown Converter Trigger
var mdButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 120, 30)];
[mdButton setTitle:@"← Markdown"];
[mdButton setTarget:self];
[mdButton setAction:@selector(convertMarkdownToRichText:)];
[toolbarView addSubview:mdButton];
currentX += 130;
// Insert Attachment Trigger
var attachButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 140, 30)];
[attachButton setTitle:@"Insert Attachment"];
@@ -145,6 +174,14 @@
[toolbarView addSubview:attachButton];
currentX += 150;
// Add Table Trigger
var tableButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 100, 30)];
[tableButton setTitle:@"Add Table"];
[tableButton setTarget:self];
[tableButton setAction:@selector(insertTable:)];
[toolbarView addSubview:tableButton];
currentX += 110;
// Text Alignment Group
var labelAlign = [[CPTextField alloc] initWithFrame:CGRectMake(currentX, 22, 45, 20)];
[labelAlign setStringValue:@"Align:"];
@@ -210,9 +247,10 @@
[leftLabel setAutoresizingMask:CPViewWidthSizable];
[leftContainer addSubview:leftLabel];
_textView = [[CPTextView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([leftContainer bounds]) - 30, CGRectGetHeight([leftContainer bounds]) - 70)];
_textView = [[CPTextView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([leftContainer bounds]) - 30, 0)];
[_textView setRichText:YES];
[_textView setBackgroundColor:[CPColor whiteColor]];
[[_textView textContainer] setWidthTracksTextView:YES];
_scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(15, 40, CGRectGetWidth([leftContainer bounds]) - 30, CGRectGetHeight([leftContainer bounds]) - 65)];
[_scrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
@@ -225,7 +263,7 @@
// Right Container: RTF Plain-Text Source and Parser Window
var rightLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 10, CGRectGetWidth([rightContainer bounds]) - 30, 20)];
[rightLabel setStringValue:@"RTF Raw Output & Source Parser Window"];
[rightLabel setStringValue:@"Markdown Source & RTF Source Code Window"];
[rightLabel setFont:[CPFont boldSystemFontOfSize:14]];
[rightLabel setAutoresizingMask:CPViewWidthSizable];
[rightContainer addSubview:rightLabel];
@@ -257,8 +295,6 @@
[editMenu addItemWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"Z"];
[mainMenu setSubmenu:editMenu forItem:item];
item = [mainMenu insertItemWithTitle:@"Format" action:nil keyEquivalent:nil atIndex:0];
// Format Menu
item = [mainMenu insertItemWithTitle:@"Format" action:nil keyEquivalent:nil atIndex:0];
var formatMenu = [[CPMenu alloc] initWithTitle:@"Format Menu"];
@@ -321,6 +357,21 @@
[_textView insertText:[[CPAttributedString alloc] initWithString:@"This paragraph has a first-line indent of 30pt, a head indent of 50pt, and a tail indent of -30pt. Check the horizontal ruler above to see how the indent markers align with this paragraph, and adjust them directly!\n"
attributes:[CPDictionary dictionaryWithObject:indentParagraph forKey:CPParagraphStyleAttributeName]]];
[_textView insertText:@"\n"];
// 5. Pre-populate Markdown editor with rich table sample content
[_textView2 setString:@"# Markdown Parser Output\n\n" +
"You can type markdown directly in this side panel and click **← Markdown** above to convert it!\n\n" +
"## Inline styling showcase\n\n" +
"• Combine ***bold and italic*** styles.\n" +
"• Monospaced `code elements` represent code blocks.\n\n" +
"## Data Table\n\n" +
"| Item Description | Quantity | Unit Price |\n" +
"| :--- | :---: | :---: |\n" +
"| Cappuccino Web Framework Lic. | 2 | $199.00 |\n" +
"| Objective-J Development Support | 5 | $150.00 |\n" +
"| Cloud Compilation VM Server | 1 | $49.00 |"];
[theWindow orderFront:self];
[CPMenu setMenuBarVisible:YES];
}
@@ -334,4 +385,49 @@
[_textView insertText: mystr];
}
// Action tied to the "RTF Round-trip ->" button in the demo app
- (void)rtfRoundTrip:(id)sender
{
// 1. Retrieve the rich text storage from the editor on the left
var textStorage = [_textView textStorage];
if (!textStorage || [textStorage length] == 0)
{
return;
}
// 2. Serialize the CPAttributedString into an RTF string
var docAttributes = @{ @"PaperSize": CPMakeSize(612, 792) };
var generatedRTF = [_CPRTFProducer produceRTF:textStorage documentAttributes:docAttributes];
// 3. Set the generated RTF string into the raw output pane on the right
[_textView2 setString:generatedRTF];
// 4. Parse that exact RTF text back into a new CPAttributedString
var parser = [[_CPRTFParser alloc] init];
var roundTrippedString = [parser parseRTF:generatedRTF];
// Safe fallback sequence
[_textView setEditable:YES];
[_textView setString:@""];
[_textView insertText:roundTrippedString];
}
// Action tied to the "Markdown ->" button to generate rich text
- (void)convertMarkdownToRichText:(id)sender
{
// 1. Retrieve markdown string from the right pane
var markdownInput = [_textView2 string];
if (!markdownInput || [markdownInput length] == 0)
{
return;
}
// 2. Parse the markdown using the updated MarkdownParser class
var parsedAttrStr = [CPMarkdownParser attributedStringFromMarkdown:markdownInput];
[_textView setEditable:YES];
[_textView setString:@""];
[_textView insertText:parsedAttrStr];
}
@end