mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-15 21:14:29 +00:00
Merge remote-tracking branch 'upstream/main' into Aristo3
This commit is contained in:
commit
b9e734a21b
7 files changed
+1657
-201
No files matched your search
@@ -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
@@ -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;
|
||||
|
||||
@@ -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]];
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user