From a27aaf8bd0b55a22bcc6727e71f1fbd3c18ab5a9 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 28 May 2026 17:11:29 +0200 Subject: [PATCH 01/46] Fixed: CPRuleEditor word-by-word translation issue (#1834) --- AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j | 170 +++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j index 12bbbe514..a0fc8d264 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j @@ -94,4 +94,174 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) return aString; } +#pragma mark - Whole Sentence Formatting & Reordering Helpers + +// Constructs the English-matching format representation of a single view +- (CPString)_englishRepresentationForView:(id)aView +{ + if ([aView isKindOfClass:[CPPopUpButton class]]) + { + var selectedItem = [aView selectedItem]; + if (selectedItem) + { + var originalTitle = [selectedItem representedObject] || selectedItem._originalTitle || [selectedItem title]; + return "%[" + originalTitle + "]@"; + } + return "%[]@"; + } + else if ([aView isKindOfClass:[CPTextField class]] && ![aView isEditable]) + { + return [aView stringValue]; + } + else + { + return "%@"; + } +} + +// Builds the current formatting lookup key (e.g. "%[property]@ %[is]@ %@") +- (CPString)formattingKeyForViews:(NSArray *)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:@" "]; +} + +// Localizes all popup menu options within the row under their proper context +- (void)localizeMenuItemsForViews:(NSArray *)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) + { + item._originalTitle = [item title]; + } + + // Temporarily select item to formulate the unique localization key + [view selectItem:item]; + + var tempKey = [self formattingKeyForViews:views]; + var tempPattern = [self localizedStringForString:tempKey]; + + if (tempPattern !== tempKey) + { + // Search for this view position's translation inside the pattern (e.g. %2$[son iguales]@) + 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; + } + } + } + } + + // Restore initial selection + if (selectedItem) + { + [view selectItem:selectedItem]; + } + } + } +} + +// Translates labels and reorders the active subviews based on positional formatting string +- (NSArray *)localizeAndReorderViews:(NSArray *)views +{ + var key = [self formattingKeyForViews:views]; + var localizedPattern = [self localizedStringForString:key]; + + if (localizedPattern === key) + { + return views; + } + + var newViews = [CPMutableArray array]; + + // Pattern to extract index and translation: %1$[translation]@ or %1$@ + var regex = /%(\d+)\$(?:\[([^\]]+)\])?@/g; + var lastIndex = 0; + var match; + + while ((match = regex.exec(localizedPattern)) !== null) + { + // 1. Insert any leading static text (e.g. " y ") + var literalText = localizedPattern.substring(lastIndex, match.index); + if (literalText.length > 0) + { + var label = [CPTextField labelWithString:literalText]; + [newViews addObject:label]; + } + + // 2. Identify the original view corresponding to the positional index + 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 (typeof originalView.setStringValue === "function") + { + [originalView setStringValue:translatedValue]; + } + } + + [newViews addObject:originalView]; + } + + lastIndex = regex.lastIndex; + } + + // 3. Append trailing static text + if (lastIndex < localizedPattern.length) + { + var literalText = localizedPattern.substring(lastIndex); + if (literalText.length > 0) + { + var label = [CPTextField labelWithString:literalText]; + [newViews addObject:label]; + } + } + + return newViews; +} + @end From d7315773e1c1c0472325301df8f17cc1bdd02272 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 28 May 2026 17:15:41 +0200 Subject: [PATCH 02/46] fixed: syntax issues --- AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j index a0fc8d264..db08bd739 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j @@ -120,7 +120,7 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } // Builds the current formatting lookup key (e.g. "%[property]@ %[is]@ %@") -- (CPString)formattingKeyForViews:(NSArray *)views +- (CPString)formattingKeyForViews:(CPArray)views { var keyParts = []; var count = [views count]; @@ -133,7 +133,7 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } // Localizes all popup menu options within the row under their proper context -- (void)localizeMenuItemsForViews:(NSArray *)views +- (void)localizeMenuItemsForViews:(CPArray)views { var count = [views count]; for (var i = 0; i < count; i++) @@ -189,7 +189,7 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } // Translates labels and reorders the active subviews based on positional formatting string -- (NSArray *)localizeAndReorderViews:(NSArray *)views +- (CPArray)localizeAndReorderViews:(CPArray)views { var key = [self formattingKeyForViews:views]; var localizedPattern = [self localizedStringForString:key]; From e856605a0991a6156c9c821765c89fcf10fcbe4d Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 28 May 2026 17:38:14 +0200 Subject: [PATCH 03/46] _CPRuleEditorViewSliceRow integration --- AppKit/CPRuleEditor/CPRuleEditor.j | 26 +++++++++++++++---- .../CPRuleEditor/_CPRuleEditorViewSliceRow.j | 22 ++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j index 1910d6153..b1f30dc30 100644 --- a/AppKit/CPRuleEditor/CPRuleEditor.j +++ b/AppKit/CPRuleEditor/CPRuleEditor.j @@ -127,7 +127,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", BOOL _isKeyDown; BOOL _nestingModeDidChange; - _CPRuleEditorLocalizer _standardLocalizer @accessors(property=standardLocalizer); + _CPRuleEditorLocalizer _standardLocalizer; CPDictionary _itemsAndValuesToAddForRowType; } @@ -384,7 +384,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", */ - (CPDictionary)formattingDictionary { - return [_standardLocalizer dictionary]; + return [[self standardLocalizer] dictionary]; } /*! @@ -396,6 +396,9 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", */ - (void)setFormattingDictionary:(CPDictionary)dictionary { + if (_standardLocalizer == nil) + _standardLocalizer = [_CPRuleEditorLocalizer new]; + [_standardLocalizer setDictionary:dictionary]; _stringsFilename = nil; } @@ -440,6 +443,19 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", } } +- (_CPRuleEditorLocalizer)standardLocalizer +{ + if (_standardLocalizer == nil) + _standardLocalizer = [_CPRuleEditorLocalizer new]; + + return _standardLocalizer; +} + +- (void)setStandardLocalizer:(_CPRuleEditorLocalizer)aLocalizer +{ + _standardLocalizer = aLocalizer; +} + /*! @name Providing Data */ @@ -1924,17 +1940,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 diff --git a/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j b/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j index 8866095d4..181f935ba 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j @@ -334,6 +334,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]; From 5e0fc1cbb549daf81c7fce461cd69a096a0938b3 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 2 Jun 2026 22:20:44 +0200 Subject: [PATCH 04/46] fixes + manual test --- AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j | 51 +++-- .../CPRuleEditor/_CPRuleEditorViewSliceRow.j | 44 ++-- .../CPRuleEditorTestSpanish/AppController.j | 92 ++++++++ .../Manual/CPRuleEditorTestSpanish/Info.plist | 12 ++ Tests/Manual/CPRuleEditorTestSpanish/Jakefile | 94 ++++++++ .../Resources/Spanish.strings | 13 ++ .../Resources/spinner.gif | Bin 0 -> 1434 bytes .../CPRuleEditorTestSpanish/RuleDelegate.j | 113 ++++++++++ .../CPRuleEditorTestSpanish/index-debug.html | 204 ++++++++++++++++++ .../Manual/CPRuleEditorTestSpanish/index.html | 166 ++++++++++++++ Tests/Manual/CPRuleEditorTestSpanish/main.j | 18 ++ 11 files changed, 771 insertions(+), 36 deletions(-) create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/AppController.j create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/Info.plist create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/Jakefile create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/Resources/spinner.gif create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/index-debug.html create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/index.html create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/main.j diff --git a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j index db08bd739..f9dddbfb9 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j @@ -94,9 +94,8 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) return aString; } -#pragma mark - Whole Sentence Formatting & Reordering Helpers +#pragma mark - Formatting & Reordering Helpers -// Constructs the English-matching format representation of a single view - (CPString)_englishRepresentationForView:(id)aView { if ([aView isKindOfClass:[CPPopUpButton class]]) @@ -104,14 +103,32 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) var selectedItem = [aView selectedItem]; if (selectedItem) { - var originalTitle = [selectedItem representedObject] || selectedItem._originalTitle || [selectedItem title]; + var originalTitle = selectedItem._originalTitle; + + // Fallback: If not cached directly, inspect representedObject payload dictionary + if (!originalTitle) + { + var rep = [selectedItem representedObject]; + if (rep && typeof rep === "object" && typeof rep.objectForKey === "function") + { + 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 stringValue]; + return aView._originalText || [aView stringValue]; } else { @@ -119,7 +136,6 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } } -// Builds the current formatting lookup key (e.g. "%[property]@ %[is]@ %@") - (CPString)formattingKeyForViews:(CPArray)views { var keyParts = []; @@ -132,7 +148,6 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) return [keyParts componentsJoinedByString:@" "]; } -// Localizes all popup menu options within the row under their proper context - (void)localizeMenuItemsForViews:(CPArray)views { var count = [views count]; @@ -151,10 +166,18 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) if (!item._originalTitle) { - item._originalTitle = [item title]; + var rep = [item representedObject]; + if (rep && typeof rep === "object" && typeof rep.objectForKey === "function") + { + item._originalTitle = [rep objectForKey:@"value"]; + } + else + { + item._originalTitle = [item title]; + } } - // Temporarily select item to formulate the unique localization key + // Temporarily select item to generate formatting key context [view selectItem:item]; var tempKey = [self formattingKeyForViews:views]; @@ -162,7 +185,6 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) if (tempPattern !== tempKey) { - // Search for this view position's translation inside the pattern (e.g. %2$[son iguales]@) var regex = /%(\d+)\$(?:\[([^\]]+)\])?@/g; var match; while ((match = regex.exec(tempPattern)) !== null) @@ -179,7 +201,6 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } } - // Restore initial selection if (selectedItem) { [view selectItem:selectedItem]; @@ -188,7 +209,6 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } } -// Translates labels and reorders the active subviews based on positional formatting string - (CPArray)localizeAndReorderViews:(CPArray)views { var key = [self formattingKeyForViews:views]; @@ -200,23 +220,19 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } var newViews = [CPMutableArray array]; - - // Pattern to extract index and translation: %1$[translation]@ or %1$@ var regex = /%(\d+)\$(?:\[([^\]]+)\])?@/g; var lastIndex = 0; var match; while ((match = regex.exec(localizedPattern)) !== null) { - // 1. Insert any leading static text (e.g. " y ") var literalText = localizedPattern.substring(lastIndex, match.index); if (literalText.length > 0) { - var label = [CPTextField labelWithString:literalText]; + var label = [CPTextField labelWithTitle:literalText]; [newViews addObject:label]; } - // 2. Identify the original view corresponding to the positional index var position = parseInt(match[1], 10) - 1; var translatedValue = match[2]; @@ -250,13 +266,12 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) lastIndex = regex.lastIndex; } - // 3. Append trailing static text if (lastIndex < localizedPattern.length) { var literalText = localizedPattern.substring(lastIndex); if (literalText.length > 0) { - var label = [CPTextField labelWithString:literalText]; + var label = [CPTextField labelWithTitle:literalText]; [newViews addObject:label]; } } diff --git a/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j b/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j index 181f935ba..a32764fbe 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j @@ -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) diff --git a/Tests/Manual/CPRuleEditorTestSpanish/AppController.j b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j new file mode 100644 index 000000000..e81076955 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j @@ -0,0 +1,92 @@ +@import +@import +@import +@import +@import +@import "RuleDelegate.j" + +@implementation AppController : CPObject +{ + CPWindow theWindow; + CPRuleEditor ruleEditor; + CPTextField predicateField; + RuleDelegate ruleDelegate; +} + +- (void)applicationDidFinishLaunching:(CPNotification)aNotification +{ + theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(50, 50, 800, 500) + styleMask:CPTitledWindowMask | CPClosableWindowMask | CPResizableWindowMask]; + [theWindow setTitle:@"Spanish CPRuleEditor Whole-Sentence Localization Test"]; + [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, 760, 24)]; + [label setFont:[CPFont boldSystemFontOfSize:14]]; + [contentView addSubview:label]; + + 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 setRowHeight:28]; + [ruleEditor setTarget:self]; + [ruleEditor setAction:@selector(ruleEditorAction:)]; + + // Populate Spanish translations programmatically + var path = [[CPBundle mainBundle] pathForResource:@"Spanish.strings"]; + if (path) + { + [[ruleEditor standardLocalizer] loadContentOfURL:[CPURL URLWithString:path]]; + } + + [contentView addSubview:ruleEditor]; + + // Populate default row + [ruleEditor addRow:self]; + + // Display Output Label + var predLabel = [CPTextField labelWithTitle:@"CPRuleEditor Sentence Localization & Positional Reordering:"]; + [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]; + + var addBtn = [CPButton buttonWithTitle:@"Añadir regla"]; + [addBtn setFrame:CGRectMake(20, 400, 120, 24)]; + [addBtn setTarget:ruleEditor]; + [addBtn setAction:@selector(addRow:)]; + [contentView addSubview:addBtn]; + + [theWindow orderFront:self]; + [self ruleEditorAction:nil]; +} + +- (void)ruleEditorAction:(id)sender +{ + var predicate = [ruleEditor predicate]; + if (predicate) + { + [predicateField setStringValue:[predicate predicateFormat]]; + } + else + { + [predicateField setStringValue:@"(No predicate evaluated)"]; + } +} + +@end diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Info.plist b/Tests/Manual/CPRuleEditorTestSpanish/Info.plist new file mode 100644 index 000000000..68f9e7d32 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/Info.plist @@ -0,0 +1,12 @@ + + + + + CPApplicationDelegateClass + AppController + CPBundleName + CPTextViewTest + CPPrincipalClass + CPApplication + + diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Jakefile b/Tests/Manual/CPRuleEditorTestSpanish/Jakefile new file mode 100644 index 000000000..c66879a92 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/Jakefile @@ -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("----------------------------"); +} diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings b/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings new file mode 100644 index 000000000..17a1738c9 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings @@ -0,0 +1,13 @@ +/* Spanish formatting patterns for CPRuleEditor */ + +"%[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]@"; + +"Add row" = "Añadir regla"; +"Delete row" = "Eliminar regla"; +"Add compound row" = "Añadir grupo de reglas"; \ No newline at end of file diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Resources/spinner.gif b/Tests/Manual/CPRuleEditorTestSpanish/Resources/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..a5e705f6cbdf914e5e714c35a8dfef807f19e3c2 GIT binary patch literal 1434 zcmZvbdrVVj7{t$-UNOu86#VHu^|s&3rWfwHBbPG_8{SrlB{Tv1uvvj4t6zP!)#d!Ogc zP^62J^$0+~?-ZcXXpBaq%jFsw8L`=H2?+@R02Yg-*Xw;gACBV^i3CMahr>Z667S!? zk3LDe>VLEsU;sWo$Py~o!gWuaIAnaG3Sv``&tvc+8DJO!| zt4fcbQ8tW}a&xZfgtQ9BnFYLvGG|PvCNlg&uh@Q^w9Pz}+I^hCj`vu9JQADPqdkyk zR40xycD9geUgJu1e;$L`Fpa)?Wl-2&6hO@U*KrPqK(I1H=1h?2fce}6GhhP1oBZC# z1e@gt-qFa6lZrl%s1>bdxg*W)Ebt__O(4sj6(*2X@ciUClwHH#U(NRM7XAjS z+scDZ32J*PCoslsgS~#ZlCCGo`r>S6F)Ghw5wVlqHioFaw?ucD(XoLJ0j;28oPoPV z9A}dR$0SKn>uExfkl#j09o;v$BZ909R=*p{ATNq8BJW;YduX2QMOU7$a$_L5e!G^g zpbkx5G4&FZ%7m14rTN}5YB(;x7$5XOc_hf3E|Hw$TVg)P#qf~}nOn03uqsp>UG>vi zWThIoO)-1Q#@u#O;fMC#WAf@Xj70-0g9YZ;dA$HH1fW1q=9-c>>_yA0S$7M*5?}vi z)8MU`Q%P!7sEBBnd$DrKgFQ2?O%6LpdaC%F8Pn-)EC2t=j~ zoaLamla$FX!GQqWi!%s>sj-#5SJX0IF!TP=r21Z}s)k#btN0?=I7uD9C)Gb$fZ#sm zaVq7g`LwZ%PfNpN^_N-IGLHj@AV`s#4&va7_{Hw63G6BkSGXHdogTZ%QOiRb bDpZ@qYcJKM>x%b%*HX74c8MnqfHi*u-bC?g literal 0 HcmV?d00001 diff --git a/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j new file mode 100644 index 000000000..1c536b7c6 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j @@ -0,0 +1,113 @@ +@import +@import +@import + +@implementation RuleDelegate : CPObject +{ +} + +// 1. Root criteria and children +- (id)ruleEditor:(CPRuleEditor)editor child:(CPInteger)index forCriterion:(id)criterion withRowType:(CPRuleEditorRowType)rowType +{ + if (criterion == nil) + { + // Root criteria + return [@[@"firstName", @"lastName", @"age"] objectAtIndex:index]; + } + + if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"]) + { + return [@[@"contains", @"is equal to"] objectAtIndex:index]; + } + + if ([criterion isEqualToString:@"age"]) + { + return [@[@"is equal to"] objectAtIndex:index]; + } + + if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"]) + { + // The child of an operator is the leaf value node + return @"value"; + } + + return nil; +} + +// 2. Number of children for a given criterion +- (CPInteger)ruleEditor:(CPRuleEditor)editor numberOfChildrenForCriterion:(id)criterion withRowType:(CPRuleEditorRowType)rowType +{ + if (criterion == nil) + { + return 3; // firstName, lastName, age + } + + if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"]) + { + return 2; // contains, is equal to + } + + if ([criterion isEqualToString:@"age"]) + { + return 1; // is equal to + } + + if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"]) + { + // Operators have one child representing the input field value + return 1; + } + + return 0; // Leaf nodes return 0 +} + +// 3. Display values (labels, popup titles, or text input fields) +- (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"]) + { + // Return the actual editable text field view for the leaf node + 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 mapping +- (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"]) + { + [parts setObject:[CPExpression expressionForConstantValue:[value stringValue]] forKey:CPRuleEditorPredicateRightExpression]; + } + + return parts; +} + +@end diff --git a/Tests/Manual/CPRuleEditorTestSpanish/index-debug.html b/Tests/Manual/CPRuleEditorTestSpanish/index-debug.html new file mode 100644 index 000000000..a36b1d3b9 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/index-debug.html @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + __project.name__ + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/CPRuleEditorTestSpanish/index.html b/Tests/Manual/CPRuleEditorTestSpanish/index.html new file mode 100644 index 000000000..ac42c98a7 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/index.html @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + __project.name__ + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/CPRuleEditorTestSpanish/main.j b/Tests/Manual/CPRuleEditorTestSpanish/main.j new file mode 100644 index 000000000..7424228fa --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/main.j @@ -0,0 +1,18 @@ +/* + * AppController.j + * CPRuleEditorCibTest + * + * Created by You on September 3, 2010. + * Copyright 2010, Your Company All rights reserved. + */ + +@import +@import + +@import "AppController.j" + + +function main(args, namedArgs) +{ + CPApplicationMain(args, namedArgs); +} From 750c123d1e5787fd6bb1a3baa9a497416bf5a223 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 3 Jun 2026 19:19:36 +0200 Subject: [PATCH 05/46] fixed: redraw issues --- AppKit/CPRuleEditor/CPRuleEditor.j | 20 +++++++++++++ AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j | 3 ++ .../CPRuleEditorTestSpanish/RuleDelegate.j | 30 +++++++++++++++++-- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j index b1f30dc30..3ac9d2b80 100644 --- a/AppKit/CPRuleEditor/CPRuleEditor.j +++ b/AppKit/CPRuleEditor/CPRuleEditor.j @@ -207,8 +207,28 @@ 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++) + { + [[_slices objectAtIndex:i] _reconfigureSubviews]; + } + } argument:nil order:0 modes:[CPDefaultRunLoopMode]]; + } +} /*! @endcond */ /*! diff --git a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j index f9dddbfb9..79ab94da8 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j @@ -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 diff --git a/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j index 1c536b7c6..842056535 100644 --- a/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j +++ b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j @@ -17,7 +17,8 @@ if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"]) { - return [@[@"contains", @"is equal to"] objectAtIndex:index]; + // "is equal to" is placed at index 0 so that the reordered Spanish sentence layout loads automatically on startup + return [@[@"is equal to", @"contains"] objectAtIndex:index]; } if ([criterion isEqualToString:@"age"]) @@ -54,7 +55,7 @@ if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"]) { - // Operators have one child representing the input field value + // Operators have 1 child representing the value node return 1; } @@ -104,7 +105,30 @@ } else if ([criterion isEqualToString:@"value"]) { - [parts setObject:[CPExpression expressionForConstantValue:[value stringValue]] forKey:CPRuleEditorPredicateRightExpression]; + // Resolve the correct active text field from the slice row on screen + 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; From e9b9d9702e6249f4f4f64136cbab88ee7992b95a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 3 Jun 2026 21:19:09 +0200 Subject: [PATCH 06/46] fixed: localizer issues --- AppKit/CPRuleEditor/CPRuleEditor.j | 32 +++++++++++++++---- AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j | 22 ++++++++++--- .../CPRuleEditorTestSpanish/AppController.j | 21 ++++++++++++ .../Resources/Spanish.strings | 3 +- .../CPRuleEditorTestSpanish/RuleDelegate.j | 24 +++++++------- 5 files changed, 77 insertions(+), 25 deletions(-) diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j index 3ac9d2b80..cbc08326f 100644 --- a/AppKit/CPRuleEditor/CPRuleEditor.j +++ b/AppKit/CPRuleEditor/CPRuleEditor.j @@ -226,6 +226,9 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", { [[_slices objectAtIndex:i] _reconfigureSubviews]; } + + [self _updatePredicate]; + [self _sendRuleAction]; } argument:nil order:0 modes:[CPDefaultRunLoopMode]]; } } @@ -574,7 +577,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; @@ -641,7 +644,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) { @@ -810,7 +813,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]; @@ -867,7 +870,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]; @@ -885,6 +887,7 @@ TODO: implement return nil; var current_index = [subrowsIndexes firstIndex]; + while (current_index !== CPNotFound) { var subpredicate = [self predicateForRow:current_index]; @@ -1392,8 +1395,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]; + } } } diff --git a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j index 79ab94da8..467445242 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j @@ -112,7 +112,7 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) if (!originalTitle) { var rep = [selectedItem representedObject]; - if (rep && typeof rep === "object" && typeof rep.objectForKey === "function") + if (rep && typeof rep === "object" && [rep respondsToSelector:@selector(objectForKey:)]) { originalTitle = [rep objectForKey:@"value"]; } @@ -170,7 +170,7 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) if (!item._originalTitle) { var rep = [item representedObject]; - if (rep && typeof rep === "object" && typeof rep.objectForKey === "function") + if (rep && typeof rep === "object" && [rep respondsToSelector:@selector(objectForKey:)]) { item._originalTitle = [rep objectForKey:@"value"]; } @@ -230,7 +230,9 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) while ((match = regex.exec(localizedPattern)) !== null) { var literalText = localizedPattern.substring(lastIndex, match.index); - if (literalText.length > 0) + + // 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]; @@ -257,9 +259,17 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) [selectedItem setTitle:translatedValue]; } } - else if (typeof originalView.setStringValue === "function") + 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]))]; + } } } @@ -272,7 +282,9 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) if (lastIndex < localizedPattern.length) { var literalText = localizedPattern.substring(lastIndex); - if (literalText.length > 0) + + // 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]; diff --git a/Tests/Manual/CPRuleEditorTestSpanish/AppController.j b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j index e81076955..d39cbf981 100644 --- a/Tests/Manual/CPRuleEditorTestSpanish/AppController.j +++ b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j @@ -35,6 +35,7 @@ [ruleEditor setAutoresizingMask:CPViewWidthSizable]; [ruleEditor setDelegate:ruleDelegate]; [ruleEditor setEditable:YES]; + [ruleEditor setNestingMode:CPRuleEditorNestingModeList]; [ruleEditor setRowHeight:28]; [ruleEditor setTarget:self]; [ruleEditor setAction:@selector(ruleEditorAction:)]; @@ -72,6 +73,12 @@ [addBtn setAction:@selector(addRow:)]; [contentView addSubview:addBtn]; + + [[CPNotificationCenter defaultCenter] addObserver:self + selector:@selector(ruleEditorRowsDidChange:) + name:CPRuleEditorRowsDidChangeNotification + object:ruleEditor]; + [theWindow orderFront:self]; [self ruleEditorAction:nil]; } @@ -89,4 +96,18 @@ } } +- (void)ruleEditorRowsDidChange:(CPNotification)note +{ + var predicate = [ruleEditor predicate]; + + if (predicate) + { + [predicateField setStringValue:[predicate predicateFormat]]; + } + else + { + [predicateField setStringValue:@"(Incomplete Predicate)"]; + } +} + @end diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings b/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings index 17a1738c9..75dd5e735 100644 --- a/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings +++ b/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings @@ -7,7 +7,8 @@ "%[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"; \ No newline at end of file +"Add compound row" = "Añadir grupo de reglas"; diff --git a/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j index 842056535..3835b5e7f 100644 --- a/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j +++ b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j @@ -2,6 +2,10 @@ @import @import +// Ensure the standard operator constants are explicitly defined +var CPEqualToPredicateOperatorType = 4, + CPContainsPredicateOperatorType = 99; + @implementation RuleDelegate : CPObject { } @@ -11,13 +15,11 @@ { if (criterion == nil) { - // Root criteria return [@[@"firstName", @"lastName", @"age"] objectAtIndex:index]; } if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"]) { - // "is equal to" is placed at index 0 so that the reordered Spanish sentence layout loads automatically on startup return [@[@"is equal to", @"contains"] objectAtIndex:index]; } @@ -28,41 +30,39 @@ if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"]) { - // The child of an operator is the leaf value node return @"value"; } return nil; } -// 2. Number of children for a given criterion +// 2. Number of children - (CPInteger)ruleEditor:(CPRuleEditor)editor numberOfChildrenForCriterion:(id)criterion withRowType:(CPRuleEditorRowType)rowType { if (criterion == nil) { - return 3; // firstName, lastName, age + return 3; } if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"]) { - return 2; // contains, is equal to + return 2; } if ([criterion isEqualToString:@"age"]) { - return 1; // is equal to + return 1; } if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"]) { - // Operators have 1 child representing the value node return 1; } - return 0; // Leaf nodes return 0 + return 0; } -// 3. Display values (labels, popup titles, or text input fields) +// 3. Display values - (id)ruleEditor:(CPRuleEditor)editor displayValueForCriterion:(id)criterion inRow:(CPInteger)row { if ([criterion isEqualToString:@"firstName"]) return @"firstName"; @@ -74,7 +74,6 @@ if ([criterion isEqualToString:@"value"]) { - // Return the actual editable text field view for the leaf node var textField = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 120, 24)]; [textField setBezeled:YES]; [textField setBezelStyle:CPTextFieldSquareBezel]; @@ -86,7 +85,7 @@ return nil; } -// 4. Predicate parts mapping +// 4. Predicate parts - (CPDictionary)ruleEditor:(CPRuleEditor)editor predicatePartsForCriterion:(id)criterion withDisplayValue:(id)value inRow:(CPInteger)row { var parts = @{}; @@ -105,7 +104,6 @@ } else if ([criterion isEqualToString:@"value"]) { - // Resolve the correct active text field from the slice row on screen var activeValue = value; var slices = [editor valueForKey:@"_slices"]; From 613a2722be1d618fbffdf5599f0081422ff12ae2 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 4 Jun 2026 08:15:52 +0200 Subject: [PATCH 07/46] new: dynamic mutilanguage demo --- AppKit/CPRuleEditor/CPRuleEditor.j | 5 +- AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j | 30 +++++ .../CPRuleEditor/_CPRuleEditorViewSliceRow.j | 3 + .../CPRuleEditorTestSpanish/AppController.j | 104 ++++++++++++++---- .../Resources/Spanish.strings | 14 --- 5 files changed, 119 insertions(+), 37 deletions(-) delete mode 100644 Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j index cbc08326f..9e5a2cbdd 100644 --- a/AppKit/CPRuleEditor/CPRuleEditor.j +++ b/AppKit/CPRuleEditor/CPRuleEditor.j @@ -214,6 +214,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", object:nil]; } + - (void)_ruleEditorLocalizerDidLoad:(CPNotification)aNotification { if ([aNotification object] === [self standardLocalizer]) @@ -224,7 +225,9 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", var count = [_slices count]; for (var i = 0; i < count; i++) { - [[_slices objectAtIndex:i] _reconfigureSubviews]; + var slice = [_slices objectAtIndex:i]; + [slice _reconfigureSubviews]; + [slice _updateButtonVisibilities]; // Force updates on row button tooltips } [self _updatePredicate]; diff --git a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j index 467445242..14404e0ed 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j @@ -202,6 +202,10 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } } } + else + { + [item setTitle:item._originalTitle]; + } } if (selectedItem) @@ -219,6 +223,32 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) 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; } diff --git a/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j b/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j index a32764fbe..ee21be432 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j @@ -440,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 diff --git a/Tests/Manual/CPRuleEditorTestSpanish/AppController.j b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j index d39cbf981..20c22eeea 100644 --- a/Tests/Manual/CPRuleEditorTestSpanish/AppController.j +++ b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j @@ -3,31 +3,53 @@ @import @import @import +@import @import "RuleDelegate.j" @implementation AppController : CPObject { - CPWindow theWindow; - CPRuleEditor ruleEditor; - CPTextField predicateField; - RuleDelegate ruleDelegate; + 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, 500) + theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(50, 50, 800, 520) styleMask:CPTitledWindowMask | CPClosableWindowMask | CPResizableWindowMask]; - [theWindow setTitle:@"Spanish CPRuleEditor Whole-Sentence Localization Test"]; + [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, 760, 24)]; + [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 @@ -40,20 +62,45 @@ [ruleEditor setTarget:self]; [ruleEditor setAction:@selector(ruleEditorAction:)]; - // Populate Spanish translations programmatically - var path = [[CPBundle mainBundle] pathForResource:@"Spanish.strings"]; - if (path) - { - [[ruleEditor standardLocalizer] loadContentOfURL:[CPURL URLWithString:path]]; - } + // 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 default row + // Populate initial rows + [ruleEditor addRow:self]; + [ruleEditor addRow:self]; [ruleEditor addRow:self]; // Display Output Label - var predLabel = [CPTextField labelWithTitle:@"CPRuleEditor Sentence Localization & Positional Reordering:"]; + var predLabel = [CPTextField labelWithTitle:@"Evaluated Predicate:"]; [predLabel setFrame:CGRectMake(20, 320, 760, 20)]; [predLabel setFont:[CPFont boldSystemFontOfSize:12]]; [contentView addSubview:predLabel]; @@ -67,13 +114,6 @@ [predicateField setFont:[CPFont systemFontOfSize:13]]; [contentView addSubview:predicateField]; - var addBtn = [CPButton buttonWithTitle:@"Añadir regla"]; - [addBtn setFrame:CGRectMake(20, 400, 120, 24)]; - [addBtn setTarget:ruleEditor]; - [addBtn setAction:@selector(addRow:)]; - [contentView addSubview:addBtn]; - - [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(ruleEditorRowsDidChange:) name:CPRuleEditorRowsDidChangeNotification @@ -83,6 +123,26 @@ [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]; diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings b/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings deleted file mode 100644 index 75dd5e735..000000000 --- a/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings +++ /dev/null @@ -1,14 +0,0 @@ -/* Spanish formatting patterns for CPRuleEditor */ - -"%[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"; From e03e0c6fc4821beedd7ba33e07af1fd369e23f42 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 4 Jun 2026 08:55:32 +0200 Subject: [PATCH 08/46] fixed: exception after deleting the last row --- AppKit/CPRuleEditor/CPRuleEditor.j | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j index 9e5a2cbdd..738b67d9d 100644 --- a/AppKit/CPRuleEditor/CPRuleEditor.j +++ b/AppKit/CPRuleEditor/CPRuleEditor.j @@ -1320,23 +1320,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 From 9115949169793e5da1a1e68a8cfd28d442dcac41 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 4 Jun 2026 21:39:02 +0200 Subject: [PATCH 09/46] new: Documentation for CPRuleEditor I8N stuff --- AppKit/CPRuleEditor/CPRuleEditor.j | 29 ++++++++++++++++++ .../AppController.j | 0 .../Info.plist | 0 .../Jakefile | 0 .../Resources/spinner.gif | Bin .../RuleDelegate.j | 0 .../index-debug.html | 0 .../index.html | 0 .../main.j | 0 9 files changed, 29 insertions(+) rename Tests/Manual/{CPRuleEditorTestSpanish => CPRuleEditorTestI8N}/AppController.j (100%) rename Tests/Manual/{CPRuleEditorTestSpanish => CPRuleEditorTestI8N}/Info.plist (100%) rename Tests/Manual/{CPRuleEditorTestSpanish => CPRuleEditorTestI8N}/Jakefile (100%) rename Tests/Manual/{CPRuleEditorTestSpanish => CPRuleEditorTestI8N}/Resources/spinner.gif (100%) rename Tests/Manual/{CPRuleEditorTestSpanish => CPRuleEditorTestI8N}/RuleDelegate.j (100%) rename Tests/Manual/{CPRuleEditorTestSpanish => CPRuleEditorTestI8N}/index-debug.html (100%) rename Tests/Manual/{CPRuleEditorTestSpanish => CPRuleEditorTestI8N}/index.html (100%) rename Tests/Manual/{CPRuleEditorTestSpanish => CPRuleEditorTestI8N}/main.j (100%) diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j index 738b67d9d..b5e57c601 100644 --- a/AppKit/CPRuleEditor/CPRuleEditor.j +++ b/AppKit/CPRuleEditor/CPRuleEditor.j @@ -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 diff --git a/Tests/Manual/CPRuleEditorTestSpanish/AppController.j b/Tests/Manual/CPRuleEditorTestI8N/AppController.j similarity index 100% rename from Tests/Manual/CPRuleEditorTestSpanish/AppController.j rename to Tests/Manual/CPRuleEditorTestI8N/AppController.j diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Info.plist b/Tests/Manual/CPRuleEditorTestI8N/Info.plist similarity index 100% rename from Tests/Manual/CPRuleEditorTestSpanish/Info.plist rename to Tests/Manual/CPRuleEditorTestI8N/Info.plist diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Jakefile b/Tests/Manual/CPRuleEditorTestI8N/Jakefile similarity index 100% rename from Tests/Manual/CPRuleEditorTestSpanish/Jakefile rename to Tests/Manual/CPRuleEditorTestI8N/Jakefile diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Resources/spinner.gif b/Tests/Manual/CPRuleEditorTestI8N/Resources/spinner.gif similarity index 100% rename from Tests/Manual/CPRuleEditorTestSpanish/Resources/spinner.gif rename to Tests/Manual/CPRuleEditorTestI8N/Resources/spinner.gif diff --git a/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j b/Tests/Manual/CPRuleEditorTestI8N/RuleDelegate.j similarity index 100% rename from Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j rename to Tests/Manual/CPRuleEditorTestI8N/RuleDelegate.j diff --git a/Tests/Manual/CPRuleEditorTestSpanish/index-debug.html b/Tests/Manual/CPRuleEditorTestI8N/index-debug.html similarity index 100% rename from Tests/Manual/CPRuleEditorTestSpanish/index-debug.html rename to Tests/Manual/CPRuleEditorTestI8N/index-debug.html diff --git a/Tests/Manual/CPRuleEditorTestSpanish/index.html b/Tests/Manual/CPRuleEditorTestI8N/index.html similarity index 100% rename from Tests/Manual/CPRuleEditorTestSpanish/index.html rename to Tests/Manual/CPRuleEditorTestI8N/index.html diff --git a/Tests/Manual/CPRuleEditorTestSpanish/main.j b/Tests/Manual/CPRuleEditorTestI8N/main.j similarity index 100% rename from Tests/Manual/CPRuleEditorTestSpanish/main.j rename to Tests/Manual/CPRuleEditorTestI8N/main.j From 524b1df4c983b3eb3e5414d2ca4292b85d893f6e Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 5 Jun 2026 17:38:04 +0200 Subject: [PATCH 10/46] fix font issue --- AppKit/CPTextField.j | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/AppKit/CPTextField.j b/AppKit/CPTextField.j index 7e51b40b4..4dc2fb37b 100644 --- a/AppKit/CPTextField.j +++ b/AppKit/CPTextField.j @@ -2004,6 +2004,41 @@ 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]; + [self setValue:aFont forThemeAttribute:@"font" inState:[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:[CPThemeStateTableDataView, CPThemeStateSelectedDataView]]; + [self setValue:aFont forThemeAttribute:@"font" inState:[CPThemeStateTableDataView, CPThemeStateSelectedDataView, CPThemeStateFirstResponder, CPThemeStateKeyWindow]]; + + [self layoutSubviews]; +} + - (void)takeValueFromKeyPath:(CPString)aKeyPath ofObjects:(CPArray)objects { var count = objects.length, From 14b5a67bf5ecd0301f08d643b00feca3010cfcc9 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 13 Jun 2026 15:42:48 +0200 Subject: [PATCH 11/46] new: Clear the cached visible rect so it gets recalculated after size changes --- AppKit/CPTextView/CPTextView.j | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 32d9c6b88..cfde9194a 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2069,8 +2069,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) From c9d3ccdf0d5df2c88bbcdda56ba9529de9f976b1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 13 Jun 2026 18:42:34 +0200 Subject: [PATCH 12/46] fixed: top was not applied to style --- AppKit/CPTextView/CPLayoutManager.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index fd0accf43..98908f044 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -1378,7 +1378,7 @@ var _objectsInRange = function(aList, aRange) if (_runs[i].view) _runs[i].view._frame.origin.y += verticalOffset; - _runs[i].elem.top = (_runs[i].elem.top + verticalOffset) + 'px'; + _runs[i].elem.style.top = parseFloat(_runs[i].elem.style.top + verticalOffset) + 'px'; _runs[i].DOMpatched = YES; } } From c9df6e4d25e978867ca2139e1a8dc469a5a705b4 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 13 Jun 2026 19:18:12 +0200 Subject: [PATCH 13/46] removed: dead code --- AppKit/CPTextView/CPLayoutManager.j | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index 98908f044..74cd87b40 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -1373,12 +1373,11 @@ 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.style.top = parseFloat(_runs[i].elem.style.top + verticalOffset) + 'px'; _runs[i].DOMpatched = YES; } } From 861c504c4153e1def1632cc05dceff10f3233a62 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 13 Jun 2026 19:47:24 +0200 Subject: [PATCH 14/46] NEW: add native table support via dynamic protocol detection --- AppKit/CPTextView/_CPRTFProducer.j | 166 +++++++++++++++++++++++++---- 1 file changed, 147 insertions(+), 19 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFProducer.j b/AppKit/CPTextView/_CPRTFProducer.j index 7333b2c55..9d9d0e6c3 100644 --- a/AppKit/CPTextView/_CPRTFProducer.j +++ b/AppKit/CPTextView/_CPRTFProducer.j @@ -1,11 +1,11 @@ /* _CPRTFProducer.j - Serialize CPAttributedString to a RTF String + Serialize CPAttributedString to an 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) + (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 @@ -77,28 +77,30 @@ function _points2twips(a) { return (a) * 20.0; } #pragma mark - -#pragma mark init methods +#pragma mark Init methods - (id)init { if (self = [super init]) { - // maintain a dictionary for the used colours - // (for rtf-header generation) + // Maintain a dictionary for the used colors + // (for RTF-header generation) colorDict = [CPMutableDictionary new]; - //maintain a dictionary for the used fonts - //(for rtf-header generation) + // Maintain a dictionary for the used fonts + // (for RTF-header generation) fontDict = [CPMutableDictionary new]; fgColor = [CPColor blackColor]; - bgColor= [CPColor whiteColor]; + bgColor = [CPColor whiteColor]; } return self; } -// private stuff follows +#pragma mark - +#pragma mark Header & Formatting Support + - (CPString)fontTable { if (![fontDict count]) @@ -313,7 +315,7 @@ function _points2twips(a) { return (a) * 20.0; } break; } - // write first line indent and left indent + // Write first line indent and left indent var twips = _points2twips([paraStyle firstLineHeadIndent]); if (twips != 0.0) @@ -354,7 +356,7 @@ function _points2twips(a) { return (a) * 20.0; } switch ([tab tabStopType]) { case CPLeftTabStopType: - // no tabkind emission needed + // No tabkind emission needed break; /* case NSRightTabStopType: headerString += @"\\tqr"; @@ -376,6 +378,104 @@ function _points2twips(a) { return (a) * 20.0; } return headerString; } +#pragma mark - +#pragma mark Duck-Typed Table Engine + +/** + * Generates a native RTF table row structure from a conforming table data object. + * This helper isolates the native RTF grid structure to ensure decoupling. + * + * @param tableObject An object implementing the dynamic getters `headers` and `rows`. + * @return A raw RTF CPString containing cell bounds, row boundaries, and escaped strings. + */ +- (CPString)rtfStringForTable:(id)tableObject +{ + var headers = [tableObject headers], + rows = [tableObject rows]; + + var numCols = [headers count]; + if (numCols == 0 && [rows count] > 0) + { + numCols = [[rows objectAtIndex:0] count]; + } + + if (numCols == 0) + { + return @""; + } + + var rtf = @""; + + // Divide printable horizontal space evenly (approx. 5.5 inches or 7920 Twips total) + var totalWidthTwips = 7920, + colWidth = Math.floor(totalWidthTwips / numCols); + + var makeRowRTF = function(cells, isHeader) { + // \trowd clears existing cell definitions. + // \trleft360 applies an indent matching a standard page margin. + var rowStr = @"\\trowd\\trgaph108\\trleft360"; + + // 1. Declare row boundaries and configure cell border widths + var currentX = 360; + for (var c = 0; c < numCols; c++) { + currentX += colWidth; + rowStr += @"\\clbrdrt\\brdrs\\brdrw10\\clbrdrb\\brdrs\\brdrw10"; + rowStr += @"\\clbrdrl\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10"; + rowStr += [CPString stringWithFormat:@"\\cellx%d", currentX]; + } + + // 2. Escape, format, and serialize cell text content + for (var c = 0; c < numCols; c++) { + var textVal = @""; + if (c < [cells count]) { + textVal = [cells objectAtIndex:c]; + } + + // Core RTF escaping (Backslashes escaped first to preserve subsequent tokens) + textVal = textVal.replace(/\\/g, '\\\\'); + textVal = textVal.replace(/{/g, '\\{'); + textVal = textVal.replace(/}/g, '\\}'); + textVal = textVal.replace(/\n/g, '\\par '); + + // Standard visual inline Markdown conversions for cellular parity + textVal = textVal.replace(/\*\*([^*]+)\*\*/g, '\\b $1\\b0 '); + textVal = textVal.replace(/\*([^*]+)\*/g, '\\i $1\\i0 '); + + rowStr += @"\\intbl "; + if (isHeader) { + rowStr += @"\\b "; + } + rowStr += textVal; + if (isHeader) { + rowStr += @"\\b0 "; + } + rowStr += @"\\cell "; + } + + rowStr += @"\\row\n"; + return rowStr; + }; + + // Construct headers + if ([headers count] > 0) { + rtf += makeRowRTF(headers, YES); + } + + // Construct content rows + for (var r = 0; r < [rows count]; r++) { + var rowData = [rows objectAtIndex:r]; + rtf += makeRowRTF(rowData, NO); + } + + // Explicitly restore standard paragraph attributes to escape table scope + rtf += @"\\pard\n"; + + return rtf; +} + +#pragma mark - +#pragma mark Attribute Processing + - (CPString)runStringForString:(CPString) substring attributes:(CPDictionary) attributes paragraphStart:(BOOL) first @@ -386,6 +486,33 @@ function _points2twips(a) { return (a) * 20.0; } attribEnum, currAttrib; + // --- DUCK-TYPING PROTOCOL DETECTION --- + // Safely scan all incoming attributes for an object supporting headers & rows. + // If found, completely divert processing to native RTF table construction. + var keys = [attributes allKeys], + count = [keys count], + tableObject = nil; + + for (var i = 0; i < count; i++) + { + var key = [keys objectAtIndex:i], + val = [attributes objectForKey:key]; + + if (val && typeof val.respondsToSelector === "function" && + [val respondsToSelector:@selector(headers)] && + [val respondsToSelector:@selector(rows)]) + { + tableObject = val; + break; + } + } + + if (tableObject) + { + return [self rtfStringForTable:tableObject]; + } + // -------------------------------------- + if (first) { var paraStyle = [attributes objectForKey:CPParagraphStyleAttributeName]; @@ -393,10 +520,10 @@ function _points2twips(a) { return (a) * 20.0; } } /* - * analyze attributes of current run + * 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 + * 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. */ @@ -407,7 +534,7 @@ function _points2twips(a) { return (a) * 20.0; } if ([currAttrib isEqualToString:CPFontAttributeName]) { /* - * handle fonts + * Handle fonts */ var font, fontName, @@ -418,13 +545,13 @@ function _points2twips(a) { return (a) * 20.0; } traits = [[CPFontManager sharedFontManager] traitsOfFont:font]; /* - * font name + * Font name */ if (currentFont == nil || ![fontName isEqualToString:[currentFont familyName]]) headerString += [self fontToken:fontName]; /* - * font size + * Font size */ if (currentFont == nil || [font size] != [currentFont size]) { @@ -435,7 +562,7 @@ function _points2twips(a) { return (a) * 20.0; } headerString += pString; } /* - * font attributes + * Font attributes */ if (traits & CPItalicFontMask) { @@ -565,7 +692,7 @@ function _points2twips(a) { return (a) * 20.0; } first = YES; // FIXME split along newline characters and run as outer loop - while (CPMaxRange(currRange) < CPMaxRange(completeRange)) // save all "runs" + while (CPMaxRange(currRange) < CPMaxRange(completeRange)) // Save all "runs" { var attributes, substring, @@ -599,7 +726,7 @@ function _points2twips(a) { return (a) * 20.0; } docDict = dict; /* - * do not change order! (esp. body has to be generated first; builds context) + * Do not change order! (esp. body has to be generated first; builds context) */ bodyString = [self bodyString]; trailerString = [self trailerString]; @@ -610,4 +737,5 @@ function _points2twips(a) { return (a) * 20.0; } output += trailerString; return output; } + @end From a0fc7723fcde82e6b6e72fd500eda814354d16a4 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 13 Jun 2026 22:36:09 +0200 Subject: [PATCH 15/46] new: table support --- AppKit/CPTextView/_CPRTFParser.j | 134 ++++++++- AppKit/CPTextView/_CPRTFProducer.j | 257 ++++++---------- AppKit/CPTextView/_CPTableTextAttachment.j | 332 +++++++++++++++++++++ 3 files changed, 557 insertions(+), 166 deletions(-) create mode 100644 AppKit/CPTextView/_CPTableTextAttachment.j diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index df1569481..4b730d62b 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -23,6 +23,7 @@ @import @import "CPFontManager.j" @import "CPParagraphStyle.j" +@import "_CPTableTextAttachment.j" @global CPLeftTextAlignment @global CPRightTextAlignment @@ -34,6 +35,7 @@ @global CPForegroundColorAttributeName @global CPBackgroundColorAttributeName @global CPParagraphStyleAttributeName +@global CPAttachmentAttributeName @global CPLeftTabStopType @global CPRightTabStopType @@ -289,7 +291,12 @@ var kRgsymRtf = { "]" : [ "]", 0, false, kRTFParserType_char, ']'], "{" : [ "{", 0, false, kRTFParserType_char, '{'], "}" : [ "}", 0, false, kRTFParserType_char, '}'], - "\\" : [ "\\", 0, false, kRTFParserType_char, '\\'] + "\\" : [ "\\", 0, false, kRTFParserType_char, '\\'], + "trowd" : [ "trowd", 0, false, kRTFParserType_spec, "ipfnTrowd"], + "cell" : [ "cell", 0, false, kRTFParserType_spec, "ipfnCell"], + "row" : [ "row", 0, false, kRTFParserType_spec, "ipfnRow"], + "cellx" : [ "cellx", 0, false, kRTFParserType_spec, "ipfnCellx"], + "intbl" : [ "intbl", 0, false, kRTFParserType_spec, "ipfnIntbl"] }; @implementation _CPRTFParser : CPObject @@ -307,6 +314,13 @@ var kRgsymRtf = { CPArray _fontArray; CPString _freename; BOOL _parsingFontTable; + + // Table parsing state + BOOL _inTableActive; + BOOL _waitingForNextRow; + CPMutableArray _tableRows; + CPMutableArray _currentRow; + CPString _currentCellText; } - (id)init @@ -324,6 +338,12 @@ var kRgsymRtf = { _fontArray = ['Arial']; // FIXME: should be name of system font _freename = ""; _parsingFontTable = NO; + + _inTableActive = NO; + _waitingForNextRow = NO; + _tableRows = nil; + _currentRow = nil; + _currentCellText = ""; } return self; @@ -420,11 +440,91 @@ 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) + { + 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]; + + // Linear height allocation estimation + var numCols = [headers count]; + var estimatedHeight = 36.0 + ([rows count] * 28.0); + var lineCount = Math.ceil(estimatedHeight / 16.0) + 1; + var newlineStr = ""; + for (var nl = 0; nl < lineCount; nl++) { + newlineStr += "\n"; + } + + var tableAttrStr = [[CPMutableAttributedString alloc] initWithString:newlineStr]; + [tableAttrStr addAttribute:@"TableAttachmentAttribute" value:attachment range:CPMakeRange(0, [tableAttrStr length])]; + [tableAttrStr addAttribute:CPAttachmentAttributeName value:attachment range:CPMakeRange(0, [tableAttrStr length])]; + + [_result appendAttributedString:tableAttrStr]; + + _tableRows = nil; + _currentRow = nil; + _currentCellText = ""; + _inTableActive = NO; + _waitingForNextRow = NO; + } +} + - (void)_flushCurrentRun { var newOffset = 0; @@ -552,6 +652,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]; @@ -744,9 +852,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 +881,11 @@ var kRgsymRtf = { { tmp = rtf.charAt(++_currentParseIndex); + if (_waitingForNextRow && tmp !== "\\" && tmp !== " " && tmp !== "\n" && tmp !== "\r" && tmp !== "\t") + { + [self _flushTableIfAny]; + } + if (tmp !== "\\" && hex.length > 0) { [self _appendPlainString: String.fromCharCode(parseInt((hex), 16))]; @@ -788,12 +908,18 @@ var kRgsymRtf = { break; case "{": + if (_waitingForNextRow) + [self _flushTableIfAny]; + if ([self pushState]) CPLogConsole("push"); break; case "}": + if (_waitingForNextRow) + [self _flushTableIfAny]; + if ([self popState]) CPLogConsole("pop"); @@ -863,6 +989,8 @@ var kRgsymRtf = { } } + [self _flushTableIfAny]; + return _result; } diff --git a/AppKit/CPTextView/_CPRTFProducer.j b/AppKit/CPTextView/_CPRTFProducer.j index 9d9d0e6c3..05e082636 100644 --- a/AppKit/CPTextView/_CPRTFProducer.j +++ b/AppKit/CPTextView/_CPRTFProducer.j @@ -1,7 +1,7 @@ /* _CPRTFProducer.j - Serialize CPAttributedString to an RTF String + Serialize CPAttributedString to a RTF String Copyright (C) 2014 Daniel Boehringer This file is based on the RTFProducer from GNUStep @@ -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", @@ -77,30 +83,28 @@ function _points2twips(a) { return (a) * 20.0; } #pragma mark - -#pragma mark Init methods +#pragma mark init methods - (id)init { if (self = [super init]) { - // Maintain a dictionary for the used colors - // (for RTF-header generation) + // 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) + //maintain a dictionary for the used fonts + //(for rtf-header generation) fontDict = [CPMutableDictionary new]; fgColor = [CPColor blackColor]; - bgColor = [CPColor whiteColor]; + bgColor= [CPColor whiteColor]; } return self; } -#pragma mark - -#pragma mark Header & Formatting Support - +// private stuff follows - (CPString)fontTable { if (![fontDict count]) @@ -315,7 +319,7 @@ function _points2twips(a) { return (a) * 20.0; } break; } - // Write first line indent and left indent + // write first line indent and left indent var twips = _points2twips([paraStyle firstLineHeadIndent]); if (twips != 0.0) @@ -356,163 +360,97 @@ function _points2twips(a) { return (a) * 20.0; } switch ([tab tabStopType]) { case CPLeftTabStopType: - // No tabkind emission needed break; -/* case NSRightTabStopType: + case CPRightTabStopType: headerString += @"\\tqr"; - break; - case NSCenterTabStopType: - headerString += @"\\tqc"; - break; - case NSDecimalTabStopType: + break; + case CPCenterTabStopType: + 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; } -#pragma mark - -#pragma mark Duck-Typed Table Engine - -/** - * Generates a native RTF table row structure from a conforming table data object. - * This helper isolates the native RTF grid structure to ensure decoupling. - * - * @param tableObject An object implementing the dynamic getters `headers` and `rows`. - * @return A raw RTF CPString containing cell bounds, row boundaries, and escaped strings. - */ -- (CPString)rtfStringForTable:(id)tableObject -{ - var headers = [tableObject headers], - rows = [tableObject rows]; - - var numCols = [headers count]; - if (numCols == 0 && [rows count] > 0) - { - numCols = [[rows objectAtIndex:0] count]; - } - - if (numCols == 0) - { - return @""; - } - - var rtf = @""; - - // Divide printable horizontal space evenly (approx. 5.5 inches or 7920 Twips total) - var totalWidthTwips = 7920, - colWidth = Math.floor(totalWidthTwips / numCols); - - var makeRowRTF = function(cells, isHeader) { - // \trowd clears existing cell definitions. - // \trleft360 applies an indent matching a standard page margin. - var rowStr = @"\\trowd\\trgaph108\\trleft360"; - - // 1. Declare row boundaries and configure cell border widths - var currentX = 360; - for (var c = 0; c < numCols; c++) { - currentX += colWidth; - rowStr += @"\\clbrdrt\\brdrs\\brdrw10\\clbrdrb\\brdrs\\brdrw10"; - rowStr += @"\\clbrdrl\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10"; - rowStr += [CPString stringWithFormat:@"\\cellx%d", currentX]; - } - - // 2. Escape, format, and serialize cell text content - for (var c = 0; c < numCols; c++) { - var textVal = @""; - if (c < [cells count]) { - textVal = [cells objectAtIndex:c]; - } - - // Core RTF escaping (Backslashes escaped first to preserve subsequent tokens) - textVal = textVal.replace(/\\/g, '\\\\'); - textVal = textVal.replace(/{/g, '\\{'); - textVal = textVal.replace(/}/g, '\\}'); - textVal = textVal.replace(/\n/g, '\\par '); - - // Standard visual inline Markdown conversions for cellular parity - textVal = textVal.replace(/\*\*([^*]+)\*\*/g, '\\b $1\\b0 '); - textVal = textVal.replace(/\*([^*]+)\*/g, '\\i $1\\i0 '); - - rowStr += @"\\intbl "; - if (isHeader) { - rowStr += @"\\b "; - } - rowStr += textVal; - if (isHeader) { - rowStr += @"\\b0 "; - } - rowStr += @"\\cell "; - } - - rowStr += @"\\row\n"; - return rowStr; - }; - - // Construct headers - if ([headers count] > 0) { - rtf += makeRowRTF(headers, YES); - } - - // Construct content rows - for (var r = 0; r < [rows count]; r++) { - var rowData = [rows objectAtIndex:r]; - rtf += makeRowRTF(rowData, NO); - } - - // Explicitly restore standard paragraph attributes to escape table scope - rtf += @"\\pard\n"; - - return rtf; -} - -#pragma mark - -#pragma mark Attribute Processing - - (CPString)runStringForString:(CPString) substring attributes:(CPDictionary) attributes paragraphStart:(BOOL) first { + var tableAttachment = [attributes objectForKey:CPAttachmentAttributeName]; + if (!tableAttachment) + tableAttachment = [attributes objectForKey:@"TableAttachmentAttribute"]; + + if (tableAttachment && [tableAttachment isKindOfClass:[_CPTableTextAttachment class]]) + { + var headers = [tableAttachment headers], + rows = [tableAttachment rows]; + + var numCols = [headers count]; + if (numCols == 0 && [rows count] > 0) + numCols = [[rows objectAtIndex:0] count]; + + 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 < [rowData count]) { + cellText = [rowData objectAtIndex:c]; + } + 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 ([headers count] > 0) { + tableRTF += writeRow(headers, YES); + } + for (var r = 0; r < [rows count]; r++) { + tableRTF += writeRow([rows objectAtIndex:r], NO); + } + + return tableRTF; + } + } + var result = "", headerString = "", trailerString = "", attribEnum, currAttrib; - // --- DUCK-TYPING PROTOCOL DETECTION --- - // Safely scan all incoming attributes for an object supporting headers & rows. - // If found, completely divert processing to native RTF table construction. - var keys = [attributes allKeys], - count = [keys count], - tableObject = nil; - - for (var i = 0; i < count; i++) - { - var key = [keys objectAtIndex:i], - val = [attributes objectForKey:key]; - - if (val && typeof val.respondsToSelector === "function" && - [val respondsToSelector:@selector(headers)] && - [val respondsToSelector:@selector(rows)]) - { - tableObject = val; - break; - } - } - - if (tableObject) - { - return [self rtfStringForTable:tableObject]; - } - // -------------------------------------- - if (first) { var paraStyle = [attributes objectForKey:CPParagraphStyleAttributeName]; @@ -520,10 +458,10 @@ function _points2twips(a) { return (a) * 20.0; } } /* - * Analyze attributes of current run + * 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 + * 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. */ @@ -534,7 +472,7 @@ function _points2twips(a) { return (a) * 20.0; } if ([currAttrib isEqualToString:CPFontAttributeName]) { /* - * Handle fonts + * handle fonts */ var font, fontName, @@ -545,13 +483,13 @@ function _points2twips(a) { return (a) * 20.0; } traits = [[CPFontManager sharedFontManager] traitsOfFont:font]; /* - * Font name + * font name */ if (currentFont == nil || ![fontName isEqualToString:[currentFont familyName]]) headerString += [self fontToken:fontName]; /* - * Font size + * font size */ if (currentFont == nil || [font size] != [currentFont size]) { @@ -562,7 +500,7 @@ function _points2twips(a) { return (a) * 20.0; } headerString += pString; } /* - * Font attributes + * font attributes */ if (traits & CPItalicFontMask) { @@ -652,8 +590,6 @@ function _points2twips(a) { return (a) * 20.0; } 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) { @@ -691,8 +627,7 @@ function _points2twips(a) { return (a) * 20.0; } completeRange = CPMakeRange(0, length), first = YES; - // FIXME split along newline characters and run as outer loop - while (CPMaxRange(currRange) < CPMaxRange(completeRange)) // Save all "runs" + while (CPMaxRange(currRange) < CPMaxRange(completeRange)) // save all "runs" { var attributes, substring, @@ -725,9 +660,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]; @@ -737,5 +669,4 @@ function _points2twips(a) { return (a) * 20.0; } output += trailerString; return output; } - @end diff --git a/AppKit/CPTextView/_CPTableTextAttachment.j b/AppKit/CPTextView/_CPTableTextAttachment.j new file mode 100644 index 000000000..00b671aac --- /dev/null +++ b/AppKit/CPTextView/_CPTableTextAttachment.j @@ -0,0 +1,332 @@ +/* _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 +@import +@import +@import + +@implementation _CPTableTextAttachment : CPView +{ + CPArray _headers; + CPArray _rows; +} + +- (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows +{ + return [self initWithHeaders:headers rows:rows width:500.0]; +} + +- (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows width:(float)totalWidth +{ + self = [super initWithFrame:CGRectMake(0, 0, totalWidth, 20)]; + if (self) + { + _headers = headers; + _rows = rows; + + var numCols = [headers count]; + + if (numCols == 0 && [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"; + } + + // Initialize header cells + if ([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 + 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]; + } + return self; +} + +- (CPArray)headers +{ + return _headers; +} + +- (CPArray)rows +{ + return _rows; +} + +- (CPView)viewForWidth:(float)width +{ + [self resizeToWidth:width]; + return self; +} + +- (CPView)createCellWithText:(CPString)text frame:(CGRect)frame isHeader:(BOOL)isHeader +{ + var initialWidth = (frame.size.width > 0) ? frame.size.width : 120.0; + var initialHeight = (frame.size.height > 0) ? frame.size.height : 28.0; + + var cellContainer = [[CPView alloc] initWithFrame:CGRectMake(frame.origin.x, frame.origin.y, initialWidth, initialHeight)]; + [cellContainer setBackgroundColor:isHeader ? [CPColor colorWithWhite:0.92 alpha:1.0] : [CPColor whiteColor]]; + + // Bottom and right edge borders to construct the grid + var borderView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, initialWidth, initialHeight)]; + [borderView setBackgroundColor:[CPColor clearColor]]; + [borderView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + if (borderView._DOMElement) { + borderView._DOMElement.style.borderBottom = "1px solid #e0e0e0"; + borderView._DOMElement.style.borderRight = "1px solid #e0e0e0"; + borderView._DOMElement.style.boxSizing = "border-box"; + } + [cellContainer addSubview:borderView]; + + var textContainer = [[CPTextContainer alloc] initWithContainerSize:CGSizeMake(initialWidth - 8, 1e7)]; + var textView = [[CPTextView alloc] initWithFrame:CGRectMake(4, 2, initialWidth - 8, initialHeight - 4) textContainer:textContainer]; + [textView setEditable:NO]; + [textView setSelectable:YES]; + [textView setBackgroundColor:[CPColor clearColor]]; + [textView setVerticallyResizable:YES]; + [textView setHorizontallyResizable:NO]; + [[textView textContainer] setWidthTracksTextView:YES]; + + // Construct cell text using standard fonts + var cellFont = isHeader ? [CPFont boldSystemFontOfSize:11.0] : [CPFont systemFontOfSize:11.0]; + var parsedText = [[CPAttributedString alloc] initWithString:text attributes:@{ + CPFontAttributeName: cellFont, + CPForegroundColorAttributeName: [CPColor blackColor] + }]; + + var storage = [textView textStorage]; + if (storage && [storage respondsToSelector:@selector(setAttributedString:)]) { + [storage setAttributedString:parsedText]; + } else { + [textView setEditable:YES]; + [textView setString:@""]; + [textView insertText:parsedText]; + [textView setEditable:NO]; + } + + [cellContainer addSubview:textView]; + return cellContainer; +} + +- (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 +{ + var numCols = [_headers count]; + if (numCols == 0 && [_rows count] > 0) { + numCols = [[_rows objectAtIndex:0] count]; + } + if (numCols == 0) 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]; + + [measureTextField setStringValue:cellText]; + [measureTextField sizeToFit]; + var naturalW = CGRectGetWidth([measureTextField frame]) + 24.0; + if (naturalW > colNaturalWidths[colIndex]) { + colNaturalWidths[colIndex] = naturalW; + } + + var words = cellText.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; + } + }; + + for (var c = 0; c < [_headers count]; c++) { + measureCell([_headers objectAtIndex:c], YES, c); + } + + 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]; + } + measureCell(cellText, NO, c); + } + } + + var totalMinWidth = 0.0; + for (var c = 0; c < numCols; c++) { + totalMinWidth += colMinWidths[c]; + } + + var colWidths = []; + + if (newWidth <= totalMinWidth) { + var remainingWidth = newWidth; + for (var c = 0; c < numCols; c++) { + var w = Math.floor((colMinWidths[c] / totalMinWidth) * newWidth); + colWidths[c] = w; + remainingWidth -= w; + } + if (numCols > 0) colWidths[numCols - 1] += remainingWidth; + } else { + for (var c = 0; c < numCols; c++) { + colWidths[c] = colMinWidths[c]; + } + + var totalGrowthCapacity = 0.0; + var growthCapacities = []; + for (var c = 0; c < numCols; c++) { + var capacity = Math.max(0.0, colNaturalWidths[c] - colMinWidths[c]); + growthCapacities[c] = capacity; + totalGrowthCapacity += capacity; + } + + var extraWidth = newWidth - totalMinWidth; + var remainingExtra = extraWidth; + + for (var c = 0; c < numCols; c++) { + if (totalGrowthCapacity > 0) { + var w = Math.floor((growthCapacities[c] / totalGrowthCapacity) * extraWidth); + colWidths[c] += w; + remainingExtra -= w; + } + } + if (numCols > 0) { + colWidths[numCols - 1] += remainingExtra; + } + } + + var cellIndex = 0; + var currentY = 0; + + var layoutRow = function(startIndex) { + var maxCellHeight = 28.0; + + for (var c = 0; c < numCols; c++) { + var idx = startIndex + c; + if (idx < [subviews count]) { + var cellView = [subviews objectAtIndex:idx]; + var textView = [self getTextViewFromCell:cellView]; + if (textView) { + var targetWidth = Math.max(10.0, colWidths[c] - 8); + [[textView textContainer] setContainerSize:CGSizeMake(targetWidth, 1e7)]; + + var usedRect = [[textView layoutManager] usedRectForTextContainer:[textView textContainer]]; + var wrappedHeight = CGRectGetHeight(usedRect) + 12.0; + if (wrappedHeight > maxCellHeight) { + maxCellHeight = wrappedHeight; + } + } + } + } + + var currentX = 0; + for (var c = 0; c < numCols; c++) { + var idx = startIndex + c; + if (idx < [subviews count]) { + var cellView = [subviews objectAtIndex:idx]; + [cellView setFrame:CGRectMake(currentX, currentY, colWidths[c], maxCellHeight)]; + + var textView = [self getTextViewFromCell:cellView]; + if (textView) { + var targetWidth = Math.max(10.0, colWidths[c] - 8); + var textY = 4.0; + var finalTextViewHeight = maxCellHeight - 8.0; + [textView setFrame:CGRectMake(4, textY, targetWidth, finalTextViewHeight)]; + } + + var cellSubviews = [cellView subviews]; + if ([cellSubviews count] > 0) { + [[cellSubviews objectAtIndex:0] setFrame:CGRectMake(0, 0, colWidths[c], maxCellHeight)]; + } + } + currentX += colWidths[c]; + } + + return maxCellHeight; + }; + + if ([_headers count] > 0) { + var headerHeight = layoutRow(cellIndex); + cellIndex += numCols; + currentY += headerHeight; + } + + for (var r = 0; r < [_rows count]; r++) { + var rowHeight = layoutRow(cellIndex); + cellIndex += numCols; + currentY += rowHeight; + } + + [self setFrameSize:CGSizeMake(newWidth, currentY)]; +} + +@end From 5897d76c0449efc2970641bcad3bc8608d52deaf Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 13 Jun 2026 22:38:36 +0200 Subject: [PATCH 16/46] fixed: imports --- AppKit/CPTextView/_CPTableTextAttachment.j | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AppKit/CPTextView/_CPTableTextAttachment.j b/AppKit/CPTextView/_CPTableTextAttachment.j index 00b671aac..8f56f684e 100644 --- a/AppKit/CPTextView/_CPTableTextAttachment.j +++ b/AppKit/CPTextView/_CPTableTextAttachment.j @@ -19,9 +19,9 @@ */ -@import -@import -@import +@import "CPView.j" +@import "CPTextView.j" +@import "CPTextField.j" @import @implementation _CPTableTextAttachment : CPView From 1cad83aba8b7812064a06ea1f55478c84313b115 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 14 Jun 2026 21:06:51 +0200 Subject: [PATCH 17/46] initial display of the table --- AppKit/CPTextView/CPLayoutManager.j | 179 ++++++++++++------- AppKit/CPTextView/CPTextView.j | 24 ++- AppKit/CPTextView/_CPTableTextAttachment.j | 193 ++++++++++++++------- Tests/Manual/CPTextView/AppController.j | 34 +++- 4 files changed, 297 insertions(+), 133 deletions(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index fd0accf43..22fb6ba19 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -292,7 +292,6 @@ _oncontextmenuhandler = function () { return false; }; // We erased all lines if (!startIndex) [self setExtraLineFragmentRect:CGRectMake(0, 0) usedRect:CGRectMake(0, 0) textContainer:nil]; - // document.title=startIndex; [_typesetter layoutGlyphsInLayoutManager:self startingAtGlyphIndex:startIndex maxNumberOfLineFragments:-1 nextGlyphIndex:nil]; @@ -532,20 +531,26 @@ _oncontextmenuhandler = function () { return false; }; var frames = [fragment glyphFrames], len = fragment._range.length; - for (var j = 0; j < len; j++) + if (frames) { - if (CGRectContainsPoint(frames[j], point)) - { - if (partialFraction) - partialFraction[0] = (point.x - frames[j].origin.x) / frames[j].size.width; + var maxLen = MIN(len, frames.length); - return fragment._range.location + j; + for (var j = 0; j < maxLen; j++) + { + var frame = frames[j]; + + if (frame && CGRectContainsPoint(frame, point)) + { + if (partialFraction) + partialFraction[0] = (point.x - frame.origin.x) / frame.size.width; + + return fragment._range.location + j; + } } } } } - // Not found, maybe a point left to the last character was clicked -> search again with broader constraints if ([[_textStorage string] length]) { for (var i = 0; i < c; i++) @@ -554,30 +559,33 @@ _oncontextmenuhandler = function () { return false; }; if (fragment._textContainer === container) { - // Within the horizontal territory of the current (not-empty) line? - if (fragment._range.length > 0 && point.y > fragment._fragmentRect.origin.y && - point.y <= fragment._fragmentRect.origin.y + fragment._fragmentRect.size.height) + if (fragment._range.length > 0 && point.y > fragment._fragmentRect.origin.y && + point.y <= fragment._fragmentRect.origin.y + fragment._fragmentRect.size.height) + { + if (i < c - 1 && _lineFragments[i + 1]._fragmentRect.origin.y === fragment._fragmentRect.origin.y) + continue; + + var nlLoc = CPMaxRange(fragment._range), + frames = [fragment glyphFrames]; + + if (frames && frames.length > 0) { - // Skip tabs and move on the last fragment in this line - if (i < c - 1 && _lineFragments[i + 1]._fragmentRect.origin.y === fragment._fragmentRect.origin.y) - continue; + var lastFrame = frames[frames.length - 1], + firstFrame = frames[0]; - var nlLoc = CPMaxRange(fragment._range), - lastFrame = [fragment glyphFrames][fragment._range.length - 1], - firstFrame = [fragment glyphFrames][0]; + if (lastFrame && firstFrame) + { + if (_isNewlineCharacter([[_textStorage string] characterAtIndex:nlLoc > 0 ? nlLoc - 1 : 0])) + nlLoc--; - // stay on the line the newline character belongs to - if (_isNewlineCharacter([[_textStorage string] characterAtIndex:nlLoc > 0 ? nlLoc - 1 : 0])) - nlLoc--; - - // Clicked right to the last character - if (point.x > CGRectGetMaxX(lastFrame)) - return nlLoc; - // Clicked left to the last character - else if (point.x <= CGRectGetMinX(firstFrame)) - return fragment._range.location; - else - return nlLoc; + if (point.x > CGRectGetMaxX(lastFrame)) + return nlLoc; + else if (point.x <= CGRectGetMinX(firstFrame)) + return fragment._range.location; + else + return nlLoc; + } + } } } } @@ -707,7 +715,11 @@ _oncontextmenuhandler = function () { return false; }; var index = location - lineFragment._range.location; - return lineFragment._glyphsOffsets[index]; + if (index < 0 || !lineFragment._glyphsOffsets || index >= lineFragment._glyphsOffsets.length) + return 0.0; + + var offset = lineFragment._glyphsOffsets[index]; + return (offset === undefined) ? 0.0 : offset; } - (double)_descentAtLocation:(unsigned)location @@ -719,7 +731,11 @@ _oncontextmenuhandler = function () { return false; }; var index = location - lineFragment._range.location; - return lineFragment._glyphsFrames[index]._descent; + if (index < 0 || !lineFragment._glyphsFrames || index >= lineFragment._glyphsFrames.length) + return 0.0; + + var frame = lineFragment._glyphsFrames[index]; + return (frame && frame._descent !== undefined) ? frame._descent : 0.0; } - (void)setLineFragmentRect:(CGRect)fragmentRect forGlyphRange:(CPRange)glyphRange usedRect:(CGRect)usedRect @@ -843,11 +859,16 @@ _oncontextmenuhandler = function () { return false; }; { if (_lineFragments.length > 0 && index >= [self numberOfGlyphs] - 1) { - var lineFragment= _lineFragments[_lineFragments.length - 1], + var lineFragment = _lineFragments[_lineFragments.length - 1], glyphFrames = [lineFragment glyphFrames]; - if (glyphFrames.length > 0) - return CGPointCreateCopy(glyphFrames[glyphFrames.length - 1].origin); + if (glyphFrames && glyphFrames.length > 0) + { + var frame = glyphFrames[glyphFrames.length - 1]; + + if (frame) + return CGPointCreateCopy(frame.origin); + } } var lineFragment = _objectWithLocationInRange(_lineFragments, index); @@ -858,8 +879,17 @@ _oncontextmenuhandler = function () { return false; }; return CGPointCreateCopy(lineFragment._location); var glyphFrames = [lineFragment glyphFrames]; + var relativeIndex = index - lineFragment._range.location; - return CGPointCreateCopy(glyphFrames[index - lineFragment._range.location].origin); + if (glyphFrames && relativeIndex >= 0 && relativeIndex < glyphFrames.length) + { + var frame = glyphFrames[relativeIndex]; + + if (frame) + return CGPointCreateCopy(frame.origin); + } + + return CGPointCreateCopy(lineFragment._location); } return CGPointMakeZero(); @@ -905,7 +935,6 @@ _oncontextmenuhandler = function () { return false; }; inTextContainer:(CPTextContainer)container rectCount:(CGRectPointer)rectCount { - var rectArray = [], lineFragments = _objectsInRange(_lineFragments, selectedCharRange); @@ -924,21 +953,29 @@ _oncontextmenuhandler = function () { return false; }; rect = nil, len = fragment._range.length; - for (var j = 0; j < len; j++) + if (frames) { - if (CPLocationInRange(fragment._range.location + j, selectedCharRange)) + for (var j = 0; j < len; j++) { - var correctedRect = CGRectCreateCopy(frames[j]); - correctedRect.size.height -= frames[j]._descent; - correctedRect.origin.y -= frames[j]._descent; + if (j < frames.length && CPLocationInRange(fragment._range.location + j, selectedCharRange)) + { + var frame = frames[j]; - if (!rect) - rect = CGRectCreateCopy(correctedRect); - else - rect = CGRectUnion(rect, correctedRect); + if (frame) + { + var correctedRect = CGRectCreateCopy(frame); + correctedRect.size.height -= frame._descent; + correctedRect.origin.y -= frame._descent; - if (_isNewlineCharacter([[_textStorage string] characterAtIndex:MAX(0, CPMaxRange(selectedCharRange) - 1)])) - rect.size.width = containerSize.width - rect.origin.x; + if (!rect) + rect = CGRectCreateCopy(correctedRect); + else + rect = CGRectUnion(rect, correctedRect); + + if (_isNewlineCharacter([[_textStorage string] characterAtIndex:MAX(0, CPMaxRange(selectedCharRange) - 1)])) + rect.size.width = containerSize.width - rect.origin.x; + } + } } } @@ -949,7 +986,7 @@ _oncontextmenuhandler = function () { return false; }; var len = rectArray.length; - for (var i = 0; i < len - 1; i++) // extend the width of all but the last one + for (var i = 0; i < len - 1; i++) { if (FLOOR(CGRectGetMaxY(rectArray[i])) == FLOOR(CGRectGetMaxY(rectArray[i + 1]))) continue; @@ -1182,13 +1219,9 @@ var _objectsInRange = function(aList, aRange) { 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}; } _runs.push(run); - } } else { @@ -1269,13 +1302,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 +1319,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 +1336,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 +1358,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); } @@ -1373,12 +1416,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; } } diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index cfde9194a..16151f9e0 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -401,6 +401,7 @@ var kDelegateRespondsTo_textShouldBeginEditing - (void)superviewFrameChanged:(CPNotification)aNotification { _exposedRect = nil; + [self sizeToFit]; } - (void)viewWillMoveToSuperview:(CPView)aView @@ -1397,7 +1398,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 +1423,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 @@ -2134,9 +2141,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 { @@ -2851,11 +2864,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]]; diff --git a/AppKit/CPTextView/_CPTableTextAttachment.j b/AppKit/CPTextView/_CPTableTextAttachment.j index 8f56f684e..5984ab11f 100644 --- a/AppKit/CPTextView/_CPTableTextAttachment.j +++ b/AppKit/CPTextView/_CPTableTextAttachment.j @@ -16,8 +16,7 @@ * 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" @@ -28,6 +27,7 @@ { CPArray _headers; CPArray _rows; + BOOL _isResizing; } - (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows @@ -42,30 +42,41 @@ { _headers = headers; _rows = rows; + _isResizing = NO; - var numCols = [headers count]; + [self _rebuildTableWithWidth:totalWidth]; + } - if (numCols == 0 && [rows count] > 0) - numCols = [[rows objectAtIndex:0] count]; + return self; +} - // 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"; +- (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 header cells - if ([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 - for (var r = 0; r < [rows count]; r++) { - var rowData = [rows objectAtIndex:r]; + } + + // 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 = @""; @@ -76,10 +87,53 @@ [self addSubview:cellView]; } } - - [self resizeToWidth:totalWidth]; } - return self; + + [self resizeToWidth:totalWidth]; + + // Trigger a parent layout manager re-layout once the table is fully reconstructed and sized. + 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 @@ -94,6 +148,7 @@ - (CPView)viewForWidth:(float)width { + // Always call resize to ensure cell views are constructed, but do not guard here [self resizeToWidth:width]; return self; } @@ -110,7 +165,9 @@ var borderView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, initialWidth, initialHeight)]; [borderView setBackgroundColor:[CPColor clearColor]]; [borderView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; - if (borderView._DOMElement) { + + if (borderView._DOMElement) + { borderView._DOMElement.style.borderBottom = "1px solid #e0e0e0"; borderView._DOMElement.style.borderRight = "1px solid #e0e0e0"; borderView._DOMElement.style.boxSizing = "border-box"; @@ -119,29 +176,18 @@ var textContainer = [[CPTextContainer alloc] initWithContainerSize:CGSizeMake(initialWidth - 8, 1e7)]; var textView = [[CPTextView alloc] initWithFrame:CGRectMake(4, 2, initialWidth - 8, initialHeight - 4) textContainer:textContainer]; - [textView setEditable:NO]; + [textView setEditable:YES]; [textView setSelectable:YES]; [textView setBackgroundColor:[CPColor clearColor]]; [textView setVerticallyResizable:YES]; [textView setHorizontallyResizable:NO]; [[textView textContainer] setWidthTracksTextView:YES]; - // Construct cell text using standard fonts + // Configure cell text style using standard CPTextView APIs var cellFont = isHeader ? [CPFont boldSystemFontOfSize:11.0] : [CPFont systemFontOfSize:11.0]; - var parsedText = [[CPAttributedString alloc] initWithString:text attributes:@{ - CPFontAttributeName: cellFont, - CPForegroundColorAttributeName: [CPColor blackColor] - }]; - - var storage = [textView textStorage]; - if (storage && [storage respondsToSelector:@selector(setAttributedString:)]) { - [storage setAttributedString:parsedText]; - } else { - [textView setEditable:YES]; - [textView setString:@""]; - [textView insertText:parsedText]; - [textView setEditable:NO]; - } + [textView setFont:cellFont]; + [textView setTextColor:[CPColor blackColor]]; + [textView setString:text]; [cellContainer addSubview:textView]; return cellContainer; @@ -161,11 +207,19 @@ - (void)resizeToWidth:(float)newWidth { - var numCols = [_headers count]; - if (numCols == 0 && [_rows count] > 0) { + if (_isResizing) + return; + + _isResizing = YES; + + var numCols = _headers ? [_headers count] : 0; + if (numCols == 0 && _rows && [_rows count] > 0) { numCols = [[_rows objectAtIndex:0] count]; } - if (numCols == 0) return; + if (numCols == 0) { + _isResizing = NO; + return; + } var subviews = [self subviews]; var colNaturalWidths = []; @@ -206,18 +260,22 @@ } }; - for (var c = 0; c < [_headers count]; c++) { - measureCell([_headers objectAtIndex:c], YES, c); + if (_headers) { + for (var c = 0; c < [_headers count]; c++) { + measureCell([_headers objectAtIndex:c], YES, c); + } } - 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]; + 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]; + } + measureCell(cellText, NO, c); } - measureCell(cellText, NO, c); } } @@ -279,8 +337,9 @@ var targetWidth = Math.max(10.0, colWidths[c] - 8); [[textView textContainer] setContainerSize:CGSizeMake(targetWidth, 1e7)]; - var usedRect = [[textView layoutManager] usedRectForTextContainer:[textView textContainer]]; - var wrappedHeight = CGRectGetHeight(usedRect) + 12.0; + var layoutManager = [textView layoutManager]; + var usedRect = layoutManager ? [layoutManager usedRectForTextContainer:[textView textContainer]] : nil; + var wrappedHeight = (usedRect ? CGRectGetHeight(usedRect) : 0.0) + 12.0; if (wrappedHeight > maxCellHeight) { maxCellHeight = wrappedHeight; } @@ -314,19 +373,29 @@ return maxCellHeight; }; - if ([_headers count] > 0) { + if (_headers && [_headers count] > 0) { var headerHeight = layoutRow(cellIndex); cellIndex += numCols; currentY += headerHeight; } - for (var r = 0; r < [_rows count]; r++) { - var rowHeight = layoutRow(cellIndex); - cellIndex += numCols; - currentY += rowHeight; + if (_rows) { + for (var r = 0; r < [_rows count]; r++) { + var rowHeight = layoutRow(cellIndex); + cellIndex += numCols; + currentY += rowHeight; + } } - [self setFrameSize:CGSizeMake(newWidth, currentY)]; + // GUARD FRAME SIZE MUTATIONS: Only execute setFrameSize if dimensions actually change. + // This stops infinite layout passes while ensuring subviews are laid out. + var currentSize = [self frame].size; + if (ABS(currentSize.width - newWidth) > 0.1 || ABS(currentSize.height - currentY) > 0.1) + { + [self setFrameSize:CGSizeMake(newWidth, currentY)]; + } + + _isResizing = NO; } @end diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index 7abb6d277..f35d0730c 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -14,6 +14,8 @@ @import @import @import +@import +@import @implementation AppController : CPObject { @@ -90,6 +92,25 @@ [_textView insertText:@" "]; } +- (void)insertTable:(id)sender +{ + var headers = [@"Item Description", @"Quantity", @"Unit Price"]; + var rows = [ + [@"Cappuccino Web Framework Lic.", @"2", @"$199.00"], + [@"Objective-J Development Support", @"5", @"$150.00"], + [@"Cloud Compilation VM Server", @"1", @"$49.00"] + ]; + + var tableAttachment = [[_CPTableTextAttachment alloc] initWithHeaders:headers rows:rows width:500.0]; + + // Insert single-character atomic text attachment + var tableAttrStr = [CPTextStorage attributedStringWithAttachment:tableAttachment]; + + [_textView insertText:@"\ntable (own line)\n"]; + [_textView insertText:tableAttrStr]; + [_textView insertText:@"\nend table"]; +} + - (void)applicationDidFinishLaunching:(CPNotification)aNotification { var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask], @@ -145,6 +166,14 @@ [toolbarView addSubview:attachButton]; currentX += 150; + // Add Table Trigger + var tableButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 100, 30)]; + [tableButton setTitle:@"Add Table"]; + [tableButton setTarget:self]; + [tableButton setAction:@selector(insertTable:)]; + [toolbarView addSubview:tableButton]; + currentX += 110; + // Text Alignment Group var labelAlign = [[CPTextField alloc] initWithFrame:CGRectMake(currentX, 22, 45, 20)]; [labelAlign setStringValue:@"Align:"]; @@ -210,9 +239,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]; @@ -321,6 +351,8 @@ [_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"]; + [theWindow orderFront:self]; [CPMenu setMenuBarVisible:YES]; } From 49b18000278509e62af9fb97233762f233cc60f2 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 14 Jun 2026 22:42:33 +0200 Subject: [PATCH 18/46] improved: table generation --- AppKit/CPTextView/_CPRTFParser.j | 56 ++++++++++- AppKit/CPTextView/_CPRTFProducer.j | 145 ++++++++++++++++++++++++----- 2 files changed, 173 insertions(+), 28 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 4b730d62b..17000ca7e 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -205,8 +205,12 @@ var cp1252Map = { if (bgColour) [ret setObject:bgColour forKey:CPBackgroundColorAttributeName]; + if (underline) + [ret setObject:[CPNumber numberWithInt:1] forKey:CPUnderlineStyleAttributeName]; + return ret; } + @end @@ -387,7 +391,13 @@ var kRgsymRtf = { [self _flushCurrentRun]; _currentRun = state.run; _currentRun._range = CPMakeRange([_result length], 0); + + if (_curState == 0) + { + _parsingFontTable = NO; + } } + return YES; } @@ -622,6 +632,23 @@ var kRgsymRtf = { case "paperh": _paper.height = param; 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; } return ''; @@ -981,11 +1008,32 @@ var kRgsymRtf = { lastchar = 0; if (_curState == 0) + { [self _appendPlainString:tmp]; - else if (tmp !== ';') - _freename += tmp; - - break; + } + else + { + if (tmp === ';') + { + if (_parsingFontTable && _freename) + { + var cleanFontName = _freename.trim(); + // strip family name prefix if present (e.g., "swiss Helvetica" -> "Helvetica") + var lastSpaceIdx = cleanFontName.lastIndexOf(' '); + if (lastSpaceIdx !== -1) + { + cleanFontName = cleanFontName.substring(lastSpaceIdx + 1); + } + _fontArray.push(cleanFontName); + _freename = ""; + } + } + else + { + _freename += tmp; + } + } + break; } } diff --git a/AppKit/CPTextView/_CPRTFProducer.j b/AppKit/CPTextView/_CPRTFProducer.j index 05e082636..b10a11000 100644 --- a/AppKit/CPTextView/_CPRTFProducer.j +++ b/AppKit/CPTextView/_CPRTFProducer.j @@ -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 @@ -382,18 +382,104 @@ function _points2twips(a) { return (a) * 20.0; } attributes:(CPDictionary) attributes paragraphStart:(BOOL) first { - var tableAttachment = [attributes objectForKey:CPAttachmentAttributeName]; - if (!tableAttachment) + var unwrap = function(obj) { + if (!obj) return null; + 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]; + } - if (tableAttachment && [tableAttachment isKindOfClass:[_CPTableTextAttachment class]]) + tableAttachment = unwrap(tableAttachment); + + // Ultimate fallback scanner for layout character placeholder sequences + if (!tableAttachment && (substring === "\uFFFC" || substring === "")) { - var headers = [tableAttachment headers], - rows = [tableAttachment rows]; + var keys = [attributes allKeys], + count = [keys count]; - var numCols = [headers count]; - if (numCols == 0 && [rows count] > 0) - numCols = [[rows objectAtIndex:0] 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; + + 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) { @@ -416,9 +502,14 @@ function _points2twips(a) { return (a) * 20.0; } } for (var c = 0; c < numCols; c++) { var cellText = ""; - if (c < [rowData count]) { - cellText = [rowData objectAtIndex:c]; + if (c < getCount(rowData)) { + cellText = getObjectAtIndex(rowData, c); } + if (cellText === null || cellText === undefined) { + cellText = ""; + } + + cellText = String(cellText); cellText = cellText.replace(/\\/g, '\\\\'); cellText = cellText.replace(/{/g, '\\{'); cellText = cellText.replace(/}/g, '\\}'); @@ -434,11 +525,12 @@ function _points2twips(a) { return (a) * 20.0; } return rowRTF; }; - if ([headers count] > 0) { + if (getCount(headers) > 0) { tableRTF += writeRow(headers, YES); } - for (var r = 0; r < [rows count]; r++) { - tableRTF += writeRow([rows objectAtIndex:r], NO); + var rowCount = getCount(rows); + for (var r = 0; r < rowCount; r++) { + tableRTF += writeRow(getObjectAtIndex(rows, r), NO); } return tableRTF; @@ -625,7 +717,7 @@ function _points2twips(a) { return (a) * 20.0; } length = [string length], currRange = CPMakeRange(loc, 0), completeRange = CPMakeRange(0, length), - first = YES; + paragraphStart = YES; while (CPMaxRange(currRange) < CPMaxRange(completeRange)) // save all "runs" { @@ -639,10 +731,15 @@ 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; + + // Dynamically compute paragraph boundaries based on standard line feeds + if (substring.length > 0 && substring.charAt(substring.length - 1) === '\n') + paragraphStart = YES; + else + paragraphStart = NO; } return result; From 5dc66c5b4e91365d7acab20f0db2bb0288b030d6 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 14 Jun 2026 22:56:23 +0200 Subject: [PATCH 19/46] fixed: roundtrip --- AppKit/CPTextView/_CPRTFParser.j | 50 +++++++++++++++---------- Tests/Manual/CPTextView/AppController.j | 42 ++++++++++++++++++++- 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 17000ca7e..00bd18053 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -36,6 +36,7 @@ @global CPBackgroundColorAttributeName @global CPParagraphStyleAttributeName @global CPAttachmentAttributeName +@global CPUnderlineStyleAttributeName @global CPLeftTabStopType @global CPRightTabStopType @@ -210,7 +211,6 @@ var cp1252Map = { return ret; } - @end @@ -348,6 +348,9 @@ var kRgsymRtf = { _tableRows = nil; _currentRow = nil; _currentCellText = ""; + + // Safe Initialization + _currentRun = [_RTFAttribute new]; } return self; @@ -390,6 +393,13 @@ var kRgsymRtf = { [self _flushCurrentRun]; _currentRun = state.run; + + // Safety guard to prevent setting properties on null + if (!_currentRun) + { + _currentRun = [_RTFAttribute new]; + } + _currentRun._range = CPMakeRange([_result length], 0); if (_curState == 0) @@ -397,7 +407,6 @@ var kRgsymRtf = { _parsingFontTable = NO; } } - return YES; } @@ -609,6 +618,23 @@ var kRgsymRtf = { break; + case "ul": // underline + if (param === 0) + { + if (_currentRun && _currentRun.underline) + [self _flushCurrentRun]; + + _currentRun.underline = NO; + } + else + { + if (_currentRun && !_currentRun.underline) + [self _flushCurrentRun]; + + _currentRun.underline = YES; + } + break; + case "qc": // paragraph center [_currentRun.paragraph setAlignment:CPCenterTextAlignment]; break; @@ -632,23 +658,6 @@ var kRgsymRtf = { case "paperh": _paper.height = param; 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; } return ''; @@ -1033,7 +1042,8 @@ var kRgsymRtf = { _freename += tmp; } } - break; + + break; } } diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index f35d0730c..a34d50663 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -154,7 +154,7 @@ 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; @@ -366,4 +366,44 @@ [_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]; + + // 5. Replace the editor's text storage with the round-tripped version + var editorStorage = [_textView textStorage]; + if (editorStorage && [editorStorage respondsToSelector:@selector(setAttributedString:)]) + { + [editorStorage setAttributedString:roundTrippedString]; + } + else + { + // Safe fallback sequence + [_textView setEditable:YES]; + [_textView setString:@""]; + [_textView insertText:roundTrippedString]; + [_textView setEditable:NO]; + } + + // 6. Force a layout pass and render update + [_textView setNeedsDisplay:YES]; + [_textView2 setNeedsDisplay:YES]; +} @end From 1b23bb8a67845ca829ef0b1310368904d5930995 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 15 Jun 2026 16:43:58 +0200 Subject: [PATCH 20/46] new: CPMarkdownParser --- AppKit/CPTextView/_CPRTFParser.j | 293 ++++++++++++++++++++++++ Tests/Manual/CPTextView/AppController.j | 47 +++- 2 files changed, 336 insertions(+), 4 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 00bd18053..9cc99df34 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -1053,3 +1053,296 @@ var kRgsymRtf = { } @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]; + + // Tabellen-Erkennung + 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]; + } + + // 1. Zuerst die absolute Summe der Natural-Breiten zur Spalten-Proportionsbestimmung ermitteln + 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; + + // Headers prüfen + if (c < headers.length) { + var parsedText = [self parseInlineMarkdown:headers[c] isHeader:YES headerLevel:3]; + [measureTextField setStringValue:[parsedText string]]; + [measureTextField sizeToFit]; + cellW = Math.max(cellW, CGRectGetWidth([measureTextField frame]) + 24.0); + } + + // Reihen prüfen + for (var r = 0; r < [rows count]; r++) { + var rowData = [rows objectAtIndex:r]; + if (c < [rowData count]) { + var parsedText = [self parseInlineMarkdown:rowData[c] isHeader:NO headerLevel:3]; + [measureTextField setStringValue:[parsedText string]]; + [measureTextField sizeToFit]; + cellW = Math.max(cellW, CGRectGetWidth([measureTextField frame]) + 24.0); + } + } + colNaturalWidths[c] = cellW; + totalNaturalW += cellW; + } + + // 2. Präzise adaptive Zeilenhöhen-Schätzung für das Newline-Sizing (Verhindert zu große Abstände) + var estimatedHeight = 36.0; // Startwert für Header-Zeile mit Padding + for (var r = 0; r < [rows count]; r++) { + var rowData = [rows objectAtIndex:r]; + var maxCellHeight = 28.0; + + for (var c = 0; c < numCols; c++) { + var cellText = @""; + if (c < [rowData count]) { + cellText = [rowData objectAtIndex:c]; + } + var charCount = cellText.length; + + // Schätzung basierend auf realistischer Spaltenbreitenverteilung + var proportion = totalNaturalW > 0 ? (colNaturalWidths[c] / totalNaturalW) : (1.0 / numCols); + var estimatedColWidth = proportion * 500.0; + var charsPerLine = Math.max(10.0, Math.floor(estimatedColWidth / 6.5)); // ca. 6.5px pro Zeichen + + var estimatedLines = Math.ceil(charCount / charsPerLine); + if (estimatedLines < 1) estimatedLines = 1; + + var cellHeight = (estimatedLines * 16.0) + 12.0; + if (cellHeight > maxCellHeight) { + maxCellHeight = cellHeight; + } + } + estimatedHeight += maxCellHeight; + } + + // Berechne die benötigten Leerzeilen (\n Zeilenhöhe ist ca. 16px) + var lineCount = Math.ceil(estimatedHeight / 16.0) + 1; // Minimaler Sicherheitsabstand (+1) + var newlineStr = ""; + for (var nl = 0; nl < lineCount; nl++) { + newlineStr += "\n"; + } + + var tableAttrStr = [[CPMutableAttributedString alloc] initWithString:newlineStr]; + var matrixView = [[_CPTableTextAttachment alloc] initWithHeaders:headers rows:rows width:500.0]; + + [tableAttrStr addAttribute:@"TableAttachmentAttribute" value:matrixView range:CPMakeRange(0, [tableAttrStr length])]; + [tableAttrStr addAttribute:CPAttachmentAttributeName value:matrixView range:CPMakeRange(0, [tableAttrStr length])]; + [result appendAttributedString:tableAttrStr]; + continue; + } + + var isHeader = false; + var headerLevel = 0; + + // Überschriften (#) + var headerMatch = line.match(/^(#{1,6})\s+(.*)$/); + if (headerMatch) { + headerLevel = headerMatch[1].length; + line = headerMatch[2]; + isHeader = true; + } + + // Listenpunkte (- oder *) + var isListItem = false; + var listMatch = line.match(/^(\*|-)\s+(.*)$/); + if (listMatch) { + line = " • " + listMatch[2]; + isListItem = true; + } + + var parsedLine = [self parseInlineMarkdown:line isHeader:isHeader headerLevel:headerLevel]; + [result appendAttributedString:parsedLine]; + + if (i < lines.length - 1) { + [result appendAttributedString:[[CPAttributedString alloc] initWithString:@"\n"]]; + } + + i++; + } + + return result; +} + ++ (BOOL)isTableHeaderLine:(CPString)line +{ + var trimmed = line.trim(); + return trimmed.indexOf('|') !== -1; +} + ++ (BOOL)isTableSeparatorLine:(CPString)line +{ + var trimmed = line.trim(); + if (trimmed.indexOf('|') === -1) return NO; + var stripped = trimmed.replace(/[\s|:\-]/g, ''); + return stripped.length === 0; +} + ++ (BOOL)isTableRowLine:(CPString)line +{ + var trimmed = line.trim(); + return trimmed.indexOf('|') !== -1; +} + ++ (CPArray)parseTableCells:(CPString)line +{ + var parts = line.split('|'); + var cells = [CPMutableArray array]; + var startIdx = 0; + var endIdx = parts.length; + if (parts[0].trim() === "") startIdx = 1; + if (parts[parts.length - 1].trim() === "") endIdx = parts.length - 1; + + for (var j = startIdx; j < endIdx; j++) { + [cells addObject:parts[j].trim()]; + } + return cells; +} + ++ (CPAttributedString)parseInlineMarkdown:(CPString)text isHeader:(BOOL)isHeader headerLevel:(int)level +{ + var baseFontSize = 11.0; + var fontSize = baseFontSize; + var isBold = isHeader; + var isItalic = NO; + + if (isHeader) { + if (level == 1) fontSize = 15.0; + else if (level == 2) fontSize = 13.0; + else fontSize = 12.0; + } + + var result = [[CPMutableAttributedString alloc] initWithString:@""]; + var currentSegment = ""; + var i = 0; + var len = text.length; + + var defaultFont = [CPFont systemFontOfSize:fontSize]; + if (isBold) { + defaultFont = [CPFont boldSystemFontOfSize:fontSize]; + } + + while (i < len) { + if (i + 2 < len && text.substr(i, 3) === "***") { + if (currentSegment.length > 0) { + [result appendAttributedString:[self attributedStringWithText:currentSegment font:defaultFont bold:isBold italic:isItalic code:NO]]; + currentSegment = ""; + } + isBold = !isBold; + isItalic = !isItalic; + i += 3; + continue; + } + if (i + 1 < len && text.substr(i, 2) === "**") { + if (currentSegment.length > 0) { + [result appendAttributedString:[self attributedStringWithText:currentSegment font:defaultFont bold:isBold italic:isItalic code:NO]]; + currentSegment = ""; + } + isBold = !isBold; + i += 2; + continue; + } + if (text.charAt(i) === "*") { + if (currentSegment.length > 0) { + [result appendAttributedString:[self attributedStringWithText:currentSegment font:defaultFont bold:isBold italic:isItalic code:NO]]; + currentSegment = ""; + } + isItalic = !isItalic; + i++; + continue; + } + if (text.charAt(i) === "`") { + if (currentSegment.length > 0) { + [result appendAttributedString:[self attributedStringWithText:currentSegment font:defaultFont bold:isBold italic:isItalic code:NO]]; + currentSegment = ""; + } + var codeText = ""; + i++; + while (i < len && text.charAt(i) !== "`") { + codeText += text.charAt(i); + i++; + } + [result appendAttributedString:[self attributedStringWithText:codeText font:defaultFont bold:NO italic:NO code:YES]]; + i++; + continue; + } + + currentSegment += text.charAt(i); + i++; + } + + if (currentSegment.length > 0) { + [result appendAttributedString:[self attributedStringWithText:currentSegment font:defaultFont bold:isBold italic:isItalic code:NO]]; + } + + return result; +} + ++ (CPAttributedString)attributedStringWithText:(CPString)text font:(CPFont)baseFont bold:(BOOL)b italic:(BOOL)it code:(BOOL)c +{ + var fontName = [baseFont familyName]; + var fontSize = [baseFont size]; + var finalFont = baseFont; + + if (c) { + finalFont = [CPFont fontWithName:@"Courier" size:fontSize]; + } else { + finalFont = [CPFont _fontWithName:fontName size:fontSize bold:b italic:it]; + } + + if (!finalFont) { + finalFont = [CPFont systemFontOfSize:fontSize]; + } + + var dict = [CPDictionary dictionaryWithObjectsAndKeys: + finalFont, CPFontAttributeName, + [CPColor blackColor], CPForegroundColorAttributeName + ]; + return [[CPAttributedString alloc] initWithString:text attributes:dict]; +} + +@end diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index a34d50663..de1d27100 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -2,7 +2,7 @@ * AppController.j * * Manual test application for the cappuccino text system - * Copyright (C) 2014 Daniel Boehringer + * Copyright (C) 2026 Daniel Boehringer */ @import @@ -158,6 +158,14 @@ [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"]; @@ -255,7 +263,7 @@ // Right Container: RTF Plain-Text Source and Parser Window var rightLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 10, CGRectGetWidth([rightContainer bounds]) - 30, 20)]; - [rightLabel setStringValue:@"RTF Raw Output & Source Parser Window"]; + [rightLabel setStringValue:@"Markdown Source & RTF Source Code Window"]; [rightLabel setFont:[CPFont boldSystemFontOfSize:14]]; [rightLabel setAutoresizingMask:CPViewWidthSizable]; [rightContainer addSubview:rightLabel]; @@ -287,8 +295,6 @@ [editMenu addItemWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"Z"]; [mainMenu setSubmenu:editMenu forItem:item]; - item = [mainMenu insertItemWithTitle:@"Format" action:nil keyEquivalent:nil atIndex:0]; - // Format Menu item = [mainMenu insertItemWithTitle:@"Format" action:nil keyEquivalent:nil atIndex:0]; var formatMenu = [[CPMenu alloc] initWithTitle:@"Format Menu"]; @@ -353,6 +359,19 @@ [_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]; } @@ -406,4 +425,24 @@ [_textView setNeedsDisplay:YES]; [_textView2 setNeedsDisplay:YES]; } + +// 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]; + [_textView setEditable:NO]; +} + @end From 2a0cebb8516ba13cecb916beebb6302f325ba7b7 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 15 Jun 2026 19:42:53 +0200 Subject: [PATCH 21/46] improved: rtf roundtrip --- AppKit/CPTextView/_CPRTFParser.j | 19 ++++++++++++++++++- AppKit/CPTextView/_CPRTFProducer.j | 18 ++++++++++++++++-- Tests/Manual/CPTextView/AppController.j | 19 +++++-------------- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 9cc99df34..a68d7af5f 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -178,7 +178,15 @@ var cp1252Map = { - (void)addTab:(float)location type:(CPTextTabType)type { - var tab = [[CPTextTab alloc] initWithType:type + var alignment = CPLeftTextAlignment; + if (type === CPCenterTabStopType || type === CPCenterTextAlignment) + alignment = CPCenterTextAlignment; + else if (type === CPRightTabStopType || type === CPRightTextAlignment) + alignment = CPRightTextAlignment; + else if (type === CPDecimalTabStopType) + alignment = CPRightTextAlignment; // Fallback alignment for decimal tab + + var tab = [[CPTextTab alloc] initWithType:alignment location:location]; if (!_tabChanged) @@ -512,6 +520,9 @@ var kRgsymRtf = { { if (_tableRows && [_tableRows count] > 0) { + // Flush active character styling runs before appending table layout changes + [self _flushCurrentRun]; + var headers = [_tableRows objectAtIndex:0]; var rows = [CPMutableArray array]; for (var idx = 1; idx < [_tableRows count]; idx++) @@ -536,6 +547,12 @@ var kRgsymRtf = { [_result appendAttributedString:tableAttrStr]; + // Update range offset of active attributes tracker + if (_currentRun) + { + _currentRun._range = CPMakeRange([_result length], 0); + } + _tableRows = nil; _currentRow = nil; _currentCellText = ""; diff --git a/AppKit/CPTextView/_CPRTFProducer.j b/AppKit/CPTextView/_CPRTFProducer.j index b10a11000..8d06ad104 100644 --- a/AppKit/CPTextView/_CPRTFProducer.j +++ b/AppKit/CPTextView/_CPRTFProducer.j @@ -357,14 +357,21 @@ 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: + case CPLeftTextAlignment: break; case CPRightTabStopType: + case CPRightTextAlignment: headerString += @"\\tqr"; break; case CPCenterTabStopType: + case CPCenterTextAlignment: headerString += @"\\tqc"; break; case CPDecimalTabStopType: @@ -384,6 +391,13 @@ function _points2twips(a) { return (a) * 20.0; } { var unwrap = function(obj) { if (!obj) return null; + + // If the object contains the table structures, bypass unwrapping + if ((typeof obj.respondsToSelector === "function" && ([obj respondsToSelector:@selector(headers)] || [obj respondsToSelector:@selector(rows)])) || + obj.headers || obj._headers || obj.rows || obj._rows) { + return obj; + } + var unwrapped = null; if (typeof obj.respondsToSelector === "function") { if ([obj respondsToSelector:@selector(attachmentCell)]) { @@ -679,7 +693,7 @@ 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, '\\}'); diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index de1d27100..a88dff417 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -406,20 +406,11 @@ var parser = [[_CPRTFParser alloc] init]; var roundTrippedString = [parser parseRTF:generatedRTF]; - // 5. Replace the editor's text storage with the round-tripped version - var editorStorage = [_textView textStorage]; - if (editorStorage && [editorStorage respondsToSelector:@selector(setAttributedString:)]) - { - [editorStorage setAttributedString:roundTrippedString]; - } - else - { - // Safe fallback sequence - [_textView setEditable:YES]; - [_textView setString:@""]; - [_textView insertText:roundTrippedString]; - [_textView setEditable:NO]; - } + // Safe fallback sequence + [_textView setEditable:YES]; + [_textView setString:@""]; + [_textView insertText:roundTrippedString]; + [_textView setEditable:NO]; // 6. Force a layout pass and render update [_textView setNeedsDisplay:YES]; From 4aaa32833ab7440bbfb34ef69845d655af63e13d Mon Sep 17 00:00:00 2001 From: daboe01 Date: Mon, 15 Jun 2026 22:33:03 +0200 Subject: [PATCH 22/46] fixed: table roundtrip --- AppKit/CPTextView/_CPRTFParser.j | 100 +++--------------------- Tests/Manual/CPTextView/AppController.j | 6 -- 2 files changed, 9 insertions(+), 97 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index a68d7af5f..d8f95f027 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -21,6 +21,7 @@ @import @import +@import "CPTextStorage.j" @import "CPFontManager.j" @import "CPParagraphStyle.j" @import "_CPTableTextAttachment.j" @@ -114,11 +115,6 @@ var cp1252Map = { if (font) return font; - //Before giving up and using a default font, we try if this is - //not the case of a font with a composite name, such as - //'Helvetica-Light'. In that case, even if we don't have - //exactly an 'Helvetica-Light' font family, we might have an - //'Helvetica' one. var range = [fontName rangeOfString:@"-"]; if (range.location != CPNotFound) @@ -128,7 +124,6 @@ var cp1252Map = { font = [CPFont fontWithName:fontFamily size:fontSize]; } - /* Last resort, default font. :-( */ if (font == nil) font = [CPFont systemFontOfSize:fontSize]; @@ -184,7 +179,7 @@ var cp1252Map = { else if (type === CPRightTabStopType || type === CPRightTextAlignment) alignment = CPRightTextAlignment; else if (type === CPDecimalTabStopType) - alignment = CPRightTextAlignment; // Fallback alignment for decimal tab + alignment = CPRightTextAlignment; var tab = [[CPTextTab alloc] initWithType:alignment location:location]; @@ -222,20 +217,15 @@ var cp1252Map = { @end -// based on https://github.com/lazygyu/RTF-parser - var kRTFParserType_char = 0, kRTFParserType_dest = 1, kRTFParserType_prop = 2, kRTFParserType_spec = 3; -// Keyword descriptions var kRgsymRtf = { - // keyword dflt fPassDflt kwd idx "b" : [ "b", 1, false, kRTFParserType_prop, "propBold"], "ul" : [ "ul", 1, false, kRTFParserType_prop, "propUnderline"], "i" : [ "i", 1, false, kRTFParserType_prop, "propItalic"], -// "li" : [ "li", 0, false, kRTFParserType_prop, "propPgnFormat"], "pgnucltr" : [ "pgnucltr", "pgULtr", true, kRTFParserType_prop, "propPgnFormat"], "pgnlcltr" : [ "pgnlcltr", "pgLLtr", true, kRTFParserType_prop, "propPgnFormat"], "qc" : [ "qc", "justC", true, kRTFParserType_prop, "propJust"], @@ -277,9 +267,7 @@ var kRgsymRtf = { "ftnsep" : [ "ftnsep", 0, false, kRTFParserType_dest, "destSkip"], "ftnsepc" : [ "ftnsepc", 0, false, kRTFParserType_dest, "destSkip"], "fprq" : [ "fprq", 0, false, kRTFParserType_dest, "destSkip"], -// "fcharset" : [ "fcharset", 0, false, kRTFParserType_dest, "destSkip"], "rquote" : [ "rquote", 0, false, kRTFParserType_char, "'"], -// "s" : [ "s", 0, false, kRTFParserType_dest, "destSkip"], "header" : [ "header", 0, false, kRTFParserType_dest, "destSkip"], "headerf" : [ "headerf", 0, false, kRTFParserType_dest, "destSkip"], "headerl" : [ "headerl", 0, false, kRTFParserType_dest, "destSkip"], @@ -373,7 +361,6 @@ var kRgsymRtf = { return sym[4]; case 1: - // CPLogConsole("skipped : " + sym[4]); return ''; default: @@ -384,7 +371,6 @@ var kRgsymRtf = { - (BOOL)pushState { - // Push stack as an object containing scoping context _states.push({ curState: _curState, run: [_currentRun copy] @@ -402,7 +388,6 @@ var kRgsymRtf = { [self _flushCurrentRun]; _currentRun = state.run; - // Safety guard to prevent setting properties on null if (!_currentRun) { _currentRun = [_RTFAttribute new]; @@ -430,7 +415,6 @@ var kRgsymRtf = { case "ipfnHex": var hex = ''; - // Konsumiere exakt 2 Zeichen nach dem \' for (var i = 0; i < 2; i++) { var nextCh = _rtf.charAt(++_currentParseIndex); @@ -520,7 +504,6 @@ var kRgsymRtf = { { if (_tableRows && [_tableRows count] > 0) { - // Flush active character styling runs before appending table layout changes [self _flushCurrentRun]; var headers = [_tableRows objectAtIndex:0]; @@ -532,27 +515,16 @@ var kRgsymRtf = { var attachment = [[_CPTableTextAttachment alloc] initWithHeaders:headers rows:rows width:500.0]; - // Linear height allocation estimation - var numCols = [headers count]; - var estimatedHeight = 36.0 + ([rows count] * 28.0); - var lineCount = Math.ceil(estimatedHeight / 16.0) + 1; - var newlineStr = ""; - for (var nl = 0; nl < lineCount; nl++) { - newlineStr += "\n"; - } - - var tableAttrStr = [[CPMutableAttributedString alloc] initWithString:newlineStr]; - [tableAttrStr addAttribute:@"TableAttachmentAttribute" value:attachment range:CPMakeRange(0, [tableAttrStr length])]; - [tableAttrStr addAttribute:CPAttachmentAttributeName value:attachment range:CPMakeRange(0, [tableAttrStr length])]; + // Use standard Cocoa/Cappuccino NSAttachmentCharacter creation method + var tableAttrStr = [CPTextStorage attributedStringWithAttachment:attachment]; [_result appendAttributedString:tableAttrStr]; - // Update range offset of active attributes tracker if (_currentRun) { _currentRun._range = CPMakeRange([_result length], 0); } - + _tableRows = nil; _currentRow = nil; _currentCellText = ""; @@ -577,7 +549,6 @@ var kRgsymRtf = { [_result setAttributes:dict range:_currentRun._range]; // flush previous run - // Deep copy the current run style for the next sequence of characters _currentRun = [_currentRun copy]; } else @@ -590,8 +561,6 @@ var kRgsymRtf = { - (CPString)_applyPropChange:sym parameter:param { - //console.log("prop : " + sym[0] + " / param : " + param+ ' '); - switch (sym[0]) { case "pard": @@ -696,7 +665,6 @@ var kRgsymRtf = { if (sym[4] == "destSkip") { - CPLogConsole("Dest skip start : [" + sym[0] + "]"); _curState++; } @@ -848,8 +816,7 @@ var kRgsymRtf = { break; default: - CPLogConsole("skip : " + keyword + " param: " + param); - + break; } return ''; @@ -965,8 +932,7 @@ var kRgsymRtf = { [self _flushTableIfAny]; if ([self pushState]) - CPLogConsole("push"); - + break; case "}": @@ -974,12 +940,9 @@ var kRgsymRtf = { [self _flushTableIfAny]; if ([self popState]) - CPLogConsole("pop"); if (_freename) { - CPLogConsole(_freename); - if (_parsingFontTable) { _fontArray.push(_freename); @@ -1008,7 +971,6 @@ var kRgsymRtf = { var byteVal = parseInt(ch, 16); var unicodeVal = byteVal; - // Windows-1252 Mapping für den Bereich 0x80 - 0x9F anwenden if (byteVal >= 0x80 && byteVal <= 0x9F) { unicodeVal = cp1252Map[byteVal] || byteVal; } @@ -1044,7 +1006,6 @@ var kRgsymRtf = { if (_parsingFontTable && _freename) { var cleanFontName = _freename.trim(); - // strip family name prefix if present (e.g., "swiss Helvetica" -> "Helvetica") var lastSpaceIdx = cleanFontName.lastIndexOf(' '); if (lastSpaceIdx !== -1) { @@ -1100,7 +1061,6 @@ var kRgsymRtf = { while (i < lines.length) { var line = lines[i]; - // Tabellen-Erkennung if ([self isTableHeaderLine:line] && i + 1 < lines.length && [self isTableSeparatorLine:lines[i+1]]) { var headers = [self parseTableCells:line]; var separatorLine = lines[i+1]; @@ -1117,7 +1077,6 @@ var kRgsymRtf = { numCols = [[rows objectAtIndex:0] count]; } - // 1. Zuerst die absolute Summe der Natural-Breiten zur Spalten-Proportionsbestimmung ermitteln var totalNaturalW = 0.0; var colNaturalWidths = []; var measureTextField = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 10000.0, 24.0)]; @@ -1126,7 +1085,6 @@ var kRgsymRtf = { for (var c = 0; c < numCols; c++) { var cellW = 80.0; - // Headers prüfen if (c < headers.length) { var parsedText = [self parseInlineMarkdown:headers[c] isHeader:YES headerLevel:3]; [measureTextField setStringValue:[parsedText string]]; @@ -1134,7 +1092,6 @@ var kRgsymRtf = { cellW = Math.max(cellW, CGRectGetWidth([measureTextField frame]) + 24.0); } - // Reihen prüfen for (var r = 0; r < [rows count]; r++) { var rowData = [rows objectAtIndex:r]; if (c < [rowData count]) { @@ -1148,47 +1105,10 @@ var kRgsymRtf = { totalNaturalW += cellW; } - // 2. Präzise adaptive Zeilenhöhen-Schätzung für das Newline-Sizing (Verhindert zu große Abstände) - var estimatedHeight = 36.0; // Startwert für Header-Zeile mit Padding - for (var r = 0; r < [rows count]; r++) { - var rowData = [rows objectAtIndex:r]; - var maxCellHeight = 28.0; - - for (var c = 0; c < numCols; c++) { - var cellText = @""; - if (c < [rowData count]) { - cellText = [rowData objectAtIndex:c]; - } - var charCount = cellText.length; - - // Schätzung basierend auf realistischer Spaltenbreitenverteilung - var proportion = totalNaturalW > 0 ? (colNaturalWidths[c] / totalNaturalW) : (1.0 / numCols); - var estimatedColWidth = proportion * 500.0; - var charsPerLine = Math.max(10.0, Math.floor(estimatedColWidth / 6.5)); // ca. 6.5px pro Zeichen - - var estimatedLines = Math.ceil(charCount / charsPerLine); - if (estimatedLines < 1) estimatedLines = 1; - - var cellHeight = (estimatedLines * 16.0) + 12.0; - if (cellHeight > maxCellHeight) { - maxCellHeight = cellHeight; - } - } - estimatedHeight += maxCellHeight; - } - - // Berechne die benötigten Leerzeilen (\n Zeilenhöhe ist ca. 16px) - var lineCount = Math.ceil(estimatedHeight / 16.0) + 1; // Minimaler Sicherheitsabstand (+1) - var newlineStr = ""; - for (var nl = 0; nl < lineCount; nl++) { - newlineStr += "\n"; - } - - var tableAttrStr = [[CPMutableAttributedString alloc] initWithString:newlineStr]; var matrixView = [[_CPTableTextAttachment alloc] initWithHeaders:headers rows:rows width:500.0]; - [tableAttrStr addAttribute:@"TableAttachmentAttribute" value:matrixView range:CPMakeRange(0, [tableAttrStr length])]; - [tableAttrStr addAttribute:CPAttachmentAttributeName value:matrixView range:CPMakeRange(0, [tableAttrStr length])]; + // Render utilizing the correct atomic attachment character string + var tableAttrStr = [CPTextStorage attributedStringWithAttachment:matrixView]; [result appendAttributedString:tableAttrStr]; continue; } @@ -1196,7 +1116,6 @@ var kRgsymRtf = { var isHeader = false; var headerLevel = 0; - // Überschriften (#) var headerMatch = line.match(/^(#{1,6})\s+(.*)$/); if (headerMatch) { headerLevel = headerMatch[1].length; @@ -1204,7 +1123,6 @@ var kRgsymRtf = { isHeader = true; } - // Listenpunkte (- oder *) var isListItem = false; var listMatch = line.match(/^(\*|-)\s+(.*)$/); if (listMatch) { diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index a88dff417..3fab7a14c 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -410,11 +410,6 @@ [_textView setEditable:YES]; [_textView setString:@""]; [_textView insertText:roundTrippedString]; - [_textView setEditable:NO]; - - // 6. Force a layout pass and render update - [_textView setNeedsDisplay:YES]; - [_textView2 setNeedsDisplay:YES]; } // Action tied to the "Markdown ->" button to generate rich text @@ -433,7 +428,6 @@ [_textView setEditable:YES]; [_textView setString:@""]; [_textView insertText:parsedAttrStr]; - [_textView setEditable:NO]; } @end From 77433a3a1cb50620ffc66d59250d5cb168aeb9a3 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 16 Jun 2026 07:16:14 +0200 Subject: [PATCH 23/46] fixed: scrolling glitch --- AppKit/CPTextView/CPTextView.j | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 16151f9e0..fa6db4378 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -1660,7 +1660,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 From da061aac1dc12e83680c069e495791377ee74d37 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 16 Jun 2026 10:30:00 +0200 Subject: [PATCH 24/46] improved: table generation --- AppKit/CPTextView/_CPRTFProducer.j | 85 +++++----- AppKit/CPTextView/_CPTableTextAttachment.j | 180 ++++++++++++++++++--- 2 files changed, 200 insertions(+), 65 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFProducer.j b/AppKit/CPTextView/_CPRTFProducer.j index 8d06ad104..1e48afda8 100644 --- a/AppKit/CPTextView/_CPRTFProducer.j +++ b/AppKit/CPTextView/_CPRTFProducer.j @@ -89,12 +89,7 @@ function _points2twips(a) { return (a) * 20.0; } { if (self = [super init]) { - // maintain a dictionary for the used colours - // (for rtf-header generation) colorDict = [CPMutableDictionary new]; - - //maintain a dictionary for the used fonts - //(for rtf-header generation) fontDict = [CPMutableDictionary new]; fgColor = [CPColor blackColor]; @@ -104,7 +99,6 @@ function _points2twips(a) { return (a) * 20.0; } return self; } -// private stuff follows - (CPString)fontTable { if (![fontDict count]) @@ -319,7 +313,6 @@ function _points2twips(a) { return (a) * 20.0; } break; } - // write first line indent and left indent var twips = _points2twips([paraStyle firstLineHeadIndent]); if (twips != 0.0) @@ -392,7 +385,6 @@ function _points2twips(a) { return (a) * 20.0; } var unwrap = function(obj) { if (!obj) return null; - // If the object contains the table structures, bypass unwrapping if ((typeof obj.respondsToSelector === "function" && ([obj respondsToSelector:@selector(headers)] || [obj respondsToSelector:@selector(rows)])) || obj.headers || obj._headers || obj.rows || obj._rows) { return obj; @@ -434,7 +426,6 @@ function _points2twips(a) { return (a) * 20.0; } tableAttachment = unwrap(tableAttachment); - // Ultimate fallback scanner for layout character placeholder sequences if (!tableAttachment && (substring === "\uFFFC" || substring === "")) { var keys = [attributes allKeys], @@ -462,20 +453,48 @@ function _points2twips(a) { return (a) * 20.0; } var headers = null, rows = null; - if (typeof tableAttachment.respondsToSelector === "function") { - if ([tableAttachment respondsToSelector:@selector(headers)]) { - headers = [tableAttachment headers]; + // Try to fetch from the active live view of the attachment first to capture user edits + var activeView = null; + if (typeof tableAttachment.respondsToSelector === "function" && [tableAttachment respondsToSelector:@selector(view)]) { + activeView = [tableAttachment view]; + } + if (!activeView) { + activeView = tableAttachment._view || tableAttachment.view; + } + + if (activeView) { + if (typeof activeView.respondsToSelector === "function") { + if ([activeView respondsToSelector:@selector(headers)]) { + headers = [activeView headers]; + } + if ([activeView respondsToSelector:@selector(rows)]) { + rows = [activeView rows]; + } } - if ([tableAttachment respondsToSelector:@selector(rows)]) { - rows = [tableAttachment rows]; + if (!headers) { + headers = activeView._headers || activeView.headers; + } + if (!rows) { + rows = activeView._rows || activeView.rows; } } - - if (!headers) { - headers = tableAttachment._headers || tableAttachment.headers; - } - if (!rows) { - rows = tableAttachment._rows || tableAttachment.rows; + + // Fall back to the attachment's parsed properties if the view is nil or lacks the properties + if (!headers || !rows) { + if (typeof tableAttachment.respondsToSelector === "function") { + if ([tableAttachment respondsToSelector:@selector(headers)]) { + headers = [tableAttachment headers]; + } + if ([tableAttachment respondsToSelector:@selector(rows)]) { + rows = [tableAttachment rows]; + } + } + if (!headers) { + headers = tableAttachment._headers || tableAttachment.headers; + } + if (!rows) { + rows = tableAttachment._rows || tableAttachment.rows; + } } var getCount = function(arr) { @@ -563,23 +582,12 @@ function _points2twips(a) { return (a) * 20.0; } headerString += [self paragraphStyle:paraStyle]; } - /* - * analyze attributes of current run - * - * FIXME: All the character attributes should be output relative to the font - * attributes of the paragraph. So if the paragraph has underline on it should - * still be possible to switch it off for some characters, which currently is - * not possible. - */ attribEnum = [attributes keyEnumerator]; while ((currAttrib = [attribEnum nextObject]) != nil) { if ([currAttrib isEqualToString:CPFontAttributeName]) { - /* - * handle fonts - */ var font, fontName, traits; @@ -588,15 +596,9 @@ function _points2twips(a) { return (a) * 20.0; } fontName = [font familyName]; traits = [[CPFontManager sharedFontManager] traitsOfFont:font]; - /* - * font name - */ if (currentFont == nil || ![fontName isEqualToString:[currentFont familyName]]) headerString += [self fontToken:fontName]; - /* - * font size - */ if (currentFont == nil || [font size] != [currentFont size]) { var points = [font size] * 2, @@ -605,9 +607,7 @@ function _points2twips(a) { return (a) * 20.0; } pString = [CPString stringWithFormat:@"\\fs%d", points]; headerString += pString; } - /* - * font attributes - */ + if (traits & CPItalicFontMask) { headerString += @"\\i"; @@ -713,7 +713,7 @@ function _points2twips(a) { return (a) * 20.0; } var nobraces; if ([headerString length]) - nobraces = [CPString stringWithFormat:@"%@ %@", headerString, substring]; + nobraces = [CPString stringWithFormat:@"%@ %@}", headerString, substring]; else nobraces = substring; @@ -733,7 +733,7 @@ function _points2twips(a) { return (a) * 20.0; } completeRange = CPMakeRange(0, length), paragraphStart = YES; - while (CPMaxRange(currRange) < CPMaxRange(completeRange)) // save all "runs" + while (CPMaxRange(currRange) < CPMaxRange(completeRange)) { var attributes, substring, @@ -749,7 +749,6 @@ function _points2twips(a) { return (a) * 20.0; } result += runString; - // Dynamically compute paragraph boundaries based on standard line feeds if (substring.length > 0 && substring.charAt(substring.length - 1) === '\n') paragraphStart = YES; else diff --git a/AppKit/CPTextView/_CPTableTextAttachment.j b/AppKit/CPTextView/_CPTableTextAttachment.j index 5984ab11f..7a7ffba92 100644 --- a/AppKit/CPTextView/_CPTableTextAttachment.j +++ b/AppKit/CPTextView/_CPTableTextAttachment.j @@ -28,20 +28,29 @@ CPArray _headers; CPArray _rows; BOOL _isResizing; + BOOL _isEditable; + BOOL _acceptsRichText; } - (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows { - return [self initWithHeaders:headers rows:rows width:500.0]; + return [self initWithHeaders:headers rows:rows width:500.0 isEditable:YES acceptsRichText:YES]; } - (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows width:(float)totalWidth +{ + return [self initWithHeaders:headers rows:rows width:totalWidth isEditable:YES acceptsRichText:NO]; +} + +- (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows width:(float)totalWidth isEditable:(BOOL)isEditable acceptsRichText:(BOOL)acceptsRichText { self = [super initWithFrame:CGRectMake(0, 0, totalWidth, 20)]; if (self) { _headers = headers; _rows = rows; + _isEditable = isEditable; + _acceptsRichText = acceptsRichText; _isResizing = NO; [self _rebuildTableWithWidth:totalWidth]; @@ -91,11 +100,13 @@ [self resizeToWidth:totalWidth]; - // Trigger a parent layout manager re-layout once the table is fully reconstructed and sized. + // Trigger parent layout engine re-calculation var textView = [self superview]; + if (textView && [textView isKindOfClass:[CPTextView class]]) { var layoutManager = [textView layoutManager]; + if (layoutManager) { var charRange = [self _findCharacterRangeInLayoutManager:layoutManager]; @@ -138,22 +149,81 @@ - (CPArray)headers { + var numCols = _headers ? [_headers count] : 0; + if (numCols == 0 && _rows && [_rows count] > 0) + numCols = [[_rows objectAtIndex:0] count]; + + if (numCols == 0) + return _headers; + + var subviews = [self subviews]; + if ([subviews count] < numCols) + return _headers; // Subviews are not yet rendered, return cached fallback + + var currentHeaders = [CPMutableArray array]; + for (var c = 0; c < numCols; c++) + { + var cellView = [subviews objectAtIndex:c]; + var textView = [self getTextViewFromCell:cellView]; + var text = @""; + if (textView) + { + text = _acceptsRichText ? [[textView textStorage] copy] : [textView string]; + } + [currentHeaders addObject:text]; + } + + _headers = currentHeaders; return _headers; } - (CPArray)rows { + var numCols = _headers ? [_headers count] : 0; + if (numCols == 0 && _rows && [_rows count] > 0) + numCols = [[_rows objectAtIndex:0] count]; + + if (numCols == 0 || !_rows) + return _rows; + + var subviews = [self subviews]; + var headerOffset = (_headers && [_headers count] > 0) ? numCols : 0; + var expectedCount = headerOffset + ([_rows count] * numCols); + + if ([subviews count] < expectedCount) + return _rows; // Subviews are not yet rendered, return cached fallback + + var currentRows = [CPMutableArray array]; + var cellIndex = headerOffset; + + for (var r = 0; r < [_rows count]; r++) + { + var rowData = [CPMutableArray array]; + for (var c = 0; c < numCols; c++) + { + var cellView = [subviews objectAtIndex:cellIndex++]; + var textView = [self getTextViewFromCell:cellView]; + var text = @""; + if (textView) + { + text = _acceptsRichText ? [[textView textStorage] copy] : [textView string]; + } + [rowData addObject:text]; + } + [currentRows addObject:rowData]; + } + + _rows = currentRows; return _rows; } - (CPView)viewForWidth:(float)width { - // Always call resize to ensure cell views are constructed, but do not guard here [self resizeToWidth:width]; return self; } -- (CPView)createCellWithText:(CPString)text frame:(CGRect)frame isHeader:(BOOL)isHeader +- (CPView)createCellWithText:(id)text frame:(CGRect)frame isHeader:(BOOL)isHeader { var initialWidth = (frame.size.width > 0) ? frame.size.width : 120.0; var initialHeight = (frame.size.height > 0) ? frame.size.height : 28.0; @@ -176,23 +246,48 @@ var textContainer = [[CPTextContainer alloc] initWithContainerSize:CGSizeMake(initialWidth - 8, 1e7)]; var textView = [[CPTextView alloc] initWithFrame:CGRectMake(4, 2, initialWidth - 8, initialHeight - 4) textContainer:textContainer]; - [textView setEditable:YES]; + [textView setEditable:_isEditable]; [textView setSelectable:YES]; [textView setBackgroundColor:[CPColor clearColor]]; [textView setVerticallyResizable:YES]; [textView setHorizontallyResizable:NO]; [[textView textContainer] setWidthTracksTextView:YES]; + // Configure cell rich text mode + [textView setRichText:_acceptsRichText]; + + // Intercept changes within cell TextViews to notify the table + [textView setDelegate:self]; + // Configure cell text style using standard CPTextView APIs var cellFont = isHeader ? [CPFont boldSystemFontOfSize:11.0] : [CPFont systemFontOfSize:11.0]; [textView setFont:cellFont]; [textView setTextColor:[CPColor blackColor]]; - [textView setString:text]; + + // Populate either rich text or plain text safely + if (text && [text isKindOfClass:[CPAttributedString class]]) + { + [[textView textStorage] setAttributedString:text]; + } + else if (text) + { + [textView setString:String(text)]; + } + else + { + [textView setString:@""]; + } [cellContainer addSubview:textView]; return cellContainer; } +- (void)textDidChange:(CPNotification)aNotification +{ + // Live update cell layouts and heights when changes are typed + [self resizeToWidth:CGRectGetWidth([self frame])]; +} + - (CPTextView)getTextViewFromCell:(CPView)cellView { var subviews = [cellView subviews]; @@ -212,9 +307,13 @@ _isResizing = YES; - var numCols = _headers ? [_headers count] : 0; - if (numCols == 0 && _rows && [_rows count] > 0) { - numCols = [[_rows objectAtIndex:0] count]; + // Use dynamic getters to fetch live edited strings + var currentHeaders = [self headers]; + var currentRows = [self rows]; + + var numCols = currentHeaders ? [currentHeaders count] : 0; + if (numCols == 0 && currentRows && [currentRows count] > 0) { + numCols = [[currentRows objectAtIndex:0] count]; } if (numCols == 0) { _isResizing = NO; @@ -236,14 +335,26 @@ var cellFont = isHeader ? [CPFont boldSystemFontOfSize:11.0] : [CPFont systemFontOfSize:11.0]; [measureTextField setFont:cellFont]; - [measureTextField setStringValue:cellText]; + var plainText = (cellText && typeof cellText.string === "function") ? [cellText string] : (cellText || @""); + plainText = String(plainText); + + if (cellText && typeof cellText.string === "function") { + if ([measureTextField respondsToSelector:@selector(setAttributedStringValue:)]) { + [measureTextField setAttributedStringValue:cellText]; + } else { + [measureTextField setStringValue:plainText]; + } + } else { + [measureTextField setStringValue:plainText]; + } + [measureTextField sizeToFit]; var naturalW = CGRectGetWidth([measureTextField frame]) + 24.0; if (naturalW > colNaturalWidths[colIndex]) { colNaturalWidths[colIndex] = naturalW; } - var words = cellText.split(/[\s\-]/); + var words = plainText.split(/[\s\-]/); var maxWordW = 50.0; for (var w = 0; w < words.length; w++) { var word = words[w].trim(); @@ -260,15 +371,15 @@ } }; - if (_headers) { - for (var c = 0; c < [_headers count]; c++) { - measureCell([_headers objectAtIndex:c], YES, c); + if (currentHeaders) { + for (var c = 0; c < [currentHeaders count]; c++) { + measureCell([currentHeaders objectAtIndex:c], YES, c); } } - if (_rows) { - for (var r = 0; r < [_rows count]; r++) { - var rowData = [_rows objectAtIndex:r]; + if (currentRows) { + for (var r = 0; r < [currentRows count]; r++) { + var rowData = [currentRows objectAtIndex:r]; for (var c = 0; c < numCols; c++) { var cellText = @""; if (c < [rowData count]) { @@ -373,26 +484,51 @@ return maxCellHeight; }; - if (_headers && [_headers count] > 0) { + if (currentHeaders && [currentHeaders count] > 0) { var headerHeight = layoutRow(cellIndex); cellIndex += numCols; currentY += headerHeight; } - if (_rows) { - for (var r = 0; r < [_rows count]; r++) { + if (currentRows) { + for (var r = 0; r < [currentRows count]; r++) { var rowHeight = layoutRow(cellIndex); cellIndex += numCols; currentY += rowHeight; } } - // GUARD FRAME SIZE MUTATIONS: Only execute setFrameSize if dimensions actually change. - // This stops infinite layout passes while ensuring subviews are laid out. + // GUARD FRAME SIZE MUTATIONS var currentSize = [self frame].size; if (ABS(currentSize.width - newWidth) > 0.1 || ABS(currentSize.height - currentY) > 0.1) { [self setFrameSize:CGSizeMake(newWidth, currentY)]; + + // we need to re-layout the textview here. + // but this is not easy as the layout engine is not re-entrant + // this does not work: (delay does not matter), layout is always off +// setTimeout(function() { +// var textView = [self superview]; +// +// if (textView && [textView isKindOfClass:[CPTextView class]]) +// { +// var layoutManager = [textView layoutManager]; +// +// if (layoutManager) +// { +// var charRange = [self _findCharacterRangeInLayoutManager:layoutManager]; +// +// if (charRange && charRange.location !== CPNotFound) +// { +// [layoutManager invalidateLayoutForCharacterRange:charRange isSoft:NO actualCharacterRange:nil]; +// [layoutManager invalidateDisplayForGlyphRange:charRange]; +// [layoutManager _validateLayoutAndGlyphs]; +// [textView sizeToFit]; +// } +// } +// } +// }, 0); + } _isResizing = NO; From d75c0ac452b0b7bfa18c4f0355fa6890fe66986a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 16 Jun 2026 17:28:03 +0200 Subject: [PATCH 25/46] improved: tab management --- AppKit/CPTextView/CPLayoutManager.j | 75 +++++++++++++++++++++++++++-- AppKit/CPTextView/CPTextView.j | 4 ++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index 22fb6ba19..32810373b 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -1132,6 +1132,10 @@ var _objectsInRange = function(aList, aRange) - (id)createDOMElementWithText:(CPString)aString andFont:(CPFont)aFont andColor:(CPColor)fgColor andBackgroundColor:(CPColor)bgColor andUnderline:(CPUnderlineStyle)aUnderline { + + if (!aString || aString.length === 0) + return nil; + #if PLATFORM(DOM) var style, span = document.createElement("span"); @@ -1212,7 +1216,8 @@ 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) @@ -1220,17 +1225,71 @@ var _objectsInRange = function(aList, aRange) if (![attributes objectForKey:_CPAttachmentInvisible]) { var view = [attributes objectForKey:_CPAttachmentView]; - var run = {_range:CPMakeRangeCopy(effectiveRange), color:nil, font:nil, elem:nil, string:nil, view:view}; } + var run = {_range:CPMakeRangeCopy(effectiveRange), color:nil, font:nil, elem:nil, string:nil, view:view, paragraphStyle:paragraphStyle}; _runs.push(run); + } } else { var color = [attributes objectForKey:CPForegroundColorAttributeName], bgcolor = [attributes objectForKey:CPBackgroundColorAttributeName], - font = [attributes objectForKey:CPFontAttributeName] || [textStorage font] || [CPFont systemFontOfSize:12.0], - run = {_range:CPMakeRangeCopy(effectiveRange), color:color, font:font, elem:nil, string:string, bgcolor:bgcolor}; + font = [attributes objectForKey:CPFontAttributeName] || [textStorage font] || [CPFont systemFontOfSize:12.0]; - _runs.push(run); + var currentLoc = effectiveRange.location, + strLen = string.length, + startIdx = 0; + + for (var i = 0; i < strLen; i++) + { + if (string.charCodeAt(i) === 9) // Tabulator-Zeichen '\t' + { + if (i > startIdx) + { + var subString = string.substring(startIdx, i), + subRange = CPMakeRange(currentLoc + startIdx, i - startIdx), + run = { + _range: subRange, + color: color, + font: font, + elem: nil, + string: subString, + bgcolor: bgcolor, + paragraphStyle: paragraphStyle + }; + _runs.push(run); + } + + var tabRange = CPMakeRange(currentLoc + i, 1), + tabRun = { + _range: tabRange, + color: nil, + font: nil, + elem: nil, + string: nil, + bgcolor: nil, + paragraphStyle: paragraphStyle + }; + _runs.push(tabRun); + + startIdx = i + 1; + } + } + + if (startIdx < strLen) + { + var subString = string.substring(startIdx, strLen), + subRange = CPMakeRange(currentLoc + startIdx, strLen - startIdx), + run = { + _range: subRange, + color: color, + font: font, + elem: nil, + string: subString, + bgcolor: bgcolor, + paragraphStyle: paragraphStyle + }; + _runs.push(run); + } } if (!CPMaxRange(effectiveRange)) @@ -1401,6 +1460,12 @@ var _objectsInRange = function(aList, aRange) if (newFragmentRuns[i].color !== oldFragmentRuns[i].color || newFragmentRuns[i].bgcolor !== oldFragmentRuns[i].bgcolor || newFragmentRuns[i].font !== oldFragmentRuns[i].font) return NO; + + var oldStyle = oldFragmentRuns[i].paragraphStyle || [CPParagraphStyle defaultParagraphStyle], + newStyle = newFragmentRuns[i].paragraphStyle || [CPParagraphStyle defaultParagraphStyle]; + + if (![oldStyle isEqual:newStyle]) + return NO; } return YES; diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index fa6db4378..ddba66801 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2648,6 +2648,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 From a981b8b04da66f238331a6540654764f47cd22fc Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 16 Jun 2026 19:17:44 +0200 Subject: [PATCH 26/46] improved: indentation markers --- AppKit/CPTextView/CPRulerView.j | 162 ++++++++++++++++++++++++++------ 1 file changed, 133 insertions(+), 29 deletions(-) diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j index d72e9803a..82ed13ab7 100644 --- a/AppKit/CPTextView/CPRulerView.j +++ b/AppKit/CPTextView/CPRulerView.j @@ -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; From cfff603508d8d785eb9f7157e324c9772d55849f Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 16 Jun 2026 19:43:45 +0200 Subject: [PATCH 27/46] Fixed: CPInvalidArgumentException in CPTextField -setFont: when handling composite theme states --- AppKit/CPTextField.j | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/AppKit/CPTextField.j b/AppKit/CPTextField.j index 4dc2fb37b..37fea460b 100644 --- a/AppKit/CPTextField.j +++ b/AppKit/CPTextField.j @@ -2024,7 +2024,9 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); [self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateBezeled]; [self setValue:aFont forThemeAttribute:@"font" inState:CPThemeStateBordered]; [self setValue:aFont forThemeAttribute:@"font" inState:CPTextFieldStateRounded]; - [self setValue:aFont forThemeAttribute:@"font" inState:[CPTextFieldStateRounded, CPThemeStateEditing]]; + + // 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]; @@ -2033,8 +2035,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); // 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:[CPThemeStateTableDataView, CPThemeStateSelectedDataView]]; - [self setValue:aFont forThemeAttribute:@"font" inState:[CPThemeStateTableDataView, CPThemeStateSelectedDataView, CPThemeStateFirstResponder, CPThemeStateKeyWindow]]; + [self setValue:aFont forThemeAttribute:@"font" inState:CPThemeState(CPThemeStateTableDataView, CPThemeStateSelectedDataView)]; + [self setValue:aFont forThemeAttribute:@"font" inState:CPThemeState(CPThemeStateTableDataView, CPThemeStateSelectedDataView, CPThemeStateFirstResponder, CPThemeStateKeyWindow)]; [self layoutSubviews]; } From 5a3d6e30cb27cd30256555f6de5fb2f79135cd3a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 17 Jun 2026 07:58:33 +0200 Subject: [PATCH 28/46] fixed: table sizing --- AppKit/CPTextView/_CPRTFParser.j | 68 +++++++++++-- AppKit/CPTextView/_CPTableTextAttachment.j | 105 ++++++++++----------- 2 files changed, 110 insertions(+), 63 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index d8f95f027..6a0ba91ea 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -1076,6 +1076,25 @@ var kRgsymRtf = { 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 = []; @@ -1085,18 +1104,18 @@ var kRgsymRtf = { for (var c = 0; c < numCols; c++) { var cellW = 80.0; - if (c < headers.length) { - var parsedText = [self parseInlineMarkdown:headers[c] isHeader:YES headerLevel:3]; - [measureTextField setStringValue:[parsedText string]]; + 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 < [rows count]; r++) { - var rowData = [rows objectAtIndex:r]; + for (var r = 0; r < [parsedRows count]; r++) { + var rowData = [parsedRows objectAtIndex:r]; if (c < [rowData count]) { - var parsedText = [self parseInlineMarkdown:rowData[c] isHeader:NO headerLevel:3]; - [measureTextField setStringValue:[parsedText string]]; + var parsedText = [rowData objectAtIndex:c]; + [measureTextField setStringValue:parsedText._string]; [measureTextField sizeToFit]; cellW = Math.max(cellW, CGRectGetWidth([measureTextField frame]) + 24.0); } @@ -1105,7 +1124,8 @@ var kRgsymRtf = { totalNaturalW += cellW; } - var matrixView = [[_CPTableTextAttachment alloc] initWithHeaders:headers rows:rows width:500.0]; + // 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]; @@ -1143,6 +1163,38 @@ var kRgsymRtf = { 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(//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(); diff --git a/AppKit/CPTextView/_CPTableTextAttachment.j b/AppKit/CPTextView/_CPTableTextAttachment.j index 7a7ffba92..d849c2e27 100644 --- a/AppKit/CPTextView/_CPTableTextAttachment.j +++ b/AppKit/CPTextView/_CPTableTextAttachment.j @@ -39,7 +39,7 @@ - (id)initWithHeaders:(CPArray)headers rows:(CPArray)rows width:(float)totalWidth { - return [self initWithHeaders:headers rows:rows width:totalWidth isEditable:YES acceptsRichText:NO]; + 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 @@ -246,6 +246,10 @@ 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]]; @@ -264,21 +268,11 @@ [textView setFont:cellFont]; [textView setTextColor:[CPColor blackColor]]; - // Populate either rich text or plain text safely - if (text && [text isKindOfClass:[CPAttributedString class]]) - { - [[textView textStorage] setAttributedString:text]; - } - else if (text) - { - [textView setString:String(text)]; - } - else - { - [textView setString:@""]; - } - + if (text) + [textView insertText:text]; + [cellContainer addSubview:textView]; + return cellContainer; } @@ -441,32 +435,55 @@ 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) { + + if (textView) + { var targetWidth = Math.max(10.0, colWidths[c] - 8); - [[textView textContainer] setContainerSize:CGSizeMake(targetWidth, 1e7)]; + + // 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]; - var usedRect = layoutManager ? [layoutManager usedRectForTextContainer:[textView textContainer]] : nil; - var wrappedHeight = (usedRect ? CGRectGetHeight(usedRect) : 0.0) + 12.0; - if (wrappedHeight > maxCellHeight) { - maxCellHeight = wrappedHeight; + 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++) { + + for (var c = 0; c < numCols; c++) + { var idx = startIndex + c; - if (idx < [subviews count]) { + + if (idx < [subviews count]) + { var cellView = [subviews objectAtIndex:idx]; [cellView setFrame:CGRectMake(currentX, currentY, colWidths[c], maxCellHeight)]; var textView = [self getTextViewFromCell:cellView]; - if (textView) { + + if (textView) + { var targetWidth = Math.max(10.0, colWidths[c] - 8); var textY = 4.0; var finalTextViewHeight = maxCellHeight - 8.0; @@ -474,9 +491,9 @@ } var cellSubviews = [cellView subviews]; - if ([cellSubviews count] > 0) { + + if ([cellSubviews count] > 0) [[cellSubviews objectAtIndex:0] setFrame:CGRectMake(0, 0, colWidths[c], maxCellHeight)]; - } } currentX += colWidths[c]; } @@ -484,14 +501,17 @@ return maxCellHeight; }; - if (currentHeaders && [currentHeaders count] > 0) { + if (currentHeaders && [currentHeaders count] > 0) + { var headerHeight = layoutRow(cellIndex); cellIndex += numCols; currentY += headerHeight; } - if (currentRows) { - for (var r = 0; r < [currentRows count]; r++) { + if (currentRows) + { + for (var r = 0; r < [currentRows count]; r++) + { var rowHeight = layoutRow(cellIndex); cellIndex += numCols; currentY += rowHeight; @@ -500,35 +520,10 @@ // 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; From 8de928d9058d5d1c09e5209a7a1fce07c6567b65 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 17 Jun 2026 08:07:26 +0200 Subject: [PATCH 29/46] formatting --- AppKit/CPTextView/_CPTableTextAttachment.j | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/AppKit/CPTextView/_CPTableTextAttachment.j b/AppKit/CPTextView/_CPTableTextAttachment.j index d849c2e27..a395a31a4 100644 --- a/AppKit/CPTextView/_CPTableTextAttachment.j +++ b/AppKit/CPTextView/_CPTableTextAttachment.j @@ -526,6 +526,31 @@ [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; } From 1223cfc96588419bbf316fe5ca2e78b7d53d1e93 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 17 Jun 2026 16:10:39 +0200 Subject: [PATCH 30/46] new: register wheel event listeners as non-passive --- AppKit/Platform/DOM/CPPlatformWindow+DOM.j | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index 7c714c263..961fb4787 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -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]; From 9731c4b394c304195bea6a411095e7ea851ab311 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 17 Jun 2026 18:18:39 +0200 Subject: [PATCH 31/46] New: standard key binding for pasteAsPlainText: --- AppKit/CPKeyBinding.j | 1 + Tests/Manual/CPTextView/AppController.j | 4 ++++ dist/cappuccino/bin/flatten | 2 +- dist/cappuccino/bin/fontinfo | Bin 69888 -> 51464 bytes dist/cappuccino/bin/imagesize | Bin 69536 -> 0 bytes dist/cappuccino/bin/objj2objcskeleton | 22 ++++++++++++++++------ 6 files changed, 22 insertions(+), 7 deletions(-) delete mode 100755 dist/cappuccino/bin/imagesize diff --git a/AppKit/CPKeyBinding.j b/AppKit/CPKeyBinding.j index 970973a3a..7c80685a9 100644 --- a/AppKit/CPKeyBinding.j +++ b/AppKit/CPKeyBinding.j @@ -30,6 +30,7 @@ CPStandardKeyBindings = { @"@.": @"cancelOperation:", @"@a": @"selectAll:", + @"@~$v": @"pasteAsPlainText:", @"^a": @"moveToBeginningOfParagraph:", @"^$a": @"moveToBeginningOfParagraphAndModifySelection:", @"^b": @"moveBackward:", diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index 3fab7a14c..8fc338845 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -289,6 +289,10 @@ [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"]; diff --git a/dist/cappuccino/bin/flatten b/dist/cappuccino/bin/flatten index 1df033252..80b9ecf8e 100755 --- a/dist/cappuccino/bin/flatten +++ b/dist/cappuccino/bin/flatten @@ -233,7 +233,7 @@ ObjectiveJFlattener.prototype.serializeFunctions = function() { var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK var relative = this.rootPath.relative(path).toString(); - this.functionsBuffer.push("ObjectiveJ.StaticResource._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); + this.functionsBuffer.push("ObjectiveJ.FileExecutable._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); } var bundle = this.context.global.CFBundle.bundleContainingURL(path); diff --git a/dist/cappuccino/bin/fontinfo b/dist/cappuccino/bin/fontinfo index f1ff06158a2525d34541aff240713c0067bd8633..211779fb4e83662db5334013b546f93e2ac4a9c6 100755 GIT binary patch literal 51464 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 diff --git a/dist/cappuccino/bin/imagesize b/dist/cappuccino/bin/imagesize deleted file mode 100755 index 5d67a1ce0a9a1e20577f82ccdfe0d3b3e94143b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69536 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 Z009sH0T2KI5C8!X009sHfo}nU{{v@rhV}pe diff --git a/dist/cappuccino/bin/objj2objcskeleton b/dist/cappuccino/bin/objj2objcskeleton index b7972dbb3..51ecab38d 100755 --- a/dist/cappuccino/bin/objj2objcskeleton +++ b/dist/cappuccino/bin/objj2objcskeleton @@ -4,14 +4,11 @@ var fs = require("fs"), acorn = require("objj-parser"), - walk = require("objj-parser/util/walk"), + walk = require("acorn-walk"), stream = ObjectiveJ.term; - debugger; - function main(args) { - debugger; args.shift(); if (args.length < 1) @@ -40,6 +37,8 @@ function raise(pos, message) throw syntaxError; } +function ignore(_node, _st, _c) {} + var errors = [], xcc = walk.make( { @@ -115,7 +114,18 @@ var errors = [], else raise(node.loc.start, "Action methods must have exactly one parameter"); } - } + }, + TypeDefStatement: ignore, + ClassStatement: ignore, + MessageSendExpression: ignore, + GlobalStatement: ignore, + ProtocolDeclarationStatement: ignore, + ArrayLiteral: ignore, + Reference: ignore, + DictionaryLiteral: ignore, + Dereference: ignore, + ImportStatement: ignore, + SelectorLiteralExpression: ignore } ); @@ -154,7 +164,7 @@ function parser(args) outputHeaderURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".h"]), outputImplementationURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".m"]), source = fs.readFileSync(sourcePath, { encoding: "utf8" }), - tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath }), + tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath, ecmaVersion: 2022 }), classesInformation = [], ObjectiveCSource = "", ObjectiveCHeader = "", From 672f25ce278b997f7d4ae6540571cca4ab72a21f Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 17 Jun 2026 18:20:39 +0200 Subject: [PATCH 32/46] New: standard key binding for pasteAsPlainText: This reverts commit 9731c4b394c304195bea6a411095e7ea851ab311. --- AppKit/CPKeyBinding.j | 1 - Tests/Manual/CPTextView/AppController.j | 4 ---- 2 files changed, 5 deletions(-) diff --git a/AppKit/CPKeyBinding.j b/AppKit/CPKeyBinding.j index 7c80685a9..970973a3a 100644 --- a/AppKit/CPKeyBinding.j +++ b/AppKit/CPKeyBinding.j @@ -30,7 +30,6 @@ CPStandardKeyBindings = { @"@.": @"cancelOperation:", @"@a": @"selectAll:", - @"@~$v": @"pasteAsPlainText:", @"^a": @"moveToBeginningOfParagraph:", @"^$a": @"moveToBeginningOfParagraphAndModifySelection:", @"^b": @"moveBackward:", diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index 8fc338845..3fab7a14c 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -289,10 +289,6 @@ [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"]; From 6dc11de431eed7bf553c9f50760eb662676149e4 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 17 Jun 2026 18:21:26 +0200 Subject: [PATCH 33/46] Revert "New: standard key binding for pasteAsPlainText:" This reverts commit 9731c4b394c304195bea6a411095e7ea851ab311. --- dist/cappuccino/bin/flatten | 2 +- dist/cappuccino/bin/fontinfo | Bin 51464 -> 69888 bytes dist/cappuccino/bin/imagesize | Bin 0 -> 69536 bytes dist/cappuccino/bin/objj2objcskeleton | 22 ++++++---------------- 4 files changed, 7 insertions(+), 17 deletions(-) create mode 100755 dist/cappuccino/bin/imagesize diff --git a/dist/cappuccino/bin/flatten b/dist/cappuccino/bin/flatten index 80b9ecf8e..1df033252 100755 --- a/dist/cappuccino/bin/flatten +++ b/dist/cappuccino/bin/flatten @@ -233,7 +233,7 @@ ObjectiveJFlattener.prototype.serializeFunctions = function() { var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK var relative = this.rootPath.relative(path).toString(); - this.functionsBuffer.push("ObjectiveJ.FileExecutable._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); + this.functionsBuffer.push("ObjectiveJ.StaticResource._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); } var bundle = this.context.global.CFBundle.bundleContainingURL(path); diff --git a/dist/cappuccino/bin/fontinfo b/dist/cappuccino/bin/fontinfo index 211779fb4e83662db5334013b546f93e2ac4a9c6..f1ff06158a2525d34541aff240713c0067bd8633 100755 GIT binary patch literal 69888 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 literal 51464 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 Z009sH0T2KI5C8!X009sHfo}nU{{v@rhV}pe literal 0 HcmV?d00001 diff --git a/dist/cappuccino/bin/objj2objcskeleton b/dist/cappuccino/bin/objj2objcskeleton index 51ecab38d..b7972dbb3 100755 --- a/dist/cappuccino/bin/objj2objcskeleton +++ b/dist/cappuccino/bin/objj2objcskeleton @@ -4,11 +4,14 @@ var fs = require("fs"), acorn = require("objj-parser"), - walk = require("acorn-walk"), + walk = require("objj-parser/util/walk"), stream = ObjectiveJ.term; + debugger; + function main(args) { + debugger; args.shift(); if (args.length < 1) @@ -37,8 +40,6 @@ function raise(pos, message) throw syntaxError; } -function ignore(_node, _st, _c) {} - var errors = [], xcc = walk.make( { @@ -114,18 +115,7 @@ var errors = [], else raise(node.loc.start, "Action methods must have exactly one parameter"); } - }, - TypeDefStatement: ignore, - ClassStatement: ignore, - MessageSendExpression: ignore, - GlobalStatement: ignore, - ProtocolDeclarationStatement: ignore, - ArrayLiteral: ignore, - Reference: ignore, - DictionaryLiteral: ignore, - Dereference: ignore, - ImportStatement: ignore, - SelectorLiteralExpression: ignore + } } ); @@ -164,7 +154,7 @@ function parser(args) outputHeaderURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".h"]), outputImplementationURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilenameWithNoExtension + ".m"]), source = fs.readFileSync(sourcePath, { encoding: "utf8" }), - tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath, ecmaVersion: 2022 }), + tokens = acorn.parse(source, { locations:true, sourceFile:sourcePath }), classesInformation = [], ObjectiveCSource = "", ObjectiveCHeader = "", From 873a102872cba89b15fcd86aaea7a9fcd8153e4e Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 17 Jun 2026 18:24:31 +0200 Subject: [PATCH 34/46] New: standard key binding for pasteAsPlainText: --- AppKit/CPKeyBinding.j | 1 + Tests/Manual/CPTextView/AppController.j | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/AppKit/CPKeyBinding.j b/AppKit/CPKeyBinding.j index 970973a3a..7c80685a9 100644 --- a/AppKit/CPKeyBinding.j +++ b/AppKit/CPKeyBinding.j @@ -30,6 +30,7 @@ CPStandardKeyBindings = { @"@.": @"cancelOperation:", @"@a": @"selectAll:", + @"@~$v": @"pasteAsPlainText:", @"^a": @"moveToBeginningOfParagraph:", @"^$a": @"moveToBeginningOfParagraphAndModifySelection:", @"^b": @"moveBackward:", diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index 3fab7a14c..8fc338845 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -289,6 +289,10 @@ [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"]; From 4c8fd58471359fbca7dddbb792a0ff02a02a06e5 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 17 Jun 2026 19:22:36 +0200 Subject: [PATCH 35/46] fixed: leaky abstraction --- AppKit/CPTextView/CPTextView.j | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index ddba66801..52f77da4c 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -3209,6 +3209,11 @@ var _CPCopyPlaceholder = '-'; if (richtext) { + var shouldPastePlainText = [[CPApp currentEvent] modifierFlags] & (CPShiftKeyMask | CPAlternateKeyMask); + + if (shouldPastePlainText && richtext._string) + richtext = richtext._string; + [currentFirstResponder _pasteString:richtext]; return; From de2ffd3aaddbcb4d93960a1a1f4b28386e129d18 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 18 Jun 2026 19:08:45 +0200 Subject: [PATCH 36/46] New: implement baseline offsets and superscript/subscript support --- AppKit/CPFont.j | 16 +++ AppKit/CPTextView/CPLayoutManager.j | 50 ++++++- AppKit/CPTextView/CPTypesetter.j | 55 ++++++-- Tests/Manual/CPTextView/AppController.j | 173 ++++++++++++++++++++++++ 4 files changed, 280 insertions(+), 14 deletions(-) diff --git a/AppKit/CPFont.j b/AppKit/CPFont.j index fa8884278..0bbd3d448 100644 --- a/AppKit/CPFont.j +++ b/AppKit/CPFont.j @@ -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 */ diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index 32810373b..b1d22ea80 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -31,8 +31,9 @@ @import "CPFont.j" @global _MakeRangeFromAbs - @global document +@global CPBaselineOffsetAttributeName +@global CPSuperscriptAttributeName @class CPTextContainer @class CPTextView @@ -1225,7 +1226,7 @@ var _objectsInRange = function(aList, aRange) if (![attributes objectForKey:_CPAttachmentInvisible]) { var view = [attributes objectForKey:_CPAttachmentView]; - var run = {_range:CPMakeRangeCopy(effectiveRange), color:nil, font:nil, elem:nil, string:nil, view:view, paragraphStyle:paragraphStyle}; + 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); } } @@ -1235,6 +1236,34 @@ var _objectsInRange = function(aList, aRange) bgcolor = [attributes objectForKey:CPBackgroundColorAttributeName], font = [attributes objectForKey:CPFontAttributeName] || [textStorage font] || [CPFont systemFontOfSize:12.0]; + 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; @@ -1254,7 +1283,9 @@ var _objectsInRange = function(aList, aRange) elem: nil, string: subString, bgcolor: bgcolor, - paragraphStyle: paragraphStyle + paragraphStyle: paragraphStyle, + underline: underline, + baselineOffset: baselineOffset }; _runs.push(run); } @@ -1267,7 +1298,9 @@ var _objectsInRange = function(aList, aRange) elem: nil, string: nil, bgcolor: nil, - paragraphStyle: paragraphStyle + paragraphStyle: paragraphStyle, + underline: underline, + baselineOffset: 0.0 }; _runs.push(tabRun); @@ -1286,7 +1319,9 @@ var _objectsInRange = function(aList, aRange) elem: nil, string: subString, bgcolor: bgcolor, - paragraphStyle: paragraphStyle + paragraphStyle: paragraphStyle, + underline: underline, + baselineOffset: baselineOffset }; _runs.push(run); } @@ -1458,7 +1493,10 @@ 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], diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 06c8a368f..074c3b175 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -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; } diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index 3fab7a14c..dd6513a89 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -17,6 +17,9 @@ @import @import +@global CPBaselineOffsetAttributeName +@global CPSuperscriptAttributeName + @implementation AppController : CPObject { CPTextView _textView; @@ -77,6 +80,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 @@ -215,6 +294,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)]; @@ -305,6 +426,13 @@ [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:@"|"]; @@ -327,6 +455,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 From e67c1419212779362b10f0e5cc62e6525e0c9034 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 18 Jun 2026 22:21:50 +0200 Subject: [PATCH 37/46] new: color panel --- Tests/Manual/CPTextView/AppController.j | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index dd6513a89..c8cb009df 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -8,6 +8,7 @@ @import @import @import +@import @import @import @import @@ -55,6 +56,11 @@ [[CPFontManager sharedFontManager] orderFrontFontPanel:self]; } +- (void)orderFrontColorPanel:(id)sender +{ + [[CPColorPanel sharedColorPanel] orderFront:self]; +} + - (void)toggleRuler:(id)sender { [_scrollView setRulersVisible:![_scrollView rulersVisible]]; @@ -420,6 +426,7 @@ 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"]; From d40a151e056eb843784d66f1079332d4165b44f8 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 19 Jun 2026 07:18:52 +0200 Subject: [PATCH 38/46] new: colorpanel sync --- AppKit/CPTextView/CPTextView.j | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index ddba66801..0398d8f46 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -1005,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]; } @@ -1799,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. From c187f5de1d77fc3d306208ebe50aa145c2cae3e3 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 19 Jun 2026 08:48:17 +0200 Subject: [PATCH 39/46] fixed: rtf roundtrip with baseline stuff --- AppKit/CPTextView/_CPRTFParser.j | 50 ++++++++++++++++++++++++++++++ AppKit/CPTextView/_CPRTFProducer.j | 22 +++++++------ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 6a0ba91ea..69bd0e224 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -38,6 +38,8 @@ @global CPParagraphStyleAttributeName @global CPAttachmentAttributeName @global CPUnderlineStyleAttributeName +@global CPBaselineOffsetAttributeName +@global CPSuperscriptAttributeName @global CPLeftTabStopType @global CPRightTabStopType @@ -72,6 +74,8 @@ var cp1252Map = { BOOL script; BOOL _tabChanged; CPTabStopType _nextTabType; + int superscript; + float baselineOffset; } - (id)init @@ -104,6 +108,8 @@ var cp1252Map = { mynew.ulColour = ulColour; mynew._tabChanged = _tabChanged; mynew._nextTabType = _nextTabType; + mynew.superscript = superscript; + mynew.baselineOffset = baselineOffset; return mynew; } @@ -169,6 +175,8 @@ var cp1252Map = { underline = 0; strikethrough = 0; script = 0; + superscript = 0; + baselineOffset = 0.0; } - (void)addTab:(float)location type:(CPTextTabType)type @@ -212,6 +220,12 @@ var cp1252Map = { 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 @@ -226,6 +240,12 @@ var kRgsymRtf = { "b" : [ "b", 1, false, kRTFParserType_prop, "propBold"], "ul" : [ "ul", 1, false, kRTFParserType_prop, "propUnderline"], "i" : [ "i", 1, false, kRTFParserType_prop, "propItalic"], + "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"], @@ -621,6 +641,36 @@ var kRgsymRtf = { } 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; diff --git a/AppKit/CPTextView/_CPRTFProducer.j b/AppKit/CPTextView/_CPRTFProducer.j index 1e48afda8..116ebf997 100644 --- a/AppKit/CPTextView/_CPRTFProducer.j +++ b/AppKit/CPTextView/_CPRTFProducer.j @@ -646,28 +646,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) { @@ -676,7 +677,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"; } } From beb4aef42422ca161379e8b269f399887176b1df Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 19 Jun 2026 09:47:39 +0200 Subject: [PATCH 40/46] new: heightTracksTextView support in CPTextContainer --- AppKit/CPTextView/CPTextContainer.j | 59 ++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/AppKit/CPTextView/CPTextContainer.j b/AppKit/CPTextView/CPTextContainer.j index 48490579e..e851de4dd 100644 --- a/AppKit/CPTextView/CPTextContainer.j +++ b/AppKit/CPTextView/CPTextContainer.j @@ -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]; } From 89445f94a2b145e4f32dc27199f6df10e2d19c41 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 19 Jun 2026 22:26:52 +0200 Subject: [PATCH 41/46] fixed: whitespace before tab --- AppKit/CPTextView/_CPRTFParser.j | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 69bd0e224..f04654a05 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -334,6 +334,7 @@ var kRgsymRtf = { CPArray _fontArray; CPString _freename; BOOL _parsingFontTable; + BOOL _keywordIsControlWord; // Table parsing state BOOL _inTableActive; @@ -353,6 +354,7 @@ var kRgsymRtf = { _states = []; _currentParseIndex = 0; _hexreturn = NO; + _keywordIsControlWord = NO; _result = [CPAttributedString new]; _colorArray = []; _fontArray = ['Arial']; // FIXME: should be name of system font @@ -889,7 +891,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)) { @@ -978,6 +985,7 @@ var kRgsymRtf = { break; case "{": + lastchar = 0; if (_waitingForNextRow) [self _flushTableIfAny]; @@ -986,6 +994,7 @@ var kRgsymRtf = { break; case "}": + lastchar = 0; if (_waitingForNextRow) [self _flushTableIfAny]; @@ -1009,7 +1018,7 @@ var kRgsymRtf = { _freename = ''; ch = [self _parseKeyword:rtf length:len]; - if (!_hexreturn && ch.length == 0) + if (!_hexreturn && _keywordIsControlWord) lastchar = 1; else lastchar = 0; @@ -1040,6 +1049,7 @@ var kRgsymRtf = { case 0x0a: case '\n': case '\r': + lastchar = 0; break; default: From 319bcdaba92dacb1b615a431ddf691f03049986b Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 19 Jun 2026 22:56:38 +0200 Subject: [PATCH 42/46] fixed: table roundtrip issue --- AppKit/CPTextView/_CPRTFProducer.j | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/AppKit/CPTextView/_CPRTFProducer.j b/AppKit/CPTextView/_CPRTFProducer.j index 116ebf997..a0625f30b 100644 --- a/AppKit/CPTextView/_CPRTFProducer.j +++ b/AppKit/CPTextView/_CPRTFProducer.j @@ -542,6 +542,17 @@ function _points2twips(a) { return (a) * 20.0; } 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, '\\{'); @@ -715,7 +726,7 @@ function _points2twips(a) { return (a) * 20.0; } var nobraces; if ([headerString length]) - nobraces = [CPString stringWithFormat:@"%@ %@}", headerString, substring]; + nobraces = [CPString stringWithFormat:@"%@ %@", headerString, substring]; else nobraces = substring; From 15bbca8d776f67fa2d1ce9380467aabf5ce027f6 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 20 Jun 2026 10:36:07 +0200 Subject: [PATCH 43/46] fixed: whitespace issue --- AppKit/CPTextView/_CPRTFParser.j | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index f04654a05..9028e49f5 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -979,7 +979,11 @@ 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; @@ -1025,7 +1029,8 @@ var kRgsymRtf = { 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; From 26b2280fc2946dde0867b624460706029b9090f3 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 20 Jun 2026 10:40:46 +0200 Subject: [PATCH 44/46] fixed: color spill issue --- AppKit/CPTextView/_CPRTFParser.j | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 9028e49f5..b2e80b295 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -797,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": From 766035f4b71aa64ec9fdfe1426c4a72424c0382a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 20 Jun 2026 12:06:24 +0200 Subject: [PATCH 45/46] Fixed: baseline alignment in _CPLineFragment to prevent overlapping text descenders --- AppKit/CPTextView/CPLayoutManager.j | 10 ++++------ AppKit/CPTextView/CPTextView.j | 28 +++++++++++++--------------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index b1d22ea80..4bd345dc4 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -965,16 +965,11 @@ _oncontextmenuhandler = function () { return false; }; if (frame) { var correctedRect = CGRectCreateCopy(frame); - correctedRect.size.height -= frame._descent; - correctedRect.origin.y -= frame._descent; if (!rect) rect = CGRectCreateCopy(correctedRect); else rect = CGRectUnion(rect, correctedRect); - - if (_isNewlineCharacter([[_textStorage string] characterAtIndex:MAX(0, CPMaxRange(selectedCharRange) - 1)])) - rect.size.width = containerSize.width - rect.origin.x; } } } @@ -1348,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; } } diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 0398d8f46..71f1b2c46 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2333,30 +2333,28 @@ Sets the selection to a range of characters in response to user action. if (_selectionRange.location == numberOfGlyphs && _isNewlineCharacter([[_textStorage string] characterAtIndex:_selectionRange.location - 1])) return CGRectCreateCopy([_layoutManager extraLineFragmentRect]); - var caretRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(_selectionRange.location, 1) inTextContainer:_textContainer]; + var caretRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(_selectionRange.location, 1) inTextContainer:_textContainer]; - var loc = (_selectionRange.location == numberOfGlyphs) ? _selectionRange.location - 1 : _selectionRange.location, - caretOffset = [_layoutManager _characterOffsetAtLocation:loc], - oldYPosition = CGRectGetMaxY(caretRect), - caretDescend = [_layoutManager _descentAtLocation:loc]; + var loc = (_selectionRange.location == numberOfGlyphs) ? _selectionRange.location - 1 : _selectionRange.location, + caretOffset = [_layoutManager _characterOffsetAtLocation: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 (caretOffset > 0) + { + caretRect.origin.y += caretOffset; + } - 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; + if (_selectionRange.location == numberOfGlyphs) + caretRect.origin.x += caretRect.size.width; caretRect.origin.x += _textContainerOrigin.x; 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; } From c8789c5450db0efd441b748bb770399a70b50e38 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 20 Jun 2026 12:32:10 +0200 Subject: [PATCH 46/46] formatting --- AppKit/CPTextView/CPTextView.j | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 71f1b2c46..b4260e217 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2333,22 +2333,22 @@ Sets the selection to a range of characters in response to user action. if (_selectionRange.location == numberOfGlyphs && _isNewlineCharacter([[_textStorage string] characterAtIndex:_selectionRange.location - 1])) return CGRectCreateCopy([_layoutManager extraLineFragmentRect]); - var caretRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(_selectionRange.location, 1) inTextContainer:_textContainer]; + var caretRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(_selectionRange.location, 1) inTextContainer:_textContainer]; - var loc = (_selectionRange.location == numberOfGlyphs) ? _selectionRange.location - 1 : _selectionRange.location, - caretOffset = [_layoutManager _characterOffsetAtLocation:loc], - font = [_textStorage attribute:CPFontAttributeName atIndex:loc effectiveRange:nil] || [self font]; + var loc = (_selectionRange.location == numberOfGlyphs) ? _selectionRange.location - 1 : _selectionRange.location, + caretOffset = [_layoutManager _characterOffsetAtLocation:loc], + font = [_textStorage attribute:CPFontAttributeName atIndex:loc effectiveRange:nil] || [self font]; - if (caretOffset > 0) - { - caretRect.origin.y += caretOffset; - } + if (caretOffset > 0) + { + caretRect.origin.y += caretOffset; + } - // Set the caret height to match the size of the active font - caretRect.size.height = [font size]; + // 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; + if (_selectionRange.location == numberOfGlyphs) + caretRect.origin.x += caretRect.size.width; caretRect.origin.x += _textContainerOrigin.x; caretRect.origin.y += _textContainerOrigin.y;