Merge remote-tracking branch 'upstream/main'

This commit is contained in:
daboe01
2026-06-20 17:52:48 +02:00
24 changed files with 3443 additions and 292 deletions
+16
View File
@@ -464,6 +464,22 @@ following:
return _CPRealFontSize(_size);
}
/*!
Returns the font size. Cocoa/AppKit compatibility alias for -size.
*/
- (float)pointSize
{
return [self size];
}
/*!
Returns the font name. Cocoa/AppKit compatibility alias for -familyName.
*/
- (CPString)fontName
{
return [self familyName];
}
/*!
Returns the font as a CSS string
*/
+1
View File
@@ -30,6 +30,7 @@ CPStandardKeyBindings = {
@"@.": @"cancelOperation:",
@"@a": @"selectAll:",
@"@~$v": @"pasteAsPlainText:",
@"^a": @"moveToBeginningOfParagraph:",
@"^$a": @"moveToBeginningOfParagraphAndModifySelection:",
@"^b": @"moveBackward:",
+113 -20
View File
@@ -80,6 +80,35 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
@n An ordered to-many relation containing the display values for the row.
@n@n @c @@"criteria"
@n An ordered to-many relation containing the criteria for the row.
@n@n
Localization & Positional Reordering
@n
CPRuleEditor supports complete localization of menu items and grammatical positional reordering (sentence structure layout adjustment) via strings resource files (.strings) or custom programmatic CPDictionary tables.
@n@n
Since sentence structures vary significantly across languages, the editor can dynamically reposition views (such as popups, static labels, and text fields) from left to right to form grammatically correct sentences.
@n@n
Formatting Keys (English representation):
@n @c %[%]@@
@n Represents a popup button displaying its selected value (e.g. @c %[firstName]@@).
@n @c %@@
@n Represents an editable text input field.
@n Static text represents a literal label placed directly inside the formatting key.
@n@n
Example English format key:
@n @c "%[firstName]@ %[is equal to]@ %@"
@n@n
Translation Patterns (Target language):
@n Positional specifiers such as @c %1$@@, @c %2$@@, @c %3$@@ dictate the visual order of views from left to right.
@n Bracketed values inside positional specifiers (e.g. @c %1$[Nombre]@@) define the localized title for popup selection items.
@n Literal text outside the specifiers (such as @c "y" or @c "und") is automatically instantiated as static text labels positioned between controls.
@n@n
Example translations:
@n@n
Spanish (Reorders to: [1: Name] y [3: Value] [2: are equal]):
@n @c "%[firstName]@ %[is equal to]@ %@" = "%1$[Nombre]@ y %3$@ %2$[son iguales]@";
@n@n
German (Reorders to: [1: First Name] und [3: Value] [2: are equal]):
@n @c "%[firstName]@ %[is equal to]@ %@" = "%1$[Vorname]@ und %3$@ %2$[sind gleich]@";
*/
@implementation CPRuleEditor : CPControl
@@ -127,7 +156,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
BOOL _isKeyDown;
BOOL _nestingModeDidChange;
_CPRuleEditorLocalizer _standardLocalizer @accessors(property=standardLocalizer);
_CPRuleEditorLocalizer _standardLocalizer;
CPDictionary _itemsAndValuesToAddForRowType;
}
@@ -207,8 +236,34 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
[self registerForDraggedTypes:[CPArray arrayWithObjects:CPRuleEditorItemPBoardType,nil]];
[_boundArrayOwner addObserver:self forKeyPath:_boundArrayKeyPath options:CPKeyValueObservingOptionOld | CPKeyValueObservingOptionNew context:boundArrayContext];
[[CPNotificationCenter defaultCenter] addObserver:self
selector:@selector(_ruleEditorLocalizerDidLoad:)
name:@"_CPRuleEditorLocalizerDidLoadNotification"
object:nil];
}
- (void)_ruleEditorLocalizerDidLoad:(CPNotification)aNotification
{
if ([aNotification object] === [self standardLocalizer])
{
// Defer execution to the next run loop cycle so that any active slice
// insertions have fully completed and are present in the `_slices` array.
[[CPRunLoop mainRunLoop] performBlock:function() {
var count = [_slices count];
for (var i = 0; i < count; i++)
{
var slice = [_slices objectAtIndex:i];
[slice _reconfigureSubviews];
[slice _updateButtonVisibilities]; // Force updates on row button tooltips
}
[self _updatePredicate];
[self _sendRuleAction];
} argument:nil order:0 modes:[CPDefaultRunLoopMode]];
}
}
/*! @endcond */
/*!
@@ -384,7 +439,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
*/
- (CPDictionary)formattingDictionary
{
return [_standardLocalizer dictionary];
return [[self standardLocalizer] dictionary];
}
/*!
@@ -396,6 +451,9 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
*/
- (void)setFormattingDictionary:(CPDictionary)dictionary
{
if (_standardLocalizer == nil)
_standardLocalizer = [_CPRuleEditorLocalizer new];
[_standardLocalizer setDictionary:dictionary];
_stringsFilename = nil;
}
@@ -440,6 +498,19 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
}
}
- (_CPRuleEditorLocalizer)standardLocalizer
{
if (_standardLocalizer == nil)
_standardLocalizer = [_CPRuleEditorLocalizer new];
return _standardLocalizer;
}
- (void)setStandardLocalizer:(_CPRuleEditorLocalizer)aLocalizer
{
_standardLocalizer = aLocalizer;
}
/*!
@name Providing Data
*/
@@ -538,7 +609,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType",
if ([self rowTypeForRow:current_index] === CPRuleEditorRowTypeCompound)
{
var candidate = [[self _rowCacheForIndex:current_index] rowObject],
subObjects = [[self _subrowObjectsOfObject:candidate] _representedObject];
subObjects = [self _subrowObjectsOfObject:candidate]; // Standard direct array query
if ([subObjects indexOfObjectIdenticalTo:targetObject] !== CPNotFound)
return current_index;
@@ -605,7 +676,7 @@ TODO: implement
for (var i = rowIndex + 1; i < count; i++)
{
var candidate = [[self _rowCacheForIndex:i] rowObject],
indexInSubrows = [[subobjects _representedObject] indexOfObjectIdenticalTo:candidate];
indexInSubrows = [subobjects indexOfObjectIdenticalTo:candidate]; // Standard direct array query
if (indexInSubrows !== CPNotFound)
{
@@ -774,7 +845,7 @@ TODO: implement
while (current_index !== CPNotFound)
{
var rowObject = [[self _rowCacheForIndex:current_index] rowObject],
relativeChildIndex = [[subrows _representedObject] indexOfObjectIdenticalTo:rowObject];
relativeChildIndex = [subrows indexOfObjectIdenticalTo:rowObject]; // Standard direct array query
if (relativeChildIndex !== CPNotFound)
[childsIndexes addIndex:relativeChildIndex];
@@ -831,7 +902,6 @@ TODO: implement
for (i = 0; i < count; i++)
{
var item = [items objectAtIndex:i],
//var displayValue = [self _queryValueForItem:item inRow:aRow]; Ask the delegate or get cached value ?.
displayValue = [[self displayValuesForRow:aRow] objectAtIndex:i],
predpart = [self _sendDelegateRuleEditorPredicatePartsForCriterion:item withDisplayValue:displayValue inRow:aRow];
@@ -849,6 +919,7 @@ TODO: implement
return nil;
var current_index = [subrowsIndexes firstIndex];
while (current_index !== CPNotFound)
{
var subpredicate = [self predicateForRow:current_index];
@@ -1278,23 +1349,28 @@ TODO: implement
while (current_index !== CPNotFound)
{
var parentIndex = [self parentRowForRow:current_index],
subrowsIndexes = [self subrowIndexesForRow:parentIndex];
if ([subrowsIndexes count] === 1)
var parentIndex = [self parentRowForRow:current_index];
// If the row has a valid parent in the editor (i.e. not a root row)
if (parentIndex !== -1)
{
if (parentIndex !== -1)
return [CPIndexSet indexSetWithIndex:0];
var subrowsIndexes = [self subrowIndexesForRow:parentIndex];
var childlessGranPa = [self _childlessParentsIfSlicesWereDeletedAtIndexes:[CPIndexSet indexSetWithIndex:parentIndex]];
[childlessParents addIndexes:childlessGranPa];
// If deleting this row leaves the parent with no remaining child rows
if ([subrowsIndexes count] === 1)
{
[childlessParents addIndex:parentIndex];
// Recursively check if deleting this parent row leaves the grandparent childless
var childlessGranPa = [self _childlessParentsIfSlicesWereDeletedAtIndexes:[CPIndexSet indexSetWithIndex:parentIndex]];
[childlessParents addIndexes:childlessGranPa];
}
}
current_index = [indexes indexGreaterThanIndex:current_index];
}
return childlessParents;
// (id)-[RuleEditor _includeSubslicesForSlicesAtIndexes:]
}
- (CPIndexSet)_includeSubslicesForSlicesAtIndexes:(CPIndexSet)indexes
@@ -1356,8 +1432,25 @@ TODO: implement
if ([self rowTypeForRow:row] === type && itemIndex < [aCriteria count])
{
var crit = [aCriteria objectAtIndex:itemIndex];
[current_criterions addObject:crit];
// Verify that this row's parent path matches the path currently being built
var pathMatches = true;
for (var p = 0; p < itemIndex; p++)
{
var criterionA = [aCriteria objectAtIndex:p],
criterionB = [items objectAtIndex:p];
if (criterionA !== criterionB && (typeof criterionA.isEqual !== "function" || ![criterionA isEqual:criterionB]))
{
pathMatches = false;
break;
}
}
if (pathMatches)
{
var crit = [aCriteria objectAtIndex:itemIndex];
[current_criterions addObject:crit];
}
}
}
@@ -1924,17 +2017,17 @@ TODO: implement
- (CPString)_toolTipForAddCompoundRowButton
{
return [_standardLocalizer localizedStringForString:@"Add compound row"];
return [[self standardLocalizer] localizedStringForString:@"Add compound row"];
}
- (CPString)_toolTipForAddSimpleRowButton
{
return [_standardLocalizer localizedStringForString:@"Add row"];
return [[self standardLocalizer] localizedStringForString:@"Add row"];
}
- (CPString)_toolTipForDeleteRowButton
{
return [_standardLocalizer localizedStringForString:@"Delete row"];
return [[self standardLocalizer] localizedStringForString:@"Delete row"];
}
- (void)_updateSliceIndentations
@@ -77,6 +77,9 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+)
}
_dictionary = [CPDictionary dictionaryWithDictionary:dict];
// Post notification to let the rule editor know the translation dictionary is ready
[[CPNotificationCenter defaultCenter] postNotificationName:@"_CPRuleEditorLocalizerDidLoadNotification" object:self];
}
- (CPString)localizedStringForString:(CPString)aString
@@ -94,4 +97,231 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+)
return aString;
}
#pragma mark - Formatting & Reordering Helpers
- (CPString)_englishRepresentationForView:(id)aView
{
if ([aView isKindOfClass:[CPPopUpButton class]])
{
var selectedItem = [aView selectedItem];
if (selectedItem)
{
var originalTitle = selectedItem._originalTitle;
// Fallback: If not cached directly, inspect representedObject payload dictionary
if (!originalTitle)
{
var rep = [selectedItem representedObject];
if (rep && typeof rep === "object" && [rep respondsToSelector:@selector(objectForKey:)])
{
originalTitle = [rep objectForKey:@"value"];
}
else if (rep && typeof rep === "string")
{
originalTitle = rep;
}
}
if (!originalTitle)
{
originalTitle = [selectedItem title];
}
return "%[" + originalTitle + "]@";
}
return "%[]@";
}
else if ([aView isKindOfClass:[CPTextField class]] && ![aView isEditable])
{
return aView._originalText || [aView stringValue];
}
else
{
return "%@";
}
}
- (CPString)formattingKeyForViews:(CPArray)views
{
var keyParts = [];
var count = [views count];
for (var i = 0; i < count; i++)
{
var view = [views objectAtIndex:i];
[keyParts addObject:[self _englishRepresentationForView:view]];
}
return [keyParts componentsJoinedByString:@" "];
}
- (void)localizeMenuItemsForViews:(CPArray)views
{
var count = [views count];
for (var i = 0; i < count; i++)
{
var view = [views objectAtIndex:i];
if ([view isKindOfClass:[CPPopUpButton class]])
{
var menuItems = [view itemArray];
var menuItemsCount = [menuItems count];
var selectedItem = [view selectedItem];
for (var j = 0; j < menuItemsCount; j++)
{
var item = [menuItems objectAtIndex:j];
if (!item._originalTitle)
{
var rep = [item representedObject];
if (rep && typeof rep === "object" && [rep respondsToSelector:@selector(objectForKey:)])
{
item._originalTitle = [rep objectForKey:@"value"];
}
else
{
item._originalTitle = [item title];
}
}
// Temporarily select item to generate formatting key context
[view selectItem:item];
var tempKey = [self formattingKeyForViews:views];
var tempPattern = [self localizedStringForString:tempKey];
if (tempPattern !== tempKey)
{
var regex = /%(\d+)\$(?:\[([^\]]+)\])?@/g;
var match;
while ((match = regex.exec(tempPattern)) !== null)
{
var position = parseInt(match[1], 10) - 1;
var translatedValue = match[2];
if (position === i && translatedValue)
{
[item setTitle:translatedValue];
break;
}
}
}
else
{
[item setTitle:item._originalTitle];
}
}
if (selectedItem)
{
[view selectItem:selectedItem];
}
}
}
}
- (CPArray)localizeAndReorderViews:(CPArray)views
{
var key = [self formattingKeyForViews:views];
var localizedPattern = [self localizedStringForString:key];
if (localizedPattern === key)
{
var count = [views count];
for (var i = 0; i < count; i++)
{
var originalView = [views objectAtIndex:i];
if ([originalView isKindOfClass:[CPPopUpButton class]])
{
var selectedItem = [originalView selectedItem];
if (selectedItem && selectedItem._originalTitle)
{
[selectedItem setTitle:selectedItem._originalTitle];
}
}
else if ([originalView respondsToSelector:@selector(setStringValue:)] && originalView._originalText)
{
[originalView setStringValue:originalView._originalText];
if ([originalView isKindOfClass:[CPTextField class]] && ![originalView isEditable])
{
var font = [originalView font] || [CPFont systemFontOfSize:[CPFont systemFontSize]],
size = [originalView._originalText sizeWithFont:font];
[originalView setFrameSize:CGSizeMake(size.width + 4, CGRectGetHeight([originalView frame]))];
}
}
}
return views;
}
var newViews = [CPMutableArray array];
var regex = /%(\d+)\$(?:\[([^\]]+)\])?@/g;
var lastIndex = 0;
var match;
while ((match = regex.exec(localizedPattern)) !== null)
{
var literalText = localizedPattern.substring(lastIndex, match.index);
// Only add a label if there are actual non-whitespace characters (like 'y')
if (literalText.length > 0 && /\S/.test(literalText))
{
var label = [CPTextField labelWithTitle:literalText];
[newViews addObject:label];
}
var position = parseInt(match[1], 10) - 1;
var translatedValue = match[2];
if (position >= 0 && position < [views count])
{
var originalView = [views objectAtIndex:position];
if (translatedValue !== undefined && translatedValue !== null)
{
if ([originalView isKindOfClass:[CPPopUpButton class]])
{
var selectedItem = [originalView selectedItem];
if (selectedItem)
{
if (!selectedItem._originalTitle)
{
selectedItem._originalTitle = [selectedItem title];
}
[selectedItem setTitle:translatedValue];
}
}
else if ([originalView respondsToSelector:@selector(setStringValue:)])
{
[originalView setStringValue:translatedValue];
// Recalculate frame size if it is a static CPTextField to avoid visual clipping
if ([originalView isKindOfClass:[CPTextField class]] && ![originalView isEditable])
{
var font = [originalView font] || [CPFont systemFontOfSize:[CPFont systemFontSize]],
size = [translatedValue sizeWithFont:font];
[originalView setFrameSize:CGSizeMake(size.width + 4, CGRectGetHeight([originalView frame]))];
}
}
}
[newViews addObject:originalView];
}
lastIndex = regex.lastIndex;
}
if (lastIndex < localizedPattern.length)
{
var literalText = localizedPattern.substring(lastIndex);
// Only add a label if there are actual non-whitespace characters
if (literalText.length > 0 && /\S/.test(literalText))
{
var label = [CPTextField labelWithTitle:literalText];
[newViews addObject:label];
}
}
return newViews;
}
@end
+51 -18
View File
@@ -114,8 +114,32 @@
- (CPMenuItem)_createMenuItemWithTitle:(CPString )title
{
title = [[_ruleEditor standardLocalizer] localizedStringForString:title];
return [[CPMenuItem alloc] initWithTitle:title action:nil keyEquivalent:@""];
var originalTitle = title;
var localizedTitle = [[_ruleEditor standardLocalizer] localizedStringForString:title];
var item = [[CPMenuItem alloc] initWithTitle:localizedTitle action:nil keyEquivalent:@""];
// Cache the raw English title for pattern-matching
item._originalTitle = originalTitle;
return item;
}
- (CPTextField)_createStaticTextFieldWithStringValue:(CPString)text
{
var textField = [[CPTextField alloc] initWithFrame:CGRectMakeZero()],
ruleEditorFont = [_ruleEditor font],
font = [CPFont fontWithName:[ruleEditorFont familyName] size:[ruleEditorFont size] + 2],
localizedText = [[_ruleEditor standardLocalizer] localizedStringForString:text],
size = [localizedText sizeWithFont:font];
[textField setFrameSize:CGSizeMake(size.width + 4, [_ruleEditor rowHeight])];
[textField setValue:font forThemeAttribute:@"font"];
[textField setValue:[_ruleEditor _verticalAlignment] forThemeAttribute:@"vertical-alignment"];
[textField setStringValue:localizedText];
// Cache the raw English text for pattern-matching
textField._originalText = text;
return textField;
}
- (CPPopUpButton)_createPopUpButtonWithItems:(CPArray)itemsArray selectedItemIndex:(int)index
@@ -142,22 +166,6 @@
return [CPMenuItem separatorItem];
}
- (CPTextField)_createStaticTextFieldWithStringValue:(CPString)text
{
var textField = [[CPTextField alloc] initWithFrame:CGRectMakeZero()],
ruleEditorFont = [_ruleEditor font],
font = [CPFont fontWithName:[ruleEditorFont familyName] size:[ruleEditorFont size] + 2],
localizedText = [[_ruleEditor standardLocalizer] localizedStringForString:text],
size = [localizedText sizeWithFont:font];
[textField setFrameSize:CGSizeMake(size.width + 4, [_ruleEditor rowHeight])];
[textField setValue:font forThemeAttribute:@"font"];
[textField setValue:[_ruleEditor _verticalAlignment] forThemeAttribute:@"vertical-alignment"];
[textField setStringValue:localizedText];
return textField;
}
- (void)_addOption:(id)sender
{
if (_rowIndex == [_ruleEditor numberOfRows] - 1)
@@ -334,6 +342,28 @@
[_correspondingRuleItems setArray:ruleItems];
// Localize drop-down options in context and reorder/insert intermediate labels natively
var localizer = [_ruleEditor standardLocalizer];
if (localizer)
{
[localizer localizeMenuItemsForViews:_ruleOptionViews];
_ruleOptionViews = [localizer localizeAndReorderViews:_ruleOptionViews];
}
// Rebuild frame configurations to match the new localized layout order
[_ruleOptionFrames removeAllObjects];
[_ruleOptionInitialViewFrames removeAllObjects];
var newCount = [_ruleOptionViews count];
for (var i = 0; i < newCount; i++)
{
var view = [_ruleOptionViews objectAtIndex:i],
frame = [view frame];
[_ruleOptionFrames addObject:frame];
[_ruleOptionInitialViewFrames addObject:frame];
}
if (!_editable)
[self _updateEnabledStateForSubviews];
@@ -410,6 +440,9 @@
{
[_addButton setHidden:[_ruleEditor _shouldHideAddButtonForSlice:self]];
[_subtractButton setHidden:[_ruleEditor _shouldHideSubtractButtonForSlice:self]];
[_addButton setToolTip:[_ruleEditor _toolTipForAddSimpleRowButton]];
[_subtractButton setToolTip:[_ruleEditor _toolTipForDeleteRowButton]];
}
- (void)_configurePlusButtonByRowType:(CPRuleEditorRowType)type
+37
View File
@@ -2004,6 +2004,43 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
[self _setCSSStyleForInputElement];
}
#pragma mark Overrides
/*!
Sets the font of the receiver.
@param aFont - A CPFont object.
*/
- (void)setFont:(CPFont)aFont
{
if ([self currentValueForThemeAttribute:@"font"] === aFont)
return;
// Apply the font to the default/normal state
[self setValue:aFont forThemeAttribute:@"font"];
// Apply to standard editing and border states
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateEditing];
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateBezeled];
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateBordered];
[self setValue:aFont forThemeAttribute:@"font" inState:CPTextFieldStateRounded];
// Use CPThemeState() function to create composite states instead of array literals
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeState(CPTextFieldStateRounded, CPThemeStateEditing)];
// Apply across all standard control size states
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateControlSizeRegular];
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateControlSizeSmall];
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateControlSizeMini];
// Apply to table data view states (ensuring Interface Builder-style lists respect the font)
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateTableDataView];
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeState(CPThemeStateTableDataView, CPThemeStateSelectedDataView)];
[self setValue:aFont forThemeAttribute:@"font" inState:CPThemeState(CPThemeStateTableDataView, CPThemeStateSelectedDataView, CPThemeStateFirstResponder, CPThemeStateKeyWindow)];
[self layoutSubviews];
}
- (void)takeValueFromKeyPath:(CPString)aKeyPath ofObjects:(CPArray)objects
{
var count = objects.length,
+219 -73
View File
@@ -31,8 +31,9 @@
@import "CPFont.j"
@global _MakeRangeFromAbs
@global document
@global CPBaselineOffsetAttributeName
@global CPSuperscriptAttributeName
@class CPTextContainer
@class CPTextView
@@ -292,7 +293,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 +532,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 +560,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 +716,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 +732,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 +860,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 +880,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 +936,6 @@ _oncontextmenuhandler = function () { return false; };
inTextContainer:(CPTextContainer)container
rectCount:(CGRectPointer)rectCount
{
var rectArray = [],
lineFragments = _objectsInRange(_lineFragments, selectedCharRange);
@@ -924,21 +954,24 @@ _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);
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);
}
}
}
}
@@ -949,7 +982,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 +1128,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 +1212,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, underline:underline, baselineOffset:0.0};
_runs.push(run);
}
}
@@ -1194,10 +1229,97 @@ 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 baselineOffset = [attributes objectForKey:CPBaselineOffsetAttributeName],
superscript = [attributes objectForKey:CPSuperscriptAttributeName];
if (baselineOffset === nil || baselineOffset === undefined || typeof baselineOffset !== "number")
baselineOffset = 0.0;
if (superscript === nil || superscript === undefined || typeof superscript !== "number")
superscript = 0;
if (superscript !== 0)
{
var size = [font size],
scaledSize = size * 0.65,
fontName = [font familyName],
isBold = [font isBold],
isItalic = [font isItalic];
font = [CPFont _fontWithName:fontName size:scaledSize bold:isBold italic:isItalic];
if (baselineOffset === 0.0)
{
if (superscript > 0)
baselineOffset = size * 0.35;
else
baselineOffset = -size * 0.15;
}
}
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,
underline: underline,
baselineOffset: baselineOffset
};
_runs.push(run);
}
var tabRange = CPMakeRange(currentLoc + i, 1),
tabRun = {
_range: tabRange,
color: nil,
font: nil,
elem: nil,
string: nil,
bgcolor: nil,
paragraphStyle: paragraphStyle,
underline: underline,
baselineOffset: 0.0
};
_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,
underline: underline,
baselineOffset: baselineOffset
};
_runs.push(run);
}
}
if (!CPMaxRange(effectiveRange))
@@ -1221,7 +1343,10 @@ var _objectsInRange = function(aList, aRange)
{
_glyphsFrames[i] = CGRectMake(origin.x, origin.y, someAdvancements[i].width, height);
_glyphsFrames[i]._descent = someAdvancements[i].descent;
_glyphsOffsets[i] = height - someAdvancements[i].height;
// Align the run's baseline with the common line baseline (_location.y)
_glyphsOffsets[i] = _location.y - someAdvancements[i].height;
origin.x += someAdvancements[i].width;
}
}
@@ -1269,13 +1394,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 +1411,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 +1428,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 +1450,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);
}
@@ -1356,7 +1491,16 @@ var _objectsInRange = function(aList, aRange)
if (!_RectEqualToRectHorizontally(newLineFragment._fragmentRect, _fragmentRect))
return NO;
if (newFragmentRuns[i].color !== oldFragmentRuns[i].color || newFragmentRuns[i].bgcolor !== oldFragmentRuns[i].bgcolor || newFragmentRuns[i].font !== oldFragmentRuns[i].font)
if (newFragmentRuns[i].color !== oldFragmentRuns[i].color ||
newFragmentRuns[i].bgcolor !== oldFragmentRuns[i].bgcolor ||
newFragmentRuns[i].font !== oldFragmentRuns[i].font ||
newFragmentRuns[i].baselineOffset !== oldFragmentRuns[i].baselineOffset)
return NO;
var oldStyle = oldFragmentRuns[i].paragraphStyle || [CPParagraphStyle defaultParagraphStyle],
newStyle = newFragmentRuns[i].paragraphStyle || [CPParagraphStyle defaultParagraphStyle];
if (![oldStyle isEqual:newStyle])
return NO;
}
@@ -1373,12 +1517,14 @@ var _objectsInRange = function(aList, aRange)
{
_runs[i]._range.location += rangeOffset;
if (verticalOffset && _runs[i].elem)
if (verticalOffset)
{
if (_runs[i].view)
_runs[i].view._frame.origin.y += verticalOffset;
_runs[i].elem.top = (_runs[i].elem.top + verticalOffset) + 'px';
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;
+46 -13
View File
@@ -148,27 +148,58 @@ CPLineMovesUp = 4;
- (void)setWidthTracksTextView:(BOOL)flag
{
_widthTracksTextView = flag;
[_textView setPostsFrameChangedNotifications:flag];
if (_widthTracksTextView === flag)
return;
if (flag && _textView)
{
[[CPNotificationCenter defaultCenter] addObserver:self
selector:@selector(textViewFrameChanged:)
name:CPViewFrameDidChangeNotification
object:_textView];
}
else
_widthTracksTextView = flag;
[self _updateFrameObserver];
}
// Controls whether the receiver adjusts the height of its bounding rectangle when its text view is resized.
- (BOOL)heightTracksTextView
{
return _heightTracksTextView;
}
- (void)setHeightTracksTextView:(BOOL)flag
{
if (_heightTracksTextView === flag)
return;
_heightTracksTextView = flag;
[self _updateFrameObserver];
}
- (void)_updateFrameObserver
{
if (_textView)
{
[[CPNotificationCenter defaultCenter] removeObserver:self
name:CPViewFrameDidChangeNotification
object:_textView];
var flag = _widthTracksTextView || _heightTracksTextView;
[_textView setPostsFrameChangedNotifications:flag];
if (flag)
{
[[CPNotificationCenter defaultCenter] addObserver:self
selector:@selector(textViewFrameChanged:)
name:CPViewFrameDidChangeNotification
object:_textView];
}
}
}
- (void)textViewFrameChanged:(CPNotification)aNotification
{
var newSize = CGSizeMake([_textView frame].size.width, _size.height);
var newSize = CGSizeMake(_size.width, _size.height);
if (_widthTracksTextView)
newSize.width = [_textView frame].size.width;
if (_heightTracksTextView)
newSize.height = [_textView frame].size.height;
[self setContainerSize:newSize];
}
@@ -177,7 +208,9 @@ CPLineMovesUp = 4;
{
if (_textView)
{
[self setWidthTracksTextView:NO]; // We only support width
[[CPNotificationCenter defaultCenter] removeObserver:self
name:CPViewFrameDidChangeNotification
object:_textView];
[_textView setTextContainer:nil];
}
@@ -185,7 +218,7 @@ CPLineMovesUp = 4;
if (_textView)
{
[self setWidthTracksTextView:_widthTracksTextView]; // We only support width
[self _updateFrameObserver];
[_textView setTextContainer:self];
}
+71 -9
View File
@@ -401,6 +401,7 @@ var kDelegateRespondsTo_textShouldBeginEditing
- (void)superviewFrameChanged:(CPNotification)aNotification
{
_exposedRect = nil;
[self sizeToFit];
}
- (void)viewWillMoveToSuperview:(CPView)aView
@@ -1004,6 +1005,25 @@ Sets the selection to a range of characters in response to user action.
if (doOverwrite && _placeholderString == nil && isNewSelection)
[self setTypingAttributes:[_textStorage attributesAtIndex:CPMaxRange(range) effectiveRange:nil]];
// Update the shared CPColorPanel with the active selection color
if ([self _isFirstResponder] && [_textStorage length] > 0)
{
var currentTextColor = [self textColor] || [CPColor blackColor];
if ([self isRichText])
{
var charIndex = _selectionRange.location;
if (charIndex >= [_textStorage length])
charIndex = MAX(0, charIndex - 1);
var attributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil];
if ([attributes objectForKey:CPForegroundColorAttributeName])
currentTextColor = [attributes objectForKey:CPForegroundColorAttributeName];
}
[[CPColorPanel sharedColorPanel] setColor:currentTextColor];
}
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeSelectionNotification object:self];
}
@@ -1397,7 +1417,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
@@ -1419,7 +1442,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
@@ -1653,7 +1679,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
@@ -1789,6 +1818,13 @@ Sets the selection to a range of characters in response to user action.
// SYNCHRONIZE ACTIVE PARAGRAPH MARKERS ON TYPING ATTRIBUTES CHANGE
[self updateRuler];
// Synchronize CPColorPanel if text view is active
if ([self _isFirstResponder])
{
var currentTextColor = [_typingAttributes objectForKey:CPForegroundColorAttributeName] || [self textColor] || [CPColor blackColor];
[[CPColorPanel sharedColorPanel] setColor:currentTextColor];
}
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
// We always clear the saved selection range from the last mouse down event here.
@@ -2069,8 +2105,16 @@ Sets the selection to a range of characters in response to user action.
[self setFrameSize:[self frameSize]];
}
- (void)setBoundsSize:(CGSize)aSize
{
_exposedRect = nil; // Clear the cached visible rect when bounds change
[super setBoundsSize:aSize];
}
- (void)setFrameSize:(CGSize)aSize
{
_exposedRect = nil; // Clear the cached visible rect so it gets recalculated at the new size
var desiredSize = CGSizeCreateCopy(aSize);
if (_isHorizontallyResizable || _isVerticallyResizable)
@@ -2126,9 +2170,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
{
@@ -2287,17 +2337,15 @@ Sets the selection to a range of characters in response to user action.
var loc = (_selectionRange.location == numberOfGlyphs) ? _selectionRange.location - 1 : _selectionRange.location,
caretOffset = [_layoutManager _characterOffsetAtLocation:loc],
oldYPosition = CGRectGetMaxY(caretRect),
caretDescend = [_layoutManager _descentAtLocation:loc];
font = [_textStorage attribute:CPFontAttributeName atIndex:loc effectiveRange:nil] || [self font];
if (caretOffset > 0)
{
caretRect.origin.y += caretOffset;
caretRect.size.height = oldYPosition - caretRect.origin.y;
}
if (caretDescend < 0)
caretRect.size.height -= caretDescend;
// Set the caret height to match the size of the active font
caretRect.size.height = [font size];
if (_selectionRange.location == numberOfGlyphs)
caretRect.origin.x += caretRect.size.width;
@@ -2306,7 +2354,7 @@ Sets the selection to a range of characters in response to user action.
caretRect.origin.y += _textContainerOrigin.y;
caretRect.size.width = MAX(1.0, caretRect.size.width);
caretRect.size.height = MAX(1.0, caretRect.size.height);
caretRect.size.height = MAX(1.0, caretRect.size.height) + 2;
return caretRect;
}
@@ -2624,6 +2672,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
@@ -2843,11 +2895,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]];
@@ -3176,6 +3233,11 @@ var _CPCopyPlaceholder = '-';
if (richtext)
{
var shouldPastePlainText = [[CPApp currentEvent] modifierFlags] & (CPShiftKeyMask | CPAlternateKeyMask);
if (shouldPastePlainText && richtext._string)
richtext = richtext._string;
[currentFirstResponder _pasteString:richtext];
return;
+47 -8
View File
@@ -30,6 +30,9 @@
@import "CPTextStorage.j"
@import "CPFont.j"
@global CPBaselineOffsetAttributeName
@global CPSuperscriptAttributeName
// forward declare these classes for type matching
@class CPLayoutManager
@class CPTextContainer
@@ -353,9 +356,41 @@ var CPSystemTypesetterFactory,
if (!currentFont)
currentFont = [_textStorage font] || [CPFont systemFontOfSize:12.0];
ascent = [currentFont ascender];
descent = [currentFont descender];
leading = (ascent - descent) * 0.2; // FAKE leading
// Safely retrieve and validate CPBaselineOffsetAttributeName
var baselineOffset = [_currentAttributes objectForKey:CPBaselineOffsetAttributeName];
if (baselineOffset === nil || baselineOffset === undefined || typeof baselineOffset !== "number")
baselineOffset = 0.0;
// Safely retrieve and validate CPSuperscriptAttributeName
var superscript = [_currentAttributes objectForKey:CPSuperscriptAttributeName];
if (superscript === nil || superscript === undefined || typeof superscript !== "number")
superscript = 0;
if (superscript !== 0)
{
var size = [currentFont size],
scaledSize = size * 0.65,
fontName = [currentFont familyName],
isBold = [currentFont isBold],
isItalic = [currentFont isItalic];
currentFont = [CPFont _fontWithName:fontName size:scaledSize bold:isBold italic:isItalic];
if (baselineOffset === 0.0)
{
if (superscript > 0)
baselineOffset = size * 0.35;
else
baselineOffset = -size * 0.15;
}
}
var fontAscent = [currentFont ascender] || 0.0,
fontDescent = [currentFont descender] || 0.0;
ascent = fontAscent + baselineOffset;
descent = fontDescent + baselineOffset;
leading = (fontAscent - fontDescent) * 0.2; // FAKE leading
currentFontLineHeight = ascent - descent + leading;
@@ -368,11 +403,15 @@ var CPSystemTypesetterFactory,
}
if (currentFontLineHeight > _lineHeight)
_lineHeight = currentFontLineHeight;
// Clean bounds logic to prevent NaN and layout calculation overhead
var currentAscent = (ascent === undefined || isNaN(ascent)) ? 0.0 : ascent,
currentLineHeight = (currentFontLineHeight === undefined || isNaN(currentFontLineHeight)) ? 12.0 : currentFontLineHeight;
if (ascent > _lineBase)
_lineBase = ascent;
if (currentLineHeight > _lineHeight)
_lineHeight = currentLineHeight;
if (currentAscent > _lineBase)
_lineBase = currentAscent;
lineRange.length++;
measuringRange.length++;
@@ -546,7 +585,7 @@ var CPSystemTypesetterFactory,
isNewline = NO;
_lineFragments = [];
_lineHeight = 0;
_lineBase = ascent;
_lineBase = 0;
isStartOfPhysicalLine = YES;
}
+576 -40
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,10 @@
@global CPForegroundColorAttributeName
@global CPBackgroundColorAttributeName
@global CPParagraphStyleAttributeName
@global CPAttachmentAttributeName
@global CPUnderlineStyleAttributeName
@global CPBaselineOffsetAttributeName
@global CPSuperscriptAttributeName
@global CPLeftTabStopType
@global CPRightTabStopType
@@ -68,6 +74,8 @@ var cp1252Map = {
BOOL script;
BOOL _tabChanged;
CPTabStopType _nextTabType;
int superscript;
float baselineOffset;
}
- (id)init
@@ -100,6 +108,8 @@ var cp1252Map = {
mynew.ulColour = ulColour;
mynew._tabChanged = _tabChanged;
mynew._nextTabType = _nextTabType;
mynew.superscript = superscript;
mynew.baselineOffset = baselineOffset;
return mynew;
}
@@ -111,11 +121,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 +130,6 @@ var cp1252Map = {
font = [CPFont fontWithName:fontFamily size:fontSize];
}
/* Last resort, default font. :-( */
if (font == nil)
font = [CPFont systemFontOfSize:fontSize];
@@ -171,11 +175,21 @@ var cp1252Map = {
underline = 0;
strikethrough = 0;
script = 0;
superscript = 0;
baselineOffset = 0.0;
}
- (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 +217,35 @@ var cp1252Map = {
if (bgColour)
[ret setObject:bgColour forKey:CPBackgroundColorAttributeName];
if (underline)
[ret setObject:[CPNumber numberWithInt:1] forKey:CPUnderlineStyleAttributeName];
if (superscript !== 0)
[ret setObject:[CPNumber numberWithInt:superscript] forKey:CPSuperscriptAttributeName];
if (baselineOffset !== 0.0)
[ret setObject:[CPNumber numberWithFloat:baselineOffset] forKey:CPBaselineOffsetAttributeName];
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"],
"super" : [ "super", 1, true, kRTFParserType_prop, "propSuper"],
"sub" : [ "sub", 1, true, kRTFParserType_prop, "propSub"],
"nosupersub" : [ "nosupersub",1, true, kRTFParserType_prop, "propNoSuperSub"],
"up" : [ "up", 6, false, kRTFParserType_prop, "propUp"],
"dn" : [ "dn", 6, false, kRTFParserType_prop, "propDn"],
"plain" : [ "plain", 0, false, kRTFParserType_prop, "propPlain"],
"pgnucltr" : [ "pgnucltr", "pgULtr", true, kRTFParserType_prop, "propPgnFormat"],
"pgnlcltr" : [ "pgnlcltr", "pgLLtr", true, kRTFParserType_prop, "propPgnFormat"],
"qc" : [ "qc", "justC", true, kRTFParserType_prop, "propJust"],
@@ -263,9 +287,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 +311,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 +334,14 @@ var kRgsymRtf = {
CPArray _fontArray;
CPString _freename;
BOOL _parsingFontTable;
BOOL _keywordIsControlWord;
// Table parsing state
BOOL _inTableActive;
BOOL _waitingForNextRow;
CPMutableArray _tableRows;
CPMutableArray _currentRow;
CPString _currentCellText;
}
- (id)init
@@ -319,11 +354,21 @@ var kRgsymRtf = {
_states = [];
_currentParseIndex = 0;
_hexreturn = NO;
_keywordIsControlWord = NO;
_result = [CPAttributedString new];
_colorArray = [];
_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 +383,6 @@ var kRgsymRtf = {
return sym[4];
case 1:
// CPLogConsole("skipped : " + sym[4]);
return '';
default:
@@ -349,7 +393,6 @@ var kRgsymRtf = {
- (BOOL)pushState
{
// Push stack as an object containing scoping context
_states.push({
curState: _curState,
run: [_currentRun copy]
@@ -366,7 +409,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 +437,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 +473,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 +571,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 +583,6 @@ var kRgsymRtf = {
- (CPString)_applyPropChange:sym parameter:param
{
//console.log("prop : " + sym[0] + " / param : " + param+ ' ');
switch (sym[0])
{
case "pard":
@@ -499,6 +626,53 @@ 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 "super":
[self _flushCurrentRun];
_currentRun.superscript = 1;
break;
case "sub":
[self _flushCurrentRun];
_currentRun.superscript = -1;
break;
case "nosupersub":
[self _flushCurrentRun];
_currentRun.superscript = 0;
break;
case "up":
[self _flushCurrentRun];
_currentRun.baselineOffset = parseFloat(param) / 2.0;
break;
case "dn":
[self _flushCurrentRun];
_currentRun.baselineOffset = -parseFloat(param) / 2.0;
break;
case "plain":
[self _flushCurrentRun];
[_currentRun resetFont];
break;
case "qc": // paragraph center
[_currentRun.paragraph setAlignment:CPCenterTextAlignment];
break;
@@ -543,7 +717,6 @@ var kRgsymRtf = {
if (sym[4] == "destSkip")
{
CPLogConsole("Dest skip start : [" + sym[0] + "]");
_curState++;
}
@@ -552,6 +725,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];
@@ -616,10 +797,15 @@ var kRgsymRtf = {
[self _flushCurrentRun];
var fontIndex = parseInt(param) - 1;
if (_currentRun && fontIndex >= 0)
_currentRun.fgColour = _colorArray[fontIndex];
if (_currentRun)
{
if (fontIndex >= 0 && fontIndex < _colorArray.length)
_currentRun.fgColour = _colorArray[fontIndex];
else
_currentRun.fgColour = nil;
}
break;
break;
case "cb": // change background color
case "highlight":
@@ -687,8 +873,7 @@ var kRgsymRtf = {
break;
default:
CPLogConsole("skip : " + keyword + " param: " + param);
break;
}
return '';
@@ -711,7 +896,12 @@ var kRgsymRtf = {
ch = rtf.charAt(_currentParseIndex);
if (!/[a-zA-Z]/.test(ch))
{
_keywordIsControlWord = NO;
return [self _translateKeyword:ch parameter:nil fParameter:fParam];
}
_keywordIsControlWord = YES;
while (new RegExp("[a-zA-Z]").test(ch))
{
@@ -744,9 +934,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 +963,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))];
@@ -782,25 +984,33 @@ var kRgsymRtf = {
else
{
_freename += tmp;
[self _appendPlainString:tmp];
// Only append literal spaces to the document if we are in the active body state
if (_curState == 0)
{
[self _appendPlainString:tmp];
}
}
break;
case "{":
if ([self pushState])
CPLogConsole("push");
lastchar = 0;
if (_waitingForNextRow)
[self _flushTableIfAny];
if ([self pushState])
break;
case "}":
lastchar = 0;
if (_waitingForNextRow)
[self _flushTableIfAny];
if ([self popState])
CPLogConsole("pop");
if (_freename)
{
CPLogConsole(_freename);
if (_parsingFontTable)
{
_fontArray.push(_freename);
@@ -817,19 +1027,19 @@ var kRgsymRtf = {
_freename = '';
ch = [self _parseKeyword:rtf length:len];
if (!_hexreturn && ch.length == 0)
if (!_hexreturn && _keywordIsControlWord)
lastchar = 1;
else
lastchar = 0;
if (_hexreturn)
{
if (ch.length > 0)
// Only append decoded characters if we are in the active body state
if (ch.length > 0 && _curState === 0)
{
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;
}
@@ -849,21 +1059,347 @@ var kRgsymRtf = {
case 0x0a:
case '\n':
case '\r':
lastchar = 0;
break;
default:
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];
}
// Convert raw header strings into CPAttributedStrings
var parsedHeaders = [CPMutableArray array];
for (var c = 0; c < [headers count]; c++) {
var cellText = [headers objectAtIndex:c];
[parsedHeaders addObject:[self parseTableCellMarkdown:cellText isHeader:YES]];
}
// Convert raw row strings into CPAttributedStrings
var parsedRows = [CPMutableArray array];
for (var r = 0; r < [rows count]; r++) {
var rowData = [rows objectAtIndex:r];
var parsedRow = [CPMutableArray array];
for (var c = 0; c < [rowData count]; c++) {
var cellText = [rowData objectAtIndex:c];
[parsedRow addObject:[self parseTableCellMarkdown:cellText isHeader:NO]];
}
[parsedRows addObject:parsedRow];
}
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 < [parsedHeaders count]) {
var parsedText = [parsedHeaders objectAtIndex:c];
[measureTextField setStringValue:parsedText._string];
[measureTextField sizeToFit];
cellW = Math.max(cellW, CGRectGetWidth([measureTextField frame]) + 24.0);
}
for (var r = 0; r < [parsedRows count]; r++) {
var rowData = [parsedRows objectAtIndex:r];
if (c < [rowData count]) {
var parsedText = [rowData objectAtIndex:c];
[measureTextField setStringValue:parsedText._string];
[measureTextField sizeToFit];
cellW = Math.max(cellW, CGRectGetWidth([measureTextField frame]) + 24.0);
}
}
colNaturalWidths[c] = cellW;
totalNaturalW += cellW;
}
// Pass the parsed attributed strings to the attachment
var matrixView = [[_CPTableTextAttachment alloc] initWithHeaders:parsedHeaders rows:parsedRows 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;
}
// Parses a single table cell's Markdown, converting HTML breaks and handling list items
+ (CPAttributedString)parseTableCellMarkdown:(CPString)cellText isHeader:(BOOL)isHeader
{
if (!cellText) {
return [[CPAttributedString alloc] initWithString:@""];
}
// Convert HTML line breaks to standard newlines
var cleanedText = cellText.replace(/<br\s*\/?>/gi, "\n");
var lines = cleanedText.split(/\r?\n/);
var cellResult = [[CPMutableAttributedString alloc] initWithString:@""];
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
// Convert dash or asterisk lists inside the cell to bullets
var listMatch = line.match(/^(\*|-)\s+(.*)$/);
if (listMatch) {
line = " • " + listMatch[2];
}
var parsedLine = [self parseInlineMarkdown:line isHeader:isHeader headerLevel:3];
[cellResult appendAttributedString:parsedLine];
if (i < lines.length - 1) {
[cellResult appendAttributedString:[[CPAttributedString alloc] initWithString:@"\n"]];
}
}
return cellResult;
}
+ (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
+252 -70
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,205 @@ 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 = "";
}
// Safely extract text representation from CPAttributedString / CPTextStorage if present
if (cellText && typeof cellText === "object") {
if (typeof cellText.string === "function") {
cellText = [cellText string];
} else if (cellText._string !== undefined) {
cellText = cellText._string;
} else if (cellText.string !== undefined) {
cellText = cellText.string;
}
}
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 +593,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 +607,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 +618,7 @@ function _points2twips(a) { return (a) * 20.0; }
pString = [CPString stringWithFormat:@"\\fs%d", points];
headerString += pString;
}
/*
* font attributes
*/
if (traits & CPItalicFontMask)
{
headerString += @"\\i";
@@ -475,28 +657,29 @@ function _points2twips(a) { return (a) * 20.0; }
else if ([currAttrib isEqualToString:CPUnderlineStyleAttributeName])
{
headerString += @"\\ul";
trailerString += @"\\ulnone";
trailerString += @"\\ulnone "; // trailing space important!
}
else if ([currAttrib isEqualToString:CPSuperscriptAttributeName])
{
var value = [attributes objectForKey:CPSuperscriptAttributeName],
svalue = [value intValue] * 6;
ivalue = [value intValue];
if (svalue > 0)
if (ivalue > 0)
{
headerString += [CPString stringWithFormat:@"\\up%d", svalue];
trailerString += @"\\up0";
headerString += @"\\super";
trailerString += @"\\nosupersub "; // trailing space important!
}
else if (svalue < 0)
else if (ivalue < 0)
{
headerString += [CPString stringWithFormat:@"\\dn-%d", svalue];
trailerString += @"\\dn0";
headerString += @"\\sub";
trailerString += @"\\nosupersub "; // trailing space important!
}
}
else if ([currAttrib isEqualToString:CPBaselineOffsetAttributeName])
{
var value = [attributes objectForKey:CPBaselineOffsetAttributeName],
svalue = [value floatValue] * 2;
fvalue = [value floatValue],
svalue = Math.round(fvalue * 2.0); // Convert standard points to RTF half-points
if (svalue > 0)
{
@@ -505,7 +688,8 @@ function _points2twips(a) { return (a) * 20.0; }
}
else if (svalue < 0)
{
headerString += [CPString stringWithFormat:@"\\dn-%d", svalue];
// Correct negative formatting using safe positive boundary
headerString += [CPString stringWithFormat:@"\\dn%d", Math.abs(svalue)];
trailerString += @"\\dn0";
}
}
@@ -522,11 +706,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)
{
@@ -562,10 +744,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 +758,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 +783,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];
+557
View File
@@ -0,0 +1,557 @@
/* _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:YES];
}
- (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];
// Set explicit zero margins inside the text view to keep height measurements aligned with usedRect
[textView setTextContainerInset:CGSizeMake(0, 0)];
[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]];
if (text)
[textView insertText:text];
[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);
// Update frame size directly so that textContainer auto-resizes.
// Set a large temporary height to allow accurate wrapping measurements.
[textView setFrameSize:CGSizeMake(targetWidth, 1e7)];
var layoutManager = [textView layoutManager];
if (layoutManager)
{
// FORCE LAYOUT RECALCULATION:
// Because the width changed, we must force the lazy layout manager
// to synchronously calculate glyphs and wraps at the new width.
[layoutManager glyphRangeForTextContainer:[textView textContainer]];
}
var usedRect = layoutManager ? [layoutManager usedRectForTextContainer:[textView textContainer]] : nil;
var textHeight = usedRect ? CGRectGetHeight(usedRect) : 0.0;
// Exact height + 8.0px padding (4.0px top, 4.0px bottom margin) inside the cell
var wrappedHeight = textHeight + 8.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
+6 -6
View File
@@ -472,9 +472,9 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
theDocument.addEventListener("touchmove", touchEventCallback, {passive: false});
theDocument.addEventListener("touchcancel", touchEventCallback, {passive: false});
_DOMWindow.addEventListener("DOMMouseScroll", scrollEventCallback, NO);
_DOMWindow.addEventListener("wheel", scrollEventCallback, NO);
_DOMWindow.addEventListener("mousewheel", scrollEventCallback, NO);
_DOMWindow.addEventListener("DOMMouseScroll", scrollEventCallback, { passive: false });
_DOMWindow.addEventListener("wheel", scrollEventCallback, { passive: false });
_DOMWindow.addEventListener("mousewheel", scrollEventCallback, { passive: false });
_DOMWindow.addEventListener("resize", resizeEventCallback, NO);
@@ -508,9 +508,9 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio
_DOMWindow.removeEventListener("focus", onFocusEventCallback, NO);
//FIXME: does firefox really need a different value?
_DOMWindow.removeEventListener("DOMMouseScroll", scrollEventCallback, NO);
_DOMWindow.removeEventListener("wheel", scrollEventCallback, NO);
_DOMWindow.removeEventListener("mousewheel", scrollEventCallback, NO);
_DOMWindow.removeEventListener("DOMMouseScroll", scrollEventCallback, { passive: false });
_DOMWindow.removeEventListener("wheel", scrollEventCallback, { passive: false });
_DOMWindow.removeEventListener("mousewheel", scrollEventCallback, { passive: false });
[PlatformWindows removeObject:self];
@@ -0,0 +1,173 @@
@import <Foundation/CPObject.j>
@import <AppKit/CPWindow.j>
@import <AppKit/CPRuleEditor.j>
@import <AppKit/CPTextField.j>
@import <AppKit/CPButton.j>
@import <AppKit/CPPopUpButton.j>
@import "RuleDelegate.j"
@implementation AppController : CPObject
{
CPWindow theWindow;
CPRuleEditor ruleEditor;
CPTextField predicateField;
RuleDelegate ruleDelegate;
CPPopUpButton langPopUp;
CPDictionary englishDict;
CPDictionary spanishDict;
CPDictionary germanDict;
}
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(50, 50, 800, 520)
styleMask:CPTitledWindowMask | CPClosableWindowMask | CPResizableWindowMask];
[theWindow setTitle:@"CPRuleEditor Multi-Language Localization Demo"];
[theWindow setFullPlatformWindow:YES];
var contentView = [theWindow contentView];
[contentView setBackgroundColor:[CPColor colorWithHexString:@"f3f4f5"]];
var label = [CPTextField labelWithTitle:@"CPRuleEditor Sentence Localization & Positional Reordering:"];
[label setFrame:CGRectMake(20, 20, 500, 24)];
[label setFont:[CPFont boldSystemFontOfSize:14]];
[contentView addSubview:label];
// Language Selector Label
var langLabel = [CPTextField labelWithTitle:@"Language:"];
[langLabel setFrame:CGRectMake(530, 20, 80, 24)];
[langLabel setFont:[CPFont boldSystemFontOfSize:12]];
[langLabel setAlignment:CPRightTextAlignment];
[contentView addSubview:langLabel];
// Language Selector PopUpButton
langPopUp = [[CPPopUpButton alloc] initWithFrame:CGRectMake(620, 16, 160, 24)];
[langPopUp addItemWithTitle:@"Spanish"];
[langPopUp addItemWithTitle:@"German"];
[langPopUp addItemWithTitle:@"English"];
[langPopUp setTarget:self];
[langPopUp setAction:@selector(changeLanguage:)];
[contentView addSubview:langPopUp];
ruleDelegate = [[RuleDelegate alloc] init];
// Create Rule Editor
ruleEditor = [[CPRuleEditor alloc] initWithFrame:CGRectMake(20, 55, 760, 250)];
[ruleEditor setAutoresizingMask:CPViewWidthSizable];
[ruleEditor setDelegate:ruleDelegate];
[ruleEditor setEditable:YES];
[ruleEditor setNestingMode:CPRuleEditorNestingModeList];
[ruleEditor setRowHeight:28];
[ruleEditor setTarget:self];
[ruleEditor setAction:@selector(ruleEditorAction:)];
// Programmatic dictionaries setup
englishDict = [CPDictionary dictionary]; // Identity fallback
spanishDict = [CPDictionary dictionaryWithDictionary:@{
@"%[firstName]@ %[is equal to]@ %@" : @"%1$[Nombre]@ y %3$@ %2$[son iguales]@",
@"%[firstName]@ %[contains]@ %@" : @"%1$[Nombre]@ %2$[contiene]@ %3$@",
@"%[lastName]@ %[is equal to]@ %@" : @"%1$[Apellido]@ y %3$@ %2$[son iguales]@",
@"%[lastName]@ %[contains]@ %@" : @"%1$[Apellido]@ %2$[contiene]@ %3$@",
@"%[age]@ %[is equal to]@ %@" : @"%1$[Edad]@ y %3$@ %2$[son iguales]@",
@"%[age]@ is equal to %@" : @"%1$[Edad]@ y %3$@ %2$[son iguales]@",
@"Add row" : @"Añadir regla",
@"Delete row" : @"Eliminar regla",
@"Add compound row" : @"Añadir grupo de reglas"
}];
germanDict = [CPDictionary dictionaryWithDictionary:@{
@"%[firstName]@ %[is equal to]@ %@" : @"%1$[Vorname]@ und %3$@ %2$[sind gleich]@",
@"%[firstName]@ %[contains]@ %@" : @"%1$[Vorname]@ %2$[enthält]@ %3$@",
@"%[lastName]@ %[is equal to]@ %@" : @"%1$[Nachname]@ und %3$@ %2$[sind gleich]@",
@"%[lastName]@ %[contains]@ %@" : @"%1$[Nachname]@ %2$[enthält]@ %3$@",
@"%[age]@ %[is equal to]@ %@" : @"%1$[Alter]@ und %3$@ %2$[sind gleich]@",
@"%[age]@ is equal to %@" : @"%1$[Alter]@ und %3$@ %2$[sind gleich]@",
@"Add row" : @"Regel hinzufügen",
@"Delete row" : @"Regel löschen",
@"Add compound row" : @"Regelgruppe hinzufügen"
}];
// Initialize with Spanish by default
[[ruleEditor standardLocalizer] setDictionary:spanishDict];
[contentView addSubview:ruleEditor];
// Populate initial rows
[ruleEditor addRow:self];
[ruleEditor addRow:self];
[ruleEditor addRow:self];
// Display Output Label
var predLabel = [CPTextField labelWithTitle:@"Evaluated Predicate:"];
[predLabel setFrame:CGRectMake(20, 320, 760, 20)];
[predLabel setFont:[CPFont boldSystemFontOfSize:12]];
[contentView addSubview:predLabel];
// Predicate string value display
predicateField = [[CPTextField alloc] initWithFrame:CGRectMake(20, 345, 760, 36)];
[predicateField setAutoresizingMask:CPViewWidthSizable];
[predicateField setBezeled:YES];
[predicateField setEditable:NO];
[predicateField setStringValue:@""];
[predicateField setFont:[CPFont systemFontOfSize:13]];
[contentView addSubview:predicateField];
[[CPNotificationCenter defaultCenter] addObserver:self
selector:@selector(ruleEditorRowsDidChange:)
name:CPRuleEditorRowsDidChangeNotification
object:ruleEditor];
[theWindow orderFront:self];
[self ruleEditorAction:nil];
}
- (void)changeLanguage:(id)sender
{
var selectedTitle = [sender titleOfSelectedItem],
targetDict = englishDict;
if ([selectedTitle isEqualToString:@"Spanish"])
{
targetDict = spanishDict;
}
else if ([selectedTitle isEqualToString:@"German"])
{
targetDict = germanDict;
}
[[ruleEditor standardLocalizer] setDictionary:targetDict];
// Post notification to trigger localized redraw across editor slices
[[CPNotificationCenter defaultCenter] postNotificationName:@"_CPRuleEditorLocalizerDidLoadNotification" object:[ruleEditor standardLocalizer]];
}
- (void)ruleEditorAction:(id)sender
{
var predicate = [ruleEditor predicate];
if (predicate)
{
[predicateField setStringValue:[predicate predicateFormat]];
}
else
{
[predicateField setStringValue:@"(No predicate evaluated)"];
}
}
- (void)ruleEditorRowsDidChange:(CPNotification)note
{
var predicate = [ruleEditor predicate];
if (predicate)
{
[predicateField setStringValue:[predicate predicateFormat]];
}
else
{
[predicateField setStringValue:@"(Incomplete Predicate)"];
}
}
@end
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CPApplicationDelegateClass</key>
<string>AppController</string>
<key>CPBundleName</key>
<string>CPTextViewTest</string>
<key>CPPrincipalClass</key>
<string>CPApplication</string>
</dict>
</plist>
+94
View File
@@ -0,0 +1,94 @@
/*
* Jakefile
* CPRuleEditorCibTest
*
* Created by You on September 3, 2010.
* Copyright 2010, Your Company All rights reserved.
*/
var ENV = require("system").env,
FILE = require("file"),
JAKE = require("jake"),
task = JAKE.task,
FileList = JAKE.FileList,
app = require("cappuccino/jake").app,
configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug",
OS = require("os");
app ("CPRuleEditorCibTest", function(task)
{
task.setBuildIntermediatesPath(FILE.join("Build", "CPRuleEditorCibTest.build", configuration));
task.setBuildPath(FILE.join("Build", configuration));
task.setProductName("CPRuleEditorCibTest");
task.setIdentifier("com.yourcompany.CPRuleEditorCibTest");
task.setVersion("1.0");
task.setAuthor("Your Company");
task.setEmail("feedback @nospam@ yourcompany.com");
task.setSummary("CPRuleEditorCibTest");
task.setSources((new FileList("**/*.j")).exclude(FILE.join("Build", "**")));
task.setResources(new FileList("Resources/**"));
task.setIndexFilePath("index.html");
task.setInfoPlistPath("Info.plist");
task.setNib2CibFlags("-R Resources/");
if (configuration === "Debug")
task.setCompilerFlags("-DDEBUG -g");
else
task.setCompilerFlags("-O");
});
task ("default", ["CPRuleEditorCibTest"], function()
{
printResults(configuration);
});
task ("build", ["default"]);
task ("debug", function()
{
ENV["CONFIGURATION"] = "Debug";
JAKE.subjake(["."], "build", ENV);
});
task ("release", function()
{
ENV["CONFIGURATION"] = "Release";
JAKE.subjake(["."], "build", ENV);
});
task ("run", ["debug"], function()
{
OS.system(["open", FILE.join("Build", "Debug", "CPRuleEditorCibTest", "index.html")]);
});
task ("run-release", ["release"], function()
{
OS.system(["open", FILE.join("Build", "Release", "CPRuleEditorCibTest", "index.html")]);
});
task ("deploy", ["release"], function()
{
FILE.mkdirs(FILE.join("Build", "Deployment", "CPRuleEditorCibTest"));
OS.system(["press", "-f", FILE.join("Build", "Release", "CPRuleEditorCibTest"), FILE.join("Build", "Deployment", "CPRuleEditorCibTest")]);
printResults("Deployment")
});
task ("desktop", ["release"], function()
{
FILE.mkdirs(FILE.join("Build", "Desktop", "CPRuleEditorCibTest"));
require("cappuccino/nativehost").buildNativeHost(FILE.join("Build", "Release", "CPRuleEditorCibTest"), FILE.join("Build", "Desktop", "CPRuleEditorCibTest", "CPRuleEditorCibTest.app"));
printResults("Desktop")
});
task ("run-desktop", ["desktop"], function()
{
OS.system([FILE.join("Build", "Desktop", "CPRuleEditorCibTest", "CPRuleEditorCibTest.app", "Contents", "MacOS", "NativeHost"), "-i"]);
});
function printResults(configuration)
{
print("----------------------------");
print(configuration+" app built at path: "+FILE.join("Build", configuration, "CPRuleEditorCibTest"));
print("----------------------------");
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,135 @@
@import <Foundation/CPObject.j>
@import <AppKit/CPRuleEditor.j>
@import <AppKit/CPTextField.j>
// Ensure the standard operator constants are explicitly defined
var CPEqualToPredicateOperatorType = 4,
CPContainsPredicateOperatorType = 99;
@implementation RuleDelegate : CPObject
{
}
// 1. Root criteria and children
- (id)ruleEditor:(CPRuleEditor)editor child:(CPInteger)index forCriterion:(id)criterion withRowType:(CPRuleEditorRowType)rowType
{
if (criterion == nil)
{
return [@[@"firstName", @"lastName", @"age"] objectAtIndex:index];
}
if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"])
{
return [@[@"is equal to", @"contains"] objectAtIndex:index];
}
if ([criterion isEqualToString:@"age"])
{
return [@[@"is equal to"] objectAtIndex:index];
}
if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"])
{
return @"value";
}
return nil;
}
// 2. Number of children
- (CPInteger)ruleEditor:(CPRuleEditor)editor numberOfChildrenForCriterion:(id)criterion withRowType:(CPRuleEditorRowType)rowType
{
if (criterion == nil)
{
return 3;
}
if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"])
{
return 2;
}
if ([criterion isEqualToString:@"age"])
{
return 1;
}
if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"])
{
return 1;
}
return 0;
}
// 3. Display values
- (id)ruleEditor:(CPRuleEditor)editor displayValueForCriterion:(id)criterion inRow:(CPInteger)row
{
if ([criterion isEqualToString:@"firstName"]) return @"firstName";
if ([criterion isEqualToString:@"lastName"]) return @"lastName";
if ([criterion isEqualToString:@"age"]) return @"age";
if ([criterion isEqualToString:@"contains"]) return @"contains";
if ([criterion isEqualToString:@"is equal to"]) return @"is equal to";
if ([criterion isEqualToString:@"value"])
{
var textField = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 120, 24)];
[textField setBezeled:YES];
[textField setBezelStyle:CPTextFieldSquareBezel];
[textField setEditable:YES];
[textField setStringValue:@""];
return textField;
}
return nil;
}
// 4. Predicate parts
- (CPDictionary)ruleEditor:(CPRuleEditor)editor predicatePartsForCriterion:(id)criterion withDisplayValue:(id)value inRow:(CPInteger)row
{
var parts = @{};
if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"] || [criterion isEqualToString:@"age"])
{
[parts setObject:[CPExpression expressionForKeyPath:criterion] forKey:CPRuleEditorPredicateLeftExpression];
}
else if ([criterion isEqualToString:@"contains"])
{
[parts setObject:[CPNumber numberWithUnsignedInt:CPContainsPredicateOperatorType] forKey:CPRuleEditorPredicateOperatorType];
}
else if ([criterion isEqualToString:@"is equal to"])
{
[parts setObject:[CPNumber numberWithUnsignedInt:CPEqualToPredicateOperatorType] forKey:CPRuleEditorPredicateOperatorType];
}
else if ([criterion isEqualToString:@"value"])
{
var activeValue = value;
var slices = [editor valueForKey:@"_slices"];
if (slices && row < [slices count])
{
var slice = [slices objectAtIndex:row];
var optionViews = [slice valueForKey:@"_ruleOptionViews"];
if (optionViews)
{
var count = [optionViews count];
for (var i = 0; i < count; i++)
{
var view = [optionViews objectAtIndex:i];
if ([view isKindOfClass:[CPTextField class]] && [view isEditable])
{
activeValue = view;
break;
}
}
}
}
[parts setObject:[CPExpression expressionForConstantValue:[activeValue stringValue]] forKey:CPRuleEditorPredicateRightExpression];
}
return parts;
}
@end
@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="en">
<!--
index-debug.html
__project.name__
Created by __user.name__ on __project.date__.
Copyright __project.year__, __organization.name__ All rights reserved.
-->
<head>
<meta charset="utf-8">
<!--[if lte IE 8]>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
<![endif]-->
<!--[if gte IE 9]>
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
<![endif]-->
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="Resources/icon.png">
<link rel="apple-touch-startup-image" href="Resources/default.png">
<title>__project.name__</title>
<!-- Custom javascript goes here -->
<!-- End custom javascript -->
<script type="text/javascript">
OBJJ_MAIN_FILE = "main.j";
OBJJ_INCLUDE_PATHS = ["Frameworks/Debug", "Frameworks"];
// The below will tell the compiler to generate debug symbols, type signatures and not inline objj_msgSend functions.
// This will affect only Objective-J code that is compiled when loading the application. It will not affect precompiled
// code like the Cappuccino frameworks.
//
// Debug symbols will give each Objective-J method a Javascript function name. Without a name it will be very hard to find
// the methods in the debugger.
// Type Signatures will give type information to the Objective-J runtime for instance variables and methods.
// Inline objj_msgSend will give a speed increase but decorators will not work. Check below on debug options for
// more information on decorators.
//
// Uncomment or comment on the line below to change the flags
OBJJ_COMPILER_FLAGS = ["IncludeDebugSymbols", "IncludeTypeSignatures", "SourceMap", "InlineMsgSend"];
var progressBar = null;
OBJJ_PROGRESS_CALLBACK = function(percent, appSize, path)
{
percent = percent * 100;
if (!progressBar)
progressBar = document.getElementById("progress-bar");
if (progressBar)
progressBar.style.width = Math.min(percent, 100) + "%";
}
var loadingHTML =
'<div id="loading">' +
' <div id="loading-text">Loading...</div>' +
' <div id="progress-indicator">' +
' <span id="progress-bar" style="width:0%"></span>' +
' </div>' +
'</div>';
</script>
<script type="text/javascript" src="Frameworks/Debug/Objective-J/Objective-J.js" charset="UTF-8"></script>
<script type="text/javascript">
objj_msgSend_reset();
// DEBUG OPTIONS:
// Decorators will only work when the code is compiled without the compiler option 'InlineMsgSend'. Check above.
// Uncomment to enable printing of backtraces on exceptions:
//objj_msgSend_decorate(objj_backtrace_decorator);
// Uncomment to supress exceptions that take place inside a message
//objj_msgSend_decorate(objj_supress_exceptions_decorator)
// Uncomment to enable runtime type checking:
//objj_msgSend_decorate(objj_typecheck_decorator);
// Uncomment (along with both above) to print backtraces on type check errors:
//objj_typecheck_prints_backtrace = true;
// Uncomment to disable the default logger (CPLogConsole if window.console exists, CPLogPopup otherwise):
//CPLogUnregister(CPLogDefault);
// Uncomment to enable a specific logger:
//CPLogRegister(CPLogConsole);
//CPLogRegister(CPLogPopup);
// Tag view DOM elements with a "data-cappuccino-view" attribute that contains
// the class name of the view that created them. Comment this or set to false to disable.
appkit_tag_dom_elements = true;
</script>
<style type="text/css">
html, body, h1, p {
margin: 0;
padding: 0;
}
/* We need a body wrapper because Cappuccino is unhappy if we change the body element */
#cappuccino-body {
/* Position it absolutely so it will fill the height without content */
position: absolute;
top: 0;
bottom: 0;
width: 100%;
/* Put it at the bottom of the stack so it doesn't interfere with UI */
z-index: 0;
}
#cappuccino-body .container {
display: table;
margin: 0 auto;
height: 100%;
}
#cappuccino-body .content {
display: table-cell;
height: 100%;
vertical-align: top;
}
#loading {
position: relative;
top: 35%;
}
#loading-text {
height: 1.5em;
color: #555;
font: normal bold 36px/36px Arial, sans-serif;
}
#progress-indicator {
padding: 0px;
height: 16px;
border: 5px solid #555;
border-radius: 18px;
background-color: white;
}
#progress-bar {
position: relative;
top: -1px;
left: -1px;
display: block;
height: 18px;
/* Compensate for moving the bar left 1px to overlap the indicator border */
border-right: 1px solid #555;
background-color: #555;
}
#noscript {
position: relative;
top: 35%;
padding: 1em 1.5em;
border: 5px solid #555;
border-radius: 16px;
background-color: white;
color: #555;
text-align: center;
font: bold 24px Arial, sans-serif;
}
#noscript a {
color: #98c0ff;
text-decoration: none;
}
</style>
</head>
<body>
<div id="cappuccino-body">
<div class="container">
<div class="content">
<script type="text/javascript">
document.write(loadingHTML);
</script>
</div>
</div>
<noscript style="position:absolute; top:0; left:0; width:100%; height:100%">
<div class="container">
<div class="content">
<div id="noscript">
<p style="font-size:120%; margin-bottom:.75em">JavaScript is required for this application.</p>
<p><a href="http://www.enable-javascript.com" target="_blank">Enable JavaScript</a></p>
</div>
</div>
</div>
</noscript>
</div>
</body>
</html>
+166
View File
@@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="en">
<!--
index.html
__project.name__
Created by __user.name__ on __project.date__.
Copyright __project.year__, __organization.name__ All rights reserved.
-->
<head>
<meta charset="utf-8">
<!--[if lte IE 8]>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7, chrome=1">
<![endif]-->
<!--[if gte IE 9]>
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
<![endif]-->
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="Resources/icon.png">
<link rel="apple-touch-startup-image" href="Resources/default.png">
<title>__project.name__</title>
<!-- Custom javascript goes here -->
<!-- End custom javascript -->
<script type="text/javascript">
OBJJ_MAIN_FILE = "main.j";
// The below will tell the compiler to not generate debug symbols but will generate type signatures and inline objj_msgSend functions.
// This will affect only Objective-J code that is compiled when loading the application. It will not affect precompiled
// code like the Cappuccino frameworks.
// Uncomment or comment on the line below to change the flags
OBJJ_COMPILER_FLAGS = [/*"IncludeDebugSymbols"*/, "IncludeTypeSignatures"/*, "SourceMap"*/, "InlineMsgSend"];
var progressBar = null;
OBJJ_PROGRESS_CALLBACK = function(percent, appSize, path)
{
percent = percent * 100;
if (!progressBar)
progressBar = document.getElementById("progress-bar");
if (progressBar)
progressBar.style.width = Math.min(percent, 100) + "%";
}
var loadingHTML =
'<div id="loading">' +
' <div id="loading-text">Loading...</div>' +
' <div id="progress-indicator">' +
' <span id="progress-bar" style="width:0%"></span>' +
' </div>' +
'</div>';
</script>
<script type="text/javascript" src="Frameworks/Objective-J/Objective-J.js" charset="UTF-8"></script>
<style type="text/css">
html, body, h1, p {
margin: 0;
padding: 0;
}
/* We need a body wrapper because Cappuccino is unhappy if we change the body element */
#cappuccino-body {
/* Position it absolutely so it will fill the height without content */
position: absolute;
top: 0;
bottom: 0;
width: 100%;
/* Put it at the bottom of the stack so it doesn't interfere with UI */
z-index: 0;
}
#cappuccino-body .container {
display: table;
margin: 0 auto;
height: 100%;
}
#cappuccino-body .content {
display: table-cell;
height: 100%;
vertical-align: top;
}
#loading {
position: relative;
top: 35%;
}
#loading-text {
height: 1.5em;
color: #555;
font: normal bold 36px/36px Arial, sans-serif;
}
#progress-indicator {
padding: 0px;
height: 16px;
border: 5px solid #555;
border-radius: 18px;
background-color: white;
}
#progress-bar {
position: relative;
top: -1px;
left: -1px;
display: block;
height: 18px;
/* Compensate for moving the bar left 1px to overlap the indicator border */
border-right: 1px solid #555;
background-color: #555;
}
#noscript {
position: relative;
top: 35%;
padding: 1em 1.5em;
border: 5px solid #555;
border-radius: 16px;
background-color: white;
color: #555;
text-align: center;
font: bold 24px Arial, sans-serif;
}
#noscript a {
color: #98c0ff;
text-decoration: none;
}
</style>
</head>
<body>
<div id="cappuccino-body">
<div class="container">
<div class="content">
<script type="text/javascript">
document.write(loadingHTML);
</script>
</div>
</div>
<noscript style="position:absolute; top:0; left:0; width:100%; height:100%">
<div class="container">
<div class="content">
<div id="noscript">
<p style="font-size:120%; margin-bottom:.75em">JavaScript is required for this application.</p>
<p><a href="http://www.enable-javascript.com" target="_blank">Enable JavaScript</a></p>
</div>
</div>
</div>
</noscript>
</div>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
/*
* AppController.j
* CPRuleEditorCibTest
*
* Created by You on September 3, 2010.
* Copyright 2010, Your Company All rights reserved.
*/
@import <Foundation/Foundation.j>
@import <AppKit/AppKit.j>
@import "AppController.j"
function main(args, namedArgs)
{
CPApplicationMain(args, namedArgs);
}
+286 -6
View File
@@ -2,18 +2,24 @@
* AppController.j
*
* Manual test application for the cappuccino text system
* Copyright (C) 2014 Daniel Boehringer
* Copyright (C) 2026 Daniel Boehringer
*/
@import <Foundation/Foundation.j>
@import <AppKit/CPTextView.j>
@import <AppKit/CPFontPanel.j>
@import <AppKit/CPColorPanel.j>
@import <AppKit/CPRulerView.j>
@import <AppKit/CPSplitView.j>
@import <AppKit/CPButton.j>
@import <AppKit/CPTextField.j>
@import <AppKit/CPScrollView.j>
@import <AppKit/CPParagraphStyle.j>
@import <AppKit/CPTextStorage.j>
@import <AppKit/_CPTableTextAttachment.j>
@global CPBaselineOffsetAttributeName
@global CPSuperscriptAttributeName
@implementation AppController : CPObject
{
@@ -50,6 +56,11 @@
[[CPFontManager sharedFontManager] orderFrontFontPanel:self];
}
- (void)orderFrontColorPanel:(id)sender
{
[[CPColorPanel sharedColorPanel] orderFront:self];
}
- (void)toggleRuler:(id)sender
{
[_scrollView setRulersVisible:![_scrollView rulersVisible]];
@@ -75,6 +86,82 @@
[_textView alignJustified:self];
}
- (void)makeSuperscript:(id)sender
{
var range = [_textView selectedRange];
if (range.length > 0)
{
var textStorage = [_textView textStorage];
[textStorage beginEditing];
[textStorage removeAttribute:CPBaselineOffsetAttributeName range:range];
[textStorage addAttribute:CPSuperscriptAttributeName value:1 range:range];
[textStorage endEditing];
[_textView setNeedsDisplay:YES];
}
}
- (void)makeSubscript:(id)sender
{
var range = [_textView selectedRange];
if (range.length > 0)
{
var textStorage = [_textView textStorage];
[textStorage beginEditing];
[textStorage removeAttribute:CPBaselineOffsetAttributeName range:range];
[textStorage addAttribute:CPSuperscriptAttributeName value:-1 range:range];
[textStorage endEditing];
[_textView setNeedsDisplay:YES];
}
}
- (void)raiseBaseline:(id)sender
{
var range = [_textView selectedRange];
if (range.length > 0)
{
var textStorage = [_textView textStorage];
[textStorage beginEditing];
var currentOffset = [textStorage attribute:CPBaselineOffsetAttributeName atIndex:range.location effectiveRange:nil] || 0.0;
var newOffset = currentOffset + 2.0;
[textStorage addAttribute:CPBaselineOffsetAttributeName value:newOffset range:range];
[textStorage endEditing];
[_textView setNeedsDisplay:YES];
}
}
- (void)lowerBaseline:(id)sender
{
var range = [_textView selectedRange];
if (range.length > 0)
{
var textStorage = [_textView textStorage];
[textStorage beginEditing];
var currentOffset = [textStorage attribute:CPBaselineOffsetAttributeName atIndex:range.location effectiveRange:nil] || 0.0;
var newOffset = currentOffset - 2.0;
[textStorage addAttribute:CPBaselineOffsetAttributeName value:newOffset range:range];
[textStorage endEditing];
[_textView setNeedsDisplay:YES];
}
}
- (void)resetBaseline:(id)sender
{
var range = [_textView selectedRange];
if (range.length > 0)
{
var textStorage = [_textView textStorage];
[textStorage beginEditing];
[textStorage removeAttribute:CPBaselineOffsetAttributeName range:range];
[textStorage removeAttribute:CPSuperscriptAttributeName range:range];
[textStorage endEditing];
[_textView setNeedsDisplay:YES];
}
}
- (void)insertAttachment:(id)sender
{
// Insert modern spinner image attachment
@@ -90,6 +177,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 +239,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 +259,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:"];
@@ -178,6 +300,48 @@
[alignJustifyBtn setTarget:self];
[alignJustifyBtn setAction:@selector(alignJustified:)];
[toolbarView addSubview:alignJustifyBtn];
currentX += 80;
// Baseline & Script Testing Group
var labelBaseline = [[CPTextField alloc] initWithFrame:CGRectMake(currentX, 22, 85, 20)];
[labelBaseline setStringValue:@"Baseline:"];
[labelBaseline setFont:[CPFont systemFontOfSize:12]];
[toolbarView addSubview:labelBaseline];
currentX += 85;
var superBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 45, 30)];
[superBtn setTitle:@"x²"];
[superBtn setTarget:self];
[superBtn setAction:@selector(makeSuperscript:)];
[toolbarView addSubview:superBtn];
currentX += 50;
var subBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 45, 30)];
[subBtn setTitle:@"x₂"];
[subBtn setTarget:self];
[subBtn setAction:@selector(makeSubscript:)];
[toolbarView addSubview:subBtn];
currentX += 50;
var raiseBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 50, 30)];
[raiseBtn setTitle:@"Base+"];
[raiseBtn setTarget:self];
[raiseBtn setAction:@selector(raiseBaseline:)];
[toolbarView addSubview:raiseBtn];
currentX += 55;
var lowerBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 50, 30)];
[lowerBtn setTitle:@"Base-"];
[lowerBtn setTarget:self];
[lowerBtn setAction:@selector(lowerBaseline:)];
[toolbarView addSubview:lowerBtn];
currentX += 55;
var normalBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 60, 30)];
[normalBtn setTitle:@"Normal"];
[normalBtn setTarget:self];
[normalBtn setAction:@selector(resetBaseline:)];
[toolbarView addSubview:normalBtn];
// Default return key target test
var returnButton = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth([contentView bounds]) - 270, 15, 250, 30)];
@@ -210,9 +374,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 +390,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];
@@ -251,24 +416,34 @@
[editMenu addItemWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"];
[editMenu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"];
[editMenu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"];
var pasteAsPlainItem = [editMenu addItemWithTitle:@"Paste as Plain Text" action:@selector(pasteAsPlainText:) keyEquivalent:@"v"];
[pasteAsPlainItem setKeyEquivalentModifierMask:CPCommandKeyMask | CPAlternateKeyMask | CPShiftKeyMask];
[editMenu addItemWithTitle:@"Delete" action:@selector(delete:) keyEquivalent:@""];
[editMenu addItemWithTitle:@"Select All" action:@selector(selectAll:) keyEquivalent:@"a"];
[editMenu addItemWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"];
[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"];
[formatMenu addItemWithTitle:@"Font panel" action:@selector(orderFrontFontPanel:) keyEquivalent:@"t"];
[formatMenu addItemWithTitle:@"Color panel" action:@selector(orderFrontColorPanel:) keyEquivalent:@"C"];
[formatMenu addItem:[CPMenuItem separatorItem]];
// Styles
[formatMenu addItemWithTitle:@"Bold" action:@selector(bold:) keyEquivalent:@"b"];
[formatMenu addItemWithTitle:@"Italic" action:@selector(italic:) keyEquivalent:@"i"];
[formatMenu addItemWithTitle:@"Underline" action:@selector(underline:) keyEquivalent:@"u"];
[formatMenu addItem:[CPMenuItem separatorItem]];
// Baseline & Scripts
[formatMenu addItemWithTitle:@"Superscript" action:@selector(makeSuperscript:) keyEquivalent:@"="];
[formatMenu addItemWithTitle:@"Subscript" action:@selector(makeSubscript:) keyEquivalent:@"-"];
[formatMenu addItemWithTitle:@"Raise Baseline" action:@selector(raiseBaseline:) keyEquivalent:@"+"];
[formatMenu addItemWithTitle:@"Lower Baseline" action:@selector(lowerBaseline:) keyEquivalent:@"_"];
[formatMenu addItemWithTitle:@"Reset Baseline" action:@selector(resetBaseline:) keyEquivalent:@"0"];
[formatMenu addItem:[CPMenuItem separatorItem]];
// Alignment
[formatMenu addItemWithTitle:@"Align Left" action:@selector(alignLeft:) keyEquivalent:@"{"];
[formatMenu addItemWithTitle:@"Center" action:@selector(alignCenter:) keyEquivalent:@"|"];
@@ -291,6 +466,51 @@
attributes:[CPDictionary dictionaryWithObjects:[centeredParagraph, [CPFont boldFontWithName:@"Arial" size:18], elegantForeground, elegantBackground]
forKeys:[CPParagraphStyleAttributeName, CPFontAttributeName, CPForegroundColorAttributeName, CPBackgroundColorAttributeName]]]];
// VISUAL TEST CASES: Baseline, Superscript, and Subscript Features
[_textView insertText:@"\n"];
var sectionHeaderColor = [CPColor colorWithRed:0.5 green:0.25 blue:0.1 alpha:1.0];
[_textView insertText:[[CPAttributedString alloc] initWithString:@"Baseline Shift & Superscript/Subscript Showcase\n"
attributes:[CPDictionary dictionaryWithObjects:[[CPFont boldFontWithName:@"Arial" size:16], sectionHeaderColor]
forKeys:[CPFontAttributeName, CPForegroundColorAttributeName]]]];
var normalFont = [CPFont systemFontOfSize:14.0];
// Test Case: E = mc² (Superscript)
var formulaEnergy = [[CPAttributedString alloc] initWithString:@" • Energy mass equivalence: E = mc" attributes:@{ CPFontAttributeName: normalFont }];
var scriptTwo = [[CPAttributedString alloc] initWithString:@"2" attributes:@{ CPFontAttributeName: normalFont, CPSuperscriptAttributeName: 1 }];
[_textView insertText:formulaEnergy];
[_textView insertText:scriptTwo];
[_textView insertText:@"\n"];
// Test Case: H₂O (Subscript)
var formulaWater = [[CPAttributedString alloc] initWithString:@" • Chemical formula: H" attributes:@{ CPFontAttributeName: normalFont }];
var scriptSubTwo = [[CPAttributedString alloc] initWithString:@"2" attributes:@{ CPFontAttributeName: normalFont, CPSuperscriptAttributeName: -1 }];
var formulaWaterEnd = [[CPAttributedString alloc] initWithString:@"O\n" attributes:@{ CPFontAttributeName: normalFont }];
[_textView insertText:formulaWater];
[_textView insertText:scriptSubTwo];
[_textView insertText:formulaWaterEnd];
// Test Case: Ordinals
var ordText = [[CPAttributedString alloc] initWithString:@" • Ordinals: 1" attributes:@{ CPFontAttributeName: normalFont }];
var st = [[CPAttributedString alloc] initWithString:@"st" attributes:@{ CPFontAttributeName: normalFont, CPSuperscriptAttributeName: 1 }];
var rdText = [[CPAttributedString alloc] initWithString:@", 3" attributes:@{ CPFontAttributeName: normalFont }];
var rd = [[CPAttributedString alloc] initWithString:@"rd" attributes:@{ CPFontAttributeName: normalFont, CPSuperscriptAttributeName: 1 }];
[_textView insertText:ordText];
[_textView insertText:st];
[_textView insertText:rdText];
[_textView insertText:rd];
[_textView insertText:@"\n"];
// Test Case: Custom Baseline Offsets
var offsetLead = [[CPAttributedString alloc] initWithString:@" • Custom Offsets: " attributes:@{ CPFontAttributeName: normalFont }];
var offsetUp = [[CPAttributedString alloc] initWithString:@"Raised " attributes:@{ CPFontAttributeName: normalFont, CPBaselineOffsetAttributeName: 4.0 }];
var offsetDown = [[CPAttributedString alloc] initWithString:@"Lowered " attributes:@{ CPFontAttributeName: normalFont, CPBaselineOffsetAttributeName: -4.0 }];
var offsetNormal = [[CPAttributedString alloc] initWithString:@"Standard\n" attributes:@{ CPFontAttributeName: normalFont }];
[_textView insertText:offsetLead];
[_textView insertText:offsetUp];
[_textView insertText:offsetDown];
[_textView insertText:offsetNormal];
// Highlighted Heading - Pine & Sage Green tones
[_textView insertText:@"\n"];
var showcaseForeground = [CPColor colorWithRed:0.15 green:0.25 blue:0.15 alpha:1.0]; // Forest Green
@@ -321,6 +541,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 +569,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