mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-09-18 16:33:31 +00:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -30,6 +30,7 @@ CPStandardKeyBindings = {
|
||||
@"@.": @"cancelOperation:",
|
||||
|
||||
@"@a": @"selectAll:",
|
||||
@"@~$v": @"pasteAsPlainText:",
|
||||
@"^a": @"moveToBeginningOfParagraph:",
|
||||
@"^$a": @"moveToBeginningOfParagraphAndModifySelection:",
|
||||
@"^b": @"moveBackward:",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user